=b.SCRIPT.id?r.text():b.DISPLAY:"text"===e&&r.size===b.DISPLAY.size?r=b.TEXT:"script"===e?r=b.SCRIPT:"scriptscript"===e&&(r=b.SCRIPTSCRIPT),r},Qr=function(e,t){var r,n=Jr(e.size,t.style),a=n.fracNum(),i=n.fracDen();r=t.havingStyle(a);var o=bt(e.numer,r,t);if(e.continued){var s=8.5/t.fontMetrics().ptPerEm,l=3.5/t.fontMetrics().ptPerEm;o.height=o.height0?3*c:7*c,d=t.fontMetrics().denom1):(m>0?(u=t.fontMetrics().num2,p=c):(u=t.fontMetrics().num3,p=3*c),d=t.fontMetrics().denom2),h){var w=t.fontMetrics().axisHeight;u-o.depth-(w+.5*m)0&&(t="."===(t=e)?null:t),t};nt({type:"genfrac",names:["\\genfrac"],props:{numArgs:6,allowedInArgument:!0,argTypes:["math","math","size","text","math","math"]},handler:function(e,t){var r,n=e.parser,a=t[4],i=t[5],o=it(t[0]),s="atom"===o.type&&"open"===o.family?rn(o.text):null,l=it(t[1]),h="atom"===l.type&&"close"===l.family?rn(l.text):null,m=Ft(t[2],"size"),c=null;r=!!m.isBlank||(c=m.value).number>0;var u="auto",p=t[3];if("ordgroup"===p.type){if(p.body.length>0){var d=Ft(p.body[0],"textord");u=tn[Number(d.text)]}}else p=Ft(p,"textord"),u=tn[Number(p.text)];return{type:"genfrac",mode:n.mode,numer:a,denom:i,continued:!1,hasBarLine:r,barSize:c,leftDelim:s,rightDelim:h,size:u}},htmlBuilder:Qr,mathmlBuilder:en}),nt({type:"infix",names:["\\above"],props:{numArgs:1,argTypes:["size"],infix:!0},handler:function(e,t){var r=e.parser,n=(e.funcName,e.token);return{type:"infix",mode:r.mode,replaceWith:"\\\\abovefrac",size:Ft(t[0],"size").value,token:n}}}),nt({type:"genfrac",names:["\\\\abovefrac"],props:{numArgs:3,argTypes:["math","size","math"]},handler:function(e,t){var r=e.parser,n=(e.funcName,t[0]),a=function(e){if(!e)throw new Error("Expected non-null, but got "+String(e));return e}(Ft(t[1],"infix").size),i=t[2],o=a.number>0;return{type:"genfrac",mode:r.mode,numer:n,denom:i,continued:!1,hasBarLine:o,barSize:a,leftDelim:null,rightDelim:null,size:"auto"}},htmlBuilder:Qr,mathmlBuilder:en});var nn=function(e,t){var r,n,a=t.style;"supsub"===e.type?(r=e.sup?bt(e.sup,t.havingStyle(a.sup()),t):bt(e.sub,t.havingStyle(a.sub()),t),n=Ft(e.base,"horizBrace")):n=Ft(e,"horizBrace");var i,o=bt(n.base,t.havingBaseStyle(b.DISPLAY)),s=Pt(n,t);if(n.isOver?(i=je.makeVList({positionType:"firstBaseline",children:[{type:"elem",elem:o},{type:"kern",size:.1},{type:"elem",elem:s}]},t)).children[0].children[0].children[1].classes.push("svg-align"):(i=je.makeVList({positionType:"bottom",positionData:o.depth+.1+s.height,children:[{type:"elem",elem:s},{type:"kern",size:.1},{type:"elem",elem:o}]},t)).children[0].children[0].children[0].classes.push("svg-align"),r){var l=je.makeSpan(["mord",n.isOver?"mover":"munder"],[i],t);i=n.isOver?je.makeVList({positionType:"firstBaseline",children:[{type:"elem",elem:l},{type:"kern",size:.2},{type:"elem",elem:r}]},t):je.makeVList({positionType:"bottom",positionData:l.depth+.2+r.height+r.depth,children:[{type:"elem",elem:r},{type:"kern",size:.2},{type:"elem",elem:l}]},t)}return je.makeSpan(["mord",n.isOver?"mover":"munder"],[i],t)};nt({type:"horizBrace",names:["\\overbrace","\\underbrace"],props:{numArgs:1},handler:function(e,t){var r=e.parser,n=e.funcName;return{type:"horizBrace",mode:r.mode,label:n,isOver:/^\\over/.test(n),base:t[0]}},htmlBuilder:nn,mathmlBuilder:function(e,t){var r=Dt(e.label);return new Mt.MathNode(e.isOver?"mover":"munder",[Nt(e.base,t),r])}}),nt({type:"href",names:["\\href"],props:{numArgs:2,argTypes:["url","original"],allowedInText:!0},handler:function(e,t){var r=e.parser,n=t[1],a=Ft(t[0],"url").url;return r.settings.isTrusted({command:"\\href",url:a})?{type:"href",mode:r.mode,href:a,body:ot(n)}:r.formatUnsupportedCmd("\\href")},htmlBuilder:function(e,t){var r=ut(e.body,t,!1);return je.makeAnchor(e.href,[],r,t)},mathmlBuilder:function(e,t){var r=qt(e.body,t);return r instanceof kt||(r=new kt("mrow",[r])),r.setAttribute("href",e.href),r}}),nt({type:"href",names:["\\url"],props:{numArgs:1,argTypes:["url"],allowedInText:!0},handler:function(e,t){var r=e.parser,n=Ft(t[0],"url").url;if(!r.settings.isTrusted({command:"\\url",url:n}))return r.formatUnsupportedCmd("\\url");for(var a=[],i=0;i0&&(n=Le(e.totalheight,t)-r,n=Number(n.toFixed(2)));var a=0;e.width.number>0&&(a=Le(e.width,t));var i={height:r+n+"em"};a>0&&(i.width=a+"em"),n>0&&(i.verticalAlign=-n+"em");var o=new C(e.src,e.alt,i);return o.height=r,o.depth=n,o},mathmlBuilder:function(e,t){var r=new Mt.MathNode("mglyph",[]);r.setAttribute("alt",e.alt);var n=Le(e.height,t),a=0;if(e.totalheight.number>0&&(a=(a=Le(e.totalheight,t)-n).toFixed(2),r.setAttribute("valign","-"+a+"em")),r.setAttribute("height",n+a+"em"),e.width.number>0){var i=Le(e.width,t);r.setAttribute("width",i+"em")}return r.setAttribute("src",e.src),r}}),nt({type:"kern",names:["\\kern","\\mkern","\\hskip","\\mskip"],props:{numArgs:1,argTypes:["size"],primitive:!0,allowedInText:!0},handler:function(e,t){var r=e.parser,n=e.funcName,a=Ft(t[0],"size");if(r.settings.strict){var i="m"===n[1],o="mu"===a.value.unit;i?(o||r.settings.reportNonstrict("mathVsTextUnits","LaTeX's "+n+" supports only mu units, not "+a.value.unit+" units"),"math"!==r.mode&&r.settings.reportNonstrict("mathVsTextUnits","LaTeX's "+n+" works only in math mode")):o&&r.settings.reportNonstrict("mathVsTextUnits","LaTeX's "+n+" doesn't support mu units")}return{type:"kern",mode:r.mode,dimension:a.value}},htmlBuilder:function(e,t){return je.makeGlue(e.dimension,t)},mathmlBuilder:function(e,t){var r=Le(e.dimension,t);return new Mt.SpaceNode(r)}}),nt({type:"lap",names:["\\mathllap","\\mathrlap","\\mathclap"],props:{numArgs:1,allowedInText:!0},handler:function(e,t){var r=e.parser,n=e.funcName,a=t[0];return{type:"lap",mode:r.mode,alignment:n.slice(5),body:a}},htmlBuilder:function(e,t){var r;"clap"===e.alignment?(r=je.makeSpan([],[bt(e.body,t)]),r=je.makeSpan(["inner"],[r],t)):r=je.makeSpan(["inner"],[bt(e.body,t)]);var n=je.makeSpan(["fix"],[]),a=je.makeSpan([e.alignment],[r,n],t),i=je.makeSpan(["strut"]);return i.style.height=a.height+a.depth+"em",i.style.verticalAlign=-a.depth+"em",a.children.unshift(i),a=je.makeSpan(["thinbox"],[a],t),je.makeSpan(["mord","vbox"],[a],t)},mathmlBuilder:function(e,t){var r=new Mt.MathNode("mpadded",[Nt(e.body,t)]);if("rlap"!==e.alignment){var n="llap"===e.alignment?"-1":"-0.5";r.setAttribute("lspace",n+"width")}return r.setAttribute("width","0px"),r}}),nt({type:"styling",names:["\\(","$"],props:{numArgs:0,allowedInText:!0,allowedInMath:!1},handler:function(e,t){var r=e.funcName,n=e.parser,a=n.mode;n.switchMode("math");var i="\\("===r?"\\)":"$",o=n.parseExpression(!1,i);return n.expect(i),n.switchMode(a),{type:"styling",mode:n.mode,style:"text",body:o}}}),nt({type:"text",names:["\\)","\\]"],props:{numArgs:0,allowedInText:!0,allowedInMath:!1},handler:function(e,t){throw new n("Mismatched "+e.funcName)}});var on=function(e,t){switch(t.style.size){case b.DISPLAY.size:return e.display;case b.TEXT.size:return e.text;case b.SCRIPT.size:return e.script;case b.SCRIPTSCRIPT.size:return e.scriptscript;default:return e.text}};nt({type:"mathchoice",names:["\\mathchoice"],props:{numArgs:4,primitive:!0},handler:function(e,t){return{type:"mathchoice",mode:e.parser.mode,display:ot(t[0]),text:ot(t[1]),script:ot(t[2]),scriptscript:ot(t[3])}},htmlBuilder:function(e,t){var r=on(e,t),n=ut(r,t,!1);return je.makeFragment(n)},mathmlBuilder:function(e,t){var r=on(e,t);return qt(r,t)}});var sn=function(e,t,r,n,a,i,o){var s,l,h;if(e=je.makeSpan([],[e]),t){var m=bt(t,n.havingStyle(a.sup()),n);l={elem:m,kern:Math.max(n.fontMetrics().bigOpSpacing1,n.fontMetrics().bigOpSpacing3-m.depth)}}if(r){var c=bt(r,n.havingStyle(a.sub()),n);s={elem:c,kern:Math.max(n.fontMetrics().bigOpSpacing2,n.fontMetrics().bigOpSpacing4-c.height)}}if(l&&s){var u=n.fontMetrics().bigOpSpacing5+s.elem.height+s.elem.depth+s.kern+e.depth+o;h=je.makeVList({positionType:"bottom",positionData:u,children:[{type:"kern",size:n.fontMetrics().bigOpSpacing5},{type:"elem",elem:s.elem,marginLeft:-i+"em"},{type:"kern",size:s.kern},{type:"elem",elem:e},{type:"kern",size:l.kern},{type:"elem",elem:l.elem,marginLeft:i+"em"},{type:"kern",size:n.fontMetrics().bigOpSpacing5}]},n)}else if(s){var p=e.height-o;h=je.makeVList({positionType:"top",positionData:p,children:[{type:"kern",size:n.fontMetrics().bigOpSpacing5},{type:"elem",elem:s.elem,marginLeft:-i+"em"},{type:"kern",size:s.kern},{type:"elem",elem:e}]},n)}else{if(!l)return e;var d=e.depth+o;h=je.makeVList({positionType:"bottom",positionData:d,children:[{type:"elem",elem:e},{type:"kern",size:l.kern},{type:"elem",elem:l.elem,marginLeft:i+"em"},{type:"kern",size:n.fontMetrics().bigOpSpacing5}]},n)}return je.makeSpan(["mop","op-limits"],[h],n)},ln=["\\smallint"],hn=function(e,t){var r,n,a,i=!1;"supsub"===e.type?(r=e.sup,n=e.sub,a=Ft(e.base,"op"),i=!0):a=Ft(e,"op");var o,s=t.style,h=!1;if(s.size===b.DISPLAY.size&&a.symbol&&!l.contains(ln,a.name)&&(h=!0),a.symbol){var m=h?"Size2-Regular":"Size1-Regular",c="";if("\\oiint"!==a.name&&"\\oiiint"!==a.name||(c=a.name.substr(1),a.name="oiint"===c?"\\iint":"\\iiint"),o=je.makeSymbol(a.name,m,"math",t,["mop","op-symbol",h?"large-op":"small-op"]),c.length>0){var u=o.italic,p=je.staticSvg(c+"Size"+(h?"2":"1"),t);o=je.makeVList({positionType:"individualShift",children:[{type:"elem",elem:o,shift:0},{type:"elem",elem:p,shift:h?.08:0}]},t),a.name="\\"+c,o.classes.unshift("mop"),o.italic=u}}else if(a.body){var d=ut(a.body,t,!0);1===d.length&&d[0]instanceof O?(o=d[0]).classes[0]="mop":o=je.makeSpan(["mop"],d,t)}else{for(var f=[],g=1;g0){for(var s=a.body.map((function(e){var t=e.text;return"string"==typeof t?{type:"textord",mode:e.mode,text:t}:e})),l=ut(s,t.withFont("mathrm"),!0),h=0;h=0?s.setAttribute("height","+"+a+"em"):(s.setAttribute("height",a+"em"),s.setAttribute("depth","+"+-a+"em")),s.setAttribute("voffset",a+"em"),s}});var fn=["\\tiny","\\sixptsize","\\scriptsize","\\footnotesize","\\small","\\normalsize","\\large","\\Large","\\LARGE","\\huge","\\Huge"];nt({type:"sizing",names:fn,props:{numArgs:0,allowedInText:!0},handler:function(e,t){var r=e.breakOnTokenText,n=e.funcName,a=e.parser,i=a.parseExpression(!1,r);return{type:"sizing",mode:a.mode,size:fn.indexOf(n)+1,body:i}},htmlBuilder:function(e,t){var r=t.havingSize(e.size);return dn(e.body,r,t)},mathmlBuilder:function(e,t){var r=t.havingSize(e.size),n=Bt(e.body,r),a=new Mt.MathNode("mstyle",n);return a.setAttribute("mathsize",r.sizeMultiplier+"em"),a}}),nt({type:"smash",names:["\\smash"],props:{numArgs:1,numOptionalArgs:1,allowedInText:!0},handler:function(e,t,r){var n=e.parser,a=!1,i=!1,o=r[0]&&Ft(r[0],"ordgroup");if(o)for(var s="",l=0;lr.height+r.depth+i&&(i=(i+c-r.height-r.depth)/2);var u=l.height-r.height-i-h;r.style.paddingLeft=m+"em";var p=je.makeVList({positionType:"firstBaseline",children:[{type:"elem",elem:r,wrapperClasses:["svg-align"]},{type:"kern",size:-(r.height+u)},{type:"elem",elem:l},{type:"kern",size:h}]},t);if(e.index){var d=t.havingStyle(b.SCRIPTSCRIPT),f=bt(e.index,d,t),g=.6*(p.height-p.depth),v=je.makeVList({positionType:"shift",positionData:-g,children:[{type:"elem",elem:f}]},t),y=je.makeSpan(["root"],[v]);return je.makeSpan(["mord","sqrt"],[y,p],t)}return je.makeSpan(["mord","sqrt"],[p],t)},mathmlBuilder:function(e,t){var r=e.body,n=e.index;return n?new Mt.MathNode("mroot",[Nt(r,t),Nt(n,t)]):new Mt.MathNode("msqrt",[Nt(r,t)])}});var gn={display:b.DISPLAY,text:b.TEXT,script:b.SCRIPT,scriptscript:b.SCRIPTSCRIPT};nt({type:"styling",names:["\\displaystyle","\\textstyle","\\scriptstyle","\\scriptscriptstyle"],props:{numArgs:0,allowedInText:!0,primitive:!0},handler:function(e,t){var r=e.breakOnTokenText,n=e.funcName,a=e.parser,i=a.parseExpression(!0,r),o=n.slice(1,n.length-5);return{type:"styling",mode:a.mode,style:o,body:i}},htmlBuilder:function(e,t){var r=gn[e.style],n=t.havingStyle(r).withFont("");return dn(e.body,n,t)},mathmlBuilder:function(e,t){var r=gn[e.style],n=t.havingStyle(r),a=Bt(e.body,n),i=new Mt.MathNode("mstyle",a),o={display:["0","true"],text:["0","false"],script:["1","false"],scriptscript:["2","false"]}[e.style];return i.setAttribute("scriptlevel",o[0]),i.setAttribute("displaystyle",o[1]),i}});var vn=function(e,t){var r=e.base;return r?"op"===r.type?r.limits&&(t.style.size===b.DISPLAY.size||r.alwaysHandleSupSub)?hn:null:"operatorname"===r.type?r.alwaysHandleSupSub&&(t.style.size===b.DISPLAY.size||r.limits)?pn:null:"accent"===r.type?l.isCharacterBox(r.base)?Ut:null:"horizBrace"===r.type&&!e.sub===r.isOver?nn:null:null};at({type:"supsub",htmlBuilder:function(e,t){var r=vn(e,t);if(r)return r(e,t);var n,a,i,o=e.base,s=e.sup,h=e.sub,m=bt(o,t),c=t.fontMetrics(),u=0,p=0,d=o&&l.isCharacterBox(o);if(s){var f=t.havingStyle(t.style.sup());n=bt(s,f,t),d||(u=m.height-f.fontMetrics().supDrop*f.sizeMultiplier/t.sizeMultiplier)}if(h){var g=t.havingStyle(t.style.sub());a=bt(h,g,t),d||(p=m.depth+g.fontMetrics().subDrop*g.sizeMultiplier/t.sizeMultiplier)}i=t.style===b.DISPLAY?c.sup1:t.style.cramped?c.sup3:c.sup2;var v,y=t.sizeMultiplier,x=.5/c.ptPerEm/y+"em",w=null;if(a){var k=e.base&&"op"===e.base.type&&e.base.name&&("\\oiint"===e.base.name||"\\oiiint"===e.base.name);(m instanceof O||k)&&(w=-m.italic+"em")}if(n&&a){u=Math.max(u,i,n.depth+.25*c.xHeight),p=Math.max(p,c.sub2);var S=4*c.defaultRuleThickness;if(u-n.depth-(a.height-p)0&&(u+=M,p-=M)}var z=[{type:"elem",elem:a,shift:p,marginRight:x,marginLeft:w},{type:"elem",elem:n,shift:-u,marginRight:x}];v=je.makeVList({positionType:"individualShift",children:z},t)}else if(a){p=Math.max(p,c.sub1,a.height-.8*c.xHeight);var A=[{type:"elem",elem:a,marginLeft:w,marginRight:x}];v=je.makeVList({positionType:"shift",positionData:p,children:A},t)}else{if(!n)throw new Error("supsub must have either sup or sub.");u=Math.max(u,i,n.depth+.25*c.xHeight),v=je.makeVList({positionType:"shift",positionData:-u,children:[{type:"elem",elem:n,marginRight:x}]},t)}var T=gt(m,"right")||"mord";return je.makeSpan([T],[m,je.makeSpan(["msupsub"],[v])],t)},mathmlBuilder:function(e,t){var r,n=!1;e.base&&"horizBrace"===e.base.type&&!!e.sup===e.base.isOver&&(n=!0,r=e.base.isOver),!e.base||"op"!==e.base.type&&"operatorname"!==e.base.type||(e.base.parentIsSupSub=!0);var a,i=[Nt(e.base,t)];if(e.sub&&i.push(Nt(e.sub,t)),e.sup&&i.push(Nt(e.sup,t)),n)a=r?"mover":"munder";else if(e.sub)if(e.sup){var o=e.base;a=o&&"op"===o.type&&o.limits&&t.style===b.DISPLAY||o&&"operatorname"===o.type&&o.alwaysHandleSupSub&&(t.style===b.DISPLAY||o.limits)?"munderover":"msubsup"}else{var s=e.base;a=s&&"op"===s.type&&s.limits&&(t.style===b.DISPLAY||s.alwaysHandleSupSub)||s&&"operatorname"===s.type&&s.alwaysHandleSupSub&&(s.limits||t.style===b.DISPLAY)?"munder":"msub"}else{var l=e.base;a=l&&"op"===l.type&&l.limits&&(t.style===b.DISPLAY||l.alwaysHandleSupSub)||l&&"operatorname"===l.type&&l.alwaysHandleSupSub&&(l.limits||t.style===b.DISPLAY)?"mover":"msup"}return new Mt.MathNode(a,i)}}),at({type:"atom",htmlBuilder:function(e,t){return je.mathsym(e.text,e.mode,t,["m"+e.family])},mathmlBuilder:function(e,t){var r=new Mt.MathNode("mo",[zt(e.text,e.mode)]);if("bin"===e.family){var n=Tt(e,t);"bold-italic"===n&&r.setAttribute("mathvariant",n)}else"punct"===e.family?r.setAttribute("separator","true"):"open"!==e.family&&"close"!==e.family||r.setAttribute("stretchy","false");return r}});var bn={mi:"italic",mn:"normal",mtext:"normal"};at({type:"mathord",htmlBuilder:function(e,t){return je.makeOrd(e,t,"mathord")},mathmlBuilder:function(e,t){var r=new Mt.MathNode("mi",[zt(e.text,e.mode,t)]),n=Tt(e,t)||"italic";return n!==bn[r.type]&&r.setAttribute("mathvariant",n),r}}),at({type:"textord",htmlBuilder:function(e,t){return je.makeOrd(e,t,"textord")},mathmlBuilder:function(e,t){var r,n=zt(e.text,e.mode,t),a=Tt(e,t)||"normal";return r="text"===e.mode?new Mt.MathNode("mtext",[n]):/[0-9]/.test(e.text)?new Mt.MathNode("mn",[n]):"\\prime"===e.text?new Mt.MathNode("mo",[n]):new Mt.MathNode("mi",[n]),a!==bn[r.type]&&r.setAttribute("mathvariant",a),r}});var yn={"\\nobreak":"nobreak","\\allowbreak":"allowbreak"},xn={" ":{},"\\ ":{},"~":{className:"nobreak"},"\\space":{},"\\nobreakspace":{className:"nobreak"}};at({type:"spacing",htmlBuilder:function(e,t){if(xn.hasOwnProperty(e.text)){var r=xn[e.text].className||"";if("text"===e.mode){var a=je.makeOrd(e,t,"textord");return a.classes.push(r),a}return je.makeSpan(["mspace",r],[je.mathsym(e.text,e.mode,t)],t)}if(yn.hasOwnProperty(e.text))return je.makeSpan(["mspace",yn[e.text]],[],t);throw new n('Unknown type of space "'+e.text+'"')},mathmlBuilder:function(e,t){if(!xn.hasOwnProperty(e.text)){if(yn.hasOwnProperty(e.text))return new Mt.MathNode("mspace");throw new n('Unknown type of space "'+e.text+'"')}return new Mt.MathNode("mtext",[new Mt.TextNode("\xa0")])}});var wn=function(){var e=new Mt.MathNode("mtd",[]);return e.setAttribute("width","50%"),e};at({type:"tag",mathmlBuilder:function(e,t){var r=new Mt.MathNode("mtable",[new Mt.MathNode("mtr",[wn(),new Mt.MathNode("mtd",[qt(e.body,t)]),wn(),new Mt.MathNode("mtd",[qt(e.tag,t)])])]);return r.setAttribute("width","100%"),r}});var kn={"\\text":void 0,"\\textrm":"textrm","\\textsf":"textsf","\\texttt":"texttt","\\textnormal":"textrm"},Sn={"\\textbf":"textbf","\\textmd":"textmd"},Mn={"\\textit":"textit","\\textup":"textup"},zn=function(e,t){var r=e.font;return r?kn[r]?t.withTextFontFamily(kn[r]):Sn[r]?t.withTextFontWeight(Sn[r]):t.withTextFontShape(Mn[r]):t};nt({type:"text",names:["\\text","\\textrm","\\textsf","\\texttt","\\textnormal","\\textbf","\\textmd","\\textit","\\textup"],props:{numArgs:1,argTypes:["text"],allowedInArgument:!0,allowedInText:!0},handler:function(e,t){var r=e.parser,n=e.funcName,a=t[0];return{type:"text",mode:r.mode,body:ot(a),font:n}},htmlBuilder:function(e,t){var r=zn(e,t),n=ut(e.body,r,!0);return je.makeSpan(["mord","text"],n,r)},mathmlBuilder:function(e,t){var r=zn(e,t);return qt(e.body,r)}}),nt({type:"underline",names:["\\underline"],props:{numArgs:1,allowedInText:!0},handler:function(e,t){return{type:"underline",mode:e.parser.mode,body:t[0]}},htmlBuilder:function(e,t){var r=bt(e.body,t),n=je.makeLineSpan("underline-line",t),a=t.fontMetrics().defaultRuleThickness,i=je.makeVList({positionType:"top",positionData:r.height,children:[{type:"kern",size:a},{type:"elem",elem:n},{type:"kern",size:3*a},{type:"elem",elem:r}]},t);return je.makeSpan(["mord","underline"],[i],t)},mathmlBuilder:function(e,t){var r=new Mt.MathNode("mo",[new Mt.TextNode("\u203e")]);r.setAttribute("stretchy","true");var n=new Mt.MathNode("munder",[Nt(e.body,t),r]);return n.setAttribute("accentunder","true"),n}}),nt({type:"vcenter",names:["\\vcenter"],props:{numArgs:1,argTypes:["original"],allowedInText:!1},handler:function(e,t){return{type:"vcenter",mode:e.parser.mode,body:t[0]}},htmlBuilder:function(e,t){var r=bt(e.body,t),n=t.fontMetrics().axisHeight,a=.5*(r.height-n-(r.depth+n));return je.makeVList({positionType:"shift",positionData:a,children:[{type:"elem",elem:r}]},t)},mathmlBuilder:function(e,t){return new Mt.MathNode("mpadded",[Nt(e.body,t)],["vcenter"])}}),nt({type:"verb",names:["\\verb"],props:{numArgs:0,allowedInText:!0},handler:function(e,t,r){throw new n("\\verb ended by end of line instead of matching delimiter")},htmlBuilder:function(e,t){for(var r=An(e),n=[],a=t.havingStyle(t.style.text()),i=0;i0&&(this.undefStack[this.undefStack.length-1][e]=t)}else{var a=this.undefStack[this.undefStack.length-1];a&&!a.hasOwnProperty(e)&&(a[e]=this.current[e])}this.current[e]=t},e}(),Rn={},En=Rn;function Hn(e,t){Rn[e]=t}Hn("\\noexpand",(function(e){var t=e.popToken();return e.isExpandable(t.text)&&(t.noexpand=!0,t.treatAsRelax=!0),{tokens:[t],numArgs:0}})),Hn("\\expandafter",(function(e){var t=e.popToken();return e.expandOnce(!0),{tokens:[t],numArgs:0}})),Hn("\\@firstoftwo",(function(e){return{tokens:e.consumeArgs(2)[0],numArgs:0}})),Hn("\\@secondoftwo",(function(e){return{tokens:e.consumeArgs(2)[1],numArgs:0}})),Hn("\\@ifnextchar",(function(e){var t=e.consumeArgs(3);e.consumeSpaces();var r=e.future();return 1===t[0].length&&t[0][0].text===r.text?{tokens:t[1],numArgs:0}:{tokens:t[2],numArgs:0}})),Hn("\\@ifstar","\\@ifnextchar *{\\@firstoftwo{#1}}"),Hn("\\TextOrMath",(function(e){var t=e.consumeArgs(2);return"text"===e.mode?{tokens:t[0],numArgs:0}:{tokens:t[1],numArgs:0}}));var Ln={0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,a:10,A:10,b:11,B:11,c:12,C:12,d:13,D:13,e:14,E:14,f:15,F:15};Hn("\\char",(function(e){var t,r=e.popToken(),a="";if("'"===r.text)t=8,r=e.popToken();else if('"'===r.text)t=16,r=e.popToken();else if("`"===r.text)if("\\"===(r=e.popToken()).text[0])a=r.text.charCodeAt(1);else{if("EOF"===r.text)throw new n("\\char` missing argument");a=r.text.charCodeAt(0)}else t=10;if(t){if(null==(a=Ln[r.text])||a>=t)throw new n("Invalid base-"+t+" digit "+r.text);for(var i;null!=(i=Ln[e.future().text])&&i":"\\dotsb","-":"\\dotsb","*":"\\dotsb",":":"\\dotsb","\\DOTSB":"\\dotsb","\\coprod":"\\dotsb","\\bigvee":"\\dotsb","\\bigwedge":"\\dotsb","\\biguplus":"\\dotsb","\\bigcap":"\\dotsb","\\bigcup":"\\dotsb","\\prod":"\\dotsb","\\sum":"\\dotsb","\\bigotimes":"\\dotsb","\\bigoplus":"\\dotsb","\\bigodot":"\\dotsb","\\bigsqcup":"\\dotsb","\\And":"\\dotsb","\\longrightarrow":"\\dotsb","\\Longrightarrow":"\\dotsb","\\longleftarrow":"\\dotsb","\\Longleftarrow":"\\dotsb","\\longleftrightarrow":"\\dotsb","\\Longleftrightarrow":"\\dotsb","\\mapsto":"\\dotsb","\\longmapsto":"\\dotsb","\\hookrightarrow":"\\dotsb","\\doteq":"\\dotsb","\\mathbin":"\\dotsb","\\mathrel":"\\dotsb","\\relbar":"\\dotsb","\\Relbar":"\\dotsb","\\xrightarrow":"\\dotsb","\\xleftarrow":"\\dotsb","\\DOTSI":"\\dotsi","\\int":"\\dotsi","\\oint":"\\dotsi","\\iint":"\\dotsi","\\iiint":"\\dotsi","\\iiiint":"\\dotsi","\\idotsint":"\\dotsi","\\DOTSX":"\\dotsx"};Hn("\\dots",(function(e){var t="\\dotso",r=e.expandAfterFuture().text;return r in Pn?t=Pn[r]:("\\not"===r.substr(0,4)||r in X.math&&l.contains(["bin","rel"],X.math[r].group))&&(t="\\dotsb"),t}));var Fn={")":!0,"]":!0,"\\rbrack":!0,"\\}":!0,"\\rbrace":!0,"\\rangle":!0,"\\rceil":!0,"\\rfloor":!0,"\\rgroup":!0,"\\rmoustache":!0,"\\right":!0,"\\bigr":!0,"\\biggr":!0,"\\Bigr":!0,"\\Biggr":!0,$:!0,";":!0,".":!0,",":!0};Hn("\\dotso",(function(e){return e.future().text in Fn?"\\ldots\\,":"\\ldots"})),Hn("\\dotsc",(function(e){var t=e.future().text;return t in Fn&&","!==t?"\\ldots\\,":"\\ldots"})),Hn("\\cdots",(function(e){return e.future().text in Fn?"\\@cdots\\,":"\\@cdots"})),Hn("\\dotsb","\\cdots"),Hn("\\dotsm","\\cdots"),Hn("\\dotsi","\\!\\cdots"),Hn("\\dotsx","\\ldots\\,"),Hn("\\DOTSI","\\relax"),Hn("\\DOTSB","\\relax"),Hn("\\DOTSX","\\relax"),Hn("\\tmspace","\\TextOrMath{\\kern#1#3}{\\mskip#1#2}\\relax"),Hn("\\,","\\tmspace+{3mu}{.1667em}"),Hn("\\thinspace","\\,"),Hn("\\>","\\mskip{4mu}"),Hn("\\:","\\tmspace+{4mu}{.2222em}"),Hn("\\medspace","\\:"),Hn("\\;","\\tmspace+{5mu}{.2777em}"),Hn("\\thickspace","\\;"),Hn("\\!","\\tmspace-{3mu}{.1667em}"),Hn("\\negthinspace","\\!"),Hn("\\negmedspace","\\tmspace-{4mu}{.2222em}"),Hn("\\negthickspace","\\tmspace-{5mu}{.277em}"),Hn("\\enspace","\\kern.5em "),Hn("\\enskip","\\hskip.5em\\relax"),Hn("\\quad","\\hskip1em\\relax"),Hn("\\qquad","\\hskip2em\\relax"),Hn("\\tag","\\@ifstar\\tag@literal\\tag@paren"),Hn("\\tag@paren","\\tag@literal{({#1})}"),Hn("\\tag@literal",(function(e){if(e.macros.get("\\df@tag"))throw new n("Multiple \\tag");return"\\gdef\\df@tag{\\text{#1}}"})),Hn("\\bmod","\\mathchoice{\\mskip1mu}{\\mskip1mu}{\\mskip5mu}{\\mskip5mu}\\mathbin{\\rm mod}\\mathchoice{\\mskip1mu}{\\mskip1mu}{\\mskip5mu}{\\mskip5mu}"),Hn("\\pod","\\allowbreak\\mathchoice{\\mkern18mu}{\\mkern8mu}{\\mkern8mu}{\\mkern8mu}(#1)"),Hn("\\pmod","\\pod{{\\rm mod}\\mkern6mu#1}"),Hn("\\mod","\\allowbreak\\mathchoice{\\mkern18mu}{\\mkern12mu}{\\mkern12mu}{\\mkern12mu}{\\rm mod}\\,\\,#1"),Hn("\\pmb","\\html@mathml{\\@binrel{#1}{\\mathrlap{#1}\\kern0.5px#1}}{\\mathbf{#1}}"),Hn("\\newline","\\\\\\relax"),Hn("\\TeX","\\textrm{\\html@mathml{T\\kern-.1667em\\raisebox{-.5ex}{E}\\kern-.125emX}{TeX}}");var Vn=D["Main-Regular"]["T".charCodeAt(0)][1]-.7*D["Main-Regular"]["A".charCodeAt(0)][1]+"em";Hn("\\LaTeX","\\textrm{\\html@mathml{L\\kern-.36em\\raisebox{"+Vn+"}{\\scriptstyle A}\\kern-.15em\\TeX}{LaTeX}}"),Hn("\\KaTeX","\\textrm{\\html@mathml{K\\kern-.17em\\raisebox{"+Vn+"}{\\scriptstyle A}\\kern-.15em\\TeX}{KaTeX}}"),Hn("\\hspace","\\@ifstar\\@hspacer\\@hspace"),Hn("\\@hspace","\\hskip #1\\relax"),Hn("\\@hspacer","\\rule{0pt}{0pt}\\hskip #1\\relax"),Hn("\\ordinarycolon",":"),Hn("\\vcentcolon","\\mathrel{\\mathop\\ordinarycolon}"),Hn("\\dblcolon",'\\html@mathml{\\mathrel{\\vcentcolon\\mathrel{\\mkern-.9mu}\\vcentcolon}}{\\mathop{\\char"2237}}'),Hn("\\coloneqq",'\\html@mathml{\\mathrel{\\vcentcolon\\mathrel{\\mkern-1.2mu}=}}{\\mathop{\\char"2254}}'),Hn("\\Coloneqq",'\\html@mathml{\\mathrel{\\dblcolon\\mathrel{\\mkern-1.2mu}=}}{\\mathop{\\char"2237\\char"3d}}'),Hn("\\coloneq",'\\html@mathml{\\mathrel{\\vcentcolon\\mathrel{\\mkern-1.2mu}\\mathrel{-}}}{\\mathop{\\char"3a\\char"2212}}'),Hn("\\Coloneq",'\\html@mathml{\\mathrel{\\dblcolon\\mathrel{\\mkern-1.2mu}\\mathrel{-}}}{\\mathop{\\char"2237\\char"2212}}'),Hn("\\eqqcolon",'\\html@mathml{\\mathrel{=\\mathrel{\\mkern-1.2mu}\\vcentcolon}}{\\mathop{\\char"2255}}'),Hn("\\Eqqcolon",'\\html@mathml{\\mathrel{=\\mathrel{\\mkern-1.2mu}\\dblcolon}}{\\mathop{\\char"3d\\char"2237}}'),Hn("\\eqcolon",'\\html@mathml{\\mathrel{\\mathrel{-}\\mathrel{\\mkern-1.2mu}\\vcentcolon}}{\\mathop{\\char"2239}}'),Hn("\\Eqcolon",'\\html@mathml{\\mathrel{\\mathrel{-}\\mathrel{\\mkern-1.2mu}\\dblcolon}}{\\mathop{\\char"2212\\char"2237}}'),Hn("\\colonapprox",'\\html@mathml{\\mathrel{\\vcentcolon\\mathrel{\\mkern-1.2mu}\\approx}}{\\mathop{\\char"3a\\char"2248}}'),Hn("\\Colonapprox",'\\html@mathml{\\mathrel{\\dblcolon\\mathrel{\\mkern-1.2mu}\\approx}}{\\mathop{\\char"2237\\char"2248}}'),Hn("\\colonsim",'\\html@mathml{\\mathrel{\\vcentcolon\\mathrel{\\mkern-1.2mu}\\sim}}{\\mathop{\\char"3a\\char"223c}}'),Hn("\\Colonsim",'\\html@mathml{\\mathrel{\\dblcolon\\mathrel{\\mkern-1.2mu}\\sim}}{\\mathop{\\char"2237\\char"223c}}'),Hn("\u2237","\\dblcolon"),Hn("\u2239","\\eqcolon"),Hn("\u2254","\\coloneqq"),Hn("\u2255","\\eqqcolon"),Hn("\u2a74","\\Coloneqq"),Hn("\\ratio","\\vcentcolon"),Hn("\\coloncolon","\\dblcolon"),Hn("\\colonequals","\\coloneqq"),Hn("\\coloncolonequals","\\Coloneqq"),Hn("\\equalscolon","\\eqqcolon"),Hn("\\equalscoloncolon","\\Eqqcolon"),Hn("\\colonminus","\\coloneq"),Hn("\\coloncolonminus","\\Coloneq"),Hn("\\minuscolon","\\eqcolon"),Hn("\\minuscoloncolon","\\Eqcolon"),Hn("\\coloncolonapprox","\\Colonapprox"),Hn("\\coloncolonsim","\\Colonsim"),Hn("\\simcolon","\\mathrel{\\sim\\mathrel{\\mkern-1.2mu}\\vcentcolon}"),Hn("\\simcoloncolon","\\mathrel{\\sim\\mathrel{\\mkern-1.2mu}\\dblcolon}"),Hn("\\approxcolon","\\mathrel{\\approx\\mathrel{\\mkern-1.2mu}\\vcentcolon}"),Hn("\\approxcoloncolon","\\mathrel{\\approx\\mathrel{\\mkern-1.2mu}\\dblcolon}"),Hn("\\notni","\\html@mathml{\\not\\ni}{\\mathrel{\\char`\u220c}}"),Hn("\\limsup","\\DOTSB\\operatorname*{lim\\,sup}"),Hn("\\liminf","\\DOTSB\\operatorname*{lim\\,inf}"),Hn("\\injlim","\\DOTSB\\operatorname*{inj\\,lim}"),Hn("\\projlim","\\DOTSB\\operatorname*{proj\\,lim}"),Hn("\\varlimsup","\\DOTSB\\operatorname*{\\overline{lim}}"),Hn("\\varliminf","\\DOTSB\\operatorname*{\\underline{lim}}"),Hn("\\varinjlim","\\DOTSB\\operatorname*{\\underrightarrow{lim}}"),Hn("\\varprojlim","\\DOTSB\\operatorname*{\\underleftarrow{lim}}"),Hn("\\gvertneqq","\\html@mathml{\\@gvertneqq}{\u2269}"),Hn("\\lvertneqq","\\html@mathml{\\@lvertneqq}{\u2268}"),Hn("\\ngeqq","\\html@mathml{\\@ngeqq}{\u2271}"),Hn("\\ngeqslant","\\html@mathml{\\@ngeqslant}{\u2271}"),Hn("\\nleqq","\\html@mathml{\\@nleqq}{\u2270}"),Hn("\\nleqslant","\\html@mathml{\\@nleqslant}{\u2270}"),Hn("\\nshortmid","\\html@mathml{\\@nshortmid}{\u2224}"),Hn("\\nshortparallel","\\html@mathml{\\@nshortparallel}{\u2226}"),Hn("\\nsubseteqq","\\html@mathml{\\@nsubseteqq}{\u2288}"),Hn("\\nsupseteqq","\\html@mathml{\\@nsupseteqq}{\u2289}"),Hn("\\varsubsetneq","\\html@mathml{\\@varsubsetneq}{\u228a}"),Hn("\\varsubsetneqq","\\html@mathml{\\@varsubsetneqq}{\u2acb}"),Hn("\\varsupsetneq","\\html@mathml{\\@varsupsetneq}{\u228b}"),Hn("\\varsupsetneqq","\\html@mathml{\\@varsupsetneqq}{\u2acc}"),Hn("\\imath","\\html@mathml{\\@imath}{\u0131}"),Hn("\\jmath","\\html@mathml{\\@jmath}{\u0237}"),Hn("\\llbracket","\\html@mathml{\\mathopen{[\\mkern-3.2mu[}}{\\mathopen{\\char`\u27e6}}"),Hn("\\rrbracket","\\html@mathml{\\mathclose{]\\mkern-3.2mu]}}{\\mathclose{\\char`\u27e7}}"),Hn("\u27e6","\\llbracket"),Hn("\u27e7","\\rrbracket"),Hn("\\lBrace","\\html@mathml{\\mathopen{\\{\\mkern-3.2mu[}}{\\mathopen{\\char`\u2983}}"),Hn("\\rBrace","\\html@mathml{\\mathclose{]\\mkern-3.2mu\\}}}{\\mathclose{\\char`\u2984}}"),Hn("\u2983","\\lBrace"),Hn("\u2984","\\rBrace"),Hn("\\minuso","\\mathbin{\\html@mathml{{\\mathrlap{\\mathchoice{\\kern{0.145em}}{\\kern{0.145em}}{\\kern{0.1015em}}{\\kern{0.0725em}}\\circ}{-}}}{\\char`\u29b5}}"),Hn("\u29b5","\\minuso"),Hn("\\darr","\\downarrow"),Hn("\\dArr","\\Downarrow"),Hn("\\Darr","\\Downarrow"),Hn("\\lang","\\langle"),Hn("\\rang","\\rangle"),Hn("\\uarr","\\uparrow"),Hn("\\uArr","\\Uparrow"),Hn("\\Uarr","\\Uparrow"),Hn("\\N","\\mathbb{N}"),Hn("\\R","\\mathbb{R}"),Hn("\\Z","\\mathbb{Z}"),Hn("\\alef","\\aleph"),Hn("\\alefsym","\\aleph"),Hn("\\Alpha","\\mathrm{A}"),Hn("\\Beta","\\mathrm{B}"),Hn("\\bull","\\bullet"),Hn("\\Chi","\\mathrm{X}"),Hn("\\clubs","\\clubsuit"),Hn("\\cnums","\\mathbb{C}"),Hn("\\Complex","\\mathbb{C}"),Hn("\\Dagger","\\ddagger"),Hn("\\diamonds","\\diamondsuit"),Hn("\\empty","\\emptyset"),Hn("\\Epsilon","\\mathrm{E}"),Hn("\\Eta","\\mathrm{H}"),Hn("\\exist","\\exists"),Hn("\\harr","\\leftrightarrow"),Hn("\\hArr","\\Leftrightarrow"),Hn("\\Harr","\\Leftrightarrow"),Hn("\\hearts","\\heartsuit"),Hn("\\image","\\Im"),Hn("\\infin","\\infty"),Hn("\\Iota","\\mathrm{I}"),Hn("\\isin","\\in"),Hn("\\Kappa","\\mathrm{K}"),Hn("\\larr","\\leftarrow"),Hn("\\lArr","\\Leftarrow"),Hn("\\Larr","\\Leftarrow"),Hn("\\lrarr","\\leftrightarrow"),Hn("\\lrArr","\\Leftrightarrow"),Hn("\\Lrarr","\\Leftrightarrow"),Hn("\\Mu","\\mathrm{M}"),Hn("\\natnums","\\mathbb{N}"),Hn("\\Nu","\\mathrm{N}"),Hn("\\Omicron","\\mathrm{O}"),Hn("\\plusmn","\\pm"),Hn("\\rarr","\\rightarrow"),Hn("\\rArr","\\Rightarrow"),Hn("\\Rarr","\\Rightarrow"),Hn("\\real","\\Re"),Hn("\\reals","\\mathbb{R}"),Hn("\\Reals","\\mathbb{R}"),Hn("\\Rho","\\mathrm{P}"),Hn("\\sdot","\\cdot"),Hn("\\sect","\\S"),Hn("\\spades","\\spadesuit"),Hn("\\sub","\\subset"),Hn("\\sube","\\subseteq"),Hn("\\supe","\\supseteq"),Hn("\\Tau","\\mathrm{T}"),Hn("\\thetasym","\\vartheta"),Hn("\\weierp","\\wp"),Hn("\\Zeta","\\mathrm{Z}"),Hn("\\argmin","\\DOTSB\\operatorname*{arg\\,min}"),Hn("\\argmax","\\DOTSB\\operatorname*{arg\\,max}"),Hn("\\plim","\\DOTSB\\mathop{\\operatorname{plim}}\\limits"),Hn("\\bra","\\mathinner{\\langle{#1}|}"),Hn("\\ket","\\mathinner{|{#1}\\rangle}"),Hn("\\braket","\\mathinner{\\langle{#1}\\rangle}"),Hn("\\Bra","\\left\\langle#1\\right|"),Hn("\\Ket","\\left|#1\\right\\rangle"),Hn("\\angln","{\\angl n}"),Hn("\\blue","\\textcolor{##6495ed}{#1}"),Hn("\\orange","\\textcolor{##ffa500}{#1}"),Hn("\\pink","\\textcolor{##ff00af}{#1}"),Hn("\\red","\\textcolor{##df0030}{#1}"),Hn("\\green","\\textcolor{##28ae7b}{#1}"),Hn("\\gray","\\textcolor{gray}{#1}"),Hn("\\purple","\\textcolor{##9d38bd}{#1}"),Hn("\\blueA","\\textcolor{##ccfaff}{#1}"),Hn("\\blueB","\\textcolor{##80f6ff}{#1}"),Hn("\\blueC","\\textcolor{##63d9ea}{#1}"),Hn("\\blueD","\\textcolor{##11accd}{#1}"),Hn("\\blueE","\\textcolor{##0c7f99}{#1}"),Hn("\\tealA","\\textcolor{##94fff5}{#1}"),Hn("\\tealB","\\textcolor{##26edd5}{#1}"),Hn("\\tealC","\\textcolor{##01d1c1}{#1}"),Hn("\\tealD","\\textcolor{##01a995}{#1}"),Hn("\\tealE","\\textcolor{##208170}{#1}"),Hn("\\greenA","\\textcolor{##b6ffb0}{#1}"),Hn("\\greenB","\\textcolor{##8af281}{#1}"),Hn("\\greenC","\\textcolor{##74cf70}{#1}"),Hn("\\greenD","\\textcolor{##1fab54}{#1}"),Hn("\\greenE","\\textcolor{##0d923f}{#1}"),Hn("\\goldA","\\textcolor{##ffd0a9}{#1}"),Hn("\\goldB","\\textcolor{##ffbb71}{#1}"),Hn("\\goldC","\\textcolor{##ff9c39}{#1}"),Hn("\\goldD","\\textcolor{##e07d10}{#1}"),Hn("\\goldE","\\textcolor{##a75a05}{#1}"),Hn("\\redA","\\textcolor{##fca9a9}{#1}"),Hn("\\redB","\\textcolor{##ff8482}{#1}"),Hn("\\redC","\\textcolor{##f9685d}{#1}"),Hn("\\redD","\\textcolor{##e84d39}{#1}"),Hn("\\redE","\\textcolor{##bc2612}{#1}"),Hn("\\maroonA","\\textcolor{##ffbde0}{#1}"),Hn("\\maroonB","\\textcolor{##ff92c6}{#1}"),Hn("\\maroonC","\\textcolor{##ed5fa6}{#1}"),Hn("\\maroonD","\\textcolor{##ca337c}{#1}"),Hn("\\maroonE","\\textcolor{##9e034e}{#1}"),Hn("\\purpleA","\\textcolor{##ddd7ff}{#1}"),Hn("\\purpleB","\\textcolor{##c6b9fc}{#1}"),Hn("\\purpleC","\\textcolor{##aa87ff}{#1}"),Hn("\\purpleD","\\textcolor{##7854ab}{#1}"),Hn("\\purpleE","\\textcolor{##543b78}{#1}"),Hn("\\mintA","\\textcolor{##f5f9e8}{#1}"),Hn("\\mintB","\\textcolor{##edf2df}{#1}"),Hn("\\mintC","\\textcolor{##e0e5cc}{#1}"),Hn("\\grayA","\\textcolor{##f6f7f7}{#1}"),Hn("\\grayB","\\textcolor{##f0f1f2}{#1}"),Hn("\\grayC","\\textcolor{##e3e5e6}{#1}"),Hn("\\grayD","\\textcolor{##d6d8da}{#1}"),Hn("\\grayE","\\textcolor{##babec2}{#1}"),Hn("\\grayF","\\textcolor{##888d93}{#1}"),Hn("\\grayG","\\textcolor{##626569}{#1}"),Hn("\\grayH","\\textcolor{##3b3e40}{#1}"),Hn("\\grayI","\\textcolor{##21242c}{#1}"),Hn("\\kaBlue","\\textcolor{##314453}{#1}"),Hn("\\kaGreen","\\textcolor{##71B307}{#1}");var Gn={"\\relax":!0,"^":!0,_:!0,"\\limits":!0,"\\nolimits":!0},Un=function(){function e(e,t,r){this.settings=void 0,this.expansionCount=void 0,this.lexer=void 0,this.macros=void 0,this.stack=void 0,this.mode=void 0,this.settings=t,this.expansionCount=0,this.feed(e),this.macros=new On(En,t.macros),this.mode=r,this.stack=[]}var t=e.prototype;return t.feed=function(e){this.lexer=new In(e,this.settings)},t.switchMode=function(e){this.mode=e},t.beginGroup=function(){this.macros.beginGroup()},t.endGroup=function(){this.macros.endGroup()},t.future=function(){return 0===this.stack.length&&this.pushToken(this.lexer.lex()),this.stack[this.stack.length-1]},t.popToken=function(){return this.future(),this.stack.pop()},t.pushToken=function(e){this.stack.push(e)},t.pushTokens=function(e){var t;(t=this.stack).push.apply(t,e)},t.scanArgument=function(e){var t,r,n;if(e){if(this.consumeSpaces(),"["!==this.future().text)return null;t=this.popToken();var a=this.consumeArg(["]"]);n=a.tokens,r=a.end}else{var i=this.consumeArg();n=i.tokens,t=i.start,r=i.end}return this.pushToken(new qn("EOF",r.loc)),this.pushTokens(n),t.range(r,"")},t.consumeSpaces=function(){for(;;){if(" "!==this.future().text)break;this.stack.pop()}},t.consumeArg=function(e){var t=[],r=e&&e.length>0;r||this.consumeSpaces();var a,i=this.future(),o=0,s=0;do{if(a=this.popToken(),t.push(a),"{"===a.text)++o;else if("}"===a.text){if(-1===--o)throw new n("Extra }",a)}else if("EOF"===a.text)throw new n("Unexpected end of input in a macro argument, expected '"+(e&&r?e[s]:"}")+"'",a);if(e&&r)if((0===o||1===o&&"{"===e[s])&&a.text===e[s]){if(++s===e.length){t.splice(-s,s);break}}else s=0}while(0!==o||r);return"{"===i.text&&"}"===t[t.length-1].text&&(t.pop(),t.shift()),t.reverse(),{tokens:t,start:i,end:a}},t.consumeArgs=function(e,t){if(t){if(t.length!==e+1)throw new n("The length of delimiters doesn't match the number of args!");for(var r=t[0],a=0;athis.settings.maxExpand)throw new n("Too many expansions: infinite loop or need to increase maxExpand setting");var i=a.tokens,o=this.consumeArgs(a.numArgs,a.delimiters);if(a.numArgs)for(var s=(i=i.slice()).length-1;s>=0;--s){var l=i[s];if("#"===l.text){if(0===s)throw new n("Incomplete placeholder at end of macro body",l);if("#"===(l=i[--s]).text)i.splice(s+1,1);else{if(!/^[1-9]$/.test(l.text))throw new n("Not a valid argument number",l);var h;(h=i).splice.apply(h,[s,2].concat(o[+l.text-1]))}}}return this.pushTokens(i),i},t.expandAfterFuture=function(){return this.expandOnce(),this.future()},t.expandNextToken=function(){for(;;){var e=this.expandOnce();if(e instanceof qn){if("\\relax"!==e.text&&!e.treatAsRelax)return this.stack.pop();this.stack.pop()}}throw new Error},t.expandMacro=function(e){return this.macros.has(e)?this.expandTokens([new qn(e)]):void 0},t.expandTokens=function(e){var t=[],r=this.stack.length;for(this.pushTokens(e);this.stack.length>r;){var n=this.expandOnce(!0);n instanceof qn&&(n.treatAsRelax&&(n.noexpand=!1,n.treatAsRelax=!1),t.push(this.stack.pop()))}return t},t.expandMacroAsText=function(e){var t=this.expandMacro(e);return t?t.map((function(e){return e.text})).join(""):t},t._getExpansion=function(e){var t=this.macros.get(e);if(null==t)return t;var r="function"==typeof t?t(this):t;if("string"==typeof r){var n=0;if(-1!==r.indexOf("#"))for(var a=r.replace(/##/g,"");-1!==a.indexOf("#"+(n+1));)++n;for(var i=new In(r,this.settings),o=[],s=i.lex();"EOF"!==s.text;)o.push(s),s=i.lex();return o.reverse(),{tokens:o,numArgs:n}}return r},t.isDefined=function(e){return this.macros.has(e)||Tn.hasOwnProperty(e)||X.math.hasOwnProperty(e)||X.text.hasOwnProperty(e)||Gn.hasOwnProperty(e)},t.isExpandable=function(e){var t=this.macros.get(e);return null!=t?"string"==typeof t||"function"==typeof t||!t.unexpandable:Tn.hasOwnProperty(e)&&!Tn[e].primitive},e}(),Yn={"\u0301":{text:"\\'",math:"\\acute"},"\u0300":{text:"\\`",math:"\\grave"},"\u0308":{text:'\\"',math:"\\ddot"},"\u0303":{text:"\\~",math:"\\tilde"},"\u0304":{text:"\\=",math:"\\bar"},"\u0306":{text:"\\u",math:"\\breve"},"\u030c":{text:"\\v",math:"\\check"},"\u0302":{text:"\\^",math:"\\hat"},"\u0307":{text:"\\.",math:"\\dot"},"\u030a":{text:"\\r",math:"\\mathring"},"\u030b":{text:"\\H"}},Wn={"\xe1":"a\u0301","\xe0":"a\u0300","\xe4":"a\u0308","\u01df":"a\u0308\u0304","\xe3":"a\u0303","\u0101":"a\u0304","\u0103":"a\u0306","\u1eaf":"a\u0306\u0301","\u1eb1":"a\u0306\u0300","\u1eb5":"a\u0306\u0303","\u01ce":"a\u030c","\xe2":"a\u0302","\u1ea5":"a\u0302\u0301","\u1ea7":"a\u0302\u0300","\u1eab":"a\u0302\u0303","\u0227":"a\u0307","\u01e1":"a\u0307\u0304","\xe5":"a\u030a","\u01fb":"a\u030a\u0301","\u1e03":"b\u0307","\u0107":"c\u0301","\u010d":"c\u030c","\u0109":"c\u0302","\u010b":"c\u0307","\u010f":"d\u030c","\u1e0b":"d\u0307","\xe9":"e\u0301","\xe8":"e\u0300","\xeb":"e\u0308","\u1ebd":"e\u0303","\u0113":"e\u0304","\u1e17":"e\u0304\u0301","\u1e15":"e\u0304\u0300","\u0115":"e\u0306","\u011b":"e\u030c","\xea":"e\u0302","\u1ebf":"e\u0302\u0301","\u1ec1":"e\u0302\u0300","\u1ec5":"e\u0302\u0303","\u0117":"e\u0307","\u1e1f":"f\u0307","\u01f5":"g\u0301","\u1e21":"g\u0304","\u011f":"g\u0306","\u01e7":"g\u030c","\u011d":"g\u0302","\u0121":"g\u0307","\u1e27":"h\u0308","\u021f":"h\u030c","\u0125":"h\u0302","\u1e23":"h\u0307","\xed":"i\u0301","\xec":"i\u0300","\xef":"i\u0308","\u1e2f":"i\u0308\u0301","\u0129":"i\u0303","\u012b":"i\u0304","\u012d":"i\u0306","\u01d0":"i\u030c","\xee":"i\u0302","\u01f0":"j\u030c","\u0135":"j\u0302","\u1e31":"k\u0301","\u01e9":"k\u030c","\u013a":"l\u0301","\u013e":"l\u030c","\u1e3f":"m\u0301","\u1e41":"m\u0307","\u0144":"n\u0301","\u01f9":"n\u0300","\xf1":"n\u0303","\u0148":"n\u030c","\u1e45":"n\u0307","\xf3":"o\u0301","\xf2":"o\u0300","\xf6":"o\u0308","\u022b":"o\u0308\u0304","\xf5":"o\u0303","\u1e4d":"o\u0303\u0301","\u1e4f":"o\u0303\u0308","\u022d":"o\u0303\u0304","\u014d":"o\u0304","\u1e53":"o\u0304\u0301","\u1e51":"o\u0304\u0300","\u014f":"o\u0306","\u01d2":"o\u030c","\xf4":"o\u0302","\u1ed1":"o\u0302\u0301","\u1ed3":"o\u0302\u0300","\u1ed7":"o\u0302\u0303","\u022f":"o\u0307","\u0231":"o\u0307\u0304","\u0151":"o\u030b","\u1e55":"p\u0301","\u1e57":"p\u0307","\u0155":"r\u0301","\u0159":"r\u030c","\u1e59":"r\u0307","\u015b":"s\u0301","\u1e65":"s\u0301\u0307","\u0161":"s\u030c","\u1e67":"s\u030c\u0307","\u015d":"s\u0302","\u1e61":"s\u0307","\u1e97":"t\u0308","\u0165":"t\u030c","\u1e6b":"t\u0307","\xfa":"u\u0301","\xf9":"u\u0300","\xfc":"u\u0308","\u01d8":"u\u0308\u0301","\u01dc":"u\u0308\u0300","\u01d6":"u\u0308\u0304","\u01da":"u\u0308\u030c","\u0169":"u\u0303","\u1e79":"u\u0303\u0301","\u016b":"u\u0304","\u1e7b":"u\u0304\u0308","\u016d":"u\u0306","\u01d4":"u\u030c","\xfb":"u\u0302","\u016f":"u\u030a","\u0171":"u\u030b","\u1e7d":"v\u0303","\u1e83":"w\u0301","\u1e81":"w\u0300","\u1e85":"w\u0308","\u0175":"w\u0302","\u1e87":"w\u0307","\u1e98":"w\u030a","\u1e8d":"x\u0308","\u1e8b":"x\u0307","\xfd":"y\u0301","\u1ef3":"y\u0300","\xff":"y\u0308","\u1ef9":"y\u0303","\u0233":"y\u0304","\u0177":"y\u0302","\u1e8f":"y\u0307","\u1e99":"y\u030a","\u017a":"z\u0301","\u017e":"z\u030c","\u1e91":"z\u0302","\u017c":"z\u0307","\xc1":"A\u0301","\xc0":"A\u0300","\xc4":"A\u0308","\u01de":"A\u0308\u0304","\xc3":"A\u0303","\u0100":"A\u0304","\u0102":"A\u0306","\u1eae":"A\u0306\u0301","\u1eb0":"A\u0306\u0300","\u1eb4":"A\u0306\u0303","\u01cd":"A\u030c","\xc2":"A\u0302","\u1ea4":"A\u0302\u0301","\u1ea6":"A\u0302\u0300","\u1eaa":"A\u0302\u0303","\u0226":"A\u0307","\u01e0":"A\u0307\u0304","\xc5":"A\u030a","\u01fa":"A\u030a\u0301","\u1e02":"B\u0307","\u0106":"C\u0301","\u010c":"C\u030c","\u0108":"C\u0302","\u010a":"C\u0307","\u010e":"D\u030c","\u1e0a":"D\u0307","\xc9":"E\u0301","\xc8":"E\u0300","\xcb":"E\u0308","\u1ebc":"E\u0303","\u0112":"E\u0304","\u1e16":"E\u0304\u0301","\u1e14":"E\u0304\u0300","\u0114":"E\u0306","\u011a":"E\u030c","\xca":"E\u0302","\u1ebe":"E\u0302\u0301","\u1ec0":"E\u0302\u0300","\u1ec4":"E\u0302\u0303","\u0116":"E\u0307","\u1e1e":"F\u0307","\u01f4":"G\u0301","\u1e20":"G\u0304","\u011e":"G\u0306","\u01e6":"G\u030c","\u011c":"G\u0302","\u0120":"G\u0307","\u1e26":"H\u0308","\u021e":"H\u030c","\u0124":"H\u0302","\u1e22":"H\u0307","\xcd":"I\u0301","\xcc":"I\u0300","\xcf":"I\u0308","\u1e2e":"I\u0308\u0301","\u0128":"I\u0303","\u012a":"I\u0304","\u012c":"I\u0306","\u01cf":"I\u030c","\xce":"I\u0302","\u0130":"I\u0307","\u0134":"J\u0302","\u1e30":"K\u0301","\u01e8":"K\u030c","\u0139":"L\u0301","\u013d":"L\u030c","\u1e3e":"M\u0301","\u1e40":"M\u0307","\u0143":"N\u0301","\u01f8":"N\u0300","\xd1":"N\u0303","\u0147":"N\u030c","\u1e44":"N\u0307","\xd3":"O\u0301","\xd2":"O\u0300","\xd6":"O\u0308","\u022a":"O\u0308\u0304","\xd5":"O\u0303","\u1e4c":"O\u0303\u0301","\u1e4e":"O\u0303\u0308","\u022c":"O\u0303\u0304","\u014c":"O\u0304","\u1e52":"O\u0304\u0301","\u1e50":"O\u0304\u0300","\u014e":"O\u0306","\u01d1":"O\u030c","\xd4":"O\u0302","\u1ed0":"O\u0302\u0301","\u1ed2":"O\u0302\u0300","\u1ed6":"O\u0302\u0303","\u022e":"O\u0307","\u0230":"O\u0307\u0304","\u0150":"O\u030b","\u1e54":"P\u0301","\u1e56":"P\u0307","\u0154":"R\u0301","\u0158":"R\u030c","\u1e58":"R\u0307","\u015a":"S\u0301","\u1e64":"S\u0301\u0307","\u0160":"S\u030c","\u1e66":"S\u030c\u0307","\u015c":"S\u0302","\u1e60":"S\u0307","\u0164":"T\u030c","\u1e6a":"T\u0307","\xda":"U\u0301","\xd9":"U\u0300","\xdc":"U\u0308","\u01d7":"U\u0308\u0301","\u01db":"U\u0308\u0300","\u01d5":"U\u0308\u0304","\u01d9":"U\u0308\u030c","\u0168":"U\u0303","\u1e78":"U\u0303\u0301","\u016a":"U\u0304","\u1e7a":"U\u0304\u0308","\u016c":"U\u0306","\u01d3":"U\u030c","\xdb":"U\u0302","\u016e":"U\u030a","\u0170":"U\u030b","\u1e7c":"V\u0303","\u1e82":"W\u0301","\u1e80":"W\u0300","\u1e84":"W\u0308","\u0174":"W\u0302","\u1e86":"W\u0307","\u1e8c":"X\u0308","\u1e8a":"X\u0307","\xdd":"Y\u0301","\u1ef2":"Y\u0300","\u0178":"Y\u0308","\u1ef8":"Y\u0303","\u0232":"Y\u0304","\u0176":"Y\u0302","\u1e8e":"Y\u0307","\u0179":"Z\u0301","\u017d":"Z\u030c","\u1e90":"Z\u0302","\u017b":"Z\u0307","\u03ac":"\u03b1\u0301","\u1f70":"\u03b1\u0300","\u1fb1":"\u03b1\u0304","\u1fb0":"\u03b1\u0306","\u03ad":"\u03b5\u0301","\u1f72":"\u03b5\u0300","\u03ae":"\u03b7\u0301","\u1f74":"\u03b7\u0300","\u03af":"\u03b9\u0301","\u1f76":"\u03b9\u0300","\u03ca":"\u03b9\u0308","\u0390":"\u03b9\u0308\u0301","\u1fd2":"\u03b9\u0308\u0300","\u1fd1":"\u03b9\u0304","\u1fd0":"\u03b9\u0306","\u03cc":"\u03bf\u0301","\u1f78":"\u03bf\u0300","\u03cd":"\u03c5\u0301","\u1f7a":"\u03c5\u0300","\u03cb":"\u03c5\u0308","\u03b0":"\u03c5\u0308\u0301","\u1fe2":"\u03c5\u0308\u0300","\u1fe1":"\u03c5\u0304","\u1fe0":"\u03c5\u0306","\u03ce":"\u03c9\u0301","\u1f7c":"\u03c9\u0300","\u038e":"\u03a5\u0301","\u1fea":"\u03a5\u0300","\u03ab":"\u03a5\u0308","\u1fe9":"\u03a5\u0304","\u1fe8":"\u03a5\u0306","\u038f":"\u03a9\u0301","\u1ffa":"\u03a9\u0300"},Xn=function(){function e(e,t){this.mode=void 0,this.gullet=void 0,this.settings=void 0,this.leftrightDepth=void 0,this.nextToken=void 0,this.mode="math",this.gullet=new Un(e,t,this.mode),this.settings=t,this.leftrightDepth=0}var t=e.prototype;return t.expect=function(e,t){if(void 0===t&&(t=!0),this.fetch().text!==e)throw new n("Expected '"+e+"', got '"+this.fetch().text+"'",this.fetch());t&&this.consume()},t.consume=function(){this.nextToken=null},t.fetch=function(){return null==this.nextToken&&(this.nextToken=this.gullet.expandNextToken()),this.nextToken},t.switchMode=function(e){this.mode=e,this.gullet.switchMode(e)},t.parse=function(){this.settings.globalGroup||this.gullet.beginGroup(),this.settings.colorIsTextColor&&this.gullet.macros.set("\\color","\\textcolor");var e=this.parseExpression(!1);return this.expect("EOF"),this.settings.globalGroup||this.gullet.endGroup(),e},t.parseExpression=function(t,r){for(var n=[];;){"math"===this.mode&&this.consumeSpaces();var a=this.fetch();if(-1!==e.endOfExpression.indexOf(a.text))break;if(r&&a.text===r)break;if(t&&Tn[a.text]&&Tn[a.text].infix)break;var i=this.parseAtom(r);if(!i)break;"internal"!==i.type&&n.push(i)}return"text"===this.mode&&this.formLigatures(n),this.handleInfixNodes(n)},t.handleInfixNodes=function(e){for(var t,r=-1,a=0;a=0&&this.settings.reportNonstrict("unicodeTextInMathMode",'Latin-1/Unicode text character "'+t[0]+'" used in math mode',e);var s,l=X[this.mode][t].group,h=Bn.range(e);if(U.hasOwnProperty(l)){var m=l;s={type:"atom",mode:this.mode,family:m,loc:h,text:t}}else s={type:l,mode:this.mode,loc:h,text:t};i=s}else{if(!(t.charCodeAt(0)>=128))return null;this.settings.strict&&(w(t.charCodeAt(0))?"math"===this.mode&&this.settings.reportNonstrict("unicodeTextInMathMode",'Unicode text character "'+t[0]+'" used in math mode',e):this.settings.reportNonstrict("unknownSymbol",'Unrecognized Unicode character "'+t[0]+'" ('+t.charCodeAt(0)+")",e)),i={type:"textord",mode:"text",loc:Bn.range(e),text:t}}if(this.consume(),o)for(var c=0;c 0
- || $(event.target).filter(".fold-unfold").length > 0;
- if (trigger) {
- $(">*:not(h2)", this).toggle(400);
- $(">h2>span.fold-unfold", this).toggleClass("glyphicon-collapse-down glyphicon-collapse-up");
- event.stopPropagation();
- }
-});
-$(".solution").each(function() {
- $(">*:not(h2)", this).toggle();
- var h2 = $("h2:first", this);
- h2.append("");
-});
-
-
-// Handle searches.
-// Relies on document having 'meta' element with name 'search-domain'.
-function google_search() {
- var query = document.getElementById("google-search").value;
- var domain = $("meta[name=search-domain]").attr("value");
- window.open("https://www.google.com/search?q=" + query + "+site:" + domain);
-}
-
-// function to shrink the life cycle bar when scrolling
-$(function(){
- $('#life-cycle').data('size','big');
-});
-
-$(window).scroll(function(){
- if($(document).scrollTop() > 0)
- {
- if($('#life-cycle').data('size') == 'big')
- {
- $('#life-cycle').data('size','small');
- $('#life-cycle').stop().animate({
- padding: '5px'
- },100);
- }
- }
- else
- {
- if($('#life-cycle').data('size') == 'small')
- {
- $('#life-cycle').data('size','big');
- $('#life-cycle').stop().animate({
- padding: '15px'
- },100);
- }
- }
-});
diff --git a/bin/boilerplate/AUTHORS b/bin/boilerplate/AUTHORS
deleted file mode 100644
index 04e1f5a..0000000
--- a/bin/boilerplate/AUTHORS
+++ /dev/null
@@ -1 +0,0 @@
-FIXME: list authors' names and email addresses.
\ No newline at end of file
diff --git a/bin/boilerplate/CITATION b/bin/boilerplate/CITATION
deleted file mode 100644
index 56ece3c..0000000
--- a/bin/boilerplate/CITATION
+++ /dev/null
@@ -1 +0,0 @@
-FIXME: describe how to cite this lesson.
\ No newline at end of file
diff --git a/bin/boilerplate/CONTRIBUTING.md b/bin/boilerplate/CONTRIBUTING.md
deleted file mode 100644
index 8c095d8..0000000
--- a/bin/boilerplate/CONTRIBUTING.md
+++ /dev/null
@@ -1,151 +0,0 @@
-# Contributing
-
-[The Carpentries][c-site] ([Software Carpentry][swc-site], [Data Carpentry][dc-site], and [Library Carpentry][lc-site]) are open source projects,
-and we welcome contributions of all kinds:
-new lessons,
-fixes to existing material,
-bug reports,
-and reviews of proposed changes are all welcome.
-
-## Contributor Agreement
-
-By contributing,
-you agree that we may redistribute your work under [our license](LICENSE.md).
-In exchange,
-we will address your issues and/or assess your change proposal as promptly as we can,
-and help you become a member of our community.
-Everyone involved in [The Carpentries][c-site]
-agrees to abide by our [code of conduct](CODE_OF_CONDUCT.md).
-
-## How to Contribute
-
-The easiest way to get started is to file an issue
-to tell us about a spelling mistake,
-some awkward wording,
-or a factual error.
-This is a good way to introduce yourself
-and to meet some of our community members.
-
-1. If you do not have a [GitHub][github] account,
- you can [send us comments by email][email].
- However,
- we will be able to respond more quickly if you use one of the other methods described below.
-
-2. If you have a [GitHub][github] account,
- or are willing to [create one][github-join],
- but do not know how to use Git,
- you can report problems or suggest improvements by [creating an issue][issues].
- This allows us to assign the item to someone
- and to respond to it in a threaded discussion.
-
-3. If you are comfortable with Git,
- and would like to add or change material,
- you can submit a pull request (PR).
- Instructions for doing this are [included below](#using-github).
-
-## Where to Contribute
-
-1. If you wish to change this lesson,
- please work in ,
- which can be viewed at .
-
-2. If you wish to change the example lesson,
- please work in ,
- which documents the format of our lessons
- and can be viewed at .
-
-3. If you wish to change the template used for workshop websites,
- please work in .
- The home page of that repository explains how to set up workshop websites,
- while the extra pages in
- provide more background on our design choices.
-
-4. If you wish to change CSS style files, tools,
- or HTML boilerplate for lessons or workshops stored in `_includes` or `_layouts`,
- please work in .
-
-## What to Contribute
-
-There are many ways to contribute,
-from writing new exercises and improving existing ones
-to updating or filling in the documentation
-and submitting [bug reports][issues]
-about things that do not work, are not clear, or are missing.
-If you are looking for ideas, please see the 'Issues' tab for
-a list of issues associated with this repository,
-or you may also look at the issues for [Data Carpentry][dc-issues],
-[Software Carpentry][swc-issues], and [Library Carpentry][lc-issues] projects.
-
-Comments on issues and reviews of pull requests are just as welcome:
-we are smarter together than we are on our own.
-Reviews from novices and newcomers are particularly valuable:
-it is easy for people who have been using these lessons for a while
-to forget how impenetrable some of this material can be,
-so fresh eyes are always welcome.
-
-## What *Not* to Contribute
-
-Our lessons already contain more material than we can cover in a typical workshop,
-so we are usually *not* looking for more concepts or tools to add to them.
-As a rule,
-if you want to introduce a new idea,
-you must (a) estimate how long it will take to teach
-and (b) explain what you would take out to make room for it.
-The first encourages contributors to be honest about requirements;
-the second, to think hard about priorities.
-
-We are also not looking for exercises or other material that will only run on one platform.
-Our workshops typically contain a mixture of Windows, macOS, and Linux users;
-in order to be usable,
-our lessons must run equally well on all three.
-
-## Using GitHub
-
-If you choose to contribute via GitHub, you may want to look at
-[How to Contribute to an Open Source Project on GitHub][how-contribute].
-To manage changes, we follow [GitHub flow][github-flow].
-Each lesson has at least two maintainers who review issues and pull requests or encourage others to do so.
-The maintainers are community volunteers and have final say over what gets merged into the lesson.
-To use the web interface for contributing to a lesson:
-
-1. Fork the originating repository to your GitHub profile.
-2. Within your version of the forked repository, move to the `gh-pages` branch and
-create a new branch for each significant change being made.
-3. Navigate to the file(s) you wish to change within the new branches and make revisions as required.
-4. Commit all changed files within the appropriate branches.
-5. Create individual pull requests from each of your changed branches
-to the `gh-pages` branch within the originating repository.
-6. If you receive feedback, make changes using your issue-specific branches of the forked
-repository and the pull requests will update automatically.
-7. Repeat as needed until all feedback has been addressed.
-
-When starting work, please make sure your clone of the originating `gh-pages` branch is up-to-date
-before creating your own revision-specific branch(es) from there.
-Additionally, please only work from your newly-created branch(es) and *not*
-your clone of the originating `gh-pages` branch.
-Lastly, published copies of all the lessons are available in the `gh-pages` branch of the originating
-repository for reference while revising.
-
-## Other Resources
-
-General discussion of [Software Carpentry][swc-site], [Data Carpentry][dc-site], and [Library Carpentry][lc-site]
-happens on the [discussion mailing list][discuss-list],
-which everyone is welcome to join.
-You can also [reach us by email][email].
-
-[email]: mailto:team@carpentries.org
-[dc-issues]: https://github.com/issues?q=user%3Adatacarpentry
-[dc-lessons]: http://datacarpentry.org/lessons/
-[dc-site]: http://datacarpentry.org/
-[discuss-list]: https://carpentries.topicbox.com/groups/discuss
-[github]: https://github.com
-[github-flow]: https://guides.github.com/introduction/flow/
-[github-join]: https://github.com/join
-[how-contribute]: https://app.egghead.io/playlists/how-to-contribute-to-an-open-source-project-on-github
-[issues]: https://guides.github.com/features/issues/
-[swc-issues]: https://github.com/issues?q=user%3Aswcarpentry
-[swc-lessons]: https://software-carpentry.org/lessons/
-[swc-site]: https://software-carpentry.org/
-[c-site]: https://carpentries.org/
-[lc-site]: https://librarycarpentry.org/
-[lc-issues]: https://github.com/issues?q=user%3Alibrarycarpentry
diff --git a/bin/boilerplate/README.md b/bin/boilerplate/README.md
deleted file mode 100644
index 060994a..0000000
--- a/bin/boilerplate/README.md
+++ /dev/null
@@ -1,40 +0,0 @@
-# FIXME Lesson title
-
-[](https://swc-slack-invite.herokuapp.com/)
-
-This repository generates the corresponding lesson website from [The Carpentries](https://carpentries.org/) repertoire of lessons.
-
-## Contributing
-
-We welcome all contributions to improve the lesson! Maintainers will do their best to help you if you have any
-questions, concerns, or experience any difficulties along the way.
-
-We'd like to ask you to familiarize yourself with our [Contribution Guide](CONTRIBUTING.md) and have a look at
-the [more detailed guidelines][lesson-example] on proper formatting, ways to render the lesson locally, and even
-how to write new episodes.
-
-Please see the current list of [issues][FIXME] for ideas for contributing to this
-repository. For making your contribution, we use the GitHub flow, which is
-nicely explained in the chapter [Contributing to a Project](http://git-scm.com/book/en/v2/GitHub-Contributing-to-a-Project) in Pro Git
-by Scott Chacon.
-Look for the tag . This indicates that the maintainers will welcome a pull request fixing this issue.
-
-
-## Maintainer(s)
-
-Current maintainers of this lesson are
-
-* FIXME
-* FIXME
-* FIXME
-
-
-## Authors
-
-A list of contributors to the lesson can be found in [AUTHORS](AUTHORS)
-
-## Citation
-
-To cite this lesson, please consult with [CITATION](CITATION)
-
-[lesson-example]: https://carpentries.github.io/lesson-example
diff --git a/bin/boilerplate/_config.yml b/bin/boilerplate/_config.yml
deleted file mode 100644
index 3fab917..0000000
--- a/bin/boilerplate/_config.yml
+++ /dev/null
@@ -1,104 +0,0 @@
-#------------------------------------------------------------
-# Values for this lesson.
-#------------------------------------------------------------
-
-# Which carpentry is this ("swc", "dc", "lc", or "cp")?
-# swc: Software Carpentry
-# dc: Data Carpentry
-# lc: Library Carpentry
-# cp: Carpentries (to use for instructor traning for instance)
-# incubator: Carpentries Incubator
-carpentry: "swc"
-
-# Overall title for pages.
-title: "Lesson Title"
-
-# Life cycle stage of the lesson
-# See this page for more details: https://cdh.carpentries.org/the-lesson-life-cycle.html
-# Possible values: "pre-alpha", "alpha", "beta", "stable"
-life_cycle: "pre-alpha"
-
-#------------------------------------------------------------
-# Generic settings (should not need to change).
-#------------------------------------------------------------
-
-# What kind of thing is this ("workshop" or "lesson")?
-kind: "lesson"
-
-# Magic to make URLs resolve both locally and on GitHub.
-# See https://help.github.com/articles/repository-metadata-on-github-pages/.
-# Please don't change it: / is correct.
-repository: /
-
-# Email address, no mailto:
-email: "team@carpentries.org"
-
-# Sites.
-coc: "https://docs.carpentries.org/topic_folders/policies/code-of-conduct.html"
-amy_site: "https://amy.carpentries.org/"
-carpentries_github: "https://github.com/carpentries"
-carpentries_pages: "https://carpentries.github.io"
-carpentries_site: "https://carpentries.org/"
-dc_site: "https://datacarpentry.org"
-example_repo: "https://github.com/carpentries/lesson-example"
-example_site: "https://carpentries.github.io/lesson-example"
-lc_site: "https://librarycarpentry.org/"
-swc_github: "https://github.com/swcarpentry"
-swc_pages: "https://swcarpentry.github.io"
-swc_site: "https://software-carpentry.org"
-template_repo: "https://github.com/carpentries/styles"
-training_site: "https://carpentries.github.io/instructor-training"
-workshop_repo: "https://github.com/carpentries/workshop-template"
-workshop_site: "https://carpentries.github.io/workshop-template"
-cc_by_human: "https://creativecommons.org/licenses/by/4.0/"
-
-# Surveys.
-pre_survey: "https://carpentries.typeform.com/to/wi32rS#slug="
-post_survey: "https://carpentries.typeform.com/to/UgVdRQ#slug="
-instructor_pre_survey: "https://carpentries.typeform.com/to/QVOarK#slug="
-instructor_post_survey: "https://carpentries.typeform.com/to/cjJ9UP#slug="
-
-# Set to 'true' for instructor training websites only.
-instructor_training: false
-
-# Start time in minutes (0 to be clock-independent, 540 to show a start at 09:00 am).
-start_time: 0
-
-# Specify that things in the episodes collection should be output.
-collections:
- episodes:
- output: true
- permalink: /:path/index.html
- extras:
- output: true
- permalink: /:path/index.html
-
-# Set the default layout for things in the episodes collection.
-defaults:
- - values:
- root: .
- layout: page
- - scope:
- path: ""
- type: episodes
- values:
- root: ..
- layout: episode
- - scope:
- path: ""
- type: extras
- values:
- root: ..
- layout: page
-
-# Files and directories that are not to be copied.
-exclude:
- - Makefile
- - bin/
- - .Rproj.user/
- - .vendor/
- - vendor/
- - .docker-vendor/
-
-# Turn on built-in syntax highlighting.
-highlighter: rouge
diff --git a/bin/boilerplate/_episodes/01-introduction.md b/bin/boilerplate/_episodes/01-introduction.md
deleted file mode 100644
index 2e156c2..0000000
--- a/bin/boilerplate/_episodes/01-introduction.md
+++ /dev/null
@@ -1,15 +0,0 @@
----
-title: "Introduction"
-teaching: 0
-exercises: 0
-questions:
-- "Key question (FIXME)"
-objectives:
-- "First learning objective. (FIXME)"
-keypoints:
-- "First key point. Brief Answer to questions. (FIXME)"
----
-FIXME
-
-{% include links.md %}
-
diff --git a/bin/boilerplate/_extras/about.md b/bin/boilerplate/_extras/about.md
deleted file mode 100644
index 5f07f65..0000000
--- a/bin/boilerplate/_extras/about.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: About
----
-{% include carpentries.html %}
-{% include links.md %}
diff --git a/bin/boilerplate/_extras/discuss.md b/bin/boilerplate/_extras/discuss.md
deleted file mode 100644
index bfc33c5..0000000
--- a/bin/boilerplate/_extras/discuss.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Discussion
----
-FIXME
-
-{% include links.md %}
diff --git a/bin/boilerplate/_extras/figures.md b/bin/boilerplate/_extras/figures.md
deleted file mode 100644
index 0012c88..0000000
--- a/bin/boilerplate/_extras/figures.md
+++ /dev/null
@@ -1,79 +0,0 @@
----
-title: Figures
----
-
-{% include base_path.html %}
-{% include manual_episode_order.html %}
-
-
-
-{% comment %} Create anchor for each one of the episodes. {% endcomment %}
-
-{% for lesson_episode in lesson_episodes %}
- {% if site.episode_order %}
- {% assign episode = site.episodes | where: "slug", lesson_episode | first %}
- {% else %}
- {% assign episode = lesson_episode %}
- {% endif %}
-
-{% endfor %}
-
-{% include links.md %}
diff --git a/bin/boilerplate/_extras/guide.md b/bin/boilerplate/_extras/guide.md
deleted file mode 100644
index 50f266f..0000000
--- a/bin/boilerplate/_extras/guide.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: "Instructor Notes"
----
-FIXME
-
-{% include links.md %}
diff --git a/bin/boilerplate/index.md b/bin/boilerplate/index.md
deleted file mode 100644
index 95ccdbd..0000000
--- a/bin/boilerplate/index.md
+++ /dev/null
@@ -1,17 +0,0 @@
----
-layout: lesson
-root: . # Is the only page that doesn't follow the pattern /:path/index.html
-permalink: index.html # Is the only page that doesn't follow the pattern /:path/index.html
----
-FIXME: home page introduction
-
-
-
-{% comment %} This is a comment in Liquid {% endcomment %}
-
-> ## Prerequisites
->
-> FIXME
-{: .prereq}
-
-{% include links.md %}
diff --git a/bin/boilerplate/reference.md b/bin/boilerplate/reference.md
deleted file mode 100644
index 8c82616..0000000
--- a/bin/boilerplate/reference.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-layout: reference
----
-
-## Glossary
-
-FIXME
-
-{% include links.md %}
diff --git a/bin/boilerplate/setup.md b/bin/boilerplate/setup.md
deleted file mode 100644
index b8c5032..0000000
--- a/bin/boilerplate/setup.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-title: Setup
----
-FIXME
-
-
-{% include links.md %}
diff --git a/bin/chunk-options.R b/bin/chunk-options.R
deleted file mode 100644
index 8e0d62a..0000000
--- a/bin/chunk-options.R
+++ /dev/null
@@ -1,70 +0,0 @@
-# These settings control the behavior of all chunks in the novice R materials.
-# For example, to generate the lessons with all the output hidden, simply change
-# `results` from "markup" to "hide".
-# For more information on available chunk options, see
-# http://yihui.name/knitr/options#chunk_options
-
-library("knitr")
-
-fix_fig_path <- function(pth) file.path("..", pth)
-
-
-## We set the path for the figures globally below, so if we want to
-## customize it for individual episodes, we can append a prefix to the
-## global path. For instance, if we call knitr_fig_path("01-") in the
-## first episode of the lesson, it will generate the figures in
-## `fig/rmd-01-`
-knitr_fig_path <- function(prefix) {
- new_path <- paste0(opts_chunk$get("fig.path"),
- prefix)
- opts_chunk$set(fig.path = new_path)
-}
-
-## We use the rmd- prefix for the figures generated by the lessons so
-## they can be easily identified and deleted by `make clean-rmd`. The
-## working directory when the lessons are generated is the root so the
-## figures need to be saved in fig/, but when the site is generated,
-## the episodes will be one level down. We fix the path using the
-## `fig.process` option.
-
-opts_chunk$set(tidy = FALSE, results = "markup", comment = NA,
- fig.align = "center", fig.path = "fig/rmd-",
- fig.process = fix_fig_path,
- fig.width = 8.5, fig.height = 8.5,
- fig.retina = 2)
-
-# The hooks below add html tags to the code chunks and their output so that they
-# are properly formatted when the site is built.
-
-hook_in <- function(x, options) {
- lg <- tolower(options$engine)
- style <- paste0(".language-", lg)
-
- stringr::str_c("\n\n~~~\n",
- paste0(x, collapse="\n"),
- "\n~~~\n{: ", style, "}\n\n")
-}
-
-hook_out <- function(x, options) {
- x <- gsub("\n$", "", x)
- stringr::str_c("\n\n~~~\n",
- paste0(x, collapse="\n"),
- "\n~~~\n{: .output}\n\n")
-}
-
-hook_error <- function(x, options) {
- x <- gsub("\n$", "", x)
- stringr::str_c("\n\n~~~\n",
- paste0(x, collapse="\n"),
- "\n~~~\n{: .error}\n\n")
-}
-
-hook_warning <- function(x, options) {
- x <- gsub("\n$", "", x)
- stringr::str_c("\n\n~~~\n",
- paste0(x, collapse = "\n"),
- "\n~~~\n{: .warning}\n\n")
-}
-
-knit_hooks$set(source = hook_in, output = hook_out, warning = hook_warning,
- error = hook_error, message = hook_out)
diff --git a/bin/dependencies.R b/bin/dependencies.R
deleted file mode 100644
index 4eeeb21..0000000
--- a/bin/dependencies.R
+++ /dev/null
@@ -1,107 +0,0 @@
-install_required_packages <- function(lib = NULL, repos = getOption("repos", default = c(CRAN = "https://cran.rstudio.com/"))) {
-
- if (is.null(lib)) {
- lib <- .libPaths()[[1]]
- }
-
- message("lib paths: ", paste(lib, collapse = ", "))
- # Note: RMarkdown is needed for renv to detect packages in Rmd documents.
- required_pkgs <- c("rprojroot", "desc", "remotes", "renv", "BiocManager", "rmarkdown")
- installed_pkgs <- rownames(installed.packages(lib.loc = lib))
- missing_pkgs <- setdiff(required_pkgs, installed_pkgs)
-
- # The default installation of R will have "@CRAN@" as the default repository,
- # which directs contrib.url() to either force the user to choose a mirror if
- # interactive or fail if not. Since we are not interactve, we need to force
- # the mirror here.
- if ("@CRAN@" %in% repos) {
- repos <- c(CRAN = "https://cran.rstudio.com/")
- }
-
- if (length(missing_pkgs) != 0) {
- install.packages(missing_pkgs, lib = lib, repos = repos)
- }
-}
-
-find_root <- function() {
-
- cfg <- rprojroot::has_file_pattern("^_config.y*ml$")
- root <- rprojroot::find_root(cfg)
-
- root
-}
-
-# set the BiocManager repositories and return a function that resets the default
-# repositories.
-#
-# @example
-# bioc_repos_example <- function() {
-# message("User repos")
-# as.data.frame(getOption("repos"))
-# reset_repos <- use_bioc_repos()
-# on.exit(reset_repos())
-# message("Bioc repos")
-# as.data.frame(getOption("repos"))
-# }
-# bioc_repos_example()
-# as.data.frame(getOption("repos")
-use_bioc_repos <- function() {
- repos <- getOption("repos")
- suppressMessages(options(repos = BiocManager::repositories()))
- function() {
- options(repos = repos)
- }
-}
-
-identify_dependencies <- function() {
-
- root <- find_root()
-
- reset_repos <- use_bioc_repos()
- on.exit(reset_repos(), add = TRUE)
- eps <- file.path(root, "_episodes_rmd")
- bin <- file.path(root, "bin")
-
- required_pkgs <- unique(c(
- ## Packages for episodes
- renv::dependencies(eps, progress = FALSE, error = "ignored")$Package,
- ## Packages for tools
- renv::dependencies(bin, progress = FALSE, error = "ignored")$Package
- ))
-
- required_pkgs
-}
-
-create_description <- function(required_pkgs) {
- d <- desc::description$new("!new")
- d$set_deps(data.frame(type = "Imports", package = required_pkgs, version = "*"))
- d$write("DESCRIPTION")
- # We have to write the description twice to get the hidden dependencies
- # because renv only considers explicit dependencies.
- #
- # This is needed because some of the hidden dependencis will require system
- # libraries to be configured.
- suppressMessages(repo <- BiocManager::repositories())
- deps <- remotes::dev_package_deps(dependencies = TRUE, repos = repo)
- deps <- deps$package[deps$diff < 0]
- if (length(deps)) {
- # only create new DESCRIPTION file if there are dependencies to install
- d$set_deps(data.frame(type = "Imports", package = deps, version = "*"))
- d$write("DESCRIPTION")
- }
-}
-
-install_dependencies <- function(required_pkgs, ...) {
-
- reset_repos <- use_bioc_repos()
- on.exit(reset_repos(), add = TRUE)
-
- create_description(required_pkgs)
- on.exit(file.remove("DESCRIPTION"), add = TRUE)
- remotes::install_deps(dependencies = TRUE, ...)
-
- if (require("knitr") && packageVersion("knitr") < '1.9.19') {
- stop("knitr must be version 1.9.20 or higher")
- }
-
-}
diff --git a/bin/generate_md_episodes.R b/bin/generate_md_episodes.R
deleted file mode 100644
index 7fb4c5a..0000000
--- a/bin/generate_md_episodes.R
+++ /dev/null
@@ -1,44 +0,0 @@
-generate_md_episodes <- function() {
-
- # avoid ansi color characters from being printed in the output
- op <- options()
- on.exit(options(op), add = TRUE)
- options(crayon.enabled = FALSE)
- ## get the Rmd file to process from the command line, and generate the path
- ## for their respective outputs
- args <- commandArgs(trailingOnly = TRUE)
- if (!identical(length(args), 2L)) {
- stop("input and output file must be passed to the script")
- }
-
- src_rmd <- args[1]
- dest_md <- args[2]
-
- ## knit the Rmd into markdown
- knitr::knit(src_rmd, output = dest_md)
-
- # Read the generated md files and add comments advising not to edit them
- add_no_edit_comment <- function(y) {
- con <- file(y)
- mdfile <- readLines(con)
- if (mdfile[1] != "---")
- stop("Input file does not have a valid header")
- mdfile <- append(
- mdfile,
- "# Please do not edit this file directly; it is auto generated.",
- after = 1
- )
- mdfile <- append(
- mdfile,
- paste("# Instead, please edit", basename(y), "in _episodes_rmd/"),
- after = 2
- )
- writeLines(mdfile, con)
- close(con)
- return(paste("Warning added to YAML header of", y))
- }
-
- vapply(dest_md, add_no_edit_comment, character(1))
-}
-
-generate_md_episodes()
diff --git a/bin/install_r_deps.sh b/bin/install_r_deps.sh
deleted file mode 100755
index 0280f24..0000000
--- a/bin/install_r_deps.sh
+++ /dev/null
@@ -1 +0,0 @@
-Rscript -e "source(file.path('bin', 'dependencies.R')); install_required_packages(); install_dependencies(identify_dependencies())"
diff --git a/bin/knit_lessons.sh b/bin/knit_lessons.sh
deleted file mode 100755
index 141c136..0000000
--- a/bin/knit_lessons.sh
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/usr/bin/env bash
-
-# Only try running R to translate files if there are some files present.
-# The Makefile passes in the names of files.
-
-if [ $# -eq 2 ] ; then
- Rscript -e "source('bin/generate_md_episodes.R')" "$@"
-fi
diff --git a/bin/lesson_check.py b/bin/lesson_check.py
deleted file mode 100644
index 86e4249..0000000
--- a/bin/lesson_check.py
+++ /dev/null
@@ -1,628 +0,0 @@
-"""
-Check lesson files and their contents.
-"""
-
-
-import os
-import glob
-import re
-import sys
-from argparse import ArgumentParser
-
-# This uses the `__all__` list in `util.py` to determine what objects to import
-# see https://docs.python.org/3/tutorial/modules.html#importing-from-a-package
-from util import *
-from reporter import Reporter
-
-__version__ = '0.3'
-
-# Where to look for source Markdown files.
-SOURCE_DIRS = ['', '_episodes', '_extras']
-
-# Where to look for source Rmd files.
-SOURCE_RMD_DIRS = ['_episodes_rmd']
-
-# Required files: each entry is ('path': YAML_required).
-# FIXME: We do not yet validate whether any files have the required
-# YAML headers, but should in the future.
-# The '%' is replaced with the source directory path for checking.
-# Episodes are handled specially, and extra files in '_extras' are also handled
-# specially. This list must include all the Markdown files listed in the
-# 'bin/initialize' script.
-REQUIRED_FILES = {
- 'CODE_OF_CONDUCT.md': True,
- 'CONTRIBUTING.md': False,
- 'LICENSE.md': True,
- 'README.md': False,
- os.path.join('_extras', 'discuss.md'): True,
- os.path.join('_extras', 'guide.md'): True,
- 'index.md': True,
- 'reference.md': True,
- 'setup.md': True,
-}
-
-# Episode filename pattern.
-P_EPISODE_FILENAME = re.compile(r'(\d\d)-[-\w]+.md$')
-
-# Pattern to match lines ending with whitespace.
-P_TRAILING_WHITESPACE = re.compile(r'\s+$')
-
-# Pattern to match figure references in HTML.
-P_FIGURE_REFS = re.compile(r'
]+src="([^"]+)"[^>]*>')
-
-# Pattern to match internally-defined Markdown links.
-P_INTERNAL_LINK_REF = re.compile(r'\[([^\]]+)\]\[([^\]]+)\]')
-
-# Pattern to match reference links (to resolve internally-defined references).
-P_INTERNAL_LINK_DEF = re.compile(r'^\[([^\]]+)\]:\s*(.+)')
-
-# Pattern to match {% include ... %} statements
-P_INTERNAL_INCLUDE_LINK = re.compile(r'^{% include ([^ ]*) %}$')
-
-# Pattern to match image-only and link-only lines
-P_LINK_IMAGE_LINE = re.compile(r'''
- [> #]* # any number of '>', '#', and spaces
- \W{,3} # up to 3 non-word characters
- !? # ! or nothing
- \[[^]]+\] # [any text]
- [([] # ( or [
- [^])]+ # 1+ characters that are neither ] nor )
- [])] # ] or )
- (?:{:[^}]+})? # {:any text} or nothing
- \W{,3} # up to 3 non-word characters
- [ ]* # any number of spaces
- \\?$ # \ or nothing + end of line''', re.VERBOSE)
-
-# What kinds of blockquotes are allowed?
-KNOWN_BLOCKQUOTES = {
- 'callout',
- 'caution',
- 'challenge',
- 'checklist',
- 'discussion',
- 'keypoints',
- 'objectives',
- 'prereq',
- 'quotation',
- 'solution',
- 'testimonial',
- 'warning'
-}
-
-# What kinds of code fragments are allowed?
-# Below we allow all 'language-*' code blocks
-KNOWN_CODEBLOCKS = {
- 'error',
- 'output',
- 'source',
- 'warning'
-}
-
-# What fields are required in teaching episode metadata?
-TEACHING_METADATA_FIELDS = {
- ('title', str),
- ('teaching', int),
- ('exercises', int),
- ('questions', list),
- ('objectives', list),
- ('keypoints', list)
-}
-
-# What fields are required in break episode metadata?
-BREAK_METADATA_FIELDS = {
- ('layout', str),
- ('title', str),
- ('break', int)
-}
-
-# How long are lines allowed to be?
-# Please keep this in sync with .editorconfig!
-MAX_LINE_LEN = 100
-
-# Contents of _config.yml
-CONFIG = {}
-
-def main():
- """Main driver."""
-
- args = parse_args()
- args.reporter = Reporter()
-
- global CONFIG
- config_file = os.path.join(args.source_dir, '_config.yml')
- CONFIG = load_yaml(config_file)
- CONFIG["config_file"] = config_file
-
- life_cycle = CONFIG.get('life_cycle', None)
- # pre-alpha lessons should report without error
- if life_cycle == "pre-alpha":
- args.permissive = True
-
- check_config(args.reporter)
- check_source_rmd(args.reporter, args.source_dir, args.parser)
-
- args.references = read_references(args.reporter, args.reference_path)
-
- docs = read_all_markdown(args.source_dir, args.parser)
- check_fileset(args.source_dir, args.reporter, list(docs.keys()))
- check_unwanted_files(args.source_dir, args.reporter)
- for filename in list(docs.keys()):
- checker = create_checker(args, filename, docs[filename])
- checker.check()
-
- args.reporter.report()
- if args.reporter.messages:
- if args.permissive:
- print("Problems detected but ignored (permissive mode).")
- else:
- print("Problems detected.")
- sys.exit(1)
- else:
- print("No problems found.")
-
- return
-
-
-def parse_args():
- """Parse command-line arguments."""
-
- parser = ArgumentParser(description="""Check episode files in a lesson.""")
- parser.add_argument('-l', '--linelen',
- default=False,
- action="store_true",
- dest='line_lengths',
- help='Check line lengths')
- parser.add_argument('-p', '--parser',
- default=None,
- dest='parser',
- help='path to Markdown parser')
- parser.add_argument('-r', '--references',
- default=None,
- dest='reference_path',
- help='path to Markdown file of external references')
- parser.add_argument('-s', '--source',
- default=os.curdir,
- dest='source_dir',
- help='source directory')
- parser.add_argument('-w', '--whitespace',
- default=False,
- action="store_true",
- dest='trailing_whitespace',
- help='Check for trailing whitespace')
- parser.add_argument('--permissive',
- default=False,
- action="store_true",
- dest='permissive',
- help='Do not raise an error even if issues are detected')
-
- args, extras = parser.parse_known_args()
- require(args.parser is not None,
- 'Path to Markdown parser not provided',
- True)
- require(not extras,
- 'Unexpected trailing command-line arguments "{0}"'.format(extras))
-
- return args
-
-def check_config(reporter):
- """Check configuration file."""
-
- reporter.check_field(CONFIG["config_file"], 'configuration',
- CONFIG, 'kind', 'lesson')
- reporter.check_field(CONFIG["config_file"], 'configuration',
- CONFIG, 'carpentry', ('swc', 'dc', 'lc', 'cp', 'incubator'))
- reporter.check_field(CONFIG["config_file"], 'configuration', CONFIG, 'title')
- reporter.check_field(CONFIG["config_file"], 'configuration', CONFIG, 'email')
-
- for defaults in [
- {'values': {'root': '.', 'layout': 'page'}},
- {'values': {'root': '..', 'layout': 'episode'}, 'scope': {'type': 'episodes', 'path': ''}},
- {'values': {'root': '..', 'layout': 'page'}, 'scope': {'type': 'extras', 'path': ''}}
- ]:
- error_text = 'incorrect settings for: root "{0}" layout "{1}"'
- root = defaults["values"]["root"]
- layout = defaults["values"]["layout"]
- error_message = error_text.format(root, layout)
-
- defaults_test = defaults in CONFIG.get('defaults', [])
- reporter.check(defaults_test, 'configuration', error_message)
-
-def check_source_rmd(reporter, source_dir, parser):
- """Check that Rmd episode files include `source: Rmd`"""
-
- episode_rmd_dir = [os.path.join(source_dir, d) for d in SOURCE_RMD_DIRS]
- episode_rmd_files = [os.path.join(d, '*.Rmd') for d in episode_rmd_dir]
- results = {}
- for pat in episode_rmd_files:
- for f in glob.glob(pat):
- data = read_markdown(parser, f)
- dy = data['metadata']
- if dy:
- reporter.check_field(f, 'episode_rmd',
- dy, 'source', 'Rmd')
-
-def read_references(reporter, ref_path):
- """Read shared file of reference links, returning dictionary of valid references
- {symbolic_name : URL}
- """
-
- if 'remote_theme' in CONFIG:
- return {}
-
- if not ref_path:
- raise Warning("No filename has been provided.")
-
- result = {}
- urls_seen = set()
-
- with open(ref_path, 'r', encoding='utf-8') as reader:
- for (num, line) in enumerate(reader, 1):
-
- # Skip empty lines
- if len(line.strip()) == 0:
- continue
-
- # Skip HTML comments
- if line.strip().startswith(""):
- continue
-
- # Skip Liquid's {% include ... %} lines
- if P_INTERNAL_INCLUDE_LINK.search(line):
- continue
-
- m = P_INTERNAL_LINK_DEF.search(line)
-
- message = '{}: {} not a valid reference: {}'
- require(m, message.format(ref_path, num, line.rstrip()))
-
- name = m.group(1)
- url = m.group(2)
-
- message = 'Empty reference at {0}:{1}'
- require(name, message.format(ref_path, num))
-
- unique_name = name not in result
- unique_url = url not in urls_seen
-
- reporter.check(unique_name,
- ref_path,
- 'Duplicate reference name {0} at line {1}',
- name, num)
-
- reporter.check(unique_url,
- ref_path,
- 'Duplicate definition of URL {0} at line {1}',
- url, num)
-
- result[name] = url
- urls_seen.add(url)
-
- return result
-
-
-def read_all_markdown(source_dir, parser):
- """Read source files, returning
- {path : {'metadata':yaml, 'metadata_len':N, 'text':text, 'lines':[(i, line, len)], 'doc':doc}}
- """
-
- all_dirs = [os.path.join(source_dir, d) for d in SOURCE_DIRS]
- all_patterns = [os.path.join(d, '*.md') for d in all_dirs]
- result = {}
- for pat in all_patterns:
- for filename in glob.glob(pat):
- data = read_markdown(parser, filename)
- if data:
- result[filename] = data
- return result
-
-
-def check_fileset(source_dir, reporter, filenames_present):
- """Are all required files present? Are extraneous files present?"""
-
- # Check files with predictable names.
- required = [os.path.join(source_dir, p) for p in REQUIRED_FILES]
- missing = set(required) - set(filenames_present)
- for m in missing:
- reporter.add(None, 'Missing required file {0}', m)
-
- # Check episode files' names.
- seen = []
- for filename in filenames_present:
- if '_episodes' not in filename:
- continue
-
- # split path to check episode name
- base_name = os.path.basename(filename)
- m = P_EPISODE_FILENAME.search(base_name)
- if m and m.group(1):
- seen.append(m.group(1))
- else:
- reporter.add(
- None, 'Episode {0} has badly-formatted filename', filename)
-
- # Check for duplicate episode numbers.
- reporter.check(len(seen) == len(set(seen)),
- None,
- 'Duplicate episode numbers {0} vs {1}',
- sorted(seen), sorted(set(seen)))
-
- # Check that numbers are consecutive.
- seen = sorted([int(s) for s in seen])
- clean = True
- for i in range(len(seen) - 1):
- clean = clean and ((seen[i+1] - seen[i]) == 1)
- reporter.check(clean,
- None,
- 'Missing or non-consecutive episode numbers {0}',
- seen)
-
-
-def create_checker(args, filename, info):
- """Create appropriate checker for file."""
-
- for (pat, cls) in CHECKERS:
- if pat.search(filename):
- return cls(args, filename, **info)
- return NotImplemented
-
-class CheckBase:
- """Base class for checking Markdown files."""
-
- def __init__(self, args, filename, metadata, metadata_len, text, lines, doc):
- """Cache arguments for checking."""
-
- self.args = args
- self.reporter = self.args.reporter # for convenience
- self.filename = filename
- self.metadata = metadata
- self.metadata_len = metadata_len
- self.text = text
- self.lines = lines
- self.doc = doc
-
- self.layout = None
-
- def check(self):
- """Run tests."""
-
- self.check_metadata()
- self.check_line_lengths()
- self.check_trailing_whitespace()
- self.check_blockquote_classes()
- self.check_codeblock_classes()
- self.check_defined_link_references()
-
- def check_metadata(self):
- """Check the YAML metadata."""
-
- self.reporter.check(self.metadata is not None,
- self.filename,
- 'Missing metadata entirely')
-
- if self.metadata and (self.layout is not None):
- self.reporter.check_field(
- self.filename, 'metadata', self.metadata, 'layout', self.layout)
-
- def check_line_lengths(self):
- """Check the raw text of the lesson body."""
-
- if self.args.line_lengths:
- over_limit = []
-
- for (i, l, n) in self.lines:
- # Report lines that are longer than the suggested
- # line length limit only if they're not
- # link-only or image-only lines.
- if n > MAX_LINE_LEN and not P_LINK_IMAGE_LINE.match(l):
- over_limit.append(i)
-
- self.reporter.check(not over_limit,
- self.filename,
- 'Line(s) too long: {0}',
- ', '.join([str(i) for i in over_limit]))
-
- def check_trailing_whitespace(self):
- """Check for whitespace at the ends of lines."""
-
- if self.args.trailing_whitespace:
- trailing = [
- i for (i, l, n) in self.lines if P_TRAILING_WHITESPACE.match(l)]
- self.reporter.check(not trailing,
- self.filename,
- 'Line(s) end with whitespace: {0}',
- ', '.join([str(i) for i in trailing]))
-
- def check_blockquote_classes(self):
- """Check that all blockquotes have known classes."""
-
- for node in self.find_all(self.doc, {'type': 'blockquote'}):
- cls = self.get_val(node, 'attr', 'class')
- self.reporter.check(cls in KNOWN_BLOCKQUOTES,
- (self.filename, self.get_loc(node)),
- 'Unknown or missing blockquote type {0}',
- cls)
-
- def check_codeblock_classes(self):
- """Check that all code blocks have known classes."""
-
- for node in self.find_all(self.doc, {'type': 'codeblock'}):
- cls = self.get_val(node, 'attr', 'class')
- self.reporter.check(cls is not None and (cls in KNOWN_CODEBLOCKS or
- cls.startswith('language-')),
- (self.filename, self.get_loc(node)),
- 'Unknown or missing code block type {0}',
- cls)
-
- def check_defined_link_references(self):
- """Check that defined links resolve in the file.
-
- Internally-defined links match the pattern [text][label].
- """
-
- result = set()
- for node in self.find_all(self.doc, {'type': 'text'}):
- for match in P_INTERNAL_LINK_REF.findall(node['value']):
- text = match[0]
- link = match[1]
- if link not in self.args.references:
- result.add('"{0}"=>"{1}"'.format(text, link))
- self.reporter.check(not result,
- self.filename,
- 'Internally-defined links may be missing definitions: {0}',
- ', '.join(sorted(result)))
-
- def find_all(self, node, pattern, accum=None):
- """Find all matches for a pattern."""
-
- assert isinstance(pattern, dict), 'Patterns must be dictionaries'
- if accum is None:
- accum = []
- if self.match(node, pattern):
- accum.append(node)
- for child in node.get('children', []):
- self.find_all(child, pattern, accum)
- return accum
-
- def match(self, node, pattern):
- """Does this node match the given pattern?"""
-
- for key in pattern:
- if key not in node:
- return False
- val = pattern[key]
- if isinstance(val, str):
- if node[key] != val:
- return False
- elif isinstance(val, dict):
- if not self.match(node[key], val):
- return False
- return True
-
- @staticmethod
- def get_val(node, *chain):
- """Get value one or more levels down."""
-
- curr = node
- for selector in chain:
- curr = curr.get(selector, None)
- if curr is None:
- break
- return curr
-
- def get_loc(self, node):
- """Convenience method to get node's line number."""
-
- result = self.get_val(node, 'options', 'location')
- if self.metadata_len is not None:
- result += self.metadata_len
- return result
-
-
-class CheckNonJekyll(CheckBase):
- """Check a file that isn't translated by Jekyll."""
-
- def check_metadata(self):
- self.reporter.check(self.metadata is None,
- self.filename,
- 'Unexpected metadata')
-
-
-class CheckIndex(CheckBase):
- """Check the main index page."""
-
- def __init__(self, args, filename, metadata, metadata_len, text, lines, doc):
- super().__init__(args, filename, metadata, metadata_len, text, lines, doc)
- self.layout = 'lesson'
-
- def check_metadata(self):
- super().check_metadata()
- self.reporter.check(self.metadata.get('root', '') == '.',
- self.filename,
- 'Root not set to "."')
-
-
-class CheckEpisode(CheckBase):
- """Check an episode page."""
-
- def check(self):
- """Run extra tests."""
-
- super().check()
- self.check_reference_inclusion()
-
- def check_metadata(self):
- super().check_metadata()
- if self.metadata:
- if 'layout' in self.metadata:
- if self.metadata['layout'] == 'break':
- self.check_metadata_fields(BREAK_METADATA_FIELDS)
- else:
- self.reporter.add(self.filename,
- 'Unknown episode layout "{0}"',
- self.metadata['layout'])
- else:
- self.check_metadata_fields(TEACHING_METADATA_FIELDS)
-
- def check_metadata_fields(self, expected):
- """Check metadata fields."""
- for (name, type_) in expected:
- if name not in self.metadata:
- self.reporter.add(self.filename,
- 'Missing metadata field {0}',
- name)
- elif not isinstance(self.metadata[name], type_):
- self.reporter.add(self.filename,
- '"{0}" has wrong type in metadata ({1} instead of {2})',
- name, type(self.metadata[name]), type_)
-
- def check_reference_inclusion(self):
- """Check that links file has been included."""
-
- if 'remote_theme' in CONFIG:
- return
-
- if not self.args.reference_path:
- return
-
- for (i, last_line, line_len) in reversed(self.lines):
- if last_line:
- break
-
- require(last_line,
- 'No non-empty lines in {0}'.format(self.filename))
-
- include_filename = os.path.split(self.args.reference_path)[-1]
- if include_filename not in last_line:
- self.reporter.add(self.filename,
- 'episode does not include "{0}"',
- include_filename)
-
-
-class CheckReference(CheckBase):
- """Check the reference page."""
-
- def __init__(self, args, filename, metadata, metadata_len, text, lines, doc):
- super().__init__(args, filename, metadata, metadata_len, text, lines, doc)
- self.layout = 'reference'
-
-
-class CheckGeneric(CheckBase):
- """Check a generic page."""
-
- def __init__(self, args, filename, metadata, metadata_len, text, lines, doc):
- super().__init__(args, filename, metadata, metadata_len, text, lines, doc)
-
-
-CHECKERS = [
- (re.compile(r'CONTRIBUTING\.md'), CheckNonJekyll),
- (re.compile(r'README\.md'), CheckNonJekyll),
- (re.compile(r'index\.md'), CheckIndex),
- (re.compile(r'reference\.md'), CheckReference),
- # '.' below is what's passed on the command line via '-s' flag
- (re.compile(os.path.join('.','_episodes', '[^/]*\.md')), CheckEpisode),
- (re.compile(r'.*\.md'), CheckGeneric)
-]
-
-
-if __name__ == '__main__':
- main()
diff --git a/bin/lesson_initialize.py b/bin/lesson_initialize.py
deleted file mode 100644
index 79ec05c..0000000
--- a/bin/lesson_initialize.py
+++ /dev/null
@@ -1,47 +0,0 @@
-"""Initialize a newly-created repository."""
-
-
-import sys
-import os
-import shutil
-
-BOILERPLATE = (
- 'AUTHORS',
- 'CITATION',
- 'CONTRIBUTING.md',
- 'README.md',
- '_config.yml',
- os.path.join('_episodes', '01-introduction.md'),
- os.path.join('_extras', 'about.md'),
- os.path.join('_extras', 'discuss.md'),
- os.path.join('_extras', 'figures.md'),
- os.path.join('_extras', 'guide.md'),
- 'index.md',
- 'reference.md',
- 'setup.md',
-)
-
-
-def main():
- """Check for collisions, then create."""
-
- # Check.
- errors = False
- for path in BOILERPLATE:
- if os.path.exists(path):
- print('Warning: {0} already exists.'.format(path), file=sys.stderr)
- errors = True
- if errors:
- print('**Exiting without creating files.**', file=sys.stderr)
- sys.exit(1)
-
- # Create.
- for path in BOILERPLATE:
- shutil.copyfile(
- os.path.join('bin', 'boilerplate', path),
- path
- )
-
-
-if __name__ == '__main__':
- main()
diff --git a/bin/markdown_ast.rb b/bin/markdown_ast.rb
deleted file mode 100755
index 2ef3f77..0000000
--- a/bin/markdown_ast.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/usr/bin/env ruby
-# frozen_string_literal: true
-
-# Use Kramdown parser to produce AST for Markdown document.
-
-require 'kramdown'
-require 'kramdown-parser-gfm'
-require 'json'
-
-markdown = $stdin.read
-doc = Kramdown::Document.new(markdown, input: 'GFM', hard_wrap: false)
-tree = doc.to_hash_a_s_t
-puts JSON.pretty_generate(tree)
diff --git a/bin/repo_check.py b/bin/repo_check.py
deleted file mode 100644
index 6988ca5..0000000
--- a/bin/repo_check.py
+++ /dev/null
@@ -1,181 +0,0 @@
-"""
-Check repository settings.
-"""
-
-
-import sys
-import os
-from subprocess import Popen, PIPE
-import re
-from argparse import ArgumentParser
-
-from util import require
-from reporter import Reporter
-
-# Import this way to produce a more useful error message.
-try:
- import requests
-except ImportError:
- print('Unable to import requests module: please install requests', file=sys.stderr)
- sys.exit(1)
-
-
-# Pattern to match Git command-line output for remotes => (user name, project name).
-P_GIT_REMOTE = re.compile(r'upstream\s+(?:https://|git@)github.com[:/]([^/]+)/([^.]+)(\.git)?\s+\(fetch\)')
-
-# Repository URL format string.
-F_REPO_URL = 'https://github.com/{0}/{1}/'
-
-# Pattern to match repository URLs => (user name, project name)
-P_REPO_URL = re.compile(r'https?://github\.com/([^.]+)/([^/]+)/?')
-
-# API URL format string.
-F_API_URL = 'https://api.github.com/repos/{0}/{1}/labels'
-
-# Expected labels and colors.
-EXPECTED = {
- 'help wanted': 'dcecc7',
- 'status:in progress': '9bcc65',
- 'status:changes requested': '679f38',
- 'status:wait': 'fff2df',
- 'status:refer to cac': 'ffdfb2',
- 'status:need more info': 'ee6c00',
- 'status:blocked': 'e55100',
- 'status:out of scope': 'eeeeee',
- 'status:duplicate': 'bdbdbd',
- 'type:typo text': 'f8bad0',
- 'type:bug': 'eb3f79',
- 'type:formatting': 'ac1357',
- 'type:template and tools': '7985cb',
- 'type:instructor guide': '00887a',
- 'type:discussion': 'b2e5fc',
- 'type:enhancement': '7fdeea',
- 'type:clarification': '00acc0',
- 'type:teaching example': 'ced8dc',
- 'good first issue': 'ffeb3a',
- 'high priority': 'd22e2e'
-}
-
-
-def main():
- """
- Main driver.
- """
-
- args = parse_args()
- reporter = Reporter()
- repo_url = get_repo_url(args.repo_url)
- check_labels(reporter, repo_url)
- reporter.report()
-
-
-def parse_args():
- """
- Parse command-line arguments.
- """
-
- parser = ArgumentParser(description="""Check repository settings.""")
- parser.add_argument('-r', '--repo',
- default=None,
- dest='repo_url',
- help='repository URL')
- parser.add_argument('-s', '--source',
- default=os.curdir,
- dest='source_dir',
- help='source directory')
-
- args, extras = parser.parse_known_args()
- require(not extras,
- 'Unexpected trailing command-line arguments "{0}"'.format(extras))
-
- return args
-
-
-def get_repo_url(repo_url):
- """
- Figure out which repository to query.
- """
-
- # Explicitly specified.
- if repo_url is not None:
- return repo_url
-
- # Guess.
- cmd = 'git remote -v'
- p = Popen(cmd, shell=True, stdin=PIPE, stdout=PIPE,
- close_fds=True, universal_newlines=True, encoding='utf-8')
- stdout_data, stderr_data = p.communicate()
- stdout_data = stdout_data.split('\n')
- matches = [P_GIT_REMOTE.match(line) for line in stdout_data]
- matches = [m for m in matches if m is not None]
- require(len(matches) == 1,
- 'Unexpected output from git remote command: "{0}"'.format(matches))
-
- username = matches[0].group(1)
- require(
- username, 'empty username in git remote output {0}'.format(matches[0]))
-
- project_name = matches[0].group(2)
- require(
- username, 'empty project name in git remote output {0}'.format(matches[0]))
-
- url = F_REPO_URL.format(username, project_name)
- return url
-
-
-def check_labels(reporter, repo_url):
- """
- Check labels in repository.
- """
-
- actual = get_labels(repo_url)
- extra = set(actual.keys()) - set(EXPECTED.keys())
-
- reporter.check(not extra,
- None,
- 'Extra label(s) in repository {0}: {1}',
- repo_url, ', '.join(sorted(extra)))
-
- missing = set(EXPECTED.keys()) - set(actual.keys())
- reporter.check(not missing,
- None,
- 'Missing label(s) in repository {0}: {1}',
- repo_url, ', '.join(sorted(missing)))
-
- overlap = set(EXPECTED.keys()).intersection(set(actual.keys()))
- for name in sorted(overlap):
- reporter.check(EXPECTED[name].lower() == actual[name].lower(),
- None,
- 'Color mis-match for label {0} in {1}: expected {2}, found {3}',
- name, repo_url, EXPECTED[name], actual[name])
-
-
-def get_labels(repo_url):
- """
- Get actual labels from repository.
- """
-
- m = P_REPO_URL.match(repo_url)
- require(
- m, 'repository URL {0} does not match expected pattern'.format(repo_url))
-
- username = m.group(1)
- require(username, 'empty username in repository URL {0}'.format(repo_url))
-
- project_name = m.group(2)
- require(
- username, 'empty project name in repository URL {0}'.format(repo_url))
-
- url = F_API_URL.format(username, project_name)
- r = requests.get(url)
- require(r.status_code == 200,
- 'Request for {0} failed with {1}'.format(url, r.status_code))
-
- result = {}
- for entry in r.json():
- result[entry['name']] = entry['color']
- return result
-
-
-if __name__ == '__main__':
- main()
diff --git a/bin/reporter.py b/bin/reporter.py
deleted file mode 100644
index 550dbf0..0000000
--- a/bin/reporter.py
+++ /dev/null
@@ -1,75 +0,0 @@
-import sys
-
-class Reporter:
- """Collect and report errors."""
-
- # Marker to show that an expected value hasn't been provided.
- # (Can't use 'None' because that might be a legitimate value.)
- _DEFAULT_REPORTER = []
-
- def __init__(self):
- """Constructor."""
- self.messages = []
-
- def check_field(self, filename, name, values, key, expected=_DEFAULT_REPORTER):
- """Check that a dictionary has an expected value."""
-
- if key not in values:
- self.add(filename, '{0} does not contain {1}', name, key)
- elif expected is self._DEFAULT_REPORTER:
- pass
- elif type(expected) in (tuple, set, list):
- if values[key] not in expected:
- self.add(
- filename, '{0} {1} value {2} is not in {3}', name, key, values[key], expected)
- elif values[key] != expected:
- self.add(filename, '{0} {1} is {2} not {3}',
- name, key, values[key], expected)
-
- def check(self, condition, location, fmt, *args):
- """Append error if condition not met."""
-
- if not condition:
- self.add(location, fmt, *args)
-
- def add(self, location, fmt, *args):
- """Append error unilaterally."""
-
- self.messages.append((location, fmt.format(*args)))
-
- @staticmethod
- def pretty(item):
- location, message = item
- if isinstance(location, type(None)):
- return message
- elif isinstance(location, str):
- return location + ': ' + message
- elif isinstance(location, tuple):
- return '{0}:{1}: '.format(*location) + message
-
- print('Unknown item "{0}"'.format(item), file=sys.stderr)
- return NotImplemented
-
- @staticmethod
- def key(item):
- location, message = item
- if isinstance(location, type(None)):
- return ('', -1, message)
- elif isinstance(location, str):
- return (location, -1, message)
- elif isinstance(location, tuple):
- return (location[0], location[1], message)
-
- print('Unknown item "{0}"'.format(item), file=sys.stderr)
- return NotImplemented
-
- def report(self, stream=sys.stdout):
- """Report all messages in order."""
-
- if not self.messages:
- return
-
- for m in sorted(self.messages, key=self.key):
- print(self.pretty(m), file=stream)
-
-
diff --git a/bin/run-make-docker-serve.sh b/bin/run-make-docker-serve.sh
deleted file mode 100755
index 1e09178..0000000
--- a/bin/run-make-docker-serve.sh
+++ /dev/null
@@ -1,10 +0,0 @@
-#!/bin/bash
-
-set -o errexit
-set -o pipefail
-set -o nounset
-
-
-bundle install
-bundle update
-exec bundle exec jekyll serve --host 0.0.0.0
diff --git a/bin/test_lesson_check.py b/bin/test_lesson_check.py
deleted file mode 100644
index 7a6d603..0000000
--- a/bin/test_lesson_check.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import unittest
-
-import lesson_check
-import reporter
-
-
-class TestFileList(unittest.TestCase):
- def setUp(self):
- self.reporter = reporter.Reporter() # TODO: refactor reporter class.
-
- def test_file_list_has_expected_entries(self):
- # For first pass, simply assume that all required files are present
-
- lesson_check.check_fileset('', self.reporter, lesson_check.REQUIRED_FILES)
- self.assertEqual(len(self.reporter.messages), 0)
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/bin/util.py b/bin/util.py
deleted file mode 100644
index 1398c37..0000000
--- a/bin/util.py
+++ /dev/null
@@ -1,111 +0,0 @@
-import sys
-import os
-import json
-from subprocess import Popen, PIPE
-
-# Import this way to produce a more useful error message.
-try:
- import yaml
-except ImportError:
- print('Unable to import YAML module: please install PyYAML', file=sys.stderr)
- sys.exit(1)
-
-__all__ = ['check_unwanted_files', 'load_yaml', 'read_markdown', 'require']
-
-# Files that shouldn't be present.
-UNWANTED_FILES = [
- '.nojekyll'
-]
-
-def read_markdown(parser, path):
- """
- Get YAML and AST for Markdown file, returning
- {'metadata':yaml, 'metadata_len':N, 'text':text, 'lines':[(i, line, len)], 'doc':doc}.
- """
-
- # Split and extract YAML (if present).
- with open(path, 'r', encoding='utf-8') as reader:
- body = reader.read()
- metadata_raw, metadata_yaml, body = split_metadata(path, body)
-
- # Split into lines.
- metadata_len = 0 if metadata_raw is None else metadata_raw.count('\n')
- lines = [(metadata_len+i+1, line, len(line))
- for (i, line) in enumerate(body.split('\n'))]
-
- # Parse Markdown.
- cmd = 'bundle exec ruby {0}'.format(parser)
- p = Popen(cmd, shell=True, stdin=PIPE, stdout=PIPE,
- close_fds=True, universal_newlines=True, encoding='utf-8')
- stdout_data, stderr_data = p.communicate(body)
- doc = json.loads(stdout_data)
-
- return {
- 'metadata': metadata_yaml,
- 'metadata_len': metadata_len,
- 'text': body,
- 'lines': lines,
- 'doc': doc
- }
-
-
-def split_metadata(path, text):
- """
- Get raw (text) metadata, metadata as YAML, and rest of body.
- If no metadata, return (None, None, body).
- """
-
- metadata_raw = None
- metadata_yaml = None
-
- pieces = text.split('---', 2)
- if len(pieces) == 3:
- metadata_raw = pieces[1]
- text = pieces[2]
- try:
- metadata_yaml = yaml.load(metadata_raw, Loader=yaml.SafeLoader)
- except yaml.YAMLError as e:
- message = 'Unable to parse YAML header in {0}:\n{1}'
- print(message.format(path, e), file=sys.stderr)
-
- return metadata_raw, metadata_yaml, text
-
-
-def load_yaml(filename):
- """
- Wrapper around YAML loading so that 'import yaml' is only needed
- in one file.
- """
-
- try:
- with open(filename, 'r', encoding='utf-8') as reader:
- return yaml.load(reader, Loader=yaml.SafeLoader)
- except yaml.YAMLError as e:
- message = 'ERROR: Unable to load YAML file {0}:\n{1}'
- print(message.format(filename, e), file=sys.stderr)
- except (FileNotFoundError, IOError):
- message = 'ERROR: File {} not found'
- print(message.format(filename), file=sys.stderr)
-
- return {}
-
-def check_unwanted_files(dir_path, reporter):
- """
- Check that unwanted files are not present.
- """
-
- for filename in UNWANTED_FILES:
- path = os.path.join(dir_path, filename)
- reporter.check(not os.path.exists(path),
- path,
- "Unwanted file found")
-
-
-def require(condition, message, fatal=False):
- """Fail if condition not met."""
-
- if not condition:
- print(message, file=sys.stderr)
-
- if fatal:
- sys.exit(1)
diff --git a/bin/workshop_check.py b/bin/workshop_check.py
deleted file mode 100644
index 312b1a1..0000000
--- a/bin/workshop_check.py
+++ /dev/null
@@ -1,419 +0,0 @@
-'''Check that a workshop's index.html metadata is valid. See the
-docstrings on the checking functions for a summary of the checks.
-'''
-
-
-import sys
-import os
-import re
-from datetime import date
-from util import split_metadata, load_yaml, check_unwanted_files
-from reporter import Reporter
-
-# Metadata field patterns.
-EMAIL_PATTERN = r'[^@]+@[^@]+\.[^@]+'
-HUMANTIME_PATTERN = r'((0?[1-9]|1[0-2]):[0-5]\d(am|pm)(-|to)(0?[1-9]|1[0-2]):[0-5]\d(am|pm))|((0?\d|1\d|2[0-3]):[0-5]\d(-|to)(0?\d|1\d|2[0-3]):[0-5]\d)'
-EVENTBRITE_PATTERN = r'\d{9,10}'
-URL_PATTERN = r'https?://.+'
-
-# Defaults.
-CARPENTRIES = ("dc", "swc", "lc", "cp")
-DEFAULT_CONTACT_EMAIL = 'team@carpentries.org'
-
-USAGE = 'Usage: "workshop_check.py path/to/root/directory"'
-
-# Country and language codes. Note that codes mean different things: 'ar'
-# is 'Arabic' as a language but 'Argentina' as a country.
-
-ISO_COUNTRY = [
- 'ad', 'ae', 'af', 'ag', 'ai', 'al', 'am', 'an', 'ao', 'aq', 'ar', 'as',
- 'at', 'au', 'aw', 'ax', 'az', 'ba', 'bb', 'bd', 'be', 'bf', 'bg', 'bh',
- 'bi', 'bj', 'bm', 'bn', 'bo', 'br', 'bs', 'bt', 'bv', 'bw', 'by', 'bz',
- 'ca', 'cc', 'cd', 'cf', 'cg', 'ch', 'ci', 'ck', 'cl', 'cm', 'cn', 'co',
- 'cr', 'cu', 'cv', 'cx', 'cy', 'cz', 'de', 'dj', 'dk', 'dm', 'do', 'dz',
- 'ec', 'ee', 'eg', 'eh', 'er', 'es', 'et', 'eu', 'fi', 'fj', 'fk', 'fm',
- 'fo', 'fr', 'ga', 'gb', 'gd', 'ge', 'gf', 'gg', 'gh', 'gi', 'gl', 'gm',
- 'gn', 'gp', 'gq', 'gr', 'gs', 'gt', 'gu', 'gw', 'gy', 'hk', 'hm', 'hn',
- 'hr', 'ht', 'hu', 'id', 'ie', 'il', 'im', 'in', 'io', 'iq', 'ir', 'is',
- 'it', 'je', 'jm', 'jo', 'jp', 'ke', 'kg', 'kh', 'ki', 'km', 'kn', 'kp',
- 'kr', 'kw', 'ky', 'kz', 'la', 'lb', 'lc', 'li', 'lk', 'lr', 'ls', 'lt',
- 'lu', 'lv', 'ly', 'ma', 'mc', 'md', 'me', 'mg', 'mh', 'mk', 'ml', 'mm',
- 'mn', 'mo', 'mp', 'mq', 'mr', 'ms', 'mt', 'mu', 'mv', 'mw', 'mx', 'my',
- 'mz', 'na', 'nc', 'ne', 'nf', 'ng', 'ni', 'nl', 'no', 'np', 'nr', 'nu',
- 'nz', 'om', 'pa', 'pe', 'pf', 'pg', 'ph', 'pk', 'pl', 'pm', 'pn', 'pr',
- 'ps', 'pt', 'pw', 'py', 'qa', 're', 'ro', 'rs', 'ru', 'rw', 'sa', 'sb',
- 'sc', 'sd', 'se', 'sg', 'sh', 'si', 'sj', 'sk', 'sl', 'sm', 'sn', 'so',
- 'sr', 'st', 'sv', 'sy', 'sz', 'tc', 'td', 'tf', 'tg', 'th', 'tj', 'tk',
- 'tl', 'tm', 'tn', 'to', 'tr', 'tt', 'tv', 'tw', 'tz', 'ua', 'ug', 'um',
- 'us', 'uy', 'uz', 'va', 'vc', 've', 'vg', 'vi', 'vn', 'vu', 'wf', 'ws',
- 'ye', 'yt', 'za', 'zm', 'zw'
-]
-
-ISO_LANGUAGE = [
- 'aa', 'ab', 'ae', 'af', 'ak', 'am', 'an', 'ar', 'as', 'av', 'ay', 'az',
- 'ba', 'be', 'bg', 'bh', 'bi', 'bm', 'bn', 'bo', 'br', 'bs', 'ca', 'ce',
- 'ch', 'co', 'cr', 'cs', 'cu', 'cv', 'cy', 'da', 'de', 'dv', 'dz', 'ee',
- 'el', 'en', 'eo', 'es', 'et', 'eu', 'fa', 'ff', 'fi', 'fj', 'fo', 'fr',
- 'fy', 'ga', 'gd', 'gl', 'gn', 'gu', 'gv', 'ha', 'he', 'hi', 'ho', 'hr',
- 'ht', 'hu', 'hy', 'hz', 'ia', 'id', 'ie', 'ig', 'ii', 'ik', 'io', 'is',
- 'it', 'iu', 'ja', 'jv', 'ka', 'kg', 'ki', 'kj', 'kk', 'kl', 'km', 'kn',
- 'ko', 'kr', 'ks', 'ku', 'kv', 'kw', 'ky', 'la', 'lb', 'lg', 'li', 'ln',
- 'lo', 'lt', 'lu', 'lv', 'mg', 'mh', 'mi', 'mk', 'ml', 'mn', 'mr', 'ms',
- 'mt', 'my', 'na', 'nb', 'nd', 'ne', 'ng', 'nl', 'nn', 'no', 'nr', 'nv',
- 'ny', 'oc', 'oj', 'om', 'or', 'os', 'pa', 'pi', 'pl', 'ps', 'pt', 'qu',
- 'rm', 'rn', 'ro', 'ru', 'rw', 'sa', 'sc', 'sd', 'se', 'sg', 'si', 'sk',
- 'sl', 'sm', 'sn', 'so', 'sq', 'sr', 'ss', 'st', 'su', 'sv', 'sw', 'ta',
- 'te', 'tg', 'th', 'ti', 'tk', 'tl', 'tn', 'to', 'tr', 'ts', 'tt', 'tw',
- 'ty', 'ug', 'uk', 'ur', 'uz', 've', 'vi', 'vo', 'wa', 'wo', 'xh', 'yi',
- 'yo', 'za', 'zh', 'zu'
-]
-
-
-def look_for_fixme(func):
- """Decorator to fail test if text argument starts with "FIXME"."""
-
- def inner(arg):
- if (arg is not None) and \
- isinstance(arg, str) and \
- arg.lstrip().startswith('FIXME'):
- return False
- return func(arg)
- return inner
-
-
-@look_for_fixme
-def check_layout(layout):
- '''"layout" in YAML header must be "workshop".'''
-
- return layout == 'workshop'
-
-
-@look_for_fixme
-def check_carpentry(layout):
- '''"carpentry" in YAML header must be "dc", "swc", "lc", or "cp".'''
-
- return layout in CARPENTRIES
-
-
-@look_for_fixme
-def check_country(country):
- '''"country" must be a lowercase ISO-3166 two-letter code.'''
-
- return country in ISO_COUNTRY
-
-
-@look_for_fixme
-def check_language(language):
- '''"language" must be a lowercase ISO-639 two-letter code.'''
-
- return language in ISO_LANGUAGE
-
-
-@look_for_fixme
-def check_humandate(date):
- """
- 'humandate' must be a human-readable date with a 3-letter month
- and 4-digit year. Examples include 'Feb 18-20, 2025' and 'Feb 18
- and 20, 2025'. It may be in languages other than English, but the
- month name should be kept short to aid formatting of the main
- Carpentries web site.
- """
-
- if ',' not in date:
- return False
-
- month_dates, year = date.split(',')
-
- # The first three characters of month_dates are not empty
- month = month_dates[:3]
- if any(char == ' ' for char in month):
- return False
-
- # But the fourth character is empty ("February" is illegal)
- if month_dates[3] != ' ':
- return False
-
- # year contains *only* numbers
- try:
- int(year)
- except:
- return False
-
- return True
-
-
-@look_for_fixme
-def check_humantime(time):
- """
- 'humantime' is a human-readable start and end time for the
- workshop, such as '09:00 - 16:00'.
- """
-
- return bool(re.match(HUMANTIME_PATTERN, time.replace(' ', '')))
-
-
-def check_date(this_date):
- """
- 'startdate' and 'enddate' are machine-readable start and end dates
- for the workshop, and must be in YYYY-MM-DD format, e.g.,
- '2015-07-01'.
- """
-
- # YAML automatically loads valid dates as datetime.date.
- return isinstance(this_date, date)
-
-
-@look_for_fixme
-def check_latitude_longitude(latlng):
- """
- 'latlng' must be a valid latitude and longitude represented as two
- floating-point numbers separated by a comma.
- """
-
- try:
- lat, lng = latlng.split(',')
- lat = float(lat)
- lng = float(lng)
- return (-90.0 <= lat <= 90.0) and (-180.0 <= lng <= 180.0)
- except ValueError:
- return False
-
-
-def check_instructors(instructors):
- """
- 'instructor' must be a non-empty comma-separated list of quoted
- names, e.g. ['First name', 'Second name', ...']. Do not use 'TBD'
- or other placeholders.
- """
-
- # YAML automatically loads list-like strings as lists.
- return isinstance(instructors, list) and len(instructors) > 0
-
-
-def check_helpers(helpers):
- """
- 'helper' must be a comma-separated list of quoted names,
- e.g. ['First name', 'Second name', ...']. The list may be empty.
- Do not use 'TBD' or other placeholders.
- """
-
- # YAML automatically loads list-like strings as lists.
- return isinstance(helpers, list) and len(helpers) >= 0
-
-
-@look_for_fixme
-def check_emails(emails):
- """
- 'emails' must be a comma-separated list of valid email addresses.
- The list may be empty. A valid email address consists of characters,
- an '@', and more characters. It should not contain the default contact
- """
-
- # YAML automatically loads list-like strings as lists.
- if (isinstance(emails, list) and len(emails) >= 0):
- for email in emails:
- if ((not bool(re.match(EMAIL_PATTERN, email))) or (email == DEFAULT_CONTACT_EMAIL)):
- return False
- else:
- return False
-
- return True
-
-
-def check_eventbrite(eventbrite):
- """
- 'eventbrite' (the Eventbrite registration key) must be 9 or more
- digits. It may appear as an integer or as a string.
- """
-
- if isinstance(eventbrite, int):
- return True
- else:
- return bool(re.match(EVENTBRITE_PATTERN, eventbrite))
-
-
-@look_for_fixme
-def check_collaborative_notes(collaborative_notes):
- """
- 'collaborative_notes' must be a valid URL.
- """
-
- return bool(re.match(URL_PATTERN, collaborative_notes))
-
-
-@look_for_fixme
-def check_pass(value):
- """
- This test always passes (it is used for 'checking' things like the
- workshop address, for which no sensible validation is feasible).
- """
-
- return True
-
-
-HANDLERS = {
- 'layout': (True, check_layout, 'layout isn\'t "workshop"'),
-
- 'carpentry': (True, check_carpentry, 'carpentry isn\'t in ' +
- ', '.join(CARPENTRIES)),
-
- 'country': (True, check_country,
- 'country invalid: must use lowercase two-letter ISO code ' +
- 'from ' + ', '.join(ISO_COUNTRY)),
-
- 'language': (False, check_language,
- 'language invalid: must use lowercase two-letter ISO code' +
- ' from ' + ', '.join(ISO_LANGUAGE)),
-
- 'humandate': (True, check_humandate,
- 'humandate invalid. Please use three-letter months like ' +
- '"Jan" and four-letter years like "2025"'),
-
- 'humantime': (True, check_humantime,
- 'humantime doesn\'t include numbers'),
-
- 'startdate': (True, check_date,
- 'startdate invalid. Must be of format year-month-day, ' +
- 'i.e., 2014-01-31'),
-
- 'enddate': (False, check_date,
- 'enddate invalid. Must be of format year-month-day, i.e.,' +
- ' 2014-01-31'),
-
- 'latlng': (True, check_latitude_longitude,
- 'latlng invalid. Check that it is two floating point ' +
- 'numbers, separated by a comma'),
-
- 'instructor': (True, check_instructors,
- 'instructor list isn\'t a valid list of format ' +
- '["First instructor", "Second instructor",..]'),
-
- 'helper': (True, check_helpers,
- 'helper list isn\'t a valid list of format ' +
- '["First helper", "Second helper",..]'),
-
- 'email': (True, check_emails,
- 'contact email list isn\'t a valid list of format ' +
- '["me@example.org", "you@example.org",..] or contains incorrectly formatted email addresses or ' +
- '"{0}".'.format(DEFAULT_CONTACT_EMAIL)),
-
- 'eventbrite': (False, check_eventbrite, 'Eventbrite key appears invalid'),
-
- 'collaborative_notes': (False, check_collaborative_notes, 'Collaborative Notes URL appears invalid'),
-
- 'venue': (False, check_pass, 'venue name not specified'),
-
- 'address': (False, check_pass, 'address not specified')
-}
-
-# REQUIRED is all required categories.
-REQUIRED = {k for k in HANDLERS if HANDLERS[k][0]}
-
-# OPTIONAL is all optional categories.
-OPTIONAL = {k for k in HANDLERS if not HANDLERS[k][0]}
-
-
-def check_blank_lines(reporter, raw):
- """
- Blank lines are not allowed in category headers.
- """
-
- lines = [(i, x) for (i, x) in enumerate(
- raw.strip().split('\n')) if not x.strip()]
- reporter.check(not lines,
- None,
- 'Blank line(s) in header: {0}',
- ', '.join(["{0}: {1}".format(i, x.rstrip()) for (i, x) in lines]))
-
-
-def check_categories(reporter, left, right, msg):
- """
- Report differences (if any) between two sets of categories.
- """
-
- diff = left - right
- reporter.check(len(diff) == 0,
- None,
- '{0}: offending entries {1}',
- msg, sorted(list(diff)))
-
-
-def check_file(reporter, path, data):
- """
- Get header from file, call all other functions, and check file for
- validity.
- """
-
- # Get metadata as text and as YAML.
- raw, header, body = split_metadata(path, data)
-
- # Do we have any blank lines in the header?
- check_blank_lines(reporter, raw)
-
- # Look through all header entries. If the category is in the input
- # file and is either required or we have actual data (as opposed to
- # a commented-out entry), we check it. If it *isn't* in the header
- # but is required, report an error.
- for category in HANDLERS:
- required, handler, message = HANDLERS[category]
- if category in header:
- if required or header[category]:
- reporter.check(handler(header[category]),
- None,
- '{0}\n actual value "{1}"',
- message, header[category])
- elif required:
- reporter.add(None,
- 'Missing mandatory key "{0}"',
- category)
-
- # Check whether we have missing or too many categories
- seen_categories = set(header.keys())
- check_categories(reporter, REQUIRED, seen_categories,
- 'Missing categories')
- check_categories(reporter, seen_categories, REQUIRED.union(OPTIONAL),
- 'Superfluous categories')
-
-
-def check_config(reporter, filename):
- """
- Check YAML configuration file.
- """
-
- config = load_yaml(filename)
-
- kind = config.get('kind', None)
- reporter.check(kind == 'workshop',
- filename,
- 'Missing or unknown kind of event: {0}',
- kind)
-
- carpentry = config.get('carpentry', None)
- reporter.check(carpentry in ('swc', 'dc', 'lc', 'cp'),
- filename,
- 'Missing or unknown carpentry: {0}',
- carpentry)
-
-
-def main():
- '''Run as the main program.'''
-
- if len(sys.argv) != 2:
- print(USAGE, file=sys.stderr)
- sys.exit(1)
-
- root_dir = sys.argv[1]
- index_file = os.path.join(root_dir, 'index.html')
- config_file = os.path.join(root_dir, '_config.yml')
-
- reporter = Reporter()
- check_config(reporter, config_file)
- check_unwanted_files(root_dir, reporter)
- with open(index_file, encoding='utf-8') as reader:
- data = reader.read()
- check_file(reporter, index_file, data)
- reporter.report()
-
-
-if __name__ == '__main__':
- main()
diff --git a/code/.gitkeep b/code/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/config.yaml b/config.yaml
new file mode 100644
index 0000000..53000c2
--- /dev/null
+++ b/config.yaml
@@ -0,0 +1,80 @@
+#------------------------------------------------------------
+# Values for this lesson.
+#------------------------------------------------------------
+
+# Which carpentry is this (swc, dc, lc, or cp)?
+# swc: Software Carpentry
+# dc: Data Carpentry
+# lc: Library Carpentry
+# cp: Carpentries (to use for instructor training for instance)
+# incubator: The Carpentries Incubator
+#
+# This option supports custom types so lessons can be branded
+# and themed with your own logo and alt-text (see `carpentry_description`)
+# See https://carpentries.github.io/sandpaper-docs/editing.html#adding-a-custom-logo
+carpentry: 'incubator'
+
+# Alt-text description of the lesson.
+carpentry_description: 'EIC Tutorials'
+
+# Overall title for pages.
+title: 'Analyzing EIC Simulation Output'
+
+# Date the lesson was created (YYYY-MM-DD, this is empty by default)
+created: 2021-01-01
+
+# Comma-separated list of keywords for the lesson
+keywords: 'EIC, ePIC, EICrecon, analysis, ROOT, uproot, RDataFrame, PODIO, reconstruction, simulation'
+
+# Life cycle stage of the lesson
+# possible values: pre-alpha, alpha, beta, stable
+life_cycle: 'stable'
+
+# License of the lesson
+license: 'CC-BY 4.0'
+
+# Link to the source repository for this lesson
+source: 'https://github.com/eic/tutorial-analysis'
+
+# Default branch of your lesson
+branch: 'main'
+
+# Who to contact if there are any issues
+contact: 'stephen.kay@york.ac.uk'
+
+# Navigation ------------------------------------------------
+#
+# Use the following menu items to specify the order of
+# individual pages in each dropdown section. Leave blank to
+# include all pages in the folder.
+
+# Order of episodes in your lesson
+episodes:
+- 01-introduction.md
+- 02-reconstruction-output.md
+- 03-analysis.md
+- 04-full_chain_analysis.md
+- 05-additional-resources.md
+
+# Information for Learners
+learners:
+- setup.md
+- reference.md
+- branch-dictionary.md
+- exercise-scripts.md
+- tree-pruning-script.md
+- examples-repo.md
+- discuss.md
+
+# Information for Instructors
+instructors:
+- instructor-notes.md
+
+# Learner Profiles
+profiles:
+- learner-profiles.md
+
+# Customisation ---------------------------------------------
+#
+# This space below is where custom yaml items (e.g. pinning
+# sandpaper and varnish versions) should live
diff --git a/data/.gitkeep b/data/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/_episodes/01-introduction.md b/episodes/01-introduction.md
similarity index 62%
rename from _episodes/01-introduction.md
rename to episodes/01-introduction.md
index 8ae51df..ae8b891 100644
--- a/_episodes/01-introduction.md
+++ b/episodes/01-introduction.md
@@ -2,35 +2,45 @@
title: "Introduction"
teaching: 5
exercises: 5
-questions:
-- "How do I locate and access the simulation output?"
-objectives:
-- "Understand how the simulation output is organised."
-- "Download a file for the next step of the tutorial."
-keypoints:
-- "Use `xrdcp` from within eic-shell to copy files to your local environment."
---
+::::::::::::::::::::::::::::::::::::::::::::: questions
+
+- How do I locate and access the simulation output?
+
+:::::::::::::::::::::::::::::::::::::::::::::
+
+::::::::::::::::::::::::::::::::::::::::::::: objectives
+
+- Understand how the simulation output is organised.
+- Download a file for the next step of the tutorial.
+
+:::::::::::::::::::::::::::::::::::::::::::::
+
More detailed information on the simulation productions, including the information presented below, can be found on the [Simulation Production Campaign Website](https://eic.github.io/epic-prod/).
-**Note that as of March 2026, Rucio will soon become the default and preferred method to browse and find files and datasets. A tutorial on using Rucio for this purpose will be presented soon, please see [here](https://eic.github.io/tutorial-file-access/) for the latest version of this tutorial.**
+**Note that as of March 2026, Rucio will soon become the default and preferred method to browse and find files and datasets. A tutorial on using Rucio for this purpose will be presented soon, please see the [File Access Tutorial](https://eic.github.io/tutorial-file-access/) for the latest version of this tutorial.**
## Simulation Files Organization
There are three broad classes of files stored on xrootd, each in their own directory:
+
- EVGEN: The input hepmc3 datasets
- E.g. some files that have been supplied by a physics event generator
- FULL: The full GEANT4 output root files (usually only saved for a fraction of runs)
- If running a simulation yourself, this would be your output from processing npsim
- RECO: The output root files from the reconstruction
- - And again, if running yourself, this would be your output from EICrecon (after you've used your awesome new reconstruction algorithm from the later tutorial of course)
+ - And again, if running yourself, this would be your output from EICrecon (after you've used your awesome new reconstruction algorithm from the [Reconstruction algorithms tutorial](https://eic.github.io/tutorial-reconstruction-algorithms/) of course)
+
+Most users will interact with the files in the RECO directory and that is what we will focus on in this tutorial. Within the RECO directory, files are organized by campaign (26.02.0 for the February 2026 campaign, for example), detector configuration and then physics process. Each physics process will have different sub directories, for example generator version, energy, or Q2. The directory structure and number of reconstructed files for each campaign can be found on the [Simulation Website reconstruction campaigns page](https://eic.github.io/epic-prod/campaigns/campaigns_reco.html).
+
+::::::::::::::::::::::::::::::::::::::::::::: callout
-Most users will interact with the files in the RECO directory and that is what we will focus on in this tutorial. Within the RECO directory, files are organized by campaign (26.02.0 for the February 2026 campaign, for example), detector configuration and then physics process. Each physics process will have different sub directories, for example generator version, energy, or Q2. The directory structure and number of reconstructed files for each campaign can be found on the Simulation Website [here](https://eic.github.io/epic-prod/campaigns/campaigns_reco.html).
+**Note that campaigns more than ~6 months old will not directly be accessible.**
+**If you are running this tutorial and encounter a file access error, check the campaign you are trying to access.**
+**Where possible, use the latest campaign available.**
-> **Note that campaigns more than ~6 months old will not directly be accessible.**
-> **If you are running this tutorial and encounter a file access error, check the campaign you are trying to access.**
-> **Where possible, use the latest campaign available.**
-{: .callout}
+:::::::::::::::::::::::::::::::::::::::::::::
## Download a file for the next step!
@@ -38,35 +48,48 @@ We will need a file to analyse going forward, if you have not done so, download
Grab a file from -
-```console
+```bash
epic:/RECO/26.02.0/epic_craterlake/DIS/BeAGLE1.03.02-1.2/eHe3/10x110/q2_2to10/
```
-> Reminder, you can check the *content* of files within this dataset via:
-> ```bash
-> rucio did content list --short epic:/RECO/26.02.0/epic_craterlake/DIS/BeAGLE1.03.02-1.2/eHe3/10x110/q2_2to10
-> ```
-> and check the location of files in the dataset via:
-> ```bash
-> rucio replica list file --protocols root --pfns --rses isopenaccess epic:/RECO/26.02.0/epic_craterlake/DIS/BeAGLE1.03.02-1.2/eHe3/10x110/q2_2to10
-> ```
-{: .callout}
+::::::::::::::::::::::::::::::::::::::::::::: callout
+
+Reminder, you can check the *content* of files within this dataset via:
+
+```bash
+rucio did content list --short epic:/RECO/26.02.0/epic_craterlake/DIS/BeAGLE1.03.02-1.2/eHe3/10x110/q2_2to10
+```
+
+and check the location of files in the dataset via:
+
+```bash
+rucio replica list file --protocols root --pfns --rses isopenaccess epic:/RECO/26.02.0/epic_craterlake/DIS/BeAGLE1.03.02-1.2/eHe3/10x110/q2_2to10
+```
+
+:::::::::::::::::::::::::::::::::::::::::::::
For example -
-```console
+```bash
xrdcp root://dtn-eic.jlab.org:1094//volatile/eic/EPIC//RECO/26.02.0/epic_craterlake/DIS/BeAGLE1.03.02-1.2/eHe3/10x110/q2_2to10/BeAGLE1.03.02-1.2_DIS_eHe3_10x110_q2_2to10_ab.0001.eicrecon.edm4eic.root
```
Note that the ./ at the end is the target location to copy to. Change this as desired.
-> Note that we can also specify a different filename to copy to as we could with a normal cp command. You might want to do this as the filename is a little cumbersome.
-> I called mine `3He_10x110_Feb26Campaign.root`, just replace ./ with your file name of choice.
-{: .callout}
+::::::::::::::::::::::::::::::::::::::::::::: callout
+
+Note that we can also specify a different filename to copy to as we could with a normal cp command. You might want to do this as the filename is a little cumbersome.
+I called mine `3He_10x110_Feb26Campaign.root`, just replace ./ with your file name of choice.
+
+:::::::::::::::::::::::::::::::::::::::::::::
-You can also stream the file if you prefer, just copy the path of the file above. You will need to modify the scripts later in the tutorial accordingly to account for this. Check the [File Access Tutorial](https://eic.github.io/epic-prod/) for information and examples on how to do this.
+You can also stream the file if you prefer, just copy the path of the file above. You will need to modify the scripts later in the tutorial accordingly to account for this. Check the [File Access Tutorial](https://eic.github.io/tutorial-file-access/) for information and examples on how to do this.
+
+::::::::::::::::::::::::::::::::::::::::::::: callout
+
+Typically, if you are processing more than a handful of files, it is probably best to stream files from the server rather than downloading a local copy of all files.
+
+:::::::::::::::::::::::::::::::::::::::::::::
-> Typically, if you are processing more than a handful of files, it is probably best to stream files from the server rather than downloading a local copy of all files.
-{: .callout}
+
+::::::::::::::::::::::::::::::::::::::::::::: keypoints
+
+- Use `xrdcp` from within eic-shell to copy files to your local environment.
+
+:::::::::::::::::::::::::::::::::::::::::::::
diff --git a/_episodes/02-reconstruction-output.md b/episodes/02-reconstruction-output.md
similarity index 73%
rename from _episodes/02-reconstruction-output.md
rename to episodes/02-reconstruction-output.md
index 78e443d..d60b28c 100644
--- a/_episodes/02-reconstruction-output.md
+++ b/episodes/02-reconstruction-output.md
@@ -2,16 +2,20 @@
title: "The Reconstruction Output Tree"
teaching: 20
exercises: 20
-questions:
-- "What information is stored in the reconstruction output and how can we access it?"
-objectives:
-- "Become familiar with the tree branches"
-
-keypoints:
-- "Output trees contain a lot of information. Take time to explore what is available, identify what you want to try and do, find the relevant branches."
-- "The MCParticles branch holds information on generator level particles, critical for use in comparing to what we actually detect!"
---
+::::::::::::::::::::::::::::::::::::::::::::: questions
+
+- What information is stored in the reconstruction output and how can we access it?
+
+:::::::::::::::::::::::::::::::::::::::::::::
+
+::::::::::::::::::::::::::::::::::::::::::::: objectives
+
+- Become familiar with the tree branches.
+
+:::::::::::::::::::::::::::::::::::::::::::::
+
What we generally call the simulation output are root trees generated by the reconstruction software, [EICrecon](https://github.com/eic/EICrecon/tree/main). The branches which appear in the output trees and their content are determined by which EICrecon factories and algorithms are run.
- If you're trying to open interactive windows such as a TBrowser via X-forwarding, it's likely to be very slow. You may wish to just copy the file to your local machine and open it there.
@@ -22,11 +26,25 @@ What we generally call the simulation output are root trees generated by the rec
The output tree contains various branches appropriate for the individual detector subsystems. For example, the calorimeter subsystems will have branches for reconstructed hits and clusters. In addition to individual subsystem information, there are also branches for reconstructed quantities (e.g. reconstructed particles, inclusive kinematics, and jets) which may combine information from several subsystems. There are also branches encoding relationships between different reconstructed quantities as well as reconstructed and truth quantities.
-> Exercise
-> - Stream a simulation output tree from within root (see previous lesson) and browse the structure by calling `new TBrowser()`
-> - Take some time to explore the branches of the tree. What information is included for various subsystems? What are some of the reconstructed quantities?
-> - Try plotting some basic quantities. Plot the cluster energy of the negative endcap ECal - do you see the peak from the scattered electron?
-{: .challenge}
+::::::::::::::::::::::::::::::::::::::::::::: challenge
+
+## Exercise
+
+- Stream a simulation output tree from within root (see previous lesson) and browse the structure by calling `new TBrowser()`
+- Take some time to explore the branches of the tree. What information is included for various subsystems? What are some of the reconstructed quantities?
+- Try plotting some basic quantities. Plot the cluster energy of the negative endcap ECal - do you see the peak from the scattered electron?
+
+::::::::::::::: solution
+
+Opening the file and calling `new TBrowser()` shows the full list of branches. Double-clicking
+`EcalEndcapNClusters.energy` draws the cluster energy distribution for the negative endcap ECal, in
+which you should be able to identify the peak from the scattered electron. Take your time exploring
+the subsystem branches (hits, clusters) and reconstructed quantities (reconstructed particles,
+inclusive kinematics, jets).
+
+:::::::::::::::
+
+:::::::::::::::::::::::::::::::::::::::::::::
For the last part, some extra tips are included below.
@@ -36,45 +54,45 @@ We can navigate around the TBrowser and get it to draw quantities by simply doub
Alternatively, we can also plot variables to histograms of our own choosing on the fly. We can do this via -
-```console
+```c++
root $FILE
events->Draw("QUANTITY")
```
where $FILE is the file we want to open and "QUANTITY" is the thing we want to draw. For example -
-```console
+```c++
events->Draw("MCParticles.momentum.z")
```
will draw the MCParticles.momentum.z branch. So far, so much like just double clicking the TBrowser. However, we could define a new histogram and fill this variable to it -
-```console
+```c++
events->Draw("MCParticles.momentum.z>>h1(100,0,100)")
```
where h1 is our new histogram. This has 100 bins from 0 to 100. We can also apply selection criteria (i.e. cuts) on the fly. These do not need to be on the same variable we are drawing! For example -
-```console
+```c++
events->Draw("MCParticles.momentum.z>>h1(360,-60,300)", "MCParticles.charge<0")
```
will fill our histogram only with negatively charged particles. We can add more conditions if we want -
-```console
+```c++
events->Draw("MCParticles.momentum.z>>h1(360,-60,300)", "MCParticles.charge<0 && MCParticles.mass>1")
```
where we now also require that the particle mass is >1.
We can also add drawing options -
-```console
+```c++
events->Draw("MCParticles.momentum.z>>h1(360,-60,300)", "MCParticles.charge<0", "HISTERR")
```
such as adding error bars. We can also make 2D histograms in the same way -
-```console
+```c++
events->Draw("MCParticles.momentum.z:MCParticles.charge>>h2(40,-2,2, 360,-60,300)", "", "COLZ")
```
note the order of defining the binning. It's not what you might expect. Also, to interpret the result (as is often the case with 2D histograms), you might want to set a log Z scale -
-```console
+```c++
gPad->SetLogz()
```
@@ -82,7 +100,7 @@ gPad->SetLogz()
As you have probably seen by now, the tree in our file contains a lot of information!
-For the rest of the tutorial, we will be focusing on a relatively small subset. This is generally true with most analyses, it's rare we'll be retaining and looking at every branch. A brief (and incomplete!) dictionary of commonly used branches, including those we'll need in this tutorial, is included in the "Extras" section. See the tab at the top of the page or follow [this link]({{ page.root }}{% link _extras/branch_dictionary.md %}).
+For the rest of the tutorial, we will be focusing on a relatively small subset. This is generally true with most analyses, it's rare we'll be retaining and looking at every branch. A brief (and incomplete!) dictionary of commonly used branches, including those we'll need in this tutorial, is included in the "Extras" section. See the tab at the top of the page or follow the [branch dictionary](../learners/branch-dictionary.md).
To find out more about a particular collection, you could also take a look at the [edm4eic datamodel](https://github.com/eic/EDM4eic/blob/main/edm4eic.yaml). For some definitions and explanations, you may need to refer to the [edm4hep datamodel](https://github.com/key4hep/EDM4hep/blob/main/edm4hep.yaml).
@@ -96,6 +114,16 @@ You may wonder how the specific branches of the tree have actually been populate
This is covered in more detail in the [Understanding the Simulation Output](https://eic.github.io/tutorial-understanding-sim-output/) tutorial. In particular, [Episode 3](https://eic.github.io/tutorial-understanding-sim-output/02-eicrecon/index.html) of that tutorial details how you can identify the sequence of algorithms utilised to generate a specific output branch. In the case of the example in this tutorial, the track reconstruction is detailed.
-> Work through the algorithms and factories tutorial. Afterwards, take a closer look at this file.
-> See if you can figure out which algorithm or factory was responsible for creating some of the included branches in this file.
-{: .callout}
+::::::::::::::::::::::::::::::::::::::::::::: callout
+
+Work through the algorithms and factories tutorial. Afterwards, take a closer look at this file.
+See if you can figure out which algorithm or factory was responsible for creating some of the included branches in this file.
+
+:::::::::::::::::::::::::::::::::::::::::::::
+
+::::::::::::::::::::::::::::::::::::::::::::: keypoints
+
+- Output trees contain a lot of information. Take time to explore what is available, identify what you want to try and do, find the relevant branches.
+- The MCParticles branch holds information on generator level particles, critical for use in comparing to what we actually detect!
+
+:::::::::::::::::::::::::::::::::::::::::::::
diff --git a/_episodes/03-analysis.md b/episodes/03-analysis.md
similarity index 63%
rename from _episodes/03-analysis.md
rename to episodes/03-analysis.md
index 213c92c..c8d46fc 100644
--- a/_episodes/03-analysis.md
+++ b/episodes/03-analysis.md
@@ -2,33 +2,42 @@
title: "Analyzing the Reconstruction Output"
teaching: 20
exercises: 40
-questions:
-- "How does one utilize the reconstruction output trees to do an analysis?"
-objectives:
-- "Become familiar with methods for reading the trees"
-- "Understand how to access truth/particle information"
-- "Find track efficiency and resolution"
-keypoints:
-- "Flat tree structure provides flexibility in analysis."
-- "The ReconstructedChargedParticles branch holds information on reconstructed tracks."
---
+::::::::::::::::::::::::::::::::::::::::::::: questions
+
+- How does one utilize the reconstruction output trees to do an analysis?
+
+:::::::::::::::::::::::::::::::::::::::::::::
+
+::::::::::::::::::::::::::::::::::::::::::::: objectives
+
+- Become familiar with methods for reading the trees.
+- Understand how to access truth/particle information.
+- Find track efficiency and resolution.
+
+:::::::::::::::::::::::::::::::::::::::::::::
+
So far, we have only looked at (and plotted) some information from our file interactively. This is very useful and can help us identify the variables we want to deal with. However, we can't really use these techniques to conduct a full analysis of the data. To do so, we typically use a script or macro. In this part of the tutorial, we will create a script that we can use to do a relatively straightforward analysis of our file.
-> Note:
-> - The [branch dictionary]({{ page.root }}{% link _extras/branch_dictionary.md %}) outlines all of the branches we will need to utilise in this section.
-> - If you want, you can prune the branches you don't need from the input file using the [TreePrune.C script]({{ page.root }}{% link _extras/tree_pruning_script.md %}).
-{: .callout}
+::::::::::::::::::::::::::::::::::::::::::::: callout
+
+Note:
+
+- The [branch dictionary](../learners/branch-dictionary.md) outlines all of the branches we will need to utilise in this section.
+- If you want, you can prune the branches you don't need from the input file using the [TreePrune.C script](../learners/tree-pruning-script.md).
+
+:::::::::::::::::::::::::::::::::::::::::::::
## Reading the Output Trees
The simulation output trees are "flat" in the sense that there is no event class structure embedded within the tree and no additional libraries are needed to handle the output. Therefore, the end user can simply read the values stored in each branch using whatever method/workflow they are most comfortable with. Examples of several common methods for reading the trees are provided below. We will see a ROOT TTreeReader based example using a ROOT macro and a python/uproot based version. There is also an example using the (relatively) new RDataFrame class of ROOT. During the tutorial, you should try the exercise using whichever language you feel most comfortable with. Five different approaches are currently provieded:
-- [TTreeReaders](https://eic.github.io/tutorial-analysis/03-analysis/index.html#sample-analysis-with-root-ttreereader-track-efficiency-and-resolution) - ROOT/C based
-- [Python/Uproot - Pythonic](https://eic.github.io/tutorial-analysis/03-analysis/index.html#sample-analysis-with-pythonuproot---pythonic-method-track-efficiency-and-resolution) - A pythonic based appoach using arrays directly
-- [Python/Uproot - ROOT/Pyroot](https://eic.github.io/tutorial-analysis/03-analysis/index.html#sample-analysis-with-pythonuproot---rootpyroot-style-track-efficiency-and-resolution) - An approach using Pyroot, halfway house between C and python
-- [ROOT RDataFrames](https://eic.github.io/tutorial-analysis/03-analysis/index.html#root-rdataframes) - An approach using RDataFrames
-- [PODIO](https://eic.github.io/tutorial-analysis/03-analysis/index.html#podio---direct-analysis) - An approach using the Plane Old Data IO (PODIO) approach. Use the flat datastructure directly
+- [TTreeReaders](#sample-analysis-with-root-ttreereader-track-efficiency-and-resolution) - ROOT/C based
+- [Python/Uproot - Pythonic](#sample-analysis-with-python-uproot-pythonic-method-track-efficiency-and-resolution) - A pythonic based appoach using arrays directly
+- [Python/Uproot - ROOT/Pyroot](#sample-analysis-with-python-uproot-root-pyroot-style-track-efficiency-and-resolution) - An approach using Pyroot, halfway house between C and python
+- [ROOT RDataFrames](#root-rdataframes) - An approach using RDataFrames
+- [PODIO](#podio-direct-analysis) - An approach using the Plane Old Data IO (PODIO) approach. Use the flat datastructure directly
## Sample Analysis with ROOT TTreeReader: Track Efficiency and Resolution
@@ -51,38 +60,42 @@ void trackAnalysis(TString infile="path_to_your_simu_file")
We will need momentum, generator status, and particle species information for the truth particles and momentum information for the reconstructed tracks. The reconstructed track information can be accessed from two different branches: CentralCKFTrackParameters and ReconstructedChargedParticles. We can access these branches using a TTreeReaderArray.
-> ROOT TTreeReaderArrays:
->
->TTreeReader and the associated TTreeReaderArray is a simple interface for reading data from a TTree. The class description and examples can be seen [here](https://root.cern/doc/v630/classTTreeReader.html). To instantiate the reader and access values from a given branch (e.g. the MCParticles branch), one would use the following calls:
->
-> ```c++
-> // Set up input file chain
-> TChain *mychain = new TChain("events");
-> mychain->Add(infile);
->
-> // Initialize reader
-> TTreeReader tree_reader(mychain);
->
-> // Access whatever data-members you need
-> TTreeReaderArray partGenStat(tree_reader, "MCParticles.generatorStatus");
-> TTreeReaderArray partMomX(tree_reader, "MCParticles.momentum.x");
-> ...
-> ```
->
-> The branches and their members can be viewed by opening a file with TBrowser (`new TBrowser()`) from within ROOT. Once you have defined the `TTreeReaderArray` objects for the data-members you want to look at, you can loop over the events and the members within that event:
->
-> ```c++
-> while(tree_reader.Next()) { // Loop over events
-> for(unsigned int i=0; i {
-> int particleStatus = partGenStat[i]; // Access data-members as you would an array
-> float particleXMomentum = partMomX[i]; // partMomX should have same number of entries as partGenStat because they are in the same branch
-> ...
-> }
-> }
-> ```
-> All members of the same branch should have the same number of entries, so it is sufficient to use any member of the branch to set the limit of your loop.
-{: .callout}
+::::::::::::::::::::::::::::::::::::::::::::: callout
+
+ROOT TTreeReaderArrays:
+
+TTreeReader and the associated TTreeReaderArray is a simple interface for reading data from a TTree. The class description and examples can be seen in [the ROOT TTreeReader class documentation](https://root.cern/doc/v630/classTTreeReader.html). To instantiate the reader and access values from a given branch (e.g. the MCParticles branch), one would use the following calls:
+
+```c++
+// Set up input file chain
+TChain *mychain = new TChain("events");
+mychain->Add(infile);
+
+// Initialize reader
+TTreeReader tree_reader(mychain);
+
+// Access whatever data-members you need
+TTreeReaderArray partGenStat(tree_reader, "MCParticles.generatorStatus");
+TTreeReaderArray partMomX(tree_reader, "MCParticles.momentum.x");
+...
+```
+
+The branches and their members can be viewed by opening a file with TBrowser (`new TBrowser()`) from within ROOT. Once you have defined the `TTreeReaderArray` objects for the data-members you want to look at, you can loop over the events and the members within that event:
+
+```c++
+while(tree_reader.Next()) { // Loop over events
+ for(unsigned int i=0; i recoAssoc(tree_reader, "_ReconstructedChargedParticleAssoc
TTreeReaderArray simuAssoc(tree_reader, "_ReconstructedChargedParticleAssociations_sim.index");
```
-The last two lines encode the association between a ReconstructedChargedParticle and an MCParticle where the matching is determined in the [ParticlesWithPID](https://github.com/eic/EICrecon/blob/main/src/algorithms/pid/ParticlesWithPID.cc) algorithm which generates the ReconstructedChargedParticle objects.
+The last two lines encode the association between a ReconstructedChargedParticle and an MCParticle where the matching is determined by the EICrecon [reconstruction algorithms](https://github.com/eic/EICrecon/tree/main/src/algorithms/reco) which generate the ReconstructedChargedParticle objects.
+
+::::::::::::::::::::::::::::::::::::::::::::: callout
+
+Compiling ROOT Macros:
-> Compiling ROOT Macros:
-> - If you are analysing a large number of events, you may wish to compile your macro to increase throughput. An example of how you can create and compile a root macro is included in the [Exercise Scripts section](https://eic.github.io/tutorial-analysis/exercise_scripts/index.html#compiled-root-scripts)
-{: .callout}
+- If you are analysing a large number of events, you may wish to compile your macro to increase throughput. An example of how you can create and compile a root macro is included in the [Exercise Scripts section](../learners/exercise-scripts.md#compiled-root-scripts).
+
+:::::::::::::::::::::::::::::::::::::::::::::
### Efficiency Analysis
-> Hint:
-> Refer to [the script template](https://eic.github.io/tutorial-analysis/exercise_scripts/index.html#efficiencyanalysisc) if you're having trouble putting things in the right place.
-{: .callout}
+::::::::::::::::::::::::::::::::::::::::::::: callout
+
+Hint:
+Refer to [the script template](../learners/exercise-scripts.md#efficiencyanalysisc) if you're having trouble putting things in the right place.
+
+:::::::::::::::::::::::::::::::::::::::::::::
Now that we have access to the data we need we will begin constructing our efficiency plots, starting with efficiency as a function of the true particle pseudorapidity. The basic strategy is outlined below:
@@ -181,24 +201,48 @@ while(tree_reader.Next()) { // Loop over events
We should now have everything we need to find the track efficiency as a function of pseudorapidity. To run the macro and produce an output file containing the histograms we defined, simply type `root -l -q trackAnalysis.C`. After the macro runs, you can open the root file to inspect the histograms. The efficiency can be found by taking the ratio of matchedPartEta over partEta.
-> Question:
-> - Do the histogram ranges make sense?
-> - We plot the distance between thrown and reconstructed charged partices, does this distribution look reasonable?
-> - When filling the matchedPartEta histogram (the numerator in our efficiency), why do we use again the true thrown eta instead of the associated reconstructed eta?
-{: .callout}
+::::::::::::::::::::::::::::::::::::::::::::: callout
+
+Question:
+
+- Do the histogram ranges make sense?
+- We plot the distance between thrown and reconstructed charged partices, does this distribution look reasonable?
+- When filling the matchedPartEta histogram (the numerator in our efficiency), why do we use again the true thrown eta instead of the associated reconstructed eta?
+
+:::::::::::::::::::::::::::::::::::::::::::::
+
+::::::::::::::::::::::::::::::::::::::::::::: challenge
+
+## Exercise
+
+For all **scattered electrons**, **charged pions** and **protons** in our events:
+
+- Find the efficiency as a function of particle momentum. Are there cuts on any other quantities you should place to get a sensible result?
+- Find the efficiency for some 2-D correlations: momentum vs eta; phi vs eta
+- Plot some kinematic distributions (momentum, eta, etc) for all ReconstructedChargedParticles, not just those that are associated with a thrown particle
-> Exercise:
-> For all **scattered electrons**, **charged pions** and **protons** in our events:
-> - Find the efficiency as a function of particle momentum. Are there cuts on any other quantities you should place to get a sensible result?
-> - Find the efficiency for some 2-D correlations: momentum vs eta; phi vs eta
-> - Plot some kinematic distributions (momentum, eta, etc) for all ReconstructedChargedParticles, not just those that are associated with a thrown particle
-{: .challenge}
+::::::::::::::: solution
+
+Build the efficiency exactly as for eta, but fill numerator/denominator histograms in the relevant
+kinematic variable and divide at the end. Select each species by its PDG code (11, 211, 2212) and
+require `generatorStatus == 1`. A momentum acceptance cut is usually needed to avoid dividing by
+near-empty bins at very low momentum. For the 2-D correlations, fill 2-D histograms of the thrown
+quantity (denominator) and the matched-thrown quantity (numerator) and divide. See the
+[efficiency script template](../learners/exercise-scripts.md#efficiencyanalysisc) for a worked
+version.
+
+:::::::::::::::
+
+:::::::::::::::::::::::::::::::::::::::::::::
### Resolution Analysis
-> Hint:
-> Refer to [the script template](https://eic.github.io/tutorial-analysis/exercise_scripts/index.html#resolutionanalysisc) if you're having trouble putting things in the right place.
-{: .callout}
+::::::::::::::::::::::::::::::::::::::::::::: callout
+
+Hint:
+Refer to [the script template](../learners/exercise-scripts.md#resolutionanalysisc) if you're having trouble putting things in the right place.
+
+:::::::::::::::::::::::::::::::::::::::::::::
Next, we will look at track momentum resolution, that is, how well the momentum of the reconstructed track matches that of the thrown particle. We should have all of the "infrastructure" we need in place to do the analysis, we just need to define the appropriate quantities and make the histograms. It only makes sense to define the resolution for tracks and particles which are associated with one another, so we will work within the loop over associations. Define the resolution expression and fill a simple histogram:
@@ -220,21 +264,43 @@ for(unsigned int j=0; j Exercise:
-> For all **scattered electrons**, **charged pions** and **protons** in our events:
-> - Make 2-D plots of resolution vs true momentum and vs true pseudorapidity.
-{: .challenge}
+::::::::::::::::::::::::::::::::::::::::::::: challenge
+
+## Exercise
+
+For all **scattered electrons**, **charged pions** and **protons** in our events:
+
+- Make 2-D plots of resolution vs true momentum and vs true pseudorapidity.
+
+::::::::::::::: solution
+
+Inside the association loop, compute `momRes = (recMom.Mag() - trueMom.Mag())/trueMom.Mag()` and
+fill a 2-D histogram with the true momentum (or true pseudorapidity) on one axis and `momRes` on the
+other, once per matched particle of the chosen species. Profiling these 2-D histograms (or taking
+the width of `momRes` in slices) gives the resolution as a function of the kinematic variable. See
+the [resolution script template](../learners/exercise-scripts.md#resolutionanalysisc).
+
+:::::::::::::::
-> Question:
-> - Will the histogram ranges for each particle species be the same?
-> - Could we present the resolution values in a more understandable way?
-{: .callout}
+:::::::::::::::::::::::::::::::::::::::::::::
+::::::::::::::::::::::::::::::::::::::::::::: callout
-## Sample Analysis with Python/uproot - Pythonic Method: Track Efficiency and Resolution
+Question:
-> For some examples of using uproot to access information in .root files, please consult [this notebook](https://github.com/eic/HSF-India/blob/main/Working_With_Uproot/Working_With_Uproot_Standalone.ipynb) which can be run in Google Collab.
-{: .callout}
+- Will the histogram ranges for each particle species be the same?
+- Could we present the resolution values in a more understandable way?
+
+:::::::::::::::::::::::::::::::::::::::::::::
+
+
+## Sample Analysis with Python uproot Pythonic Method: Track Efficiency and Resolution
+
+::::::::::::::::::::::::::::::::::::::::::::: callout
+
+For some examples of using uproot to access information in .root files, please consult [this notebook](https://github.com/eic/HSF-India/blob/main/Working_With_Uproot/Working_With_Uproot_Standalone.ipynb) which can be run in Google Collab.
+
+:::::::::::::::::::::::::::::::::::::::::::::
If you are more familiar with python than you are with C/C++, you might find that using a python based root macro is easier for you. Outlined below are sample blocks of code for creating and running a python based analysis script.
@@ -306,11 +372,16 @@ partPdg = tree["MCParticles.PDG"].array()
```
Uproot effectively takes the information in the tree, and turns it into an array. We can then access and manipulate this array in the same way that we can with any array in python.
-> Warning: Note that if you are using an older version of uproot (v2.x.x), you will need to access the branches slightly differently via -
-> ```python
-> partGenStat = tree.array("MCParticles.generatorStatus")
-> ```
-{: .callout}
+::::::::::::::::::::::::::::::::::::::::::::: callout
+
+Warning: Note that if you are using an older version of uproot (v2.x.x), you will need to access the branches slightly differently via -
+
+```python
+partGenStat = tree.array("MCParticles.generatorStatus")
+```
+
+:::::::::::::::::::::::::::::::::::::::::::::
+
Once you create your script and add the template code in, you can try running it with``python3 trackAnalysis.py`` or ``python trackAnalysis.py``. At the moment, it shouldn't *do* anything, but we can change that!
Try assigning a quantity to an array, such as the MC particles PDG values above and printing some values of that array. Or, perhaps try printing the length of that array. We could also quickly make a plot of the values with -
@@ -322,13 +393,18 @@ plt.clf # Clear figure
```
The script should now write out a figure, ``TestOut.png`` when you run it, showing the MC PDG values of entries in the file.
-> We did not specify a number of bins or a range, so our plot looks a bit odd. What might be a useful range and number of bins to use here?
-> We can specify our number of bins and our range with -
-> ```python
-> plt.hist(ak.flatten(partPdg),bins=NBins, range=(X,Y),alpha=0.75, color=kP6[0])
-> ```
-> With NBins being our number of bins and X and Y being our min/max range - think carefully about these numbers!
-{: .callout}
+::::::::::::::::::::::::::::::::::::::::::::: callout
+
+We did not specify a number of bins or a range, so our plot looks a bit odd. What might be a useful range and number of bins to use here?
+We can specify our number of bins and our range with -
+
+```python
+plt.hist(ak.flatten(partPdg),bins=NBins, range=(X,Y),alpha=0.75, color=kP6[0])
+```
+
+With NBins being our number of bins and X and Y being our min/max range - think carefully about these numbers!
+
+:::::::::::::::::::::::::::::::::::::::::::::
Note that we don't really need to define individual arrays either, we can just directly access the array we want once we've converted the branch to a series of arrays -
@@ -338,13 +414,18 @@ plt.hist(ak.flatten(MCPartBr["MCParticles.PDG"]),alpha=0.75, color=kP6[0])
plt.savefig("TestOut.png", dpi = (160))
```
-> Note:
-> Remember to call:
-> ```python
-> plt.clf() # Clear figure
-> ```
-> After a figure to avoid drawing on the same plot.
-{: .callout}
+::::::::::::::::::::::::::::::::::::::::::::: callout
+
+Note:
+Remember to call:
+
+```python
+plt.clf() # Clear figure
+```
+
+After a figure to avoid drawing on the same plot.
+
+:::::::::::::::::::::::::::::::::::::::::::::
We can also define and apply filters to our arrays as we plot or print from them -
@@ -363,14 +444,20 @@ plt.hist(ak.flatten(MCPartBr["MCParticles.PDG"][BoolStablePos]),alpha=0.75, colo
plt.savefig("TestOut3.png", dpi = (160))
```
-> We did not specify a number of bins or a range, so our plot looks a bit odd. What might be a useful range and number of bins to use here?
-{: .callout}
+::::::::::::::::::::::::::::::::::::::::::::: callout
+
+We did not specify a number of bins or a range, so our plot looks a bit odd. What might be a useful range and number of bins to use here?
+
+:::::::::::::::::::::::::::::::::::::::::::::
### Efficiency Analysis
-> Hint:
-> Refer to [the script template](https://eic.github.io/tutorial-analysis/exercise_scripts/index.html#pythonic_efficiencyanalysispy) if you're having trouble putting things in the right place.
-{: .callout}
+::::::::::::::::::::::::::::::::::::::::::::: callout
+
+Hint:
+Refer to [the script template](../learners/exercise-scripts.md#pythonic_efficiencyanalysispy) if you're having trouble putting things in the right place.
+
+:::::::::::::::::::::::::::::::::::::::::::::
Our approach here is a bit different to the TTreeReader example, but we will still need to utilise our simulation and reconstruction association IDs. We can access them via -
@@ -463,37 +550,66 @@ For our efficiency. We need to compare our thrown particles of a given type to o
- How can you select the particles we detected of a specific type?
- Note, this does not mean we need our reconstructed values.
-> Exercise:
-> For all **scattered electrons**, **charged pions** and **protons** in our events:
-> - Find the efficiency as a function of particle momentum. Are there cuts on any other quantities you should place to get a sensible result?
-> - Find the efficiency for some 2-D correlations: momentum vs eta; phi vs eta
-> - Plot some kinematic distributions (momentum, eta, etc) for all ReconstructedChargedParticles, not just those that are associated with a thrown particle
-{: .challenge}
-
-> Hint:
-> Getting the right arrays here is a bit tricky. We want three different things -
-> - Our MC particles (truth information), regardless of whether we have a matching track or not. This is just:
-> - "MCPartBr['MCParticles.QUANTITY']"[SELECTION_CUTS] - We do not need to index this by the SimID
-> - Our MC particles (truth information) that do have a matching reconstructed track, we just need to index these by the SimID:
-> - "MCPartBr['MCParticles.QUANTITY'][SimID]" - We can then apply selection criteria
-> - The Reconstructed particle information for events which correspond to a real MC track, we just need to index these by our RecID:
-> - "ReconChPartBr['ReconstructedChargedParticles.QUANTITY'][RecID]"
-{: .callout}
-
-> 2D Histograms: We can make 2D histograms in python via -
-> ```python
-> plt.hist2d(np.asarray(ak.flatten(Quantity1)), np.asarray(ak.flatten(Quantity2)), bins=[NBinsX,NBinsY], range=[[XLow,XHigh],[YLow,YHigh]], cmin=1)
-> cb = plt.colorbar()
-> cb.set_label('Counts/bin')
-> ```
-> We can set titles etc as usual. Simply swap on the bin values and ranges, as well as the quantities as needed. Make sure your arrays contain equal numbers of entries.
-{: .callout}
+::::::::::::::::::::::::::::::::::::::::::::: challenge
+
+## Exercise
+
+For all **scattered electrons**, **charged pions** and **protons** in our events:
+
+- Find the efficiency as a function of particle momentum. Are there cuts on any other quantities you should place to get a sensible result?
+- Find the efficiency for some 2-D correlations: momentum vs eta; phi vs eta
+- Plot some kinematic distributions (momentum, eta, etc) for all ReconstructedChargedParticles, not just those that are associated with a thrown particle
+
+::::::::::::::: solution
+
+The denominator is the truth distribution for the chosen species (select on
+`MCParticles.PDG` and `generatorStatus == 1`, **not** indexed by `SimID`). The numerator is the
+truth distribution for particles that were reconstructed (the same selection, indexed by `SimID`).
+Dividing these two `np.histogram` outputs bin-by-bin (guarding against divide-by-zero as shown
+above) gives the efficiency in that variable. For the 2-D correlations use `np.histogram2d` for
+numerator and denominator and divide. See the
+[pythonic efficiency template](../learners/exercise-scripts.md#pythonic_efficiencyanalysispy).
+
+:::::::::::::::
+
+:::::::::::::::::::::::::::::::::::::::::::::
+
+::::::::::::::::::::::::::::::::::::::::::::: callout
+
+Hint:
+Getting the right arrays here is a bit tricky. We want three different things -
+
+- Our MC particles (truth information), regardless of whether we have a matching track or not. This is just:
+ - "MCPartBr['MCParticles.QUANTITY']"[SELECTION_CUTS] - We do not need to index this by the SimID
+- Our MC particles (truth information) that do have a matching reconstructed track, we just need to index these by the SimID:
+ - "MCPartBr['MCParticles.QUANTITY'][SimID]" - We can then apply selection criteria
+- The Reconstructed particle information for events which correspond to a real MC track, we just need to index these by our RecID:
+ - "ReconChPartBr['ReconstructedChargedParticles.QUANTITY'][RecID]"
+
+:::::::::::::::::::::::::::::::::::::::::::::
+
+::::::::::::::::::::::::::::::::::::::::::::: callout
+
+2D Histograms: We can make 2D histograms in python via -
+
+```python
+plt.hist2d(np.asarray(ak.flatten(Quantity1)), np.asarray(ak.flatten(Quantity2)), bins=[NBinsX,NBinsY], range=[[XLow,XHigh],[YLow,YHigh]], cmin=1)
+cb = plt.colorbar()
+cb.set_label('Counts/bin')
+```
+
+We can set titles etc as usual. Simply swap on the bin values and ranges, as well as the quantities as needed. Make sure your arrays contain equal numbers of entries.
+
+:::::::::::::::::::::::::::::::::::::::::::::
### Resolution Analysis
-> Hint:
-> Refer to [the script template](https://eic.github.io/tutorial-analysis/exercise_scripts/index.html#pythonic_resolutionanalysispy) if you're having trouble putting things in the right place.
-{: .callout}
+::::::::::::::::::::::::::::::::::::::::::::: callout
+
+Hint:
+Refer to [the script template](../learners/exercise-scripts.md#pythonic_resolutionanalysispy) if you're having trouble putting things in the right place.
+
+:::::::::::::::::::::::::::::::::::::::::::::
Next, we will look at track momentum resolution. The resolution tells us how well we can reconstruct our "true" value. For example. we might want to know how well we can determine the energy of our particles. As such, we could calculate the energy resolution. Our resolution is simply -
@@ -513,22 +629,45 @@ plt.savefig("TestOut5.png", dpi = (160))
Here we've calculated and plotted the X momentum resolution for our charged tracks that correspond to true electrons in our sample. Whilst this plot will give us a sense of what the tracking resolution is, we don't expect the resolution to be constant for all momenta or eta. We can get a more complete picture by plotting the resolution as a function of different kinematic quantities.
-> Exercise:
-> For all **scattered electrons**, **charged pions** and **protons** in our events:
-> - Make 2-D plots of resolution vs true momentum and vs true pseudorapidity.
-{: .challenge}
+::::::::::::::::::::::::::::::::::::::::::::: challenge
+
+## Exercise
+
+For all **scattered electrons**, **charged pions** and **protons** in our events:
+
+- Make 2-D plots of resolution vs true momentum and vs true pseudorapidity.
+
+::::::::::::::: solution
+
+Compute the resolution array `(reco - true)/true` for the matched particles of each species (both
+quantities indexed by `RecID`/`SimID` and filtered by species), then fill a `plt.hist2d` with the
+true momentum (or true pseudorapidity) on the x-axis and the resolution on the y-axis. Slicing the
+2-D histogram in x and taking the width of the resolution distribution gives the resolution as a
+function of that variable. See the
+[pythonic resolution template](../learners/exercise-scripts.md#pythonic_resolutionanalysispy).
+
+:::::::::::::::
-> Question:
-> - Will the histogram ranges for each particle species be the same?
-> - Could we present the resolution values in a more understandable way?
-{: .callout}
+:::::::::::::::::::::::::::::::::::::::::::::
-## Sample Analysis with Python/uproot - ROOT/Pyroot Style: Track Efficiency and Resolution
+::::::::::::::::::::::::::::::::::::::::::::: callout
-> Comment:
-> Despite using python/uproot, I have written these in a very "ROOT"/C way. Uproot converts our branches to arrays, so you can manipulate them in various fun ways using more pythonic methods if you want.
-> See the previous method for an example of this approach.
-{: .callout}
+Question:
+
+- Will the histogram ranges for each particle species be the same?
+- Could we present the resolution values in a more understandable way?
+
+:::::::::::::::::::::::::::::::::::::::::::::
+
+## Sample Analysis with Python uproot ROOT Pyroot Style: Track Efficiency and Resolution
+
+::::::::::::::::::::::::::::::::::::::::::::: callout
+
+Comment:
+Despite using python/uproot, I have written these in a very "ROOT"/C way. Uproot converts our branches to arrays, so you can manipulate them in various fun ways using more pythonic methods if you want.
+See the previous method for an example of this approach.
+
+:::::::::::::::::::::::::::::::::::::::::::::
If you are more familiar with python than you are with C/C++, you might find that using a python based root macro is easier for you. Outlined below are sample blocks of code for creating and running a python based analysis script.
@@ -562,40 +701,48 @@ ofile.Close()
```
Note that we are using the module uproot to access the data here. See [further documentation here](https://masonproffitt.github.io/uproot-tutorial/03-trees/index.html). You may also need some of the other included packages too.
-> We will use uproot a little bit like we use the TTreeReader in the other example. We can define the branches we want and assign them to arrays with uproot.
-> We can do this via:
-> ```python
-> # Open input file and define branches we want to look at with uproot
-> events_tree = up.open(infile)["events"]
-> # Get particle information# Get particle information
-> partGenStat = events_tree["MCParticles.generatorStatus"].array()
-> partMomX = events_tree["MCP articles.momentum.x"].array()
-> partMomY = events_tree["MCParticles.momentum.y"].array()
-> partMomZ = events_tree["MCParticles.momentum.z"].array()
-> partPdg = events_tree["MCParticles.PDG"].array()
->
-> # Get reconstructed track information
-> trackMomX = events_tree["ReconstructedChargedParticles.momentum.x"].array()
-> trackMomY = events_tree["ReconstructedChargedParticles.momentum.y"].array()
-> trackMomZ = events_tree["ReconstructedChargedParticles.momentum.z"].array()
-> ...
-> ```
-> We can then access them as an array in a loop -
-> ```python
-> # Add main analysis loop(s) below
-> for i in range(0, len(partGenStat)): # Loop over all events
-> for j in range(0, len(partGenStat[i])): # Loop over all thrown particles
-> if partGenStat[i][j] == 1: # Select stable particles
-> pdg = abs(partPdg[i][j]) # Get PDG for each stable particle
-> ...
-> ```
-> Uproot effectively takes the information in the tree, and turns it into an array. We can then access and manipulate this array in the same way that we can with any array in python.
->
-> Note that if you are using an older version of uproot (v2.x.x), you will need to access the branches slightly differently via -
-> ```python
-> partGenStat = events_tree.array("MCParticles.generatorStatus")
-> ```
-{: .callout}
+::::::::::::::::::::::::::::::::::::::::::::: callout
+
+We will use uproot a little bit like we use the TTreeReader in the other example. We can define the branches we want and assign them to arrays with uproot.
+We can do this via:
+
+```python
+# Open input file and define branches we want to look at with uproot
+events_tree = up.open(infile)["events"]
+# Get particle information# Get particle information
+partGenStat = events_tree["MCParticles.generatorStatus"].array()
+partMomX = events_tree["MCP articles.momentum.x"].array()
+partMomY = events_tree["MCParticles.momentum.y"].array()
+partMomZ = events_tree["MCParticles.momentum.z"].array()
+partPdg = events_tree["MCParticles.PDG"].array()
+
+# Get reconstructed track information
+trackMomX = events_tree["ReconstructedChargedParticles.momentum.x"].array()
+trackMomY = events_tree["ReconstructedChargedParticles.momentum.y"].array()
+trackMomZ = events_tree["ReconstructedChargedParticles.momentum.z"].array()
+ ...
+```
+
+We can then access them as an array in a loop -
+
+```python
+# Add main analysis loop(s) below
+for i in range(0, len(partGenStat)): # Loop over all events
+ for j in range(0, len(partGenStat[i])): # Loop over all thrown particles
+ if partGenStat[i][j] == 1: # Select stable particles
+ pdg = abs(partPdg[i][j]) # Get PDG for each stable particle
+ ...
+```
+
+Uproot effectively takes the information in the tree, and turns it into an array. We can then access and manipulate this array in the same way that we can with any array in python.
+
+Note that if you are using an older version of uproot (v2.x.x), you will need to access the branches slightly differently via -
+
+```python
+partGenStat = events_tree.array("MCParticles.generatorStatus")
+```
+
+:::::::::::::::::::::::::::::::::::::::::::::
You can run this file with ``python3 trackAnalysis.py``. It should open your file and create an empty output root file as specified. We will add histograms to this script and fill them in the next step.
@@ -603,9 +750,12 @@ Note that depending upon your setup, ``python trackAnalysis.py`` may work too.
### Efficiency Analysis
-> Hint:
-> Refer to [the script template](https://eic.github.io/tutorial-analysis/exercise_scripts/index.html#efficiencyanalysispy) if you're having trouble putting things in the right place.
-{: .callout}
+::::::::::::::::::::::::::::::::::::::::::::: callout
+
+Hint:
+Refer to [the script template](../learners/exercise-scripts.md#efficiencyanalysispy) if you're having trouble putting things in the right place.
+
+:::::::::::::::::::::::::::::::::::::::::::::
As with the ROOT TTreeReader example, we will find the tracking eficiency and resolution. We will need to access the reconstructed track information and the truth particle information and we will have to associate the individual tracks and particles to one another.
@@ -657,24 +807,47 @@ ofile.Close()
```
Insert this block of code appropriately. We should now have everything we need to find the track efficiency as a function of pseudorapidity. Run the script with `python3 trackAnalysis.py``. This should produce a root file with a few histograms in place. The efficiency can be found by taking the ratio of matchedPartEta over partEta.
-> Question:
-> - Do the hisotgram ranges make sense?
-> - We plot the distance between thrown and reconstructed charged partices, does this distribution look reasonable?
-> - When filling the matchedPartEta histogram (the numerator in our efficiency), why do we use again the true thrown eta instead of the associated reconstructed eta?
-{: .callout}
+::::::::::::::::::::::::::::::::::::::::::::: callout
-> Exercise:
-> For all **scattered electrons**, **charged pions** and **protons** in our events:
-> - Find the efficiency as a function of particle momentum. Are there cuts on any other quantities you should place to get a sensible result?
-> - Find the efficiency for some 2-D correlations: momentum vs eta; phi vs eta
-> - Plot some kinematic distributions (momentum, eta, etc) for all ReconstructedChargedParticles, not just those that are associated with a thrown particle
-{: .challenge}
+Question:
+
+- Do the hisotgram ranges make sense?
+- We plot the distance between thrown and reconstructed charged partices, does this distribution look reasonable?
+- When filling the matchedPartEta histogram (the numerator in our efficiency), why do we use again the true thrown eta instead of the associated reconstructed eta?
+
+:::::::::::::::::::::::::::::::::::::::::::::
+
+::::::::::::::::::::::::::::::::::::::::::::: challenge
+
+## Exercise
+
+For all **scattered electrons**, **charged pions** and **protons** in our events:
+
+- Find the efficiency as a function of particle momentum. Are there cuts on any other quantities you should place to get a sensible result?
+- Find the efficiency for some 2-D correlations: momentum vs eta; phi vs eta
+- Plot some kinematic distributions (momentum, eta, etc) for all ReconstructedChargedParticles, not just those that are associated with a thrown particle
+
+::::::::::::::: solution
+
+As in the TTreeReader example, fill a denominator histogram with the thrown quantity for each
+species and a numerator histogram with the same quantity only for particles that have a matching
+track (found via the association loop), then divide with `TH1::Divide` (or `TH2::Divide` for the
+2-D correlations). Select species by PDG code and require `generatorStatus == 1`, and apply a
+momentum cut to avoid unstable low-statistics bins. See the
+[Pyroot efficiency template](../learners/exercise-scripts.md#efficiencyanalysispy).
+
+:::::::::::::::
+
+:::::::::::::::::::::::::::::::::::::::::::::
### Resolution Analysis
-> Hint:
-> Refer to [the script template](https://eic.github.io/tutorial-analysis/exercise_scripts/index.html#resolutionanalysispy) if you're having trouble putting things in the right place.
-{: .callout}
+::::::::::::::::::::::::::::::::::::::::::::: callout
+
+Hint:
+Refer to [the script template](../learners/exercise-scripts.md#resolutionanalysispy) if you're having trouble putting things in the right place.
+
+:::::::::::::::::::::::::::::::::::::::::::::
Next, we will look at track momentum resolution, that is, how well the momentum of the reconstructed track matches that of the thrown particle. We should have all of the "infrastructure" we need in place to do the analysis, we just need to define the appropriate quantities and make the histograms. It only makes sense to define the resolution for tracks and particles which are associated with one another, so we will work within the loop over associations. Define the resolution expression and fill a simple histogram by inserting this block of code appropriately:
@@ -691,21 +864,44 @@ trackMomentumRes = ROOT.TH1D("trackMomentumRes","Track Momentum Resolution",2000
Remember to write this histogram to the output file too! While this plot will give us a sense of what the tracking resolution is, we don't expect the resolution to be constant for all momenta or eta. We can get a more complete picture by plotting the resolution as a function of different kinematic quantities.
-> Exercise:
-> For all **scattered electrons**, **charged pions** and **protons** in our events:
-> - Make 2-D plots of resolution vs true momentum and vs true pseudorapidity.
-{: .challenge}
+::::::::::::::::::::::::::::::::::::::::::::: challenge
+
+## Exercise
+
+For all **scattered electrons**, **charged pions** and **protons** in our events:
+
+- Make 2-D plots of resolution vs true momentum and vs true pseudorapidity.
+
+::::::::::::::: solution
+
+Within the association loop, compute `momRes = (recMom.Mag() - trueMom.Mag())/trueMom.Mag()` for the
+matched particle and fill a `TH2D` with the true momentum (or true pseudorapidity) on one axis and
+`momRes` on the other, once per species. A `TProfile` or slice-by-slice fit of the 2-D histogram
+then gives the resolution as a function of the kinematic variable. See the
+[Pyroot resolution template](../learners/exercise-scripts.md#resolutionanalysispy).
+
+:::::::::::::::
+
+:::::::::::::::::::::::::::::::::::::::::::::
+
+::::::::::::::::::::::::::::::::::::::::::::: callout
+
+Question:
+
+- Will the histogram ranges for each particle species be the same?
+- Could we present the resolution values in a more understandable way?
-> Question:
-> - Will the histogram ranges for each particle species be the same?
-> - Could we present the resolution values in a more understandable way?
-{: .callout}
+:::::::::::::::::::::::::::::::::::::::::::::
## ROOT RDataFrames
-> Note:
-> - This method does actually need you to be within eic-shell (or somewhere else with the correct EDM4hep/EDM4eic libraries installed).
-{: .callout}
+::::::::::::::::::::::::::::::::::::::::::::: callout
+
+Note:
+
+- This method does actually need you to be within eic-shell (or somewhere else with the correct EDM4hep/EDM4eic libraries installed).
+
+:::::::::::::::::::::::::::::::::::::::::::::
Newer versions of root, such as the version in eic-shell, have access to a relatively new class, [RDataFrames](https://root.cern/doc/master/classROOT_1_1RDataFrame.html). These are similar to pythonic data frame style structures that you may be familiar with. Some people are moving towards utilising RDataFrames in their analysis. If you are more familiar with working with data frames, you may wish to investigate these further.
@@ -773,17 +969,29 @@ void EfficiencyAnalysisRDF(TString infile="PATH_TO_FILE"){
ofile->Close(); // Close output file
}
```
-> Note:
-> - You will need to run this script with the command `root -l -q EfficiencyAnalysisRDF.C++`, within eic-shell (or somewhere else with the correct EDM4hep/EDM4eic libraries installed).
-> - Remember to put in the correct file path.
-{: .callout}
+
+::::::::::::::::::::::::::::::::::::::::::::: callout
+
+Note:
+
+- You will need to run this script with the command `root -l -q EfficiencyAnalysisRDF.C++`, within eic-shell (or somewhere else with the correct EDM4hep/EDM4eic libraries installed).
+- Remember to put in the correct file path.
+
+:::::::::::::::::::::::::::::::::::::::::::::
If you like, you can try completing the exercises using this example to start from.
-## PODIO - Direct Analysis
+## PODIO Direct Analysis
If you want to avoid ROOT entirely, you can analyse the PODIO files directly in a variety of ways.
See [Wouter's example use cases](https://indico.cern.ch/event/1343984/contributions/5908856/attachments/2842958/4970156/2024-04-23%20-%20Examples%20for%20Data%20Model%20Usage.pdf) from 23/04/24. Wouter shows a few ways in which the PODIO file can be accessed and analysed directly.
**As of March 2026, a full example and version of this method will be provided in the near future.**
+
+::::::::::::::::::::::::::::::::::::::::::::: keypoints
+
+- Flat tree structure provides flexibility in analysis.
+- The ReconstructedChargedParticles branch holds information on reconstructed tracks.
+
+:::::::::::::::::::::::::::::::::::::::::::::
diff --git a/_episodes/04-full_chain_analysis.md b/episodes/04-full_chain_analysis.md
similarity index 80%
rename from _episodes/04-full_chain_analysis.md
rename to episodes/04-full_chain_analysis.md
index 3420b6c..301c46e 100644
--- a/_episodes/04-full_chain_analysis.md
+++ b/episodes/04-full_chain_analysis.md
@@ -2,21 +2,26 @@
title: "Full Chain Analysis"
teaching: 15
exercises: 10
-questions:
-- "How do I bring all of this together if I'm starting from scratch?"
-objectives:
-- "Become familiar with the full analysis chain"
-keypoints:
-- "There are a few steps to go through before we get to the file we analysed previously."
-- "Good for testing, but use simulation campaign output where possible!."
---
+::::::::::::::::::::::::::::::::::::::::::::: questions
+
+- How do I bring all of this together if I'm starting from scratch?
+
+:::::::::::::::::::::::::::::::::::::::::::::
+
+::::::::::::::::::::::::::::::::::::::::::::: objectives
+
+- Become familiar with the full analysis chain.
+
+:::::::::::::::::::::::::::::::::::::::::::::
+
In this short session, we'll go through a brief run through of how we actually ended up with a file like the one we ran our script on before. There are 5 basic steps which we'll look at individually, and then combine together:
1. Generate an input file (typically hepmc, other formats are useable). This is usually from some external event generator.
2. Afterburn the file and apply beam effects (might be skipped in some cases).
-3. Process the input through the simulation, DD4HEP.
-4. Reconstruct the DD4HEP output with EICrecon.
+3. Process the input through the simulation, DD4HEP (covered in depth in the [Simulations with npsim and Geant4 tutorial](https://eic.github.io/tutorial-simulations-using-npsim-and-geant4/)).
+4. Reconstruct the DD4HEP output with EICrecon (covered in depth in the [Reconstruction framework](https://eic.github.io/tutorial-jana2/) and [Reconstruction algorithms](https://eic.github.io/tutorial-reconstruction-algorithms/) tutorials).
5. Analyse the EICrecon output with analysis script.
Note that for low level analyses, you could also directly analyse the DD4HEP output from step 3. You may also wish to consult [Holly's slides from the April 2024 software meeting for an overview](https://indico.cern.ch/event/1343984/contributions/5927492/attachments/2843633/4971409/tutorial_overview.pdf) of each of these steps and how they fit into this production chain.
@@ -36,19 +41,25 @@ I won't say too much on this since this strongly depends upon the channel you wa
... and may others. However, regardless of what you use, the output is likely some form of .hepmc file with event by event particle/vertex info. For example -
-> Example HEPMC Event:
-> An example event from a HEPMC file is shown below. In this example event, we have an input 5 GeV electron on a 41 GeV proton. We have one vertex and three outgoing particles, a scattered electron, a pion, and a neutron. In our header, we also have an event weight included.
->
-> E 1 1 5
-> U GEV MM
-> A 0 weight 4.813926604168258e-07
-> P 1 0 11 6.123233963758798e-16 0.000000000000000e+00 -4.999999973888007e+00 5.000000000000000e+00 5.109989488070365e-04 4
-> P 2 0 2212 -0.000000000000000e+00 -0.000000000000000e+00 4.100000000000000e+01 4.101073462535657e+01 9.382720881600054e-01 4
-> V -1 0 [1,2]
-> P 3 -1 11 -6.872312444834133e-01 1.924351128807063e+00 -4.281657822517654e+00 4.744260534644128e+00 5.109989488070365e-04 1
-> P 4 -1 211 1.042011265882083e+00 -1.600831989262599e+00 1.404460452649878e+00 2.374960954263115e+00 1.395701800000037e-01 1
-> P 5 -1 2112 -3.547800213986697e-01 -3.235191395444645e-01 3.887719739597977e+01 3.889151313644933e+01 9.395654204998098e-01 1
-{: .callout}
+::::::::::::::::::::::::::::::::::::::::::::: callout
+
+## Example HEPMC Event
+
+An example event from a HEPMC file is shown below. In this example event, we have an input 5 GeV electron on a 41 GeV proton. We have one vertex and three outgoing particles, a scattered electron, a pion, and a neutron. In our header, we also have an event weight included.
+
+```text
+E 1 1 5
+U GEV MM
+A 0 weight 4.813926604168258e-07
+P 1 0 11 6.123233963758798e-16 0.000000000000000e+00 -4.999999973888007e+00 5.000000000000000e+00 5.109989488070365e-04 4
+P 2 0 2212 -0.000000000000000e+00 -0.000000000000000e+00 4.100000000000000e+01 4.101073462535657e+01 9.382720881600054e-01 4
+V -1 0 [1,2]
+P 3 -1 11 -6.872312444834133e-01 1.924351128807063e+00 -4.281657822517654e+00 4.744260534644128e+00 5.109989488070365e-04 1
+P 4 -1 211 1.042011265882083e+00 -1.600831989262599e+00 1.404460452649878e+00 2.374960954263115e+00 1.395701800000037e-01 1
+P 5 -1 2112 -3.547800213986697e-01 -3.235191395444645e-01 3.887719739597977e+01 3.889151313644933e+01 9.395654204998098e-01 1
+```
+
+:::::::::::::::::::::::::::::::::::::::::::::
Typically, we also need to incorporate beam effects. This is done via the use of the afterburner.
@@ -56,19 +67,19 @@ Typically, we also need to incorporate beam effects. This is done via the use of
Afterburner applies beam effects to an existing hepmc file. These include effects due to the crabbing of the beam bunches and the crossing angle. Afterburner is pre-installed in eic-shell. We can run it via -
-```console
+```bash
abconv
```
However, we'll need an input file to do anything, we can also check other options quickly with -
-```console
+```bash
abconv -h
```
Note that when we run Afterburner, it will try to pick up the input beam energies and apply the relevant configuration. We can force a different configuration if we want (see the options from the help printout). We could for example though run -
-```console
+```bash
abconv $File -o $OutputFilename
```
@@ -80,14 +91,14 @@ Regardless of whether we want or need to afterburn the file, we can feed in our
To process our events through the simulation, we need to get the detector geometry. The simplest way is simply to source the nightly build within eic-shell -
-```console
+```bash
./eic-shell
source /opt/detector/epic-main/bin/thisepic.sh
```
We can check this worked as intended by checking that the DETECTOR_PATH variable is now set. Do so via -
-```console
+```bash
ls $DETECTOR_PATH
```
@@ -95,19 +106,19 @@ If we do this without sourcing thisepic.sh, we should get an error. Now, we shou
We can now process a simulation. Be aware that this may take some time, so to test it, try processing a small number of events first. Check the options we can provide via -
-```console
+```bash
npsim -h
```
A typical simulation command might look something like -
-```console
+```bash
npsim --compactFile $DETECTOR_PATH/epic_craterlake.xml --numberOfEvents 1000 --inputFiles input.hepmc --outputFile output.edm4hep.root
```
Most of the arguments are pretty self explanatory. As a quick demo, I'll run -
-```console
+```bash
npsim --compactFile $DETECTOR_PATH/epic_craterlake_5x41.xml --numberOfEvents 10 --inputFiles eic_DEMPgen_5on41_ip6_pi+_1B_1.hepmc --outputFile DEMPgen_5on41_pi+_10_TestOutput.edm4hep.root
```
@@ -117,19 +128,19 @@ When we run this, we'll get lots of printouts to screen, we can supress this by
We can run eicrecon pretty straightforwardly, within eicshell, try -
-```console
+```bash
eicrecon -h
```
which should again, print out the various options we have available. An example command to run the reconstruction on a file might look like this -
-```console
+```bash
eicrecon -Ppodio:output_file=eicrecon_out.root -Pjana:nevents=1000 -Pdd4hep:xml_files=epic_craterlake.xml sim_output.edm4hep.root
```
Again, this might take a long time. So test a small sample of events first. Following up on my simulation demo, I'll run -
-```console
+```bash
eicrecon -Ppodio:output_file=DEMPgen_5on41_pi+_10_TestReconOutput.edm4hep.root -Pjana:nevents=10 -Pdd4hep:xml_files=epic_craterlake_5x41.xml DEMPgen_5on41_pi+_10_TestOutput.edm4hep.root
```
@@ -139,7 +150,7 @@ eicrecon will look for the detector .xml file in $DETECTOR_PATH, so make sure th
Ok, great. We now have a file we could run our earlier analysis script on. But what if we wanted to do all of this from scratch? Well, the easiest way might be to put all of this in a shell script. So, pulling all of our commands together -
-```console
+```bash
#! /bin/bash
source /opt/detector/epic-main/bin/thisepic.sh
@@ -165,7 +176,7 @@ Ok, great. We can do (almost) all of the processes we need in one command. But a
For our first point, one easy (and not recommended for Condor jobs!) way to do this is via an EOF line -
-```console
+```bash
#! /bin/bash
cat < ## Prerequisites
-> Knowledge of CERN ROOT and/or Python/RDataFrames
-> This tutorial follows other tutorial in the EIC series:
-> - [Setting Up Your Environment](https://eic.github.io/tutorial-setting-up-environment/)
->
-> Further information is included in other tutorials:
->
-> - [Geometry Development with DD4hep](https://eic.github.io/tutorial-geometry-development-using-dd4hep/)
-> - [Simulations Using DDsim and Geant4](https://eic.github.io/tutorial-simulations-using-npsim-and-geant4/)
-> - [Reconstruction Algorithms in JANA2](https://eic.github.io/tutorial-jana2)
-> - [Developing Benchmarks](https://eic.github.io/tutorial-developing-benchmarks/)
-{: .prereq}
-
-{% include links.md %}
+
+Welcome to the EIC Tutorial on analyzing simulation output!
+
+This tutorial covers how to access, explore, and analyze the reconstructed simulation output produced
+by the ePIC software stack (EICrecon), using ROOT, Python/uproot, RDataFrame, and PODIO based
+approaches.
+
+::::::::::::::::::::::::::::::::::::::::::::: prereq
+
+## Prerequisites
+
+Knowledge of CERN ROOT and/or Python/RDataFrames is assumed. Please take a look at the
+[Setup](learners/setup.md) page for necessary prerequisites for this lesson.
+
+This tutorial follows other tutorials in the EIC series:
+
+- [Setting Up Your Environment](https://eic.github.io/tutorial-setting-up-environment/)
+
+Further information is included in other tutorials:
+
+- [Geometry Development with DD4hep](https://eic.github.io/tutorial-geometry-development-using-dd4hep/)
+- [Simulations Using DDsim and Geant4](https://eic.github.io/tutorial-simulations-using-npsim-and-geant4/)
+- [Reconstruction Algorithms in JANA2](https://eic.github.io/tutorial-jana2)
+- [Developing Benchmarks](https://eic.github.io/tutorial-developing-benchmarks/)
+
+:::::::::::::::::::::::::::::::::::::::::::::
diff --git a/instructors/instructor-notes.md b/instructors/instructor-notes.md
new file mode 100644
index 0000000..240cd85
--- /dev/null
+++ b/instructors/instructor-notes.md
@@ -0,0 +1,21 @@
+---
+title: "Instructor Notes"
+---
+
+This tutorial teaches learners how to analyze the reconstructed simulation output produced by
+EICrecon. It follows the [Setting Up Your Environment](https://eic.github.io/tutorial-setting-up-environment/)
+tutorial, which establishes the `eic-shell` environment learners rely on here.
+
+## Before the session
+
+- Ask learners to complete the [Setup](../learners/setup.md) page in advance, in particular
+ downloading a reconstruction output file (50-80 MB) from the current simulation campaign, since
+ this depends on network access to the JLab xrootd server.
+- Confirm learners have a working, interactive ROOT install (either inside `eic-shell` or ROOT
+ 6.30+ locally) as described on the Setup page.
+
+## Timing
+
+Most of the lesson is hands-on analysis. Learners should work the exercises in whichever language
+(ROOT C++, Python/uproot, RDataFrame, or PODIO) they are most comfortable with; you do not need to
+cover every approach live.
diff --git a/_extras/branch_dictionary.md b/learners/branch-dictionary.md
similarity index 94%
rename from _extras/branch_dictionary.md
rename to learners/branch-dictionary.md
index 4d08df1..ce1ec66 100644
--- a/_extras/branch_dictionary.md
+++ b/learners/branch-dictionary.md
@@ -8,25 +8,25 @@ Data files from EICrecon can sometimes look a little overwhelming with the large
The naming convention of many of the branches is based around the detector they correspond to. For example, take the branch -
-```console
+```bash
EcalEndcapNClusters
```
Breaking this down piece by piece, the branch tells us at the start which detector it corresponds to -
-```console
+```bash
EcalEndcap
```
ECal -> Electromagnetic calorimeter, Endcap -> The endcap (as opposed to the barrel). The second part tells us a little bit more about which information pertaining to that detector is in this branch -
-```console
+```bash
...NClusters
```
Clusters tells us this is probably something to do with the clusters of hits in this calorimeter. The N is telling is that this is for negatively charged clusters in this detector. If we open this branch, we see the sort of information we might expect to be associated with calorimeter clusters, for example -
-```console
+```bash
EcalEndcapNClusters.energy
EcalEndcapNClusters.nhits
EcalEndcapNClusters.position.x
@@ -38,7 +38,7 @@ So stored in these branches is event by event information on the energy of clust
The MCParticles branch contains truth level information on the input events into the simulation and reconstruction. Many simulation studies will involve a comparison to this "true" information. This branch contains event level information for many quantities -
-```console
+```bash
MCParticles.PDG
MCParticles.charge
MCParticles.vertex.x
@@ -46,7 +46,7 @@ MCParticles.momentum.x
...
```
-The PDG value tells us what the input particle actually was, we can cross-reference the output of this with the [PDG code](https://pdg.lbl.gov/2007/reviews/montecarlorpp.pdf) to see what our inputs actually were. We can also easily extract the charge, vertex (x component) and momentum (x component) of the particles in our event using the leaves above.
+The PDG value tells us what the input particle actually was, we can cross-reference the output of this with the [PDG code](https://pdg.lbl.gov/2007/reviews/montecarlorpp.pdf) to see what our inputs actually were. We can also easily extract the charge, vertex (x component) and momentum (x component) of the particles in our event using the leaves above.
## ReconstructedParticles
@@ -58,16 +58,8 @@ Similar to reconstructed particles. In this case though, we only get information
## ReconstructedParticlesAssociations
-This branch contains information on associations betwen MC truth information and reconstructed particles. In this case, this is for all reconstructed particles. We can find the association index matching the index of the chagred particle and compare what we have reconstructed to its actual "true" information. See the examples in [episode 3]({{page.root}}{% link _episodes/03-analysis.md %}) for some example usage of this.
+This branch contains information on associations betwen MC truth information and reconstructed particles. In this case, this is for all reconstructed particles. We can find the association index matching the index of the chagred particle and compare what we have reconstructed to its actual "true" information. See the examples in [episode 3](../episodes/03-analysis.md) for some example usage of this.
## ReconstructedChargedParticlesAssociations
Similar to ReconstructedParticlesAssociations, but for reconstructed charged particles.
-
-
-
-
-
-
-
-
diff --git a/learners/discuss.md b/learners/discuss.md
new file mode 100644
index 0000000..273226f
--- /dev/null
+++ b/learners/discuss.md
@@ -0,0 +1,8 @@
+---
+title: Discussion
+---
+
+For questions and discussion about analyzing EIC simulation output, see the
+[EIC getting-started guide](https://eic.github.io/documentation/getstarted) and the list of
+[EIC tutorials](https://eic.github.io/documentation/tutorials.html), and ask in the
+[ePIC software-tutorials channel on Mattermost](https://chat.epic-eic.org/main/channels/software-tutorials).
diff --git a/_extras/examples-repo.md b/learners/examples-repo.md
similarity index 96%
rename from _extras/examples-repo.md
rename to learners/examples-repo.md
index 698e3a4..3699ba7 100644
--- a/_extras/examples-repo.md
+++ b/learners/examples-repo.md
@@ -22,5 +22,3 @@ Included below are a list of links to some analysis scripts/codes that are curre
## Semi-Inclusive Physics Scripts
1.
-
-{% include links.md %}
diff --git a/_extras/exercise_scripts.md b/learners/exercise-scripts.md
similarity index 99%
rename from _extras/exercise_scripts.md
rename to learners/exercise-scripts.md
index 0bccc3a..de5924d 100644
--- a/_extras/exercise_scripts.md
+++ b/learners/exercise-scripts.md
@@ -79,7 +79,6 @@ void EfficiencyAnalysis(TString infile="PATH_TO_INPUT_FILE"){
ofile->Close(); // Close output file
}
```
-
### ResolutionAnalysis.C
@@ -301,7 +299,6 @@ void ResolutionAnalysis(TString infile="PATH_TO_INPUT_FILE"){
ofile->Close(); // Close output file
}
```
-
### Compiled ROOT Scripts
@@ -494,10 +490,10 @@ Note that you should delete the \ characters in this block.
The contents of CMakeLists.txt are -
-```c++
+```cmake
# CMakeLists.txt for helloroot.
# More complicated than needed but demonstrates making and linking your own libraries
-# cf. https://cliutils.gitlab.io/modern-cmake/chapters/packages/ROOT.html
+# cf. https://cliutils.gitlab.io/modern-cmake/
# https://root.cern/manual/integrate_root_into_my_cmake_project/
cmake_minimum_required(VERSION 3.10)
@@ -637,7 +633,7 @@ Please consult the README and script comments for further instructions.
## Python Uproot Scripts - Pythonic Versions
-Some template scripts that utilise an python array based approach are included below. For some examples of using uproot to access information in .root files, please consult (this notebook)[https://github.com/eic/HSF-India/blob/main/Working_With_Uproot/Working_With_Uproot_Standalone.ipynb] which can be run in Google Collab.
+Some template scripts that utilise an python array based approach are included below. For some examples of using uproot to access information in .root files, please consult [this notebook](https://github.com/eic/HSF-India/blob/main/Working_With_Uproot/Working_With_Uproot_Standalone.ipynb) which can be run in Google Colab.
### Pythonic_EfficiencyAnalysis.py
@@ -739,7 +735,6 @@ plt.savefig("EfficiencyAnalysis_Out.png", dpi = (160))
#plt.bar(Bars, Division, width=BarWidth, alpha=0.5, color='kP6[0]')
```
-
### Pythonic_ResolutionAnalysis.py
@@ -991,7 +985,6 @@ plt.savefig("ResolutionAnalysis_Out.png", dpi = (160))
```
-
## Python Uproot Script - C/ROOT Style (Slow, not recommended!)
@@ -1206,7 +1198,6 @@ matchedPartTrackDeltaR.Write()
# Close files
ofile.Close()
```
-
### ResolutionAnalysis.py
Create a file called `ResolutionAnalysis.py` and copy in the code below to get started on the resolution analysis exercise. Note that you will need to correctly specify your input file path in the variable `infile`.
@@ -1421,7 +1411,6 @@ matchedPartTrackDeltaMom.Write()
# Close files
ofile.Close()
```
-
## RDataFrames Example
Note that only the initial stage of the efficiency example is presented here in RDF format. This example was kindly created by [Simon](https://github.com/simonge/EIC_Analysis/blob/main/Analysis-Tutorial/EfficiencyAnalysisRDF.C).
@@ -1632,7 +1620,6 @@ void EfficiencyAnalysisRDF(TString infile="PATH_TO_FILE"){
ofile->Close(); // Close output file
}
```
-
-{% include links.md %}
diff --git a/files/TreePrune.C b/learners/files/TreePrune.C
similarity index 100%
rename from files/TreePrune.C
rename to learners/files/TreePrune.C
diff --git a/learners/reference.md b/learners/reference.md
new file mode 100644
index 0000000..b224e8c
--- /dev/null
+++ b/learners/reference.md
@@ -0,0 +1,17 @@
+---
+title: 'Reference'
+---
+
+## Glossary
+
+EIC
+: Electron-Ion Collider
+
+EICrecon
+: The EIC reconstruction software framework, whose output ROOT trees are analyzed in this lesson.
+
+EDM4eic / EDM4hep
+: The event data models describing the branches stored in the reconstruction output trees.
+
+PODIO
+: The Plain Old Data I/O library underlying the EIC event data model.
diff --git a/setup.md b/learners/setup.md
similarity index 77%
rename from setup.md
rename to learners/setup.md
index 39e155f..458c2fa 100644
--- a/setup.md
+++ b/learners/setup.md
@@ -1,11 +1,14 @@
---
title: Setup
---
-If you have not done so already, please follow the instructions [here](https://eic.github.io/tutorial-setting-up-environment/setup.html) well before the start of the tutorial to ensure your system is ready.
+
+If you have not done so already, please follow the
+[Setting Up Your Environment tutorial setup instructions](https://eic.github.io/tutorial-setting-up-environment/setup.html)
+well before the start of the tutorial to ensure your system is ready.
This tutorial will go over how to analyze the reconstructed simulation, so you will need to download a file to work with locally. The files are on the order of 50-80MB each. For consistency, we will use neutral current DIS events from the February 2026 campaign (26.02.0) with minimum Q2 = 10 GeV2 and at the highest electron-proton beam energy combination (if you wish to make an energy comparison, you can download additional files). To browse the available files, you can run the following commands from within the eic-shell environment:
-```console
+```bash
xrdfs root://dtn-eic.jlab.org
ls /volatile/eic/EPIC/RECO/26.02.0/epic_craterlake/DIS/NC/18x275/minQ2=10
exit
@@ -13,15 +16,18 @@ exit
You can download any of the files you want in here. You can do this by (still within eic-shell environment) navigating to the directory you will store your file(s) and run the command:
-```console
+```bash
xrdcp root://dtn-eic.jlab.org//volatile/eic/EPIC/RECO/26.02.0/epic_craterlake/DIS/NC/18x275/minQ2=10/pythia8NCDIS_18x275_minQ2=10_beamEffects_xAngle=-0.025_hiDiv_5.0001.eicrecon.tree.edm4eic.root ./
```
Do not forget the trailing ./ (or just . works too) as this tells the progam to put the file in your current dir.
-> Note that we can also specify a different filename to copy to as we could with a normal cp command. You might want to do this as the filename is a little cumbersome.
-> I called mine NC_DIS_18x275_Feb26Campaign.root, just replace ./ with your file name of choice.
-{: .callout}
+::::::::::::::::::::::::::::::::::::::::::::: callout
+
+Note that we can also specify a different filename to copy to as we could with a normal cp command. You might want to do this as the filename is a little cumbersome.
+I called mine NC_DIS_18x275_Feb26Campaign.root, just replace ./ with your file name of choice.
+
+:::::::::::::::::::::::::::::::::::::::::::::
This command will download the file (0001) specified. You can of course, download a different file in the same directory if you want.
@@ -33,5 +39,3 @@ Note that this tutorial is a little odd in that, for the most part, we don't rel
2. A working version (and relatively recent, 6.30 or above, ideally 6.34.02) of ROOT on your local machine.
If you use option 2, note that you will not be able to "stream" files to your ROOT script unless you have xrootd installed too, you will need them available locally. I will be using option 2 for this tutorial.
-
-{% include links.md %}
diff --git a/_extras/tree_pruning_script.md b/learners/tree-pruning-script.md
similarity index 95%
rename from _extras/tree_pruning_script.md
rename to learners/tree-pruning-script.md
index 602600b..0e4db1d 100644
--- a/_extras/tree_pruning_script.md
+++ b/learners/tree-pruning-script.md
@@ -6,15 +6,15 @@ Included below is a short script which can be utilised to prune an input tree. T
## TreePrune.C
-Copy paste this to a new file called "TreePrune.C". Execute it via -
+Copy paste the code below to a new file called "TreePrune.C" (or download [TreePrune.C](files/TreePrune.C)). Execute it via -
-```console
+```bash
root -l TreePrune.C
```
You will be prompted for an input file name. Alternatively run the script with the input already specified -
-```console
+```bash
root -l 'TreePrune.C("InputFilePath")'
```
diff --git a/links.md b/links.md
new file mode 100644
index 0000000..4c5cd2f
--- /dev/null
+++ b/links.md
@@ -0,0 +1,10 @@
+
+
+[pandoc]: https://pandoc.org/MANUAL.html
+[r-markdown]: https://rmarkdown.rstudio.com/
+[rstudio]: https://www.rstudio.com/
+[carpentries-workbench]: https://carpentries.github.io/sandpaper-docs/
+
diff --git a/profiles/learner-profiles.md b/profiles/learner-profiles.md
new file mode 100644
index 0000000..3c3877a
--- /dev/null
+++ b/profiles/learner-profiles.md
@@ -0,0 +1,9 @@
+---
+title: 'Learner Profiles'
+---
+
+This lesson targets members of the ePIC collaboration — students, postdocs, and researchers — who
+have a working `eic-shell` (or a recent ROOT/Python install) and want to start analyzing the
+reconstructed simulation output produced by EICrecon. Learners are expected to have some familiarity
+with CERN ROOT and/or Python (uproot, RDataFrame). No prior experience with the EIC data model
+(EDM4eic/EDM4hep) is assumed.
diff --git a/reference.md b/reference.md
deleted file mode 100644
index d6fe4ff..0000000
--- a/reference.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-layout: reference
----
-
-## Glossary
-
-EIC: Electron-Ion Collider
-
-{% include links.md %}