diff --git a/404.html b/404.html index bd95ee2..3c58b90 100644 --- a/404.html +++ b/404.html @@ -16,7 +16,7 @@ - + @@ -835,7 +835,7 @@

404 - Not found

- + diff --git a/apps/docker/index.html b/apps/docker/index.html index 2b5b1bd..3d7d2a9 100644 --- a/apps/docker/index.html +++ b/apps/docker/index.html @@ -22,7 +22,7 @@ - + @@ -1104,7 +1104,7 @@

Running a - + diff --git a/apps/framework/index.html b/apps/framework/index.html index 87d055c..b1a3a5a 100644 --- a/apps/framework/index.html +++ b/apps/framework/index.html @@ -22,7 +22,7 @@ - + @@ -1097,7 +1097,7 @@

What are BIDS Derivatives?{"base": "../..", "features": ["tabs"], "search": "../../assets/javascripts/workers/search.07f07601.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + diff --git a/apps/singularity/index.html b/apps/singularity/index.html index 85b41d5..a5ecce6 100644 --- a/apps/singularity/index.html +++ b/apps/singularity/index.html @@ -22,7 +22,7 @@ - + @@ -1404,7 +1404,7 @@

Running Singularity on a SLURM sy - + diff --git a/assets/ORN-Workshop/presentation/index.html b/assets/ORN-Workshop/presentation/index.html index b61fb50..73743c5 100644 --- a/assets/ORN-Workshop/presentation/index.html +++ b/assets/ORN-Workshop/presentation/index.html @@ -18,7 +18,7 @@ - + @@ -1565,7 +1565,7 @@

Questions?{"base": "../../..", "features": ["tabs"], "search": "../../javascripts/workers/search.07f07601.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + diff --git a/assets/bhd2020/presentation/index.html b/assets/bhd2020/presentation/index.html index 371eeba..b34c9cc 100644 --- a/assets/bhd2020/presentation/index.html +++ b/assets/bhd2020/presentation/index.html @@ -18,7 +18,7 @@ - + @@ -1688,7 +1688,7 @@

Questions?{"base": "../../..", "features": ["tabs"], "search": "../../javascripts/workers/search.07f07601.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + diff --git a/assets/javascripts/workers/search.07f07601.min.js b/assets/javascripts/workers/search.6ce7567c.min.js similarity index 94% rename from assets/javascripts/workers/search.07f07601.min.js rename to assets/javascripts/workers/search.6ce7567c.min.js index f3dbf56..8e7e550 100644 --- a/assets/javascripts/workers/search.07f07601.min.js +++ b/assets/javascripts/workers/search.6ce7567c.min.js @@ -1,4 +1,4 @@ -"use strict";(()=>{var xe=Object.create;var U=Object.defineProperty,ve=Object.defineProperties,Se=Object.getOwnPropertyDescriptor,Te=Object.getOwnPropertyDescriptors,Qe=Object.getOwnPropertyNames,Y=Object.getOwnPropertySymbols,Ee=Object.getPrototypeOf,X=Object.prototype.hasOwnProperty,be=Object.prototype.propertyIsEnumerable;var Z=Math.pow,J=(t,e,r)=>e in t?U(t,e,{enumerable:!0,configurable:!0,writable:!0,value:r}):t[e]=r,A=(t,e)=>{for(var r in e||(e={}))X.call(e,r)&&J(t,r,e[r]);if(Y)for(var r of Y(e))be.call(e,r)&&J(t,r,e[r]);return t},G=(t,e)=>ve(t,Te(e));var Le=(t,e)=>()=>(e||t((e={exports:{}}).exports,e),e.exports);var we=(t,e,r,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of Qe(e))!X.call(t,i)&&i!==r&&U(t,i,{get:()=>e[i],enumerable:!(n=Se(e,i))||n.enumerable});return t};var Pe=(t,e,r)=>(r=t!=null?xe(Ee(t)):{},we(e||!t||!t.__esModule?U(r,"default",{value:t,enumerable:!0}):r,t));var B=(t,e,r)=>new Promise((n,i)=>{var s=u=>{try{a(r.next(u))}catch(c){i(c)}},o=u=>{try{a(r.throw(u))}catch(c){i(c)}},a=u=>u.done?n(u.value):Promise.resolve(u.value).then(s,o);a((r=r.apply(t,e)).next())});var te=Le((K,ee)=>{/** +"use strict";(()=>{var xe=Object.create;var U=Object.defineProperty,ve=Object.defineProperties,Se=Object.getOwnPropertyDescriptor,Te=Object.getOwnPropertyDescriptors,Qe=Object.getOwnPropertyNames,J=Object.getOwnPropertySymbols,Ee=Object.getPrototypeOf,Z=Object.prototype.hasOwnProperty,be=Object.prototype.propertyIsEnumerable;var K=Math.pow,X=(t,e,r)=>e in t?U(t,e,{enumerable:!0,configurable:!0,writable:!0,value:r}):t[e]=r,A=(t,e)=>{for(var r in e||(e={}))Z.call(e,r)&&X(t,r,e[r]);if(J)for(var r of J(e))be.call(e,r)&&X(t,r,e[r]);return t},G=(t,e)=>ve(t,Te(e));var Le=(t,e)=>()=>(e||t((e={exports:{}}).exports,e),e.exports);var we=(t,e,r,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of Qe(e))!Z.call(t,i)&&i!==r&&U(t,i,{get:()=>e[i],enumerable:!(n=Se(e,i))||n.enumerable});return t};var Pe=(t,e,r)=>(r=t!=null?xe(Ee(t)):{},we(e||!t||!t.__esModule?U(r,"default",{value:t,enumerable:!0}):r,t));var B=(t,e,r)=>new Promise((n,i)=>{var s=u=>{try{a(r.next(u))}catch(c){i(c)}},o=u=>{try{a(r.throw(u))}catch(c){i(c)}},a=u=>u.done?n(u.value):Promise.resolve(u.value).then(s,o);a((r=r.apply(t,e)).next())});var re=Le((ee,te)=>{/** * lunr - http://lunrjs.com - A bit like Solr, but much smaller and not as bright - 2.3.9 * Copyright (C) 2020 Oliver Nightingale * @license MIT @@ -37,6 +37,6 @@ */t.Index=function(e){this.invertedIndex=e.invertedIndex,this.fieldVectors=e.fieldVectors,this.tokenSet=e.tokenSet,this.fields=e.fields,this.pipeline=e.pipeline},t.Index.prototype.search=function(e){return this.query(function(r){var n=new t.QueryParser(e,r);n.parse()})},t.Index.prototype.query=function(e){for(var r=new t.Query(this.fields),n=Object.create(null),i=Object.create(null),s=Object.create(null),o=Object.create(null),a=Object.create(null),u=0;u1?this._b=1:this._b=e},t.Builder.prototype.k1=function(e){this._k1=e},t.Builder.prototype.add=function(e,r){var n=e[this._ref],i=Object.keys(this._fields);this._documents[n]=r||{},this.documentCount+=1;for(var s=0;s=this.length)return t.QueryLexer.EOS;var e=this.str.charAt(this.pos);return this.pos+=1,e},t.QueryLexer.prototype.width=function(){return this.pos-this.start},t.QueryLexer.prototype.ignore=function(){this.start==this.pos&&(this.pos+=1),this.start=this.pos},t.QueryLexer.prototype.backup=function(){this.pos-=1},t.QueryLexer.prototype.acceptDigitRun=function(){var e,r;do e=this.next(),r=e.charCodeAt(0);while(r>47&&r<58);e!=t.QueryLexer.EOS&&this.backup()},t.QueryLexer.prototype.more=function(){return this.pos1&&(e.backup(),e.emit(t.QueryLexer.TERM)),e.ignore(),e.more())return t.QueryLexer.lexText},t.QueryLexer.lexEditDistance=function(e){return e.ignore(),e.acceptDigitRun(),e.emit(t.QueryLexer.EDIT_DISTANCE),t.QueryLexer.lexText},t.QueryLexer.lexBoost=function(e){return e.ignore(),e.acceptDigitRun(),e.emit(t.QueryLexer.BOOST),t.QueryLexer.lexText},t.QueryLexer.lexEOS=function(e){e.width()>0&&e.emit(t.QueryLexer.TERM)},t.QueryLexer.termSeparator=t.tokenizer.separator,t.QueryLexer.lexText=function(e){for(;;){var r=e.next();if(r==t.QueryLexer.EOS)return t.QueryLexer.lexEOS;if(r.charCodeAt(0)==92){e.escapeCharacter();continue}if(r==":")return t.QueryLexer.lexField;if(r=="~")return e.backup(),e.width()>0&&e.emit(t.QueryLexer.TERM),t.QueryLexer.lexEditDistance;if(r=="^")return e.backup(),e.width()>0&&e.emit(t.QueryLexer.TERM),t.QueryLexer.lexBoost;if(r=="+"&&e.width()===1||r=="-"&&e.width()===1)return e.emit(t.QueryLexer.PRESENCE),t.QueryLexer.lexText;if(r.match(t.QueryLexer.termSeparator))return t.QueryLexer.lexTerm}},t.QueryParser=function(e,r){this.lexer=new t.QueryLexer(e),this.query=r,this.currentClause={},this.lexemeIdx=0},t.QueryParser.prototype.parse=function(){this.lexer.run(),this.lexemes=this.lexer.lexemes;for(var e=t.QueryParser.parseClause;e;)e=e(this);return this.query},t.QueryParser.prototype.peekLexeme=function(){return this.lexemes[this.lexemeIdx]},t.QueryParser.prototype.consumeLexeme=function(){var e=this.peekLexeme();return this.lexemeIdx+=1,e},t.QueryParser.prototype.nextClause=function(){var e=this.currentClause;this.query.clause(e),this.currentClause={}},t.QueryParser.parseClause=function(e){var r=e.peekLexeme();if(r!=null)switch(r.type){case t.QueryLexer.PRESENCE:return t.QueryParser.parsePresence;case t.QueryLexer.FIELD:return t.QueryParser.parseField;case t.QueryLexer.TERM:return t.QueryParser.parseTerm;default:var n="expected either a field or a term, found "+r.type;throw r.str.length>=1&&(n+=" with value '"+r.str+"'"),new t.QueryParseError(n,r.start,r.end)}},t.QueryParser.parsePresence=function(e){var r=e.consumeLexeme();if(r!=null){switch(r.str){case"-":e.currentClause.presence=t.Query.presence.PROHIBITED;break;case"+":e.currentClause.presence=t.Query.presence.REQUIRED;break;default:var n="unrecognised presence operator'"+r.str+"'";throw new t.QueryParseError(n,r.start,r.end)}var i=e.peekLexeme();if(i==null){var n="expecting term or field, found nothing";throw new t.QueryParseError(n,r.start,r.end)}switch(i.type){case t.QueryLexer.FIELD:return t.QueryParser.parseField;case t.QueryLexer.TERM:return t.QueryParser.parseTerm;default:var n="expecting term or field, found '"+i.type+"'";throw new t.QueryParseError(n,i.start,i.end)}}},t.QueryParser.parseField=function(e){var r=e.consumeLexeme();if(r!=null){if(e.query.allFields.indexOf(r.str)==-1){var n=e.query.allFields.map(function(o){return"'"+o+"'"}).join(", "),i="unrecognised field '"+r.str+"', possible fields: "+n;throw new t.QueryParseError(i,r.start,r.end)}e.currentClause.fields=[r.str];var s=e.peekLexeme();if(s==null){var i="expecting term, found nothing";throw new t.QueryParseError(i,r.start,r.end)}switch(s.type){case t.QueryLexer.TERM:return t.QueryParser.parseTerm;default:var i="expecting term, found '"+s.type+"'";throw new t.QueryParseError(i,s.start,s.end)}}},t.QueryParser.parseTerm=function(e){var r=e.consumeLexeme();if(r!=null){e.currentClause.term=r.str.toLowerCase(),r.str.indexOf("*")!=-1&&(e.currentClause.usePipeline=!1);var n=e.peekLexeme();if(n==null){e.nextClause();return}switch(n.type){case t.QueryLexer.TERM:return e.nextClause(),t.QueryParser.parseTerm;case t.QueryLexer.FIELD:return e.nextClause(),t.QueryParser.parseField;case t.QueryLexer.EDIT_DISTANCE:return t.QueryParser.parseEditDistance;case t.QueryLexer.BOOST:return t.QueryParser.parseBoost;case t.QueryLexer.PRESENCE:return e.nextClause(),t.QueryParser.parsePresence;default:var i="Unexpected lexeme type '"+n.type+"'";throw new t.QueryParseError(i,n.start,n.end)}}},t.QueryParser.parseEditDistance=function(e){var r=e.consumeLexeme();if(r!=null){var n=parseInt(r.str,10);if(isNaN(n)){var i="edit distance must be numeric";throw new t.QueryParseError(i,r.start,r.end)}e.currentClause.editDistance=n;var s=e.peekLexeme();if(s==null){e.nextClause();return}switch(s.type){case t.QueryLexer.TERM:return e.nextClause(),t.QueryParser.parseTerm;case t.QueryLexer.FIELD:return e.nextClause(),t.QueryParser.parseField;case t.QueryLexer.EDIT_DISTANCE:return t.QueryParser.parseEditDistance;case t.QueryLexer.BOOST:return t.QueryParser.parseBoost;case t.QueryLexer.PRESENCE:return e.nextClause(),t.QueryParser.parsePresence;default:var i="Unexpected lexeme type '"+s.type+"'";throw new t.QueryParseError(i,s.start,s.end)}}},t.QueryParser.parseBoost=function(e){var r=e.consumeLexeme();if(r!=null){var n=parseInt(r.str,10);if(isNaN(n)){var i="boost must be numeric";throw new t.QueryParseError(i,r.start,r.end)}e.currentClause.boost=n;var s=e.peekLexeme();if(s==null){e.nextClause();return}switch(s.type){case t.QueryLexer.TERM:return e.nextClause(),t.QueryParser.parseTerm;case t.QueryLexer.FIELD:return e.nextClause(),t.QueryParser.parseField;case t.QueryLexer.EDIT_DISTANCE:return t.QueryParser.parseEditDistance;case t.QueryLexer.BOOST:return t.QueryParser.parseBoost;case t.QueryLexer.PRESENCE:return e.nextClause(),t.QueryParser.parsePresence;default:var i="Unexpected lexeme type '"+s.type+"'";throw new t.QueryParseError(i,s.start,s.end)}}},function(e,r){typeof define=="function"&&define.amd?define(r):typeof K=="object"?ee.exports=r():e.lunr=r()}(this,function(){return t})})()});var de=Pe(te());function re(t,e=document){let r=ke(t,e);if(typeof r=="undefined")throw new ReferenceError(`Missing element: expected "${t}" to be present`);return r}function ke(t,e=document){return e.querySelector(t)||void 0}Object.entries||(Object.entries=function(t){let e=[];for(let r of Object.keys(t))e.push([r,t[r]]);return e});Object.values||(Object.values=function(t){let e=[];for(let r of Object.keys(t))e.push(t[r]);return e});typeof Element!="undefined"&&(Element.prototype.scrollTo||(Element.prototype.scrollTo=function(t,e){typeof t=="object"?(this.scrollLeft=t.left,this.scrollTop=t.top):(this.scrollLeft=t,this.scrollTop=e)}),Element.prototype.replaceWith||(Element.prototype.replaceWith=function(...t){let e=this.parentNode;if(e){t.length===0&&e.removeChild(this);for(let r=t.length-1;r>=0;r--){let n=t[r];typeof n=="string"?n=document.createTextNode(n):n.parentNode&&n.parentNode.removeChild(n),r?e.insertBefore(this.previousSibling,n):e.replaceChild(n,this)}}}));function ne(t){let e=new Map;for(let r of t){let[n]=r.location.split("#"),i=e.get(n);typeof i=="undefined"?e.set(n,r):(e.set(r.location,r),r.parent=i)}return e}function W(t,e,r){var s;e=new RegExp(e,"g");let n,i=0;do{n=e.exec(t);let o=(s=n==null?void 0:n.index)!=null?s:t.length;if(in?e(r,1,n,n=i):t.charAt(i)===">"&&(t.charAt(n+1)==="/"?--s===0&&e(r++,2,n,i+1):t.charAt(i-1)!=="/"&&s++===0&&e(r,0,n,i+1),n=i+1);i>n&&e(r,1,n,i)}function se(t,e,r,n=!1){return q([t],e,r,n).pop()}function q(t,e,r,n=!1){let i=[0];for(let s=1;s>>2&1023,c=a[0]>>>12;i.push(+(u>c)+i[i.length-1])}return t.map((s,o)=>{let a=0,u=new Map;for(let f of r.sort((g,l)=>g-l)){let g=f&1048575,l=f>>>20;if(i[l]!==o)continue;let m=u.get(l);typeof m=="undefined"&&u.set(l,m=[]),m.push(g)}if(u.size===0)return s;let c=[];for(let[f,g]of u){let l=e[f],m=l[0]>>>12,x=l[l.length-1]>>>12,v=l[l.length-1]>>>2&1023;n&&m>a&&c.push(s.slice(a,m));let d=s.slice(m,x+v);for(let y of g.sort((b,E)=>E-b)){let b=(l[y]>>>12)-m,E=(l[y]>>>2&1023)+b;d=[d.slice(0,b),"",d.slice(b,E),"",d.slice(E)].join("")}if(a=x+v,c.push(d)===2)break}return n&&a{var f;switch(i[f=o+=s]||(i[f]=[]),a){case 0:case 2:i[o].push(u<<12|c-u<<2|a);break;case 1:let g=r[n].slice(u,c);W(g,lunr.tokenizer.separator,(l,m)=>{if(typeof lunr.segmenter!="undefined"){let x=g.slice(l,m);if(/^[MHIK]$/.test(lunr.segmenter.ctype_(x))){let v=lunr.segmenter.segment(x);for(let d=0,y=0;dr){return t.trim().split(/"([^"]+)"/g).map((r,n)=>n&1?r.replace(/^\b|^(?![^\x00-\x7F]|$)|\s+/g," +"):r).join("").replace(/"|(?:^|\s+)[*+\-:^~]+(?=\s+|$)/g,"").split(/\s+/g).reduce((r,n)=>{let i=e(n);return[...r,...Array.isArray(i)?i:[i]]},[]).map(r=>/([~^]$)/.test(r)?`${r}1`:r).map(r=>/(^[+-]|[~^]\d+$)/.test(r)?r:`${r}*`).join(" ")}function ue(t){return ae(t,e=>{let r=[],n=new lunr.QueryLexer(e);n.run();for(let{type:i,str:s,start:o,end:a}of n.lexemes)switch(i){case"FIELD":["title","text","tags"].includes(s)||(e=[e.slice(0,a)," ",e.slice(a+1)].join(""));break;case"TERM":W(s,lunr.tokenizer.separator,(...u)=>{r.push([e.slice(0,o),s.slice(...u),e.slice(a)].join(""))})}return r})}function ce(t){let e=new lunr.Query(["title","text","tags"]);new lunr.QueryParser(t,e).parse();for(let n of e.clauses)n.usePipeline=!0,n.term.startsWith("*")&&(n.wildcard=lunr.Query.wildcard.LEADING,n.term=n.term.slice(1)),n.term.endsWith("*")&&(n.wildcard=lunr.Query.wildcard.TRAILING,n.term=n.term.slice(0,-1));return e.clauses}function le(t,e){var i;let r=new Set(t),n={};for(let s=0;s0;){let o=i[--s];for(let u=1;un[o]-u&&(r.add(t.slice(o,o+u)),i[s++]=o+u);let a=o+n[o];n[a]&&ar=>{if(typeof r[e]=="undefined")return;let n=[r.location,e].join(":");return t.set(n,lunr.tokenizer.table=[]),r[e]}}function Re(t,e){let[r,n]=[new Set(t),new Set(e)];return[...new Set([...r].filter(i=>!n.has(i)))]}var H=class{constructor({config:e,docs:r,options:n}){let i=Oe(this.table=new Map);this.map=ne(r),this.options=n,this.index=lunr(function(){this.metadataWhitelist=["position"],this.b(0),e.lang.length===1&&e.lang[0]!=="en"?this.use(lunr[e.lang[0]]):e.lang.length>1&&this.use(lunr.multiLanguage(...e.lang)),this.tokenizer=oe,lunr.tokenizer.separator=new RegExp(e.separator),lunr.segmenter="TinySegmenter"in lunr?new lunr.TinySegmenter:void 0;let s=Re(["trimmer","stopWordFilter","stemmer"],e.pipeline);for(let o of e.lang.map(a=>a==="en"?lunr:lunr[a]))for(let a of s)this.pipeline.remove(o[a]),this.searchPipeline.remove(o[a]);this.ref("location"),this.field("title",{boost:1e3,extractor:i("title")}),this.field("text",{boost:1,extractor:i("text")}),this.field("tags",{boost:1e6,extractor:i("tags")});for(let o of r)this.add(o,{boost:o.boost})})}search(e){if(e=e.replace(new RegExp("\\p{sc=Han}+","gu"),s=>[...he(s,this.index.invertedIndex)].join("* ")),e=ue(e),!e)return{items:[]};let r=ce(e).filter(s=>s.presence!==lunr.Query.presence.PROHIBITED),n=this.index.search(e).reduce((s,{ref:o,score:a,matchData:u})=>{let c=this.map.get(o);if(typeof c!="undefined"){c=A({},c),c.tags&&(c.tags=[...c.tags]);let f=le(r,Object.keys(u.metadata));for(let l of this.index.fields){if(typeof c[l]=="undefined")continue;let m=[];for(let d of Object.values(u.metadata))typeof d[l]!="undefined"&&m.push(...d[l].position);if(!m.length)continue;let x=this.table.get([c.location,l].join(":")),v=Array.isArray(c[l])?q:se;c[l]=v(c[l],x,m,l!=="text")}let g=+!c.parent+Object.values(f).filter(l=>l).length/Object.keys(f).length;s.push(G(A({},c),{score:a*(1+Z(g,2)),terms:f}))}return s},[]).sort((s,o)=>o.score-s.score).reduce((s,o)=>{let a=this.map.get(o.location);if(typeof a!="undefined"){let u=a.parent?a.parent.location:a.location;s.set(u,[...s.get(u)||[],o])}return s},new Map);for(let[s,o]of n)if(!o.find(a=>a.location===s)){let a=this.map.get(s);o.push(G(A({},a),{score:0,terms:{}}))}let i;if(this.options.suggest){let s=this.index.query(o=>{for(let a of r)o.term(a.term,{fields:["title"],presence:lunr.Query.presence.REQUIRED,wildcard:lunr.Query.wildcard.TRAILING})});i=s.length?Object.keys(s[0].matchData.metadata):[]}return A({items:[...n.values()]},typeof i!="undefined"&&{suggest:i})}};var fe;function Ie(t){return B(this,null,function*(){let e="../lunr";if(typeof parent!="undefined"&&"IFrameWorker"in parent){let n=re("script[src]"),[i]=n.src.split("/worker");e=e.replace("..",i)}let r=[];for(let n of t.lang){switch(n){case"ja":r.push(`${e}/tinyseg.js`);break;case"hi":case"th":r.push(`${e}/wordcut.js`);break}n!=="en"&&r.push(`${e}/min/lunr.${n}.min.js`)}t.lang.length>1&&r.push(`${e}/min/lunr.multi.min.js`),r.length&&(yield importScripts(`${e}/min/lunr.stemmer.support.min.js`,...r))})}function Fe(t){return B(this,null,function*(){switch(t.type){case 0:return yield Ie(t.data.config),fe=new H(t.data),{type:1};case 2:let e=t.data;try{return{type:3,data:fe.search(e)}}catch(r){return console.warn(`Invalid query: ${e} \u2013 see https://bit.ly/2s3ChXG`),console.warn(r),{type:3,data:{items:[]}}}default:throw new TypeError("Invalid message type")}})}self.lunr=de.default;addEventListener("message",t=>B(void 0,null,function*(){postMessage(yield Fe(t.data))}));})(); -//# sourceMappingURL=search.07f07601.min.js.map + */t.Builder=function(){this._ref="id",this._fields=Object.create(null),this._documents=Object.create(null),this.invertedIndex=Object.create(null),this.fieldTermFrequencies={},this.fieldLengths={},this.tokenizer=t.tokenizer,this.pipeline=new t.Pipeline,this.searchPipeline=new t.Pipeline,this.documentCount=0,this._b=.75,this._k1=1.2,this.termIndex=0,this.metadataWhitelist=[]},t.Builder.prototype.ref=function(e){this._ref=e},t.Builder.prototype.field=function(e,r){if(/\//.test(e))throw new RangeError("Field '"+e+"' contains illegal character '/'");this._fields[e]=r||{}},t.Builder.prototype.b=function(e){e<0?this._b=0:e>1?this._b=1:this._b=e},t.Builder.prototype.k1=function(e){this._k1=e},t.Builder.prototype.add=function(e,r){var n=e[this._ref],i=Object.keys(this._fields);this._documents[n]=r||{},this.documentCount+=1;for(var s=0;s=this.length)return t.QueryLexer.EOS;var e=this.str.charAt(this.pos);return this.pos+=1,e},t.QueryLexer.prototype.width=function(){return this.pos-this.start},t.QueryLexer.prototype.ignore=function(){this.start==this.pos&&(this.pos+=1),this.start=this.pos},t.QueryLexer.prototype.backup=function(){this.pos-=1},t.QueryLexer.prototype.acceptDigitRun=function(){var e,r;do e=this.next(),r=e.charCodeAt(0);while(r>47&&r<58);e!=t.QueryLexer.EOS&&this.backup()},t.QueryLexer.prototype.more=function(){return this.pos1&&(e.backup(),e.emit(t.QueryLexer.TERM)),e.ignore(),e.more())return t.QueryLexer.lexText},t.QueryLexer.lexEditDistance=function(e){return e.ignore(),e.acceptDigitRun(),e.emit(t.QueryLexer.EDIT_DISTANCE),t.QueryLexer.lexText},t.QueryLexer.lexBoost=function(e){return e.ignore(),e.acceptDigitRun(),e.emit(t.QueryLexer.BOOST),t.QueryLexer.lexText},t.QueryLexer.lexEOS=function(e){e.width()>0&&e.emit(t.QueryLexer.TERM)},t.QueryLexer.termSeparator=t.tokenizer.separator,t.QueryLexer.lexText=function(e){for(;;){var r=e.next();if(r==t.QueryLexer.EOS)return t.QueryLexer.lexEOS;if(r.charCodeAt(0)==92){e.escapeCharacter();continue}if(r==":")return t.QueryLexer.lexField;if(r=="~")return e.backup(),e.width()>0&&e.emit(t.QueryLexer.TERM),t.QueryLexer.lexEditDistance;if(r=="^")return e.backup(),e.width()>0&&e.emit(t.QueryLexer.TERM),t.QueryLexer.lexBoost;if(r=="+"&&e.width()===1||r=="-"&&e.width()===1)return e.emit(t.QueryLexer.PRESENCE),t.QueryLexer.lexText;if(r.match(t.QueryLexer.termSeparator))return t.QueryLexer.lexTerm}},t.QueryParser=function(e,r){this.lexer=new t.QueryLexer(e),this.query=r,this.currentClause={},this.lexemeIdx=0},t.QueryParser.prototype.parse=function(){this.lexer.run(),this.lexemes=this.lexer.lexemes;for(var e=t.QueryParser.parseClause;e;)e=e(this);return this.query},t.QueryParser.prototype.peekLexeme=function(){return this.lexemes[this.lexemeIdx]},t.QueryParser.prototype.consumeLexeme=function(){var e=this.peekLexeme();return this.lexemeIdx+=1,e},t.QueryParser.prototype.nextClause=function(){var e=this.currentClause;this.query.clause(e),this.currentClause={}},t.QueryParser.parseClause=function(e){var r=e.peekLexeme();if(r!=null)switch(r.type){case t.QueryLexer.PRESENCE:return t.QueryParser.parsePresence;case t.QueryLexer.FIELD:return t.QueryParser.parseField;case t.QueryLexer.TERM:return t.QueryParser.parseTerm;default:var n="expected either a field or a term, found "+r.type;throw r.str.length>=1&&(n+=" with value '"+r.str+"'"),new t.QueryParseError(n,r.start,r.end)}},t.QueryParser.parsePresence=function(e){var r=e.consumeLexeme();if(r!=null){switch(r.str){case"-":e.currentClause.presence=t.Query.presence.PROHIBITED;break;case"+":e.currentClause.presence=t.Query.presence.REQUIRED;break;default:var n="unrecognised presence operator'"+r.str+"'";throw new t.QueryParseError(n,r.start,r.end)}var i=e.peekLexeme();if(i==null){var n="expecting term or field, found nothing";throw new t.QueryParseError(n,r.start,r.end)}switch(i.type){case t.QueryLexer.FIELD:return t.QueryParser.parseField;case t.QueryLexer.TERM:return t.QueryParser.parseTerm;default:var n="expecting term or field, found '"+i.type+"'";throw new t.QueryParseError(n,i.start,i.end)}}},t.QueryParser.parseField=function(e){var r=e.consumeLexeme();if(r!=null){if(e.query.allFields.indexOf(r.str)==-1){var n=e.query.allFields.map(function(o){return"'"+o+"'"}).join(", "),i="unrecognised field '"+r.str+"', possible fields: "+n;throw new t.QueryParseError(i,r.start,r.end)}e.currentClause.fields=[r.str];var s=e.peekLexeme();if(s==null){var i="expecting term, found nothing";throw new t.QueryParseError(i,r.start,r.end)}switch(s.type){case t.QueryLexer.TERM:return t.QueryParser.parseTerm;default:var i="expecting term, found '"+s.type+"'";throw new t.QueryParseError(i,s.start,s.end)}}},t.QueryParser.parseTerm=function(e){var r=e.consumeLexeme();if(r!=null){e.currentClause.term=r.str.toLowerCase(),r.str.indexOf("*")!=-1&&(e.currentClause.usePipeline=!1);var n=e.peekLexeme();if(n==null){e.nextClause();return}switch(n.type){case t.QueryLexer.TERM:return e.nextClause(),t.QueryParser.parseTerm;case t.QueryLexer.FIELD:return e.nextClause(),t.QueryParser.parseField;case t.QueryLexer.EDIT_DISTANCE:return t.QueryParser.parseEditDistance;case t.QueryLexer.BOOST:return t.QueryParser.parseBoost;case t.QueryLexer.PRESENCE:return e.nextClause(),t.QueryParser.parsePresence;default:var i="Unexpected lexeme type '"+n.type+"'";throw new t.QueryParseError(i,n.start,n.end)}}},t.QueryParser.parseEditDistance=function(e){var r=e.consumeLexeme();if(r!=null){var n=parseInt(r.str,10);if(isNaN(n)){var i="edit distance must be numeric";throw new t.QueryParseError(i,r.start,r.end)}e.currentClause.editDistance=n;var s=e.peekLexeme();if(s==null){e.nextClause();return}switch(s.type){case t.QueryLexer.TERM:return e.nextClause(),t.QueryParser.parseTerm;case t.QueryLexer.FIELD:return e.nextClause(),t.QueryParser.parseField;case t.QueryLexer.EDIT_DISTANCE:return t.QueryParser.parseEditDistance;case t.QueryLexer.BOOST:return t.QueryParser.parseBoost;case t.QueryLexer.PRESENCE:return e.nextClause(),t.QueryParser.parsePresence;default:var i="Unexpected lexeme type '"+s.type+"'";throw new t.QueryParseError(i,s.start,s.end)}}},t.QueryParser.parseBoost=function(e){var r=e.consumeLexeme();if(r!=null){var n=parseInt(r.str,10);if(isNaN(n)){var i="boost must be numeric";throw new t.QueryParseError(i,r.start,r.end)}e.currentClause.boost=n;var s=e.peekLexeme();if(s==null){e.nextClause();return}switch(s.type){case t.QueryLexer.TERM:return e.nextClause(),t.QueryParser.parseTerm;case t.QueryLexer.FIELD:return e.nextClause(),t.QueryParser.parseField;case t.QueryLexer.EDIT_DISTANCE:return t.QueryParser.parseEditDistance;case t.QueryLexer.BOOST:return t.QueryParser.parseBoost;case t.QueryLexer.PRESENCE:return e.nextClause(),t.QueryParser.parsePresence;default:var i="Unexpected lexeme type '"+s.type+"'";throw new t.QueryParseError(i,s.start,s.end)}}},function(e,r){typeof define=="function"&&define.amd?define(r):typeof ee=="object"?te.exports=r():e.lunr=r()}(this,function(){return t})})()});var Y=Pe(re());function ne(t,e=document){let r=ke(t,e);if(typeof r=="undefined")throw new ReferenceError(`Missing element: expected "${t}" to be present`);return r}function ke(t,e=document){return e.querySelector(t)||void 0}Object.entries||(Object.entries=function(t){let e=[];for(let r of Object.keys(t))e.push([r,t[r]]);return e});Object.values||(Object.values=function(t){let e=[];for(let r of Object.keys(t))e.push(t[r]);return e});typeof Element!="undefined"&&(Element.prototype.scrollTo||(Element.prototype.scrollTo=function(t,e){typeof t=="object"?(this.scrollLeft=t.left,this.scrollTop=t.top):(this.scrollLeft=t,this.scrollTop=e)}),Element.prototype.replaceWith||(Element.prototype.replaceWith=function(...t){let e=this.parentNode;if(e){t.length===0&&e.removeChild(this);for(let r=t.length-1;r>=0;r--){let n=t[r];typeof n=="string"?n=document.createTextNode(n):n.parentNode&&n.parentNode.removeChild(n),r?e.insertBefore(this.previousSibling,n):e.replaceChild(n,this)}}}));function ie(t){let e=new Map;for(let r of t){let[n]=r.location.split("#"),i=e.get(n);typeof i=="undefined"?e.set(n,r):(e.set(r.location,r),r.parent=i)}return e}function W(t,e,r){var s;e=new RegExp(e,"g");let n,i=0;do{n=e.exec(t);let o=(s=n==null?void 0:n.index)!=null?s:t.length;if(in?e(r,1,n,n=i):t.charAt(i)===">"&&(t.charAt(n+1)==="/"?--s===0&&e(r++,2,n,i+1):t.charAt(i-1)!=="/"&&s++===0&&e(r,0,n,i+1),n=i+1);i>n&&e(r,1,n,i)}function oe(t,e,r,n=!1){return q([t],e,r,n).pop()}function q(t,e,r,n=!1){let i=[0];for(let s=1;s>>2&1023,c=a[0]>>>12;i.push(+(u>c)+i[i.length-1])}return t.map((s,o)=>{let a=0,u=new Map;for(let f of r.sort((g,l)=>g-l)){let g=f&1048575,l=f>>>20;if(i[l]!==o)continue;let m=u.get(l);typeof m=="undefined"&&u.set(l,m=[]),m.push(g)}if(u.size===0)return s;let c=[];for(let[f,g]of u){let l=e[f],m=l[0]>>>12,x=l[l.length-1]>>>12,v=l[l.length-1]>>>2&1023;n&&m>a&&c.push(s.slice(a,m));let d=s.slice(m,x+v);for(let y of g.sort((b,E)=>E-b)){let b=(l[y]>>>12)-m,E=(l[y]>>>2&1023)+b;d=[d.slice(0,b),"",d.slice(b,E),"",d.slice(E)].join("")}if(a=x+v,c.push(d)===2)break}return n&&a{var f;switch(i[f=o+=s]||(i[f]=[]),a){case 0:case 2:i[o].push(u<<12|c-u<<2|a);break;case 1:let g=r[n].slice(u,c);W(g,lunr.tokenizer.separator,(l,m)=>{if(typeof lunr.segmenter!="undefined"){let x=g.slice(l,m);if(/^[MHIK]$/.test(lunr.segmenter.ctype_(x))){let v=lunr.segmenter.segment(x);for(let d=0,y=0;dr){return t.trim().split(/"([^"]+)"/g).map((r,n)=>n&1?r.replace(/^\b|^(?![^\x00-\x7F]|$)|\s+/g," +"):r).join("").replace(/"|(?:^|\s+)[*+\-:^~]+(?=\s+|$)/g,"").split(/\s+/g).reduce((r,n)=>{let i=e(n);return[...r,...Array.isArray(i)?i:[i]]},[]).map(r=>/([~^]$)/.test(r)?`${r}1`:r).map(r=>/(^[+-]|[~^]\d+$)/.test(r)?r:`${r}*`).join(" ")}function ce(t){return ue(t,e=>{let r=[],n=new lunr.QueryLexer(e);n.run();for(let{type:i,str:s,start:o,end:a}of n.lexemes)switch(i){case"FIELD":["title","text","tags"].includes(s)||(e=[e.slice(0,a)," ",e.slice(a+1)].join(""));break;case"TERM":W(s,lunr.tokenizer.separator,(...u)=>{r.push([e.slice(0,o),s.slice(...u),e.slice(a)].join(""))})}return r})}function le(t){let e=new lunr.Query(["title","text","tags"]);new lunr.QueryParser(t,e).parse();for(let n of e.clauses)n.usePipeline=!0,n.term.startsWith("*")&&(n.wildcard=lunr.Query.wildcard.LEADING,n.term=n.term.slice(1)),n.term.endsWith("*")&&(n.wildcard=lunr.Query.wildcard.TRAILING,n.term=n.term.slice(0,-1));return e.clauses}function he(t,e){var i;let r=new Set(t),n={};for(let s=0;s0;){let o=i[--s];for(let u=1;un[o]-u&&(r.add(t.slice(o,o+u)),i[s++]=o+u);let a=o+n[o];n[a]&&ar=>{if(typeof r[e]=="undefined")return;let n=[r.location,e].join(":");return t.set(n,lunr.tokenizer.table=[]),r[e]}}function Re(t,e){let[r,n]=[new Set(t),new Set(e)];return[...new Set([...r].filter(i=>!n.has(i)))]}var H=class{constructor({config:e,docs:r,options:n}){let i=Oe(this.table=new Map);this.map=ie(r),this.options=n,this.index=lunr(function(){this.metadataWhitelist=["position"],this.b(0),e.lang.length===1&&e.lang[0]!=="en"?this.use(lunr[e.lang[0]]):e.lang.length>1&&this.use(lunr.multiLanguage(...e.lang)),this.tokenizer=ae,lunr.tokenizer.separator=new RegExp(e.separator),lunr.segmenter="TinySegmenter"in lunr?new lunr.TinySegmenter:void 0;let s=Re(["trimmer","stopWordFilter","stemmer"],e.pipeline);for(let o of e.lang.map(a=>a==="en"?lunr:lunr[a]))for(let a of s)this.pipeline.remove(o[a]),this.searchPipeline.remove(o[a]);this.ref("location"),this.field("title",{boost:1e3,extractor:i("title")}),this.field("text",{boost:1,extractor:i("text")}),this.field("tags",{boost:1e6,extractor:i("tags")});for(let o of r)this.add(o,{boost:o.boost})})}search(e){if(e=e.replace(new RegExp("\\p{sc=Han}+","gu"),s=>[...fe(s,this.index.invertedIndex)].join("* ")),e=ce(e),!e)return{items:[]};let r=le(e).filter(s=>s.presence!==lunr.Query.presence.PROHIBITED),n=this.index.search(e).reduce((s,{ref:o,score:a,matchData:u})=>{let c=this.map.get(o);if(typeof c!="undefined"){c=A({},c),c.tags&&(c.tags=[...c.tags]);let f=he(r,Object.keys(u.metadata));for(let l of this.index.fields){if(typeof c[l]=="undefined")continue;let m=[];for(let d of Object.values(u.metadata))typeof d[l]!="undefined"&&m.push(...d[l].position);if(!m.length)continue;let x=this.table.get([c.location,l].join(":")),v=Array.isArray(c[l])?q:oe;c[l]=v(c[l],x,m,l!=="text")}let g=+!c.parent+Object.values(f).filter(l=>l).length/Object.keys(f).length;s.push(G(A({},c),{score:a*(1+K(g,2)),terms:f}))}return s},[]).sort((s,o)=>o.score-s.score).reduce((s,o)=>{let a=this.map.get(o.location);if(typeof a!="undefined"){let u=a.parent?a.parent.location:a.location;s.set(u,[...s.get(u)||[],o])}return s},new Map);for(let[s,o]of n)if(!o.find(a=>a.location===s)){let a=this.map.get(s);o.push(G(A({},a),{score:0,terms:{}}))}let i;if(this.options.suggest){let s=this.index.query(o=>{for(let a of r)o.term(a.term,{fields:["title"],presence:lunr.Query.presence.REQUIRED,wildcard:lunr.Query.wildcard.TRAILING})});i=s.length?Object.keys(s[0].matchData.metadata):[]}return A({items:[...n.values()]},typeof i!="undefined"&&{suggest:i})}};var de;function Ie(t){return B(this,null,function*(){let e="../lunr";if(typeof parent!="undefined"&&"IFrameWorker"in parent){let n=ne("script[src]"),[i]=n.src.split("/worker");e=e.replace("..",i)}let r=[];for(let n of t.lang){switch(n){case"ja":r.push(`${e}/tinyseg.js`);break;case"hi":case"th":r.push(`${e}/wordcut.js`);break}n!=="en"&&r.push(`${e}/min/lunr.${n}.min.js`)}t.lang.length>1&&r.push(`${e}/min/lunr.multi.min.js`),r.length&&(yield importScripts(`${e}/min/lunr.stemmer.support.min.js`,...r))})}function Fe(t){return B(this,null,function*(){switch(t.type){case 0:return yield Ie(t.data.config),de=new H(t.data),{type:1};case 2:let e=t.data;try{return{type:3,data:de.search(e)}}catch(r){return console.warn(`Invalid query: ${e} \u2013 see https://bit.ly/2s3ChXG`),console.warn(r),{type:3,data:{items:[]}}}default:throw new TypeError("Invalid message type")}})}self.lunr=Y.default;Y.default.utils.warn=console.warn;addEventListener("message",t=>B(void 0,null,function*(){postMessage(yield Fe(t.data))}));})(); +//# sourceMappingURL=search.6ce7567c.min.js.map diff --git a/assets/javascripts/workers/search.07f07601.min.js.map b/assets/javascripts/workers/search.6ce7567c.min.js.map similarity index 78% rename from assets/javascripts/workers/search.07f07601.min.js.map rename to assets/javascripts/workers/search.6ce7567c.min.js.map index 7fdd4d0..e7c69d2 100644 --- a/assets/javascripts/workers/search.07f07601.min.js.map +++ b/assets/javascripts/workers/search.6ce7567c.min.js.map @@ -1,7 +1,7 @@ { "version": 3, "sources": ["node_modules/lunr/lunr.js", "src/templates/assets/javascripts/integrations/search/worker/main/index.ts", "src/templates/assets/javascripts/browser/element/_/index.ts", "src/templates/assets/javascripts/polyfills/index.ts", "src/templates/assets/javascripts/integrations/search/config/index.ts", "src/templates/assets/javascripts/integrations/search/internal/_/index.ts", "src/templates/assets/javascripts/integrations/search/internal/extract/index.ts", "src/templates/assets/javascripts/integrations/search/internal/highlight/index.ts", "src/templates/assets/javascripts/integrations/search/internal/tokenize/index.ts", "src/templates/assets/javascripts/integrations/search/query/transform/index.ts", "src/templates/assets/javascripts/integrations/search/query/_/index.ts", "src/templates/assets/javascripts/integrations/search/query/segment/index.ts", "src/templates/assets/javascripts/integrations/search/_/index.ts"], - "sourcesContent": ["/**\n * lunr - http://lunrjs.com - A bit like Solr, but much smaller and not as bright - 2.3.9\n * Copyright (C) 2020 Oliver Nightingale\n * @license MIT\n */\n\n;(function(){\n\n/**\n * A convenience function for configuring and constructing\n * a new lunr Index.\n *\n * A lunr.Builder instance is created and the pipeline setup\n * with a trimmer, stop word filter and stemmer.\n *\n * This builder object is yielded to the configuration function\n * that is passed as a parameter, allowing the list of fields\n * and other builder parameters to be customised.\n *\n * All documents _must_ be added within the passed config function.\n *\n * @example\n * var idx = lunr(function () {\n * this.field('title')\n * this.field('body')\n * this.ref('id')\n *\n * documents.forEach(function (doc) {\n * this.add(doc)\n * }, this)\n * })\n *\n * @see {@link lunr.Builder}\n * @see {@link lunr.Pipeline}\n * @see {@link lunr.trimmer}\n * @see {@link lunr.stopWordFilter}\n * @see {@link lunr.stemmer}\n * @namespace {function} lunr\n */\nvar lunr = function (config) {\n var builder = new lunr.Builder\n\n builder.pipeline.add(\n lunr.trimmer,\n lunr.stopWordFilter,\n lunr.stemmer\n )\n\n builder.searchPipeline.add(\n lunr.stemmer\n )\n\n config.call(builder, builder)\n return builder.build()\n}\n\nlunr.version = \"2.3.9\"\n/*!\n * lunr.utils\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * A namespace containing utils for the rest of the lunr library\n * @namespace lunr.utils\n */\nlunr.utils = {}\n\n/**\n * Print a warning message to the console.\n *\n * @param {String} message The message to be printed.\n * @memberOf lunr.utils\n * @function\n */\nlunr.utils.warn = (function (global) {\n /* eslint-disable no-console */\n return function (message) {\n if (global.console && console.warn) {\n console.warn(message)\n }\n }\n /* eslint-enable no-console */\n})(this)\n\n/**\n * Convert an object to a string.\n *\n * In the case of `null` and `undefined` the function returns\n * the empty string, in all other cases the result of calling\n * `toString` on the passed object is returned.\n *\n * @param {Any} obj The object to convert to a string.\n * @return {String} string representation of the passed object.\n * @memberOf lunr.utils\n */\nlunr.utils.asString = function (obj) {\n if (obj === void 0 || obj === null) {\n return \"\"\n } else {\n return obj.toString()\n }\n}\n\n/**\n * Clones an object.\n *\n * Will create a copy of an existing object such that any mutations\n * on the copy cannot affect the original.\n *\n * Only shallow objects are supported, passing a nested object to this\n * function will cause a TypeError.\n *\n * Objects with primitives, and arrays of primitives are supported.\n *\n * @param {Object} obj The object to clone.\n * @return {Object} a clone of the passed object.\n * @throws {TypeError} when a nested object is passed.\n * @memberOf Utils\n */\nlunr.utils.clone = function (obj) {\n if (obj === null || obj === undefined) {\n return obj\n }\n\n var clone = Object.create(null),\n keys = Object.keys(obj)\n\n for (var i = 0; i < keys.length; i++) {\n var key = keys[i],\n val = obj[key]\n\n if (Array.isArray(val)) {\n clone[key] = val.slice()\n continue\n }\n\n if (typeof val === 'string' ||\n typeof val === 'number' ||\n typeof val === 'boolean') {\n clone[key] = val\n continue\n }\n\n throw new TypeError(\"clone is not deep and does not support nested objects\")\n }\n\n return clone\n}\nlunr.FieldRef = function (docRef, fieldName, stringValue) {\n this.docRef = docRef\n this.fieldName = fieldName\n this._stringValue = stringValue\n}\n\nlunr.FieldRef.joiner = \"/\"\n\nlunr.FieldRef.fromString = function (s) {\n var n = s.indexOf(lunr.FieldRef.joiner)\n\n if (n === -1) {\n throw \"malformed field ref string\"\n }\n\n var fieldRef = s.slice(0, n),\n docRef = s.slice(n + 1)\n\n return new lunr.FieldRef (docRef, fieldRef, s)\n}\n\nlunr.FieldRef.prototype.toString = function () {\n if (this._stringValue == undefined) {\n this._stringValue = this.fieldName + lunr.FieldRef.joiner + this.docRef\n }\n\n return this._stringValue\n}\n/*!\n * lunr.Set\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * A lunr set.\n *\n * @constructor\n */\nlunr.Set = function (elements) {\n this.elements = Object.create(null)\n\n if (elements) {\n this.length = elements.length\n\n for (var i = 0; i < this.length; i++) {\n this.elements[elements[i]] = true\n }\n } else {\n this.length = 0\n }\n}\n\n/**\n * A complete set that contains all elements.\n *\n * @static\n * @readonly\n * @type {lunr.Set}\n */\nlunr.Set.complete = {\n intersect: function (other) {\n return other\n },\n\n union: function () {\n return this\n },\n\n contains: function () {\n return true\n }\n}\n\n/**\n * An empty set that contains no elements.\n *\n * @static\n * @readonly\n * @type {lunr.Set}\n */\nlunr.Set.empty = {\n intersect: function () {\n return this\n },\n\n union: function (other) {\n return other\n },\n\n contains: function () {\n return false\n }\n}\n\n/**\n * Returns true if this set contains the specified object.\n *\n * @param {object} object - Object whose presence in this set is to be tested.\n * @returns {boolean} - True if this set contains the specified object.\n */\nlunr.Set.prototype.contains = function (object) {\n return !!this.elements[object]\n}\n\n/**\n * Returns a new set containing only the elements that are present in both\n * this set and the specified set.\n *\n * @param {lunr.Set} other - set to intersect with this set.\n * @returns {lunr.Set} a new set that is the intersection of this and the specified set.\n */\n\nlunr.Set.prototype.intersect = function (other) {\n var a, b, elements, intersection = []\n\n if (other === lunr.Set.complete) {\n return this\n }\n\n if (other === lunr.Set.empty) {\n return other\n }\n\n if (this.length < other.length) {\n a = this\n b = other\n } else {\n a = other\n b = this\n }\n\n elements = Object.keys(a.elements)\n\n for (var i = 0; i < elements.length; i++) {\n var element = elements[i]\n if (element in b.elements) {\n intersection.push(element)\n }\n }\n\n return new lunr.Set (intersection)\n}\n\n/**\n * Returns a new set combining the elements of this and the specified set.\n *\n * @param {lunr.Set} other - set to union with this set.\n * @return {lunr.Set} a new set that is the union of this and the specified set.\n */\n\nlunr.Set.prototype.union = function (other) {\n if (other === lunr.Set.complete) {\n return lunr.Set.complete\n }\n\n if (other === lunr.Set.empty) {\n return this\n }\n\n return new lunr.Set(Object.keys(this.elements).concat(Object.keys(other.elements)))\n}\n/**\n * A function to calculate the inverse document frequency for\n * a posting. This is shared between the builder and the index\n *\n * @private\n * @param {object} posting - The posting for a given term\n * @param {number} documentCount - The total number of documents.\n */\nlunr.idf = function (posting, documentCount) {\n var documentsWithTerm = 0\n\n for (var fieldName in posting) {\n if (fieldName == '_index') continue // Ignore the term index, its not a field\n documentsWithTerm += Object.keys(posting[fieldName]).length\n }\n\n var x = (documentCount - documentsWithTerm + 0.5) / (documentsWithTerm + 0.5)\n\n return Math.log(1 + Math.abs(x))\n}\n\n/**\n * A token wraps a string representation of a token\n * as it is passed through the text processing pipeline.\n *\n * @constructor\n * @param {string} [str=''] - The string token being wrapped.\n * @param {object} [metadata={}] - Metadata associated with this token.\n */\nlunr.Token = function (str, metadata) {\n this.str = str || \"\"\n this.metadata = metadata || {}\n}\n\n/**\n * Returns the token string that is being wrapped by this object.\n *\n * @returns {string}\n */\nlunr.Token.prototype.toString = function () {\n return this.str\n}\n\n/**\n * A token update function is used when updating or optionally\n * when cloning a token.\n *\n * @callback lunr.Token~updateFunction\n * @param {string} str - The string representation of the token.\n * @param {Object} metadata - All metadata associated with this token.\n */\n\n/**\n * Applies the given function to the wrapped string token.\n *\n * @example\n * token.update(function (str, metadata) {\n * return str.toUpperCase()\n * })\n *\n * @param {lunr.Token~updateFunction} fn - A function to apply to the token string.\n * @returns {lunr.Token}\n */\nlunr.Token.prototype.update = function (fn) {\n this.str = fn(this.str, this.metadata)\n return this\n}\n\n/**\n * Creates a clone of this token. Optionally a function can be\n * applied to the cloned token.\n *\n * @param {lunr.Token~updateFunction} [fn] - An optional function to apply to the cloned token.\n * @returns {lunr.Token}\n */\nlunr.Token.prototype.clone = function (fn) {\n fn = fn || function (s) { return s }\n return new lunr.Token (fn(this.str, this.metadata), this.metadata)\n}\n/*!\n * lunr.tokenizer\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * A function for splitting a string into tokens ready to be inserted into\n * the search index. Uses `lunr.tokenizer.separator` to split strings, change\n * the value of this property to change how strings are split into tokens.\n *\n * This tokenizer will convert its parameter to a string by calling `toString` and\n * then will split this string on the character in `lunr.tokenizer.separator`.\n * Arrays will have their elements converted to strings and wrapped in a lunr.Token.\n *\n * Optional metadata can be passed to the tokenizer, this metadata will be cloned and\n * added as metadata to every token that is created from the object to be tokenized.\n *\n * @static\n * @param {?(string|object|object[])} obj - The object to convert into tokens\n * @param {?object} metadata - Optional metadata to associate with every token\n * @returns {lunr.Token[]}\n * @see {@link lunr.Pipeline}\n */\nlunr.tokenizer = function (obj, metadata) {\n if (obj == null || obj == undefined) {\n return []\n }\n\n if (Array.isArray(obj)) {\n return obj.map(function (t) {\n return new lunr.Token(\n lunr.utils.asString(t).toLowerCase(),\n lunr.utils.clone(metadata)\n )\n })\n }\n\n var str = obj.toString().toLowerCase(),\n len = str.length,\n tokens = []\n\n for (var sliceEnd = 0, sliceStart = 0; sliceEnd <= len; sliceEnd++) {\n var char = str.charAt(sliceEnd),\n sliceLength = sliceEnd - sliceStart\n\n if ((char.match(lunr.tokenizer.separator) || sliceEnd == len)) {\n\n if (sliceLength > 0) {\n var tokenMetadata = lunr.utils.clone(metadata) || {}\n tokenMetadata[\"position\"] = [sliceStart, sliceLength]\n tokenMetadata[\"index\"] = tokens.length\n\n tokens.push(\n new lunr.Token (\n str.slice(sliceStart, sliceEnd),\n tokenMetadata\n )\n )\n }\n\n sliceStart = sliceEnd + 1\n }\n\n }\n\n return tokens\n}\n\n/**\n * The separator used to split a string into tokens. Override this property to change the behaviour of\n * `lunr.tokenizer` behaviour when tokenizing strings. By default this splits on whitespace and hyphens.\n *\n * @static\n * @see lunr.tokenizer\n */\nlunr.tokenizer.separator = /[\\s\\-]+/\n/*!\n * lunr.Pipeline\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * lunr.Pipelines maintain an ordered list of functions to be applied to all\n * tokens in documents entering the search index and queries being ran against\n * the index.\n *\n * An instance of lunr.Index created with the lunr shortcut will contain a\n * pipeline with a stop word filter and an English language stemmer. Extra\n * functions can be added before or after either of these functions or these\n * default functions can be removed.\n *\n * When run the pipeline will call each function in turn, passing a token, the\n * index of that token in the original list of all tokens and finally a list of\n * all the original tokens.\n *\n * The output of functions in the pipeline will be passed to the next function\n * in the pipeline. To exclude a token from entering the index the function\n * should return undefined, the rest of the pipeline will not be called with\n * this token.\n *\n * For serialisation of pipelines to work, all functions used in an instance of\n * a pipeline should be registered with lunr.Pipeline. Registered functions can\n * then be loaded. If trying to load a serialised pipeline that uses functions\n * that are not registered an error will be thrown.\n *\n * If not planning on serialising the pipeline then registering pipeline functions\n * is not necessary.\n *\n * @constructor\n */\nlunr.Pipeline = function () {\n this._stack = []\n}\n\nlunr.Pipeline.registeredFunctions = Object.create(null)\n\n/**\n * A pipeline function maps lunr.Token to lunr.Token. A lunr.Token contains the token\n * string as well as all known metadata. A pipeline function can mutate the token string\n * or mutate (or add) metadata for a given token.\n *\n * A pipeline function can indicate that the passed token should be discarded by returning\n * null, undefined or an empty string. This token will not be passed to any downstream pipeline\n * functions and will not be added to the index.\n *\n * Multiple tokens can be returned by returning an array of tokens. Each token will be passed\n * to any downstream pipeline functions and all will returned tokens will be added to the index.\n *\n * Any number of pipeline functions may be chained together using a lunr.Pipeline.\n *\n * @interface lunr.PipelineFunction\n * @param {lunr.Token} token - A token from the document being processed.\n * @param {number} i - The index of this token in the complete list of tokens for this document/field.\n * @param {lunr.Token[]} tokens - All tokens for this document/field.\n * @returns {(?lunr.Token|lunr.Token[])}\n */\n\n/**\n * Register a function with the pipeline.\n *\n * Functions that are used in the pipeline should be registered if the pipeline\n * needs to be serialised, or a serialised pipeline needs to be loaded.\n *\n * Registering a function does not add it to a pipeline, functions must still be\n * added to instances of the pipeline for them to be used when running a pipeline.\n *\n * @param {lunr.PipelineFunction} fn - The function to check for.\n * @param {String} label - The label to register this function with\n */\nlunr.Pipeline.registerFunction = function (fn, label) {\n if (label in this.registeredFunctions) {\n lunr.utils.warn('Overwriting existing registered function: ' + label)\n }\n\n fn.label = label\n lunr.Pipeline.registeredFunctions[fn.label] = fn\n}\n\n/**\n * Warns if the function is not registered as a Pipeline function.\n *\n * @param {lunr.PipelineFunction} fn - The function to check for.\n * @private\n */\nlunr.Pipeline.warnIfFunctionNotRegistered = function (fn) {\n var isRegistered = fn.label && (fn.label in this.registeredFunctions)\n\n if (!isRegistered) {\n lunr.utils.warn('Function is not registered with pipeline. This may cause problems when serialising the index.\\n', fn)\n }\n}\n\n/**\n * Loads a previously serialised pipeline.\n *\n * All functions to be loaded must already be registered with lunr.Pipeline.\n * If any function from the serialised data has not been registered then an\n * error will be thrown.\n *\n * @param {Object} serialised - The serialised pipeline to load.\n * @returns {lunr.Pipeline}\n */\nlunr.Pipeline.load = function (serialised) {\n var pipeline = new lunr.Pipeline\n\n serialised.forEach(function (fnName) {\n var fn = lunr.Pipeline.registeredFunctions[fnName]\n\n if (fn) {\n pipeline.add(fn)\n } else {\n throw new Error('Cannot load unregistered function: ' + fnName)\n }\n })\n\n return pipeline\n}\n\n/**\n * Adds new functions to the end of the pipeline.\n *\n * Logs a warning if the function has not been registered.\n *\n * @param {lunr.PipelineFunction[]} functions - Any number of functions to add to the pipeline.\n */\nlunr.Pipeline.prototype.add = function () {\n var fns = Array.prototype.slice.call(arguments)\n\n fns.forEach(function (fn) {\n lunr.Pipeline.warnIfFunctionNotRegistered(fn)\n this._stack.push(fn)\n }, this)\n}\n\n/**\n * Adds a single function after a function that already exists in the\n * pipeline.\n *\n * Logs a warning if the function has not been registered.\n *\n * @param {lunr.PipelineFunction} existingFn - A function that already exists in the pipeline.\n * @param {lunr.PipelineFunction} newFn - The new function to add to the pipeline.\n */\nlunr.Pipeline.prototype.after = function (existingFn, newFn) {\n lunr.Pipeline.warnIfFunctionNotRegistered(newFn)\n\n var pos = this._stack.indexOf(existingFn)\n if (pos == -1) {\n throw new Error('Cannot find existingFn')\n }\n\n pos = pos + 1\n this._stack.splice(pos, 0, newFn)\n}\n\n/**\n * Adds a single function before a function that already exists in the\n * pipeline.\n *\n * Logs a warning if the function has not been registered.\n *\n * @param {lunr.PipelineFunction} existingFn - A function that already exists in the pipeline.\n * @param {lunr.PipelineFunction} newFn - The new function to add to the pipeline.\n */\nlunr.Pipeline.prototype.before = function (existingFn, newFn) {\n lunr.Pipeline.warnIfFunctionNotRegistered(newFn)\n\n var pos = this._stack.indexOf(existingFn)\n if (pos == -1) {\n throw new Error('Cannot find existingFn')\n }\n\n this._stack.splice(pos, 0, newFn)\n}\n\n/**\n * Removes a function from the pipeline.\n *\n * @param {lunr.PipelineFunction} fn The function to remove from the pipeline.\n */\nlunr.Pipeline.prototype.remove = function (fn) {\n var pos = this._stack.indexOf(fn)\n if (pos == -1) {\n return\n }\n\n this._stack.splice(pos, 1)\n}\n\n/**\n * Runs the current list of functions that make up the pipeline against the\n * passed tokens.\n *\n * @param {Array} tokens The tokens to run through the pipeline.\n * @returns {Array}\n */\nlunr.Pipeline.prototype.run = function (tokens) {\n var stackLength = this._stack.length\n\n for (var i = 0; i < stackLength; i++) {\n var fn = this._stack[i]\n var memo = []\n\n for (var j = 0; j < tokens.length; j++) {\n var result = fn(tokens[j], j, tokens)\n\n if (result === null || result === void 0 || result === '') continue\n\n if (Array.isArray(result)) {\n for (var k = 0; k < result.length; k++) {\n memo.push(result[k])\n }\n } else {\n memo.push(result)\n }\n }\n\n tokens = memo\n }\n\n return tokens\n}\n\n/**\n * Convenience method for passing a string through a pipeline and getting\n * strings out. This method takes care of wrapping the passed string in a\n * token and mapping the resulting tokens back to strings.\n *\n * @param {string} str - The string to pass through the pipeline.\n * @param {?object} metadata - Optional metadata to associate with the token\n * passed to the pipeline.\n * @returns {string[]}\n */\nlunr.Pipeline.prototype.runString = function (str, metadata) {\n var token = new lunr.Token (str, metadata)\n\n return this.run([token]).map(function (t) {\n return t.toString()\n })\n}\n\n/**\n * Resets the pipeline by removing any existing processors.\n *\n */\nlunr.Pipeline.prototype.reset = function () {\n this._stack = []\n}\n\n/**\n * Returns a representation of the pipeline ready for serialisation.\n *\n * Logs a warning if the function has not been registered.\n *\n * @returns {Array}\n */\nlunr.Pipeline.prototype.toJSON = function () {\n return this._stack.map(function (fn) {\n lunr.Pipeline.warnIfFunctionNotRegistered(fn)\n\n return fn.label\n })\n}\n/*!\n * lunr.Vector\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * A vector is used to construct the vector space of documents and queries. These\n * vectors support operations to determine the similarity between two documents or\n * a document and a query.\n *\n * Normally no parameters are required for initializing a vector, but in the case of\n * loading a previously dumped vector the raw elements can be provided to the constructor.\n *\n * For performance reasons vectors are implemented with a flat array, where an elements\n * index is immediately followed by its value. E.g. [index, value, index, value]. This\n * allows the underlying array to be as sparse as possible and still offer decent\n * performance when being used for vector calculations.\n *\n * @constructor\n * @param {Number[]} [elements] - The flat list of element index and element value pairs.\n */\nlunr.Vector = function (elements) {\n this._magnitude = 0\n this.elements = elements || []\n}\n\n\n/**\n * Calculates the position within the vector to insert a given index.\n *\n * This is used internally by insert and upsert. If there are duplicate indexes then\n * the position is returned as if the value for that index were to be updated, but it\n * is the callers responsibility to check whether there is a duplicate at that index\n *\n * @param {Number} insertIdx - The index at which the element should be inserted.\n * @returns {Number}\n */\nlunr.Vector.prototype.positionForIndex = function (index) {\n // For an empty vector the tuple can be inserted at the beginning\n if (this.elements.length == 0) {\n return 0\n }\n\n var start = 0,\n end = this.elements.length / 2,\n sliceLength = end - start,\n pivotPoint = Math.floor(sliceLength / 2),\n pivotIndex = this.elements[pivotPoint * 2]\n\n while (sliceLength > 1) {\n if (pivotIndex < index) {\n start = pivotPoint\n }\n\n if (pivotIndex > index) {\n end = pivotPoint\n }\n\n if (pivotIndex == index) {\n break\n }\n\n sliceLength = end - start\n pivotPoint = start + Math.floor(sliceLength / 2)\n pivotIndex = this.elements[pivotPoint * 2]\n }\n\n if (pivotIndex == index) {\n return pivotPoint * 2\n }\n\n if (pivotIndex > index) {\n return pivotPoint * 2\n }\n\n if (pivotIndex < index) {\n return (pivotPoint + 1) * 2\n }\n}\n\n/**\n * Inserts an element at an index within the vector.\n *\n * Does not allow duplicates, will throw an error if there is already an entry\n * for this index.\n *\n * @param {Number} insertIdx - The index at which the element should be inserted.\n * @param {Number} val - The value to be inserted into the vector.\n */\nlunr.Vector.prototype.insert = function (insertIdx, val) {\n this.upsert(insertIdx, val, function () {\n throw \"duplicate index\"\n })\n}\n\n/**\n * Inserts or updates an existing index within the vector.\n *\n * @param {Number} insertIdx - The index at which the element should be inserted.\n * @param {Number} val - The value to be inserted into the vector.\n * @param {function} fn - A function that is called for updates, the existing value and the\n * requested value are passed as arguments\n */\nlunr.Vector.prototype.upsert = function (insertIdx, val, fn) {\n this._magnitude = 0\n var position = this.positionForIndex(insertIdx)\n\n if (this.elements[position] == insertIdx) {\n this.elements[position + 1] = fn(this.elements[position + 1], val)\n } else {\n this.elements.splice(position, 0, insertIdx, val)\n }\n}\n\n/**\n * Calculates the magnitude of this vector.\n *\n * @returns {Number}\n */\nlunr.Vector.prototype.magnitude = function () {\n if (this._magnitude) return this._magnitude\n\n var sumOfSquares = 0,\n elementsLength = this.elements.length\n\n for (var i = 1; i < elementsLength; i += 2) {\n var val = this.elements[i]\n sumOfSquares += val * val\n }\n\n return this._magnitude = Math.sqrt(sumOfSquares)\n}\n\n/**\n * Calculates the dot product of this vector and another vector.\n *\n * @param {lunr.Vector} otherVector - The vector to compute the dot product with.\n * @returns {Number}\n */\nlunr.Vector.prototype.dot = function (otherVector) {\n var dotProduct = 0,\n a = this.elements, b = otherVector.elements,\n aLen = a.length, bLen = b.length,\n aVal = 0, bVal = 0,\n i = 0, j = 0\n\n while (i < aLen && j < bLen) {\n aVal = a[i], bVal = b[j]\n if (aVal < bVal) {\n i += 2\n } else if (aVal > bVal) {\n j += 2\n } else if (aVal == bVal) {\n dotProduct += a[i + 1] * b[j + 1]\n i += 2\n j += 2\n }\n }\n\n return dotProduct\n}\n\n/**\n * Calculates the similarity between this vector and another vector.\n *\n * @param {lunr.Vector} otherVector - The other vector to calculate the\n * similarity with.\n * @returns {Number}\n */\nlunr.Vector.prototype.similarity = function (otherVector) {\n return this.dot(otherVector) / this.magnitude() || 0\n}\n\n/**\n * Converts the vector to an array of the elements within the vector.\n *\n * @returns {Number[]}\n */\nlunr.Vector.prototype.toArray = function () {\n var output = new Array (this.elements.length / 2)\n\n for (var i = 1, j = 0; i < this.elements.length; i += 2, j++) {\n output[j] = this.elements[i]\n }\n\n return output\n}\n\n/**\n * A JSON serializable representation of the vector.\n *\n * @returns {Number[]}\n */\nlunr.Vector.prototype.toJSON = function () {\n return this.elements\n}\n/* eslint-disable */\n/*!\n * lunr.stemmer\n * Copyright (C) 2020 Oliver Nightingale\n * Includes code from - http://tartarus.org/~martin/PorterStemmer/js.txt\n */\n\n/**\n * lunr.stemmer is an english language stemmer, this is a JavaScript\n * implementation of the PorterStemmer taken from http://tartarus.org/~martin\n *\n * @static\n * @implements {lunr.PipelineFunction}\n * @param {lunr.Token} token - The string to stem\n * @returns {lunr.Token}\n * @see {@link lunr.Pipeline}\n * @function\n */\nlunr.stemmer = (function(){\n var step2list = {\n \"ational\" : \"ate\",\n \"tional\" : \"tion\",\n \"enci\" : \"ence\",\n \"anci\" : \"ance\",\n \"izer\" : \"ize\",\n \"bli\" : \"ble\",\n \"alli\" : \"al\",\n \"entli\" : \"ent\",\n \"eli\" : \"e\",\n \"ousli\" : \"ous\",\n \"ization\" : \"ize\",\n \"ation\" : \"ate\",\n \"ator\" : \"ate\",\n \"alism\" : \"al\",\n \"iveness\" : \"ive\",\n \"fulness\" : \"ful\",\n \"ousness\" : \"ous\",\n \"aliti\" : \"al\",\n \"iviti\" : \"ive\",\n \"biliti\" : \"ble\",\n \"logi\" : \"log\"\n },\n\n step3list = {\n \"icate\" : \"ic\",\n \"ative\" : \"\",\n \"alize\" : \"al\",\n \"iciti\" : \"ic\",\n \"ical\" : \"ic\",\n \"ful\" : \"\",\n \"ness\" : \"\"\n },\n\n c = \"[^aeiou]\", // consonant\n v = \"[aeiouy]\", // vowel\n C = c + \"[^aeiouy]*\", // consonant sequence\n V = v + \"[aeiou]*\", // vowel sequence\n\n mgr0 = \"^(\" + C + \")?\" + V + C, // [C]VC... is m>0\n meq1 = \"^(\" + C + \")?\" + V + C + \"(\" + V + \")?$\", // [C]VC[V] is m=1\n mgr1 = \"^(\" + C + \")?\" + V + C + V + C, // [C]VCVC... is m>1\n s_v = \"^(\" + C + \")?\" + v; // vowel in stem\n\n var re_mgr0 = new RegExp(mgr0);\n var re_mgr1 = new RegExp(mgr1);\n var re_meq1 = new RegExp(meq1);\n var re_s_v = new RegExp(s_v);\n\n var re_1a = /^(.+?)(ss|i)es$/;\n var re2_1a = /^(.+?)([^s])s$/;\n var re_1b = /^(.+?)eed$/;\n var re2_1b = /^(.+?)(ed|ing)$/;\n var re_1b_2 = /.$/;\n var re2_1b_2 = /(at|bl|iz)$/;\n var re3_1b_2 = new RegExp(\"([^aeiouylsz])\\\\1$\");\n var re4_1b_2 = new RegExp(\"^\" + C + v + \"[^aeiouwxy]$\");\n\n var re_1c = /^(.+?[^aeiou])y$/;\n var re_2 = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/;\n\n var re_3 = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/;\n\n var re_4 = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/;\n var re2_4 = /^(.+?)(s|t)(ion)$/;\n\n var re_5 = /^(.+?)e$/;\n var re_5_1 = /ll$/;\n var re3_5 = new RegExp(\"^\" + C + v + \"[^aeiouwxy]$\");\n\n var porterStemmer = function porterStemmer(w) {\n var stem,\n suffix,\n firstch,\n re,\n re2,\n re3,\n re4;\n\n if (w.length < 3) { return w; }\n\n firstch = w.substr(0,1);\n if (firstch == \"y\") {\n w = firstch.toUpperCase() + w.substr(1);\n }\n\n // Step 1a\n re = re_1a\n re2 = re2_1a;\n\n if (re.test(w)) { w = w.replace(re,\"$1$2\"); }\n else if (re2.test(w)) { w = w.replace(re2,\"$1$2\"); }\n\n // Step 1b\n re = re_1b;\n re2 = re2_1b;\n if (re.test(w)) {\n var fp = re.exec(w);\n re = re_mgr0;\n if (re.test(fp[1])) {\n re = re_1b_2;\n w = w.replace(re,\"\");\n }\n } else if (re2.test(w)) {\n var fp = re2.exec(w);\n stem = fp[1];\n re2 = re_s_v;\n if (re2.test(stem)) {\n w = stem;\n re2 = re2_1b_2;\n re3 = re3_1b_2;\n re4 = re4_1b_2;\n if (re2.test(w)) { w = w + \"e\"; }\n else if (re3.test(w)) { re = re_1b_2; w = w.replace(re,\"\"); }\n else if (re4.test(w)) { w = w + \"e\"; }\n }\n }\n\n // Step 1c - replace suffix y or Y by i if preceded by a non-vowel which is not the first letter of the word (so cry -> cri, by -> by, say -> say)\n re = re_1c;\n if (re.test(w)) {\n var fp = re.exec(w);\n stem = fp[1];\n w = stem + \"i\";\n }\n\n // Step 2\n re = re_2;\n if (re.test(w)) {\n var fp = re.exec(w);\n stem = fp[1];\n suffix = fp[2];\n re = re_mgr0;\n if (re.test(stem)) {\n w = stem + step2list[suffix];\n }\n }\n\n // Step 3\n re = re_3;\n if (re.test(w)) {\n var fp = re.exec(w);\n stem = fp[1];\n suffix = fp[2];\n re = re_mgr0;\n if (re.test(stem)) {\n w = stem + step3list[suffix];\n }\n }\n\n // Step 4\n re = re_4;\n re2 = re2_4;\n if (re.test(w)) {\n var fp = re.exec(w);\n stem = fp[1];\n re = re_mgr1;\n if (re.test(stem)) {\n w = stem;\n }\n } else if (re2.test(w)) {\n var fp = re2.exec(w);\n stem = fp[1] + fp[2];\n re2 = re_mgr1;\n if (re2.test(stem)) {\n w = stem;\n }\n }\n\n // Step 5\n re = re_5;\n if (re.test(w)) {\n var fp = re.exec(w);\n stem = fp[1];\n re = re_mgr1;\n re2 = re_meq1;\n re3 = re3_5;\n if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) {\n w = stem;\n }\n }\n\n re = re_5_1;\n re2 = re_mgr1;\n if (re.test(w) && re2.test(w)) {\n re = re_1b_2;\n w = w.replace(re,\"\");\n }\n\n // and turn initial Y back to y\n\n if (firstch == \"y\") {\n w = firstch.toLowerCase() + w.substr(1);\n }\n\n return w;\n };\n\n return function (token) {\n return token.update(porterStemmer);\n }\n})();\n\nlunr.Pipeline.registerFunction(lunr.stemmer, 'stemmer')\n/*!\n * lunr.stopWordFilter\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * lunr.generateStopWordFilter builds a stopWordFilter function from the provided\n * list of stop words.\n *\n * The built in lunr.stopWordFilter is built using this generator and can be used\n * to generate custom stopWordFilters for applications or non English languages.\n *\n * @function\n * @param {Array} token The token to pass through the filter\n * @returns {lunr.PipelineFunction}\n * @see lunr.Pipeline\n * @see lunr.stopWordFilter\n */\nlunr.generateStopWordFilter = function (stopWords) {\n var words = stopWords.reduce(function (memo, stopWord) {\n memo[stopWord] = stopWord\n return memo\n }, {})\n\n return function (token) {\n if (token && words[token.toString()] !== token.toString()) return token\n }\n}\n\n/**\n * lunr.stopWordFilter is an English language stop word list filter, any words\n * contained in the list will not be passed through the filter.\n *\n * This is intended to be used in the Pipeline. If the token does not pass the\n * filter then undefined will be returned.\n *\n * @function\n * @implements {lunr.PipelineFunction}\n * @params {lunr.Token} token - A token to check for being a stop word.\n * @returns {lunr.Token}\n * @see {@link lunr.Pipeline}\n */\nlunr.stopWordFilter = lunr.generateStopWordFilter([\n 'a',\n 'able',\n 'about',\n 'across',\n 'after',\n 'all',\n 'almost',\n 'also',\n 'am',\n 'among',\n 'an',\n 'and',\n 'any',\n 'are',\n 'as',\n 'at',\n 'be',\n 'because',\n 'been',\n 'but',\n 'by',\n 'can',\n 'cannot',\n 'could',\n 'dear',\n 'did',\n 'do',\n 'does',\n 'either',\n 'else',\n 'ever',\n 'every',\n 'for',\n 'from',\n 'get',\n 'got',\n 'had',\n 'has',\n 'have',\n 'he',\n 'her',\n 'hers',\n 'him',\n 'his',\n 'how',\n 'however',\n 'i',\n 'if',\n 'in',\n 'into',\n 'is',\n 'it',\n 'its',\n 'just',\n 'least',\n 'let',\n 'like',\n 'likely',\n 'may',\n 'me',\n 'might',\n 'most',\n 'must',\n 'my',\n 'neither',\n 'no',\n 'nor',\n 'not',\n 'of',\n 'off',\n 'often',\n 'on',\n 'only',\n 'or',\n 'other',\n 'our',\n 'own',\n 'rather',\n 'said',\n 'say',\n 'says',\n 'she',\n 'should',\n 'since',\n 'so',\n 'some',\n 'than',\n 'that',\n 'the',\n 'their',\n 'them',\n 'then',\n 'there',\n 'these',\n 'they',\n 'this',\n 'tis',\n 'to',\n 'too',\n 'twas',\n 'us',\n 'wants',\n 'was',\n 'we',\n 'were',\n 'what',\n 'when',\n 'where',\n 'which',\n 'while',\n 'who',\n 'whom',\n 'why',\n 'will',\n 'with',\n 'would',\n 'yet',\n 'you',\n 'your'\n])\n\nlunr.Pipeline.registerFunction(lunr.stopWordFilter, 'stopWordFilter')\n/*!\n * lunr.trimmer\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * lunr.trimmer is a pipeline function for trimming non word\n * characters from the beginning and end of tokens before they\n * enter the index.\n *\n * This implementation may not work correctly for non latin\n * characters and should either be removed or adapted for use\n * with languages with non-latin characters.\n *\n * @static\n * @implements {lunr.PipelineFunction}\n * @param {lunr.Token} token The token to pass through the filter\n * @returns {lunr.Token}\n * @see lunr.Pipeline\n */\nlunr.trimmer = function (token) {\n return token.update(function (s) {\n return s.replace(/^\\W+/, '').replace(/\\W+$/, '')\n })\n}\n\nlunr.Pipeline.registerFunction(lunr.trimmer, 'trimmer')\n/*!\n * lunr.TokenSet\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * A token set is used to store the unique list of all tokens\n * within an index. Token sets are also used to represent an\n * incoming query to the index, this query token set and index\n * token set are then intersected to find which tokens to look\n * up in the inverted index.\n *\n * A token set can hold multiple tokens, as in the case of the\n * index token set, or it can hold a single token as in the\n * case of a simple query token set.\n *\n * Additionally token sets are used to perform wildcard matching.\n * Leading, contained and trailing wildcards are supported, and\n * from this edit distance matching can also be provided.\n *\n * Token sets are implemented as a minimal finite state automata,\n * where both common prefixes and suffixes are shared between tokens.\n * This helps to reduce the space used for storing the token set.\n *\n * @constructor\n */\nlunr.TokenSet = function () {\n this.final = false\n this.edges = {}\n this.id = lunr.TokenSet._nextId\n lunr.TokenSet._nextId += 1\n}\n\n/**\n * Keeps track of the next, auto increment, identifier to assign\n * to a new tokenSet.\n *\n * TokenSets require a unique identifier to be correctly minimised.\n *\n * @private\n */\nlunr.TokenSet._nextId = 1\n\n/**\n * Creates a TokenSet instance from the given sorted array of words.\n *\n * @param {String[]} arr - A sorted array of strings to create the set from.\n * @returns {lunr.TokenSet}\n * @throws Will throw an error if the input array is not sorted.\n */\nlunr.TokenSet.fromArray = function (arr) {\n var builder = new lunr.TokenSet.Builder\n\n for (var i = 0, len = arr.length; i < len; i++) {\n builder.insert(arr[i])\n }\n\n builder.finish()\n return builder.root\n}\n\n/**\n * Creates a token set from a query clause.\n *\n * @private\n * @param {Object} clause - A single clause from lunr.Query.\n * @param {string} clause.term - The query clause term.\n * @param {number} [clause.editDistance] - The optional edit distance for the term.\n * @returns {lunr.TokenSet}\n */\nlunr.TokenSet.fromClause = function (clause) {\n if ('editDistance' in clause) {\n return lunr.TokenSet.fromFuzzyString(clause.term, clause.editDistance)\n } else {\n return lunr.TokenSet.fromString(clause.term)\n }\n}\n\n/**\n * Creates a token set representing a single string with a specified\n * edit distance.\n *\n * Insertions, deletions, substitutions and transpositions are each\n * treated as an edit distance of 1.\n *\n * Increasing the allowed edit distance will have a dramatic impact\n * on the performance of both creating and intersecting these TokenSets.\n * It is advised to keep the edit distance less than 3.\n *\n * @param {string} str - The string to create the token set from.\n * @param {number} editDistance - The allowed edit distance to match.\n * @returns {lunr.Vector}\n */\nlunr.TokenSet.fromFuzzyString = function (str, editDistance) {\n var root = new lunr.TokenSet\n\n var stack = [{\n node: root,\n editsRemaining: editDistance,\n str: str\n }]\n\n while (stack.length) {\n var frame = stack.pop()\n\n // no edit\n if (frame.str.length > 0) {\n var char = frame.str.charAt(0),\n noEditNode\n\n if (char in frame.node.edges) {\n noEditNode = frame.node.edges[char]\n } else {\n noEditNode = new lunr.TokenSet\n frame.node.edges[char] = noEditNode\n }\n\n if (frame.str.length == 1) {\n noEditNode.final = true\n }\n\n stack.push({\n node: noEditNode,\n editsRemaining: frame.editsRemaining,\n str: frame.str.slice(1)\n })\n }\n\n if (frame.editsRemaining == 0) {\n continue\n }\n\n // insertion\n if (\"*\" in frame.node.edges) {\n var insertionNode = frame.node.edges[\"*\"]\n } else {\n var insertionNode = new lunr.TokenSet\n frame.node.edges[\"*\"] = insertionNode\n }\n\n if (frame.str.length == 0) {\n insertionNode.final = true\n }\n\n stack.push({\n node: insertionNode,\n editsRemaining: frame.editsRemaining - 1,\n str: frame.str\n })\n\n // deletion\n // can only do a deletion if we have enough edits remaining\n // and if there are characters left to delete in the string\n if (frame.str.length > 1) {\n stack.push({\n node: frame.node,\n editsRemaining: frame.editsRemaining - 1,\n str: frame.str.slice(1)\n })\n }\n\n // deletion\n // just removing the last character from the str\n if (frame.str.length == 1) {\n frame.node.final = true\n }\n\n // substitution\n // can only do a substitution if we have enough edits remaining\n // and if there are characters left to substitute\n if (frame.str.length >= 1) {\n if (\"*\" in frame.node.edges) {\n var substitutionNode = frame.node.edges[\"*\"]\n } else {\n var substitutionNode = new lunr.TokenSet\n frame.node.edges[\"*\"] = substitutionNode\n }\n\n if (frame.str.length == 1) {\n substitutionNode.final = true\n }\n\n stack.push({\n node: substitutionNode,\n editsRemaining: frame.editsRemaining - 1,\n str: frame.str.slice(1)\n })\n }\n\n // transposition\n // can only do a transposition if there are edits remaining\n // and there are enough characters to transpose\n if (frame.str.length > 1) {\n var charA = frame.str.charAt(0),\n charB = frame.str.charAt(1),\n transposeNode\n\n if (charB in frame.node.edges) {\n transposeNode = frame.node.edges[charB]\n } else {\n transposeNode = new lunr.TokenSet\n frame.node.edges[charB] = transposeNode\n }\n\n if (frame.str.length == 1) {\n transposeNode.final = true\n }\n\n stack.push({\n node: transposeNode,\n editsRemaining: frame.editsRemaining - 1,\n str: charA + frame.str.slice(2)\n })\n }\n }\n\n return root\n}\n\n/**\n * Creates a TokenSet from a string.\n *\n * The string may contain one or more wildcard characters (*)\n * that will allow wildcard matching when intersecting with\n * another TokenSet.\n *\n * @param {string} str - The string to create a TokenSet from.\n * @returns {lunr.TokenSet}\n */\nlunr.TokenSet.fromString = function (str) {\n var node = new lunr.TokenSet,\n root = node\n\n /*\n * Iterates through all characters within the passed string\n * appending a node for each character.\n *\n * When a wildcard character is found then a self\n * referencing edge is introduced to continually match\n * any number of any characters.\n */\n for (var i = 0, len = str.length; i < len; i++) {\n var char = str[i],\n final = (i == len - 1)\n\n if (char == \"*\") {\n node.edges[char] = node\n node.final = final\n\n } else {\n var next = new lunr.TokenSet\n next.final = final\n\n node.edges[char] = next\n node = next\n }\n }\n\n return root\n}\n\n/**\n * Converts this TokenSet into an array of strings\n * contained within the TokenSet.\n *\n * This is not intended to be used on a TokenSet that\n * contains wildcards, in these cases the results are\n * undefined and are likely to cause an infinite loop.\n *\n * @returns {string[]}\n */\nlunr.TokenSet.prototype.toArray = function () {\n var words = []\n\n var stack = [{\n prefix: \"\",\n node: this\n }]\n\n while (stack.length) {\n var frame = stack.pop(),\n edges = Object.keys(frame.node.edges),\n len = edges.length\n\n if (frame.node.final) {\n /* In Safari, at this point the prefix is sometimes corrupted, see:\n * https://github.com/olivernn/lunr.js/issues/279 Calling any\n * String.prototype method forces Safari to \"cast\" this string to what\n * it's supposed to be, fixing the bug. */\n frame.prefix.charAt(0)\n words.push(frame.prefix)\n }\n\n for (var i = 0; i < len; i++) {\n var edge = edges[i]\n\n stack.push({\n prefix: frame.prefix.concat(edge),\n node: frame.node.edges[edge]\n })\n }\n }\n\n return words\n}\n\n/**\n * Generates a string representation of a TokenSet.\n *\n * This is intended to allow TokenSets to be used as keys\n * in objects, largely to aid the construction and minimisation\n * of a TokenSet. As such it is not designed to be a human\n * friendly representation of the TokenSet.\n *\n * @returns {string}\n */\nlunr.TokenSet.prototype.toString = function () {\n // NOTE: Using Object.keys here as this.edges is very likely\n // to enter 'hash-mode' with many keys being added\n //\n // avoiding a for-in loop here as it leads to the function\n // being de-optimised (at least in V8). From some simple\n // benchmarks the performance is comparable, but allowing\n // V8 to optimize may mean easy performance wins in the future.\n\n if (this._str) {\n return this._str\n }\n\n var str = this.final ? '1' : '0',\n labels = Object.keys(this.edges).sort(),\n len = labels.length\n\n for (var i = 0; i < len; i++) {\n var label = labels[i],\n node = this.edges[label]\n\n str = str + label + node.id\n }\n\n return str\n}\n\n/**\n * Returns a new TokenSet that is the intersection of\n * this TokenSet and the passed TokenSet.\n *\n * This intersection will take into account any wildcards\n * contained within the TokenSet.\n *\n * @param {lunr.TokenSet} b - An other TokenSet to intersect with.\n * @returns {lunr.TokenSet}\n */\nlunr.TokenSet.prototype.intersect = function (b) {\n var output = new lunr.TokenSet,\n frame = undefined\n\n var stack = [{\n qNode: b,\n output: output,\n node: this\n }]\n\n while (stack.length) {\n frame = stack.pop()\n\n // NOTE: As with the #toString method, we are using\n // Object.keys and a for loop instead of a for-in loop\n // as both of these objects enter 'hash' mode, causing\n // the function to be de-optimised in V8\n var qEdges = Object.keys(frame.qNode.edges),\n qLen = qEdges.length,\n nEdges = Object.keys(frame.node.edges),\n nLen = nEdges.length\n\n for (var q = 0; q < qLen; q++) {\n var qEdge = qEdges[q]\n\n for (var n = 0; n < nLen; n++) {\n var nEdge = nEdges[n]\n\n if (nEdge == qEdge || qEdge == '*') {\n var node = frame.node.edges[nEdge],\n qNode = frame.qNode.edges[qEdge],\n final = node.final && qNode.final,\n next = undefined\n\n if (nEdge in frame.output.edges) {\n // an edge already exists for this character\n // no need to create a new node, just set the finality\n // bit unless this node is already final\n next = frame.output.edges[nEdge]\n next.final = next.final || final\n\n } else {\n // no edge exists yet, must create one\n // set the finality bit and insert it\n // into the output\n next = new lunr.TokenSet\n next.final = final\n frame.output.edges[nEdge] = next\n }\n\n stack.push({\n qNode: qNode,\n output: next,\n node: node\n })\n }\n }\n }\n }\n\n return output\n}\nlunr.TokenSet.Builder = function () {\n this.previousWord = \"\"\n this.root = new lunr.TokenSet\n this.uncheckedNodes = []\n this.minimizedNodes = {}\n}\n\nlunr.TokenSet.Builder.prototype.insert = function (word) {\n var node,\n commonPrefix = 0\n\n if (word < this.previousWord) {\n throw new Error (\"Out of order word insertion\")\n }\n\n for (var i = 0; i < word.length && i < this.previousWord.length; i++) {\n if (word[i] != this.previousWord[i]) break\n commonPrefix++\n }\n\n this.minimize(commonPrefix)\n\n if (this.uncheckedNodes.length == 0) {\n node = this.root\n } else {\n node = this.uncheckedNodes[this.uncheckedNodes.length - 1].child\n }\n\n for (var i = commonPrefix; i < word.length; i++) {\n var nextNode = new lunr.TokenSet,\n char = word[i]\n\n node.edges[char] = nextNode\n\n this.uncheckedNodes.push({\n parent: node,\n char: char,\n child: nextNode\n })\n\n node = nextNode\n }\n\n node.final = true\n this.previousWord = word\n}\n\nlunr.TokenSet.Builder.prototype.finish = function () {\n this.minimize(0)\n}\n\nlunr.TokenSet.Builder.prototype.minimize = function (downTo) {\n for (var i = this.uncheckedNodes.length - 1; i >= downTo; i--) {\n var node = this.uncheckedNodes[i],\n childKey = node.child.toString()\n\n if (childKey in this.minimizedNodes) {\n node.parent.edges[node.char] = this.minimizedNodes[childKey]\n } else {\n // Cache the key for this node since\n // we know it can't change anymore\n node.child._str = childKey\n\n this.minimizedNodes[childKey] = node.child\n }\n\n this.uncheckedNodes.pop()\n }\n}\n/*!\n * lunr.Index\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * An index contains the built index of all documents and provides a query interface\n * to the index.\n *\n * Usually instances of lunr.Index will not be created using this constructor, instead\n * lunr.Builder should be used to construct new indexes, or lunr.Index.load should be\n * used to load previously built and serialized indexes.\n *\n * @constructor\n * @param {Object} attrs - The attributes of the built search index.\n * @param {Object} attrs.invertedIndex - An index of term/field to document reference.\n * @param {Object} attrs.fieldVectors - Field vectors\n * @param {lunr.TokenSet} attrs.tokenSet - An set of all corpus tokens.\n * @param {string[]} attrs.fields - The names of indexed document fields.\n * @param {lunr.Pipeline} attrs.pipeline - The pipeline to use for search terms.\n */\nlunr.Index = function (attrs) {\n this.invertedIndex = attrs.invertedIndex\n this.fieldVectors = attrs.fieldVectors\n this.tokenSet = attrs.tokenSet\n this.fields = attrs.fields\n this.pipeline = attrs.pipeline\n}\n\n/**\n * A result contains details of a document matching a search query.\n * @typedef {Object} lunr.Index~Result\n * @property {string} ref - The reference of the document this result represents.\n * @property {number} score - A number between 0 and 1 representing how similar this document is to the query.\n * @property {lunr.MatchData} matchData - Contains metadata about this match including which term(s) caused the match.\n */\n\n/**\n * Although lunr provides the ability to create queries using lunr.Query, it also provides a simple\n * query language which itself is parsed into an instance of lunr.Query.\n *\n * For programmatically building queries it is advised to directly use lunr.Query, the query language\n * is best used for human entered text rather than program generated text.\n *\n * At its simplest queries can just be a single term, e.g. `hello`, multiple terms are also supported\n * and will be combined with OR, e.g `hello world` will match documents that contain either 'hello'\n * or 'world', though those that contain both will rank higher in the results.\n *\n * Wildcards can be included in terms to match one or more unspecified characters, these wildcards can\n * be inserted anywhere within the term, and more than one wildcard can exist in a single term. Adding\n * wildcards will increase the number of documents that will be found but can also have a negative\n * impact on query performance, especially with wildcards at the beginning of a term.\n *\n * Terms can be restricted to specific fields, e.g. `title:hello`, only documents with the term\n * hello in the title field will match this query. Using a field not present in the index will lead\n * to an error being thrown.\n *\n * Modifiers can also be added to terms, lunr supports edit distance and boost modifiers on terms. A term\n * boost will make documents matching that term score higher, e.g. `foo^5`. Edit distance is also supported\n * to provide fuzzy matching, e.g. 'hello~2' will match documents with hello with an edit distance of 2.\n * Avoid large values for edit distance to improve query performance.\n *\n * Each term also supports a presence modifier. By default a term's presence in document is optional, however\n * this can be changed to either required or prohibited. For a term's presence to be required in a document the\n * term should be prefixed with a '+', e.g. `+foo bar` is a search for documents that must contain 'foo' and\n * optionally contain 'bar'. Conversely a leading '-' sets the terms presence to prohibited, i.e. it must not\n * appear in a document, e.g. `-foo bar` is a search for documents that do not contain 'foo' but may contain 'bar'.\n *\n * To escape special characters the backslash character '\\' can be used, this allows searches to include\n * characters that would normally be considered modifiers, e.g. `foo\\~2` will search for a term \"foo~2\" instead\n * of attempting to apply a boost of 2 to the search term \"foo\".\n *\n * @typedef {string} lunr.Index~QueryString\n * @example Simple single term query\n * hello\n * @example Multiple term query\n * hello world\n * @example term scoped to a field\n * title:hello\n * @example term with a boost of 10\n * hello^10\n * @example term with an edit distance of 2\n * hello~2\n * @example terms with presence modifiers\n * -foo +bar baz\n */\n\n/**\n * Performs a search against the index using lunr query syntax.\n *\n * Results will be returned sorted by their score, the most relevant results\n * will be returned first. For details on how the score is calculated, please see\n * the {@link https://lunrjs.com/guides/searching.html#scoring|guide}.\n *\n * For more programmatic querying use lunr.Index#query.\n *\n * @param {lunr.Index~QueryString} queryString - A string containing a lunr query.\n * @throws {lunr.QueryParseError} If the passed query string cannot be parsed.\n * @returns {lunr.Index~Result[]}\n */\nlunr.Index.prototype.search = function (queryString) {\n return this.query(function (query) {\n var parser = new lunr.QueryParser(queryString, query)\n parser.parse()\n })\n}\n\n/**\n * A query builder callback provides a query object to be used to express\n * the query to perform on the index.\n *\n * @callback lunr.Index~queryBuilder\n * @param {lunr.Query} query - The query object to build up.\n * @this lunr.Query\n */\n\n/**\n * Performs a query against the index using the yielded lunr.Query object.\n *\n * If performing programmatic queries against the index, this method is preferred\n * over lunr.Index#search so as to avoid the additional query parsing overhead.\n *\n * A query object is yielded to the supplied function which should be used to\n * express the query to be run against the index.\n *\n * Note that although this function takes a callback parameter it is _not_ an\n * asynchronous operation, the callback is just yielded a query object to be\n * customized.\n *\n * @param {lunr.Index~queryBuilder} fn - A function that is used to build the query.\n * @returns {lunr.Index~Result[]}\n */\nlunr.Index.prototype.query = function (fn) {\n // for each query clause\n // * process terms\n // * expand terms from token set\n // * find matching documents and metadata\n // * get document vectors\n // * score documents\n\n var query = new lunr.Query(this.fields),\n matchingFields = Object.create(null),\n queryVectors = Object.create(null),\n termFieldCache = Object.create(null),\n requiredMatches = Object.create(null),\n prohibitedMatches = Object.create(null)\n\n /*\n * To support field level boosts a query vector is created per\n * field. An empty vector is eagerly created to support negated\n * queries.\n */\n for (var i = 0; i < this.fields.length; i++) {\n queryVectors[this.fields[i]] = new lunr.Vector\n }\n\n fn.call(query, query)\n\n for (var i = 0; i < query.clauses.length; i++) {\n /*\n * Unless the pipeline has been disabled for this term, which is\n * the case for terms with wildcards, we need to pass the clause\n * term through the search pipeline. A pipeline returns an array\n * of processed terms. Pipeline functions may expand the passed\n * term, which means we may end up performing multiple index lookups\n * for a single query term.\n */\n var clause = query.clauses[i],\n terms = null,\n clauseMatches = lunr.Set.empty\n\n if (clause.usePipeline) {\n terms = this.pipeline.runString(clause.term, {\n fields: clause.fields\n })\n } else {\n terms = [clause.term]\n }\n\n for (var m = 0; m < terms.length; m++) {\n var term = terms[m]\n\n /*\n * Each term returned from the pipeline needs to use the same query\n * clause object, e.g. the same boost and or edit distance. The\n * simplest way to do this is to re-use the clause object but mutate\n * its term property.\n */\n clause.term = term\n\n /*\n * From the term in the clause we create a token set which will then\n * be used to intersect the indexes token set to get a list of terms\n * to lookup in the inverted index\n */\n var termTokenSet = lunr.TokenSet.fromClause(clause),\n expandedTerms = this.tokenSet.intersect(termTokenSet).toArray()\n\n /*\n * If a term marked as required does not exist in the tokenSet it is\n * impossible for the search to return any matches. We set all the field\n * scoped required matches set to empty and stop examining any further\n * clauses.\n */\n if (expandedTerms.length === 0 && clause.presence === lunr.Query.presence.REQUIRED) {\n for (var k = 0; k < clause.fields.length; k++) {\n var field = clause.fields[k]\n requiredMatches[field] = lunr.Set.empty\n }\n\n break\n }\n\n for (var j = 0; j < expandedTerms.length; j++) {\n /*\n * For each term get the posting and termIndex, this is required for\n * building the query vector.\n */\n var expandedTerm = expandedTerms[j],\n posting = this.invertedIndex[expandedTerm],\n termIndex = posting._index\n\n for (var k = 0; k < clause.fields.length; k++) {\n /*\n * For each field that this query term is scoped by (by default\n * all fields are in scope) we need to get all the document refs\n * that have this term in that field.\n *\n * The posting is the entry in the invertedIndex for the matching\n * term from above.\n */\n var field = clause.fields[k],\n fieldPosting = posting[field],\n matchingDocumentRefs = Object.keys(fieldPosting),\n termField = expandedTerm + \"/\" + field,\n matchingDocumentsSet = new lunr.Set(matchingDocumentRefs)\n\n /*\n * if the presence of this term is required ensure that the matching\n * documents are added to the set of required matches for this clause.\n *\n */\n if (clause.presence == lunr.Query.presence.REQUIRED) {\n clauseMatches = clauseMatches.union(matchingDocumentsSet)\n\n if (requiredMatches[field] === undefined) {\n requiredMatches[field] = lunr.Set.complete\n }\n }\n\n /*\n * if the presence of this term is prohibited ensure that the matching\n * documents are added to the set of prohibited matches for this field,\n * creating that set if it does not yet exist.\n */\n if (clause.presence == lunr.Query.presence.PROHIBITED) {\n if (prohibitedMatches[field] === undefined) {\n prohibitedMatches[field] = lunr.Set.empty\n }\n\n prohibitedMatches[field] = prohibitedMatches[field].union(matchingDocumentsSet)\n\n /*\n * Prohibited matches should not be part of the query vector used for\n * similarity scoring and no metadata should be extracted so we continue\n * to the next field\n */\n continue\n }\n\n /*\n * The query field vector is populated using the termIndex found for\n * the term and a unit value with the appropriate boost applied.\n * Using upsert because there could already be an entry in the vector\n * for the term we are working with. In that case we just add the scores\n * together.\n */\n queryVectors[field].upsert(termIndex, clause.boost, function (a, b) { return a + b })\n\n /**\n * If we've already seen this term, field combo then we've already collected\n * the matching documents and metadata, no need to go through all that again\n */\n if (termFieldCache[termField]) {\n continue\n }\n\n for (var l = 0; l < matchingDocumentRefs.length; l++) {\n /*\n * All metadata for this term/field/document triple\n * are then extracted and collected into an instance\n * of lunr.MatchData ready to be returned in the query\n * results\n */\n var matchingDocumentRef = matchingDocumentRefs[l],\n matchingFieldRef = new lunr.FieldRef (matchingDocumentRef, field),\n metadata = fieldPosting[matchingDocumentRef],\n fieldMatch\n\n if ((fieldMatch = matchingFields[matchingFieldRef]) === undefined) {\n matchingFields[matchingFieldRef] = new lunr.MatchData (expandedTerm, field, metadata)\n } else {\n fieldMatch.add(expandedTerm, field, metadata)\n }\n\n }\n\n termFieldCache[termField] = true\n }\n }\n }\n\n /**\n * If the presence was required we need to update the requiredMatches field sets.\n * We do this after all fields for the term have collected their matches because\n * the clause terms presence is required in _any_ of the fields not _all_ of the\n * fields.\n */\n if (clause.presence === lunr.Query.presence.REQUIRED) {\n for (var k = 0; k < clause.fields.length; k++) {\n var field = clause.fields[k]\n requiredMatches[field] = requiredMatches[field].intersect(clauseMatches)\n }\n }\n }\n\n /**\n * Need to combine the field scoped required and prohibited\n * matching documents into a global set of required and prohibited\n * matches\n */\n var allRequiredMatches = lunr.Set.complete,\n allProhibitedMatches = lunr.Set.empty\n\n for (var i = 0; i < this.fields.length; i++) {\n var field = this.fields[i]\n\n if (requiredMatches[field]) {\n allRequiredMatches = allRequiredMatches.intersect(requiredMatches[field])\n }\n\n if (prohibitedMatches[field]) {\n allProhibitedMatches = allProhibitedMatches.union(prohibitedMatches[field])\n }\n }\n\n var matchingFieldRefs = Object.keys(matchingFields),\n results = [],\n matches = Object.create(null)\n\n /*\n * If the query is negated (contains only prohibited terms)\n * we need to get _all_ fieldRefs currently existing in the\n * index. This is only done when we know that the query is\n * entirely prohibited terms to avoid any cost of getting all\n * fieldRefs unnecessarily.\n *\n * Additionally, blank MatchData must be created to correctly\n * populate the results.\n */\n if (query.isNegated()) {\n matchingFieldRefs = Object.keys(this.fieldVectors)\n\n for (var i = 0; i < matchingFieldRefs.length; i++) {\n var matchingFieldRef = matchingFieldRefs[i]\n var fieldRef = lunr.FieldRef.fromString(matchingFieldRef)\n matchingFields[matchingFieldRef] = new lunr.MatchData\n }\n }\n\n for (var i = 0; i < matchingFieldRefs.length; i++) {\n /*\n * Currently we have document fields that match the query, but we\n * need to return documents. The matchData and scores are combined\n * from multiple fields belonging to the same document.\n *\n * Scores are calculated by field, using the query vectors created\n * above, and combined into a final document score using addition.\n */\n var fieldRef = lunr.FieldRef.fromString(matchingFieldRefs[i]),\n docRef = fieldRef.docRef\n\n if (!allRequiredMatches.contains(docRef)) {\n continue\n }\n\n if (allProhibitedMatches.contains(docRef)) {\n continue\n }\n\n var fieldVector = this.fieldVectors[fieldRef],\n score = queryVectors[fieldRef.fieldName].similarity(fieldVector),\n docMatch\n\n if ((docMatch = matches[docRef]) !== undefined) {\n docMatch.score += score\n docMatch.matchData.combine(matchingFields[fieldRef])\n } else {\n var match = {\n ref: docRef,\n score: score,\n matchData: matchingFields[fieldRef]\n }\n matches[docRef] = match\n results.push(match)\n }\n }\n\n /*\n * Sort the results objects by score, highest first.\n */\n return results.sort(function (a, b) {\n return b.score - a.score\n })\n}\n\n/**\n * Prepares the index for JSON serialization.\n *\n * The schema for this JSON blob will be described in a\n * separate JSON schema file.\n *\n * @returns {Object}\n */\nlunr.Index.prototype.toJSON = function () {\n var invertedIndex = Object.keys(this.invertedIndex)\n .sort()\n .map(function (term) {\n return [term, this.invertedIndex[term]]\n }, this)\n\n var fieldVectors = Object.keys(this.fieldVectors)\n .map(function (ref) {\n return [ref, this.fieldVectors[ref].toJSON()]\n }, this)\n\n return {\n version: lunr.version,\n fields: this.fields,\n fieldVectors: fieldVectors,\n invertedIndex: invertedIndex,\n pipeline: this.pipeline.toJSON()\n }\n}\n\n/**\n * Loads a previously serialized lunr.Index\n *\n * @param {Object} serializedIndex - A previously serialized lunr.Index\n * @returns {lunr.Index}\n */\nlunr.Index.load = function (serializedIndex) {\n var attrs = {},\n fieldVectors = {},\n serializedVectors = serializedIndex.fieldVectors,\n invertedIndex = Object.create(null),\n serializedInvertedIndex = serializedIndex.invertedIndex,\n tokenSetBuilder = new lunr.TokenSet.Builder,\n pipeline = lunr.Pipeline.load(serializedIndex.pipeline)\n\n if (serializedIndex.version != lunr.version) {\n lunr.utils.warn(\"Version mismatch when loading serialised index. Current version of lunr '\" + lunr.version + \"' does not match serialized index '\" + serializedIndex.version + \"'\")\n }\n\n for (var i = 0; i < serializedVectors.length; i++) {\n var tuple = serializedVectors[i],\n ref = tuple[0],\n elements = tuple[1]\n\n fieldVectors[ref] = new lunr.Vector(elements)\n }\n\n for (var i = 0; i < serializedInvertedIndex.length; i++) {\n var tuple = serializedInvertedIndex[i],\n term = tuple[0],\n posting = tuple[1]\n\n tokenSetBuilder.insert(term)\n invertedIndex[term] = posting\n }\n\n tokenSetBuilder.finish()\n\n attrs.fields = serializedIndex.fields\n\n attrs.fieldVectors = fieldVectors\n attrs.invertedIndex = invertedIndex\n attrs.tokenSet = tokenSetBuilder.root\n attrs.pipeline = pipeline\n\n return new lunr.Index(attrs)\n}\n/*!\n * lunr.Builder\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * lunr.Builder performs indexing on a set of documents and\n * returns instances of lunr.Index ready for querying.\n *\n * All configuration of the index is done via the builder, the\n * fields to index, the document reference, the text processing\n * pipeline and document scoring parameters are all set on the\n * builder before indexing.\n *\n * @constructor\n * @property {string} _ref - Internal reference to the document reference field.\n * @property {string[]} _fields - Internal reference to the document fields to index.\n * @property {object} invertedIndex - The inverted index maps terms to document fields.\n * @property {object} documentTermFrequencies - Keeps track of document term frequencies.\n * @property {object} documentLengths - Keeps track of the length of documents added to the index.\n * @property {lunr.tokenizer} tokenizer - Function for splitting strings into tokens for indexing.\n * @property {lunr.Pipeline} pipeline - The pipeline performs text processing on tokens before indexing.\n * @property {lunr.Pipeline} searchPipeline - A pipeline for processing search terms before querying the index.\n * @property {number} documentCount - Keeps track of the total number of documents indexed.\n * @property {number} _b - A parameter to control field length normalization, setting this to 0 disabled normalization, 1 fully normalizes field lengths, the default value is 0.75.\n * @property {number} _k1 - A parameter to control how quickly an increase in term frequency results in term frequency saturation, the default value is 1.2.\n * @property {number} termIndex - A counter incremented for each unique term, used to identify a terms position in the vector space.\n * @property {array} metadataWhitelist - A list of metadata keys that have been whitelisted for entry in the index.\n */\nlunr.Builder = function () {\n this._ref = \"id\"\n this._fields = Object.create(null)\n this._documents = Object.create(null)\n this.invertedIndex = Object.create(null)\n this.fieldTermFrequencies = {}\n this.fieldLengths = {}\n this.tokenizer = lunr.tokenizer\n this.pipeline = new lunr.Pipeline\n this.searchPipeline = new lunr.Pipeline\n this.documentCount = 0\n this._b = 0.75\n this._k1 = 1.2\n this.termIndex = 0\n this.metadataWhitelist = []\n}\n\n/**\n * Sets the document field used as the document reference. Every document must have this field.\n * The type of this field in the document should be a string, if it is not a string it will be\n * coerced into a string by calling toString.\n *\n * The default ref is 'id'.\n *\n * The ref should _not_ be changed during indexing, it should be set before any documents are\n * added to the index. Changing it during indexing can lead to inconsistent results.\n *\n * @param {string} ref - The name of the reference field in the document.\n */\nlunr.Builder.prototype.ref = function (ref) {\n this._ref = ref\n}\n\n/**\n * A function that is used to extract a field from a document.\n *\n * Lunr expects a field to be at the top level of a document, if however the field\n * is deeply nested within a document an extractor function can be used to extract\n * the right field for indexing.\n *\n * @callback fieldExtractor\n * @param {object} doc - The document being added to the index.\n * @returns {?(string|object|object[])} obj - The object that will be indexed for this field.\n * @example Extracting a nested field\n * function (doc) { return doc.nested.field }\n */\n\n/**\n * Adds a field to the list of document fields that will be indexed. Every document being\n * indexed should have this field. Null values for this field in indexed documents will\n * not cause errors but will limit the chance of that document being retrieved by searches.\n *\n * All fields should be added before adding documents to the index. Adding fields after\n * a document has been indexed will have no effect on already indexed documents.\n *\n * Fields can be boosted at build time. This allows terms within that field to have more\n * importance when ranking search results. Use a field boost to specify that matches within\n * one field are more important than other fields.\n *\n * @param {string} fieldName - The name of a field to index in all documents.\n * @param {object} attributes - Optional attributes associated with this field.\n * @param {number} [attributes.boost=1] - Boost applied to all terms within this field.\n * @param {fieldExtractor} [attributes.extractor] - Function to extract a field from a document.\n * @throws {RangeError} fieldName cannot contain unsupported characters '/'\n */\nlunr.Builder.prototype.field = function (fieldName, attributes) {\n if (/\\//.test(fieldName)) {\n throw new RangeError (\"Field '\" + fieldName + \"' contains illegal character '/'\")\n }\n\n this._fields[fieldName] = attributes || {}\n}\n\n/**\n * A parameter to tune the amount of field length normalisation that is applied when\n * calculating relevance scores. A value of 0 will completely disable any normalisation\n * and a value of 1 will fully normalise field lengths. The default is 0.75. Values of b\n * will be clamped to the range 0 - 1.\n *\n * @param {number} number - The value to set for this tuning parameter.\n */\nlunr.Builder.prototype.b = function (number) {\n if (number < 0) {\n this._b = 0\n } else if (number > 1) {\n this._b = 1\n } else {\n this._b = number\n }\n}\n\n/**\n * A parameter that controls the speed at which a rise in term frequency results in term\n * frequency saturation. The default value is 1.2. Setting this to a higher value will give\n * slower saturation levels, a lower value will result in quicker saturation.\n *\n * @param {number} number - The value to set for this tuning parameter.\n */\nlunr.Builder.prototype.k1 = function (number) {\n this._k1 = number\n}\n\n/**\n * Adds a document to the index.\n *\n * Before adding fields to the index the index should have been fully setup, with the document\n * ref and all fields to index already having been specified.\n *\n * The document must have a field name as specified by the ref (by default this is 'id') and\n * it should have all fields defined for indexing, though null or undefined values will not\n * cause errors.\n *\n * Entire documents can be boosted at build time. Applying a boost to a document indicates that\n * this document should rank higher in search results than other documents.\n *\n * @param {object} doc - The document to add to the index.\n * @param {object} attributes - Optional attributes associated with this document.\n * @param {number} [attributes.boost=1] - Boost applied to all terms within this document.\n */\nlunr.Builder.prototype.add = function (doc, attributes) {\n var docRef = doc[this._ref],\n fields = Object.keys(this._fields)\n\n this._documents[docRef] = attributes || {}\n this.documentCount += 1\n\n for (var i = 0; i < fields.length; i++) {\n var fieldName = fields[i],\n extractor = this._fields[fieldName].extractor,\n field = extractor ? extractor(doc) : doc[fieldName],\n tokens = this.tokenizer(field, {\n fields: [fieldName]\n }),\n terms = this.pipeline.run(tokens),\n fieldRef = new lunr.FieldRef (docRef, fieldName),\n fieldTerms = Object.create(null)\n\n this.fieldTermFrequencies[fieldRef] = fieldTerms\n this.fieldLengths[fieldRef] = 0\n\n // store the length of this field for this document\n this.fieldLengths[fieldRef] += terms.length\n\n // calculate term frequencies for this field\n for (var j = 0; j < terms.length; j++) {\n var term = terms[j]\n\n if (fieldTerms[term] == undefined) {\n fieldTerms[term] = 0\n }\n\n fieldTerms[term] += 1\n\n // add to inverted index\n // create an initial posting if one doesn't exist\n if (this.invertedIndex[term] == undefined) {\n var posting = Object.create(null)\n posting[\"_index\"] = this.termIndex\n this.termIndex += 1\n\n for (var k = 0; k < fields.length; k++) {\n posting[fields[k]] = Object.create(null)\n }\n\n this.invertedIndex[term] = posting\n }\n\n // add an entry for this term/fieldName/docRef to the invertedIndex\n if (this.invertedIndex[term][fieldName][docRef] == undefined) {\n this.invertedIndex[term][fieldName][docRef] = Object.create(null)\n }\n\n // store all whitelisted metadata about this token in the\n // inverted index\n for (var l = 0; l < this.metadataWhitelist.length; l++) {\n var metadataKey = this.metadataWhitelist[l],\n metadata = term.metadata[metadataKey]\n\n if (this.invertedIndex[term][fieldName][docRef][metadataKey] == undefined) {\n this.invertedIndex[term][fieldName][docRef][metadataKey] = []\n }\n\n this.invertedIndex[term][fieldName][docRef][metadataKey].push(metadata)\n }\n }\n\n }\n}\n\n/**\n * Calculates the average document length for this index\n *\n * @private\n */\nlunr.Builder.prototype.calculateAverageFieldLengths = function () {\n\n var fieldRefs = Object.keys(this.fieldLengths),\n numberOfFields = fieldRefs.length,\n accumulator = {},\n documentsWithField = {}\n\n for (var i = 0; i < numberOfFields; i++) {\n var fieldRef = lunr.FieldRef.fromString(fieldRefs[i]),\n field = fieldRef.fieldName\n\n documentsWithField[field] || (documentsWithField[field] = 0)\n documentsWithField[field] += 1\n\n accumulator[field] || (accumulator[field] = 0)\n accumulator[field] += this.fieldLengths[fieldRef]\n }\n\n var fields = Object.keys(this._fields)\n\n for (var i = 0; i < fields.length; i++) {\n var fieldName = fields[i]\n accumulator[fieldName] = accumulator[fieldName] / documentsWithField[fieldName]\n }\n\n this.averageFieldLength = accumulator\n}\n\n/**\n * Builds a vector space model of every document using lunr.Vector\n *\n * @private\n */\nlunr.Builder.prototype.createFieldVectors = function () {\n var fieldVectors = {},\n fieldRefs = Object.keys(this.fieldTermFrequencies),\n fieldRefsLength = fieldRefs.length,\n termIdfCache = Object.create(null)\n\n for (var i = 0; i < fieldRefsLength; i++) {\n var fieldRef = lunr.FieldRef.fromString(fieldRefs[i]),\n fieldName = fieldRef.fieldName,\n fieldLength = this.fieldLengths[fieldRef],\n fieldVector = new lunr.Vector,\n termFrequencies = this.fieldTermFrequencies[fieldRef],\n terms = Object.keys(termFrequencies),\n termsLength = terms.length\n\n\n var fieldBoost = this._fields[fieldName].boost || 1,\n docBoost = this._documents[fieldRef.docRef].boost || 1\n\n for (var j = 0; j < termsLength; j++) {\n var term = terms[j],\n tf = termFrequencies[term],\n termIndex = this.invertedIndex[term]._index,\n idf, score, scoreWithPrecision\n\n if (termIdfCache[term] === undefined) {\n idf = lunr.idf(this.invertedIndex[term], this.documentCount)\n termIdfCache[term] = idf\n } else {\n idf = termIdfCache[term]\n }\n\n score = idf * ((this._k1 + 1) * tf) / (this._k1 * (1 - this._b + this._b * (fieldLength / this.averageFieldLength[fieldName])) + tf)\n score *= fieldBoost\n score *= docBoost\n scoreWithPrecision = Math.round(score * 1000) / 1000\n // Converts 1.23456789 to 1.234.\n // Reducing the precision so that the vectors take up less\n // space when serialised. Doing it now so that they behave\n // the same before and after serialisation. Also, this is\n // the fastest approach to reducing a number's precision in\n // JavaScript.\n\n fieldVector.insert(termIndex, scoreWithPrecision)\n }\n\n fieldVectors[fieldRef] = fieldVector\n }\n\n this.fieldVectors = fieldVectors\n}\n\n/**\n * Creates a token set of all tokens in the index using lunr.TokenSet\n *\n * @private\n */\nlunr.Builder.prototype.createTokenSet = function () {\n this.tokenSet = lunr.TokenSet.fromArray(\n Object.keys(this.invertedIndex).sort()\n )\n}\n\n/**\n * Builds the index, creating an instance of lunr.Index.\n *\n * This completes the indexing process and should only be called\n * once all documents have been added to the index.\n *\n * @returns {lunr.Index}\n */\nlunr.Builder.prototype.build = function () {\n this.calculateAverageFieldLengths()\n this.createFieldVectors()\n this.createTokenSet()\n\n return new lunr.Index({\n invertedIndex: this.invertedIndex,\n fieldVectors: this.fieldVectors,\n tokenSet: this.tokenSet,\n fields: Object.keys(this._fields),\n pipeline: this.searchPipeline\n })\n}\n\n/**\n * Applies a plugin to the index builder.\n *\n * A plugin is a function that is called with the index builder as its context.\n * Plugins can be used to customise or extend the behaviour of the index\n * in some way. A plugin is just a function, that encapsulated the custom\n * behaviour that should be applied when building the index.\n *\n * The plugin function will be called with the index builder as its argument, additional\n * arguments can also be passed when calling use. The function will be called\n * with the index builder as its context.\n *\n * @param {Function} plugin The plugin to apply.\n */\nlunr.Builder.prototype.use = function (fn) {\n var args = Array.prototype.slice.call(arguments, 1)\n args.unshift(this)\n fn.apply(this, args)\n}\n/**\n * Contains and collects metadata about a matching document.\n * A single instance of lunr.MatchData is returned as part of every\n * lunr.Index~Result.\n *\n * @constructor\n * @param {string} term - The term this match data is associated with\n * @param {string} field - The field in which the term was found\n * @param {object} metadata - The metadata recorded about this term in this field\n * @property {object} metadata - A cloned collection of metadata associated with this document.\n * @see {@link lunr.Index~Result}\n */\nlunr.MatchData = function (term, field, metadata) {\n var clonedMetadata = Object.create(null),\n metadataKeys = Object.keys(metadata || {})\n\n // Cloning the metadata to prevent the original\n // being mutated during match data combination.\n // Metadata is kept in an array within the inverted\n // index so cloning the data can be done with\n // Array#slice\n for (var i = 0; i < metadataKeys.length; i++) {\n var key = metadataKeys[i]\n clonedMetadata[key] = metadata[key].slice()\n }\n\n this.metadata = Object.create(null)\n\n if (term !== undefined) {\n this.metadata[term] = Object.create(null)\n this.metadata[term][field] = clonedMetadata\n }\n}\n\n/**\n * An instance of lunr.MatchData will be created for every term that matches a\n * document. However only one instance is required in a lunr.Index~Result. This\n * method combines metadata from another instance of lunr.MatchData with this\n * objects metadata.\n *\n * @param {lunr.MatchData} otherMatchData - Another instance of match data to merge with this one.\n * @see {@link lunr.Index~Result}\n */\nlunr.MatchData.prototype.combine = function (otherMatchData) {\n var terms = Object.keys(otherMatchData.metadata)\n\n for (var i = 0; i < terms.length; i++) {\n var term = terms[i],\n fields = Object.keys(otherMatchData.metadata[term])\n\n if (this.metadata[term] == undefined) {\n this.metadata[term] = Object.create(null)\n }\n\n for (var j = 0; j < fields.length; j++) {\n var field = fields[j],\n keys = Object.keys(otherMatchData.metadata[term][field])\n\n if (this.metadata[term][field] == undefined) {\n this.metadata[term][field] = Object.create(null)\n }\n\n for (var k = 0; k < keys.length; k++) {\n var key = keys[k]\n\n if (this.metadata[term][field][key] == undefined) {\n this.metadata[term][field][key] = otherMatchData.metadata[term][field][key]\n } else {\n this.metadata[term][field][key] = this.metadata[term][field][key].concat(otherMatchData.metadata[term][field][key])\n }\n\n }\n }\n }\n}\n\n/**\n * Add metadata for a term/field pair to this instance of match data.\n *\n * @param {string} term - The term this match data is associated with\n * @param {string} field - The field in which the term was found\n * @param {object} metadata - The metadata recorded about this term in this field\n */\nlunr.MatchData.prototype.add = function (term, field, metadata) {\n if (!(term in this.metadata)) {\n this.metadata[term] = Object.create(null)\n this.metadata[term][field] = metadata\n return\n }\n\n if (!(field in this.metadata[term])) {\n this.metadata[term][field] = metadata\n return\n }\n\n var metadataKeys = Object.keys(metadata)\n\n for (var i = 0; i < metadataKeys.length; i++) {\n var key = metadataKeys[i]\n\n if (key in this.metadata[term][field]) {\n this.metadata[term][field][key] = this.metadata[term][field][key].concat(metadata[key])\n } else {\n this.metadata[term][field][key] = metadata[key]\n }\n }\n}\n/**\n * A lunr.Query provides a programmatic way of defining queries to be performed\n * against a {@link lunr.Index}.\n *\n * Prefer constructing a lunr.Query using the {@link lunr.Index#query} method\n * so the query object is pre-initialized with the right index fields.\n *\n * @constructor\n * @property {lunr.Query~Clause[]} clauses - An array of query clauses.\n * @property {string[]} allFields - An array of all available fields in a lunr.Index.\n */\nlunr.Query = function (allFields) {\n this.clauses = []\n this.allFields = allFields\n}\n\n/**\n * Constants for indicating what kind of automatic wildcard insertion will be used when constructing a query clause.\n *\n * This allows wildcards to be added to the beginning and end of a term without having to manually do any string\n * concatenation.\n *\n * The wildcard constants can be bitwise combined to select both leading and trailing wildcards.\n *\n * @constant\n * @default\n * @property {number} wildcard.NONE - The term will have no wildcards inserted, this is the default behaviour\n * @property {number} wildcard.LEADING - Prepend the term with a wildcard, unless a leading wildcard already exists\n * @property {number} wildcard.TRAILING - Append a wildcard to the term, unless a trailing wildcard already exists\n * @see lunr.Query~Clause\n * @see lunr.Query#clause\n * @see lunr.Query#term\n * @example query term with trailing wildcard\n * query.term('foo', { wildcard: lunr.Query.wildcard.TRAILING })\n * @example query term with leading and trailing wildcard\n * query.term('foo', {\n * wildcard: lunr.Query.wildcard.LEADING | lunr.Query.wildcard.TRAILING\n * })\n */\n\nlunr.Query.wildcard = new String (\"*\")\nlunr.Query.wildcard.NONE = 0\nlunr.Query.wildcard.LEADING = 1\nlunr.Query.wildcard.TRAILING = 2\n\n/**\n * Constants for indicating what kind of presence a term must have in matching documents.\n *\n * @constant\n * @enum {number}\n * @see lunr.Query~Clause\n * @see lunr.Query#clause\n * @see lunr.Query#term\n * @example query term with required presence\n * query.term('foo', { presence: lunr.Query.presence.REQUIRED })\n */\nlunr.Query.presence = {\n /**\n * Term's presence in a document is optional, this is the default value.\n */\n OPTIONAL: 1,\n\n /**\n * Term's presence in a document is required, documents that do not contain\n * this term will not be returned.\n */\n REQUIRED: 2,\n\n /**\n * Term's presence in a document is prohibited, documents that do contain\n * this term will not be returned.\n */\n PROHIBITED: 3\n}\n\n/**\n * A single clause in a {@link lunr.Query} contains a term and details on how to\n * match that term against a {@link lunr.Index}.\n *\n * @typedef {Object} lunr.Query~Clause\n * @property {string[]} fields - The fields in an index this clause should be matched against.\n * @property {number} [boost=1] - Any boost that should be applied when matching this clause.\n * @property {number} [editDistance] - Whether the term should have fuzzy matching applied, and how fuzzy the match should be.\n * @property {boolean} [usePipeline] - Whether the term should be passed through the search pipeline.\n * @property {number} [wildcard=lunr.Query.wildcard.NONE] - Whether the term should have wildcards appended or prepended.\n * @property {number} [presence=lunr.Query.presence.OPTIONAL] - The terms presence in any matching documents.\n */\n\n/**\n * Adds a {@link lunr.Query~Clause} to this query.\n *\n * Unless the clause contains the fields to be matched all fields will be matched. In addition\n * a default boost of 1 is applied to the clause.\n *\n * @param {lunr.Query~Clause} clause - The clause to add to this query.\n * @see lunr.Query~Clause\n * @returns {lunr.Query}\n */\nlunr.Query.prototype.clause = function (clause) {\n if (!('fields' in clause)) {\n clause.fields = this.allFields\n }\n\n if (!('boost' in clause)) {\n clause.boost = 1\n }\n\n if (!('usePipeline' in clause)) {\n clause.usePipeline = true\n }\n\n if (!('wildcard' in clause)) {\n clause.wildcard = lunr.Query.wildcard.NONE\n }\n\n if ((clause.wildcard & lunr.Query.wildcard.LEADING) && (clause.term.charAt(0) != lunr.Query.wildcard)) {\n clause.term = \"*\" + clause.term\n }\n\n if ((clause.wildcard & lunr.Query.wildcard.TRAILING) && (clause.term.slice(-1) != lunr.Query.wildcard)) {\n clause.term = \"\" + clause.term + \"*\"\n }\n\n if (!('presence' in clause)) {\n clause.presence = lunr.Query.presence.OPTIONAL\n }\n\n this.clauses.push(clause)\n\n return this\n}\n\n/**\n * A negated query is one in which every clause has a presence of\n * prohibited. These queries require some special processing to return\n * the expected results.\n *\n * @returns boolean\n */\nlunr.Query.prototype.isNegated = function () {\n for (var i = 0; i < this.clauses.length; i++) {\n if (this.clauses[i].presence != lunr.Query.presence.PROHIBITED) {\n return false\n }\n }\n\n return true\n}\n\n/**\n * Adds a term to the current query, under the covers this will create a {@link lunr.Query~Clause}\n * to the list of clauses that make up this query.\n *\n * The term is used as is, i.e. no tokenization will be performed by this method. Instead conversion\n * to a token or token-like string should be done before calling this method.\n *\n * The term will be converted to a string by calling `toString`. Multiple terms can be passed as an\n * array, each term in the array will share the same options.\n *\n * @param {object|object[]} term - The term(s) to add to the query.\n * @param {object} [options] - Any additional properties to add to the query clause.\n * @returns {lunr.Query}\n * @see lunr.Query#clause\n * @see lunr.Query~Clause\n * @example adding a single term to a query\n * query.term(\"foo\")\n * @example adding a single term to a query and specifying search fields, term boost and automatic trailing wildcard\n * query.term(\"foo\", {\n * fields: [\"title\"],\n * boost: 10,\n * wildcard: lunr.Query.wildcard.TRAILING\n * })\n * @example using lunr.tokenizer to convert a string to tokens before using them as terms\n * query.term(lunr.tokenizer(\"foo bar\"))\n */\nlunr.Query.prototype.term = function (term, options) {\n if (Array.isArray(term)) {\n term.forEach(function (t) { this.term(t, lunr.utils.clone(options)) }, this)\n return this\n }\n\n var clause = options || {}\n clause.term = term.toString()\n\n this.clause(clause)\n\n return this\n}\nlunr.QueryParseError = function (message, start, end) {\n this.name = \"QueryParseError\"\n this.message = message\n this.start = start\n this.end = end\n}\n\nlunr.QueryParseError.prototype = new Error\nlunr.QueryLexer = function (str) {\n this.lexemes = []\n this.str = str\n this.length = str.length\n this.pos = 0\n this.start = 0\n this.escapeCharPositions = []\n}\n\nlunr.QueryLexer.prototype.run = function () {\n var state = lunr.QueryLexer.lexText\n\n while (state) {\n state = state(this)\n }\n}\n\nlunr.QueryLexer.prototype.sliceString = function () {\n var subSlices = [],\n sliceStart = this.start,\n sliceEnd = this.pos\n\n for (var i = 0; i < this.escapeCharPositions.length; i++) {\n sliceEnd = this.escapeCharPositions[i]\n subSlices.push(this.str.slice(sliceStart, sliceEnd))\n sliceStart = sliceEnd + 1\n }\n\n subSlices.push(this.str.slice(sliceStart, this.pos))\n this.escapeCharPositions.length = 0\n\n return subSlices.join('')\n}\n\nlunr.QueryLexer.prototype.emit = function (type) {\n this.lexemes.push({\n type: type,\n str: this.sliceString(),\n start: this.start,\n end: this.pos\n })\n\n this.start = this.pos\n}\n\nlunr.QueryLexer.prototype.escapeCharacter = function () {\n this.escapeCharPositions.push(this.pos - 1)\n this.pos += 1\n}\n\nlunr.QueryLexer.prototype.next = function () {\n if (this.pos >= this.length) {\n return lunr.QueryLexer.EOS\n }\n\n var char = this.str.charAt(this.pos)\n this.pos += 1\n return char\n}\n\nlunr.QueryLexer.prototype.width = function () {\n return this.pos - this.start\n}\n\nlunr.QueryLexer.prototype.ignore = function () {\n if (this.start == this.pos) {\n this.pos += 1\n }\n\n this.start = this.pos\n}\n\nlunr.QueryLexer.prototype.backup = function () {\n this.pos -= 1\n}\n\nlunr.QueryLexer.prototype.acceptDigitRun = function () {\n var char, charCode\n\n do {\n char = this.next()\n charCode = char.charCodeAt(0)\n } while (charCode > 47 && charCode < 58)\n\n if (char != lunr.QueryLexer.EOS) {\n this.backup()\n }\n}\n\nlunr.QueryLexer.prototype.more = function () {\n return this.pos < this.length\n}\n\nlunr.QueryLexer.EOS = 'EOS'\nlunr.QueryLexer.FIELD = 'FIELD'\nlunr.QueryLexer.TERM = 'TERM'\nlunr.QueryLexer.EDIT_DISTANCE = 'EDIT_DISTANCE'\nlunr.QueryLexer.BOOST = 'BOOST'\nlunr.QueryLexer.PRESENCE = 'PRESENCE'\n\nlunr.QueryLexer.lexField = function (lexer) {\n lexer.backup()\n lexer.emit(lunr.QueryLexer.FIELD)\n lexer.ignore()\n return lunr.QueryLexer.lexText\n}\n\nlunr.QueryLexer.lexTerm = function (lexer) {\n if (lexer.width() > 1) {\n lexer.backup()\n lexer.emit(lunr.QueryLexer.TERM)\n }\n\n lexer.ignore()\n\n if (lexer.more()) {\n return lunr.QueryLexer.lexText\n }\n}\n\nlunr.QueryLexer.lexEditDistance = function (lexer) {\n lexer.ignore()\n lexer.acceptDigitRun()\n lexer.emit(lunr.QueryLexer.EDIT_DISTANCE)\n return lunr.QueryLexer.lexText\n}\n\nlunr.QueryLexer.lexBoost = function (lexer) {\n lexer.ignore()\n lexer.acceptDigitRun()\n lexer.emit(lunr.QueryLexer.BOOST)\n return lunr.QueryLexer.lexText\n}\n\nlunr.QueryLexer.lexEOS = function (lexer) {\n if (lexer.width() > 0) {\n lexer.emit(lunr.QueryLexer.TERM)\n }\n}\n\n// This matches the separator used when tokenising fields\n// within a document. These should match otherwise it is\n// not possible to search for some tokens within a document.\n//\n// It is possible for the user to change the separator on the\n// tokenizer so it _might_ clash with any other of the special\n// characters already used within the search string, e.g. :.\n//\n// This means that it is possible to change the separator in\n// such a way that makes some words unsearchable using a search\n// string.\nlunr.QueryLexer.termSeparator = lunr.tokenizer.separator\n\nlunr.QueryLexer.lexText = function (lexer) {\n while (true) {\n var char = lexer.next()\n\n if (char == lunr.QueryLexer.EOS) {\n return lunr.QueryLexer.lexEOS\n }\n\n // Escape character is '\\'\n if (char.charCodeAt(0) == 92) {\n lexer.escapeCharacter()\n continue\n }\n\n if (char == \":\") {\n return lunr.QueryLexer.lexField\n }\n\n if (char == \"~\") {\n lexer.backup()\n if (lexer.width() > 0) {\n lexer.emit(lunr.QueryLexer.TERM)\n }\n return lunr.QueryLexer.lexEditDistance\n }\n\n if (char == \"^\") {\n lexer.backup()\n if (lexer.width() > 0) {\n lexer.emit(lunr.QueryLexer.TERM)\n }\n return lunr.QueryLexer.lexBoost\n }\n\n // \"+\" indicates term presence is required\n // checking for length to ensure that only\n // leading \"+\" are considered\n if (char == \"+\" && lexer.width() === 1) {\n lexer.emit(lunr.QueryLexer.PRESENCE)\n return lunr.QueryLexer.lexText\n }\n\n // \"-\" indicates term presence is prohibited\n // checking for length to ensure that only\n // leading \"-\" are considered\n if (char == \"-\" && lexer.width() === 1) {\n lexer.emit(lunr.QueryLexer.PRESENCE)\n return lunr.QueryLexer.lexText\n }\n\n if (char.match(lunr.QueryLexer.termSeparator)) {\n return lunr.QueryLexer.lexTerm\n }\n }\n}\n\nlunr.QueryParser = function (str, query) {\n this.lexer = new lunr.QueryLexer (str)\n this.query = query\n this.currentClause = {}\n this.lexemeIdx = 0\n}\n\nlunr.QueryParser.prototype.parse = function () {\n this.lexer.run()\n this.lexemes = this.lexer.lexemes\n\n var state = lunr.QueryParser.parseClause\n\n while (state) {\n state = state(this)\n }\n\n return this.query\n}\n\nlunr.QueryParser.prototype.peekLexeme = function () {\n return this.lexemes[this.lexemeIdx]\n}\n\nlunr.QueryParser.prototype.consumeLexeme = function () {\n var lexeme = this.peekLexeme()\n this.lexemeIdx += 1\n return lexeme\n}\n\nlunr.QueryParser.prototype.nextClause = function () {\n var completedClause = this.currentClause\n this.query.clause(completedClause)\n this.currentClause = {}\n}\n\nlunr.QueryParser.parseClause = function (parser) {\n var lexeme = parser.peekLexeme()\n\n if (lexeme == undefined) {\n return\n }\n\n switch (lexeme.type) {\n case lunr.QueryLexer.PRESENCE:\n return lunr.QueryParser.parsePresence\n case lunr.QueryLexer.FIELD:\n return lunr.QueryParser.parseField\n case lunr.QueryLexer.TERM:\n return lunr.QueryParser.parseTerm\n default:\n var errorMessage = \"expected either a field or a term, found \" + lexeme.type\n\n if (lexeme.str.length >= 1) {\n errorMessage += \" with value '\" + lexeme.str + \"'\"\n }\n\n throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end)\n }\n}\n\nlunr.QueryParser.parsePresence = function (parser) {\n var lexeme = parser.consumeLexeme()\n\n if (lexeme == undefined) {\n return\n }\n\n switch (lexeme.str) {\n case \"-\":\n parser.currentClause.presence = lunr.Query.presence.PROHIBITED\n break\n case \"+\":\n parser.currentClause.presence = lunr.Query.presence.REQUIRED\n break\n default:\n var errorMessage = \"unrecognised presence operator'\" + lexeme.str + \"'\"\n throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end)\n }\n\n var nextLexeme = parser.peekLexeme()\n\n if (nextLexeme == undefined) {\n var errorMessage = \"expecting term or field, found nothing\"\n throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end)\n }\n\n switch (nextLexeme.type) {\n case lunr.QueryLexer.FIELD:\n return lunr.QueryParser.parseField\n case lunr.QueryLexer.TERM:\n return lunr.QueryParser.parseTerm\n default:\n var errorMessage = \"expecting term or field, found '\" + nextLexeme.type + \"'\"\n throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end)\n }\n}\n\nlunr.QueryParser.parseField = function (parser) {\n var lexeme = parser.consumeLexeme()\n\n if (lexeme == undefined) {\n return\n }\n\n if (parser.query.allFields.indexOf(lexeme.str) == -1) {\n var possibleFields = parser.query.allFields.map(function (f) { return \"'\" + f + \"'\" }).join(', '),\n errorMessage = \"unrecognised field '\" + lexeme.str + \"', possible fields: \" + possibleFields\n\n throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end)\n }\n\n parser.currentClause.fields = [lexeme.str]\n\n var nextLexeme = parser.peekLexeme()\n\n if (nextLexeme == undefined) {\n var errorMessage = \"expecting term, found nothing\"\n throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end)\n }\n\n switch (nextLexeme.type) {\n case lunr.QueryLexer.TERM:\n return lunr.QueryParser.parseTerm\n default:\n var errorMessage = \"expecting term, found '\" + nextLexeme.type + \"'\"\n throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end)\n }\n}\n\nlunr.QueryParser.parseTerm = function (parser) {\n var lexeme = parser.consumeLexeme()\n\n if (lexeme == undefined) {\n return\n }\n\n parser.currentClause.term = lexeme.str.toLowerCase()\n\n if (lexeme.str.indexOf(\"*\") != -1) {\n parser.currentClause.usePipeline = false\n }\n\n var nextLexeme = parser.peekLexeme()\n\n if (nextLexeme == undefined) {\n parser.nextClause()\n return\n }\n\n switch (nextLexeme.type) {\n case lunr.QueryLexer.TERM:\n parser.nextClause()\n return lunr.QueryParser.parseTerm\n case lunr.QueryLexer.FIELD:\n parser.nextClause()\n return lunr.QueryParser.parseField\n case lunr.QueryLexer.EDIT_DISTANCE:\n return lunr.QueryParser.parseEditDistance\n case lunr.QueryLexer.BOOST:\n return lunr.QueryParser.parseBoost\n case lunr.QueryLexer.PRESENCE:\n parser.nextClause()\n return lunr.QueryParser.parsePresence\n default:\n var errorMessage = \"Unexpected lexeme type '\" + nextLexeme.type + \"'\"\n throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end)\n }\n}\n\nlunr.QueryParser.parseEditDistance = function (parser) {\n var lexeme = parser.consumeLexeme()\n\n if (lexeme == undefined) {\n return\n }\n\n var editDistance = parseInt(lexeme.str, 10)\n\n if (isNaN(editDistance)) {\n var errorMessage = \"edit distance must be numeric\"\n throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end)\n }\n\n parser.currentClause.editDistance = editDistance\n\n var nextLexeme = parser.peekLexeme()\n\n if (nextLexeme == undefined) {\n parser.nextClause()\n return\n }\n\n switch (nextLexeme.type) {\n case lunr.QueryLexer.TERM:\n parser.nextClause()\n return lunr.QueryParser.parseTerm\n case lunr.QueryLexer.FIELD:\n parser.nextClause()\n return lunr.QueryParser.parseField\n case lunr.QueryLexer.EDIT_DISTANCE:\n return lunr.QueryParser.parseEditDistance\n case lunr.QueryLexer.BOOST:\n return lunr.QueryParser.parseBoost\n case lunr.QueryLexer.PRESENCE:\n parser.nextClause()\n return lunr.QueryParser.parsePresence\n default:\n var errorMessage = \"Unexpected lexeme type '\" + nextLexeme.type + \"'\"\n throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end)\n }\n}\n\nlunr.QueryParser.parseBoost = function (parser) {\n var lexeme = parser.consumeLexeme()\n\n if (lexeme == undefined) {\n return\n }\n\n var boost = parseInt(lexeme.str, 10)\n\n if (isNaN(boost)) {\n var errorMessage = \"boost must be numeric\"\n throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end)\n }\n\n parser.currentClause.boost = boost\n\n var nextLexeme = parser.peekLexeme()\n\n if (nextLexeme == undefined) {\n parser.nextClause()\n return\n }\n\n switch (nextLexeme.type) {\n case lunr.QueryLexer.TERM:\n parser.nextClause()\n return lunr.QueryParser.parseTerm\n case lunr.QueryLexer.FIELD:\n parser.nextClause()\n return lunr.QueryParser.parseField\n case lunr.QueryLexer.EDIT_DISTANCE:\n return lunr.QueryParser.parseEditDistance\n case lunr.QueryLexer.BOOST:\n return lunr.QueryParser.parseBoost\n case lunr.QueryLexer.PRESENCE:\n parser.nextClause()\n return lunr.QueryParser.parsePresence\n default:\n var errorMessage = \"Unexpected lexeme type '\" + nextLexeme.type + \"'\"\n throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end)\n }\n}\n\n /**\n * export the module via AMD, CommonJS or as a browser global\n * Export code from https://github.com/umdjs/umd/blob/master/returnExports.js\n */\n ;(function (root, factory) {\n if (typeof define === 'function' && define.amd) {\n // AMD. Register as an anonymous module.\n define(factory)\n } else if (typeof exports === 'object') {\n /**\n * Node. Does not work with strict CommonJS, but\n * only CommonJS-like enviroments that support module.exports,\n * like Node.\n */\n module.exports = factory()\n } else {\n // Browser globals (root is window)\n root.lunr = factory()\n }\n }(this, function () {\n /**\n * Just return a value to define the module export.\n * This example returns an object, but the module\n * can return a function as the exported value.\n */\n return lunr\n }))\n})();\n", "/*\n * Copyright (c) 2016-2024 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A RTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport lunr from \"lunr\"\n\nimport { getElement } from \"~/browser/element/_\"\nimport \"~/polyfills\"\n\nimport { Search } from \"../../_\"\nimport { SearchConfig } from \"../../config\"\nimport {\n SearchMessage,\n SearchMessageType\n} from \"../message\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Add support for `iframe-worker` shim\n *\n * While `importScripts` is synchronous when executed inside of a web worker,\n * it's not possible to provide a synchronous shim implementation. The cool\n * thing is that awaiting a non-Promise will convert it into a Promise, so\n * extending the type definition to return a `Promise` shouldn't break anything.\n *\n * @see https://bit.ly/2PjDnXi - GitHub comment\n *\n * @param urls - Scripts to load\n *\n * @returns Promise resolving with no result\n */\ndeclare global {\n function importScripts(...urls: string[]): Promise | void\n}\n\n/* ----------------------------------------------------------------------------\n * Data\n * ------------------------------------------------------------------------- */\n\n/**\n * Search index\n */\nlet index: Search\n\n/* ----------------------------------------------------------------------------\n * Helper functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Fetch (= import) multi-language support through `lunr-languages`\n *\n * This function automatically imports the stemmers necessary to process the\n * languages which are defined as part of the search configuration.\n *\n * If the worker runs inside of an `iframe` (when using `iframe-worker` as\n * a shim), the base URL for the stemmers to be loaded must be determined by\n * searching for the first `script` element with a `src` attribute, which will\n * contain the contents of this script.\n *\n * @param config - Search configuration\n *\n * @returns Promise resolving with no result\n */\nasync function setupSearchLanguages(\n config: SearchConfig\n): Promise {\n let base = \"../lunr\"\n\n /* Detect `iframe-worker` and fix base URL */\n if (typeof parent !== \"undefined\" && \"IFrameWorker\" in parent) {\n const worker = getElement(\"script[src]\")\n const [path] = worker.src.split(\"/worker\")\n\n /* Prefix base with path */\n base = base.replace(\"..\", path)\n }\n\n /* Add scripts for languages */\n const scripts = []\n for (const lang of config.lang) {\n switch (lang) {\n\n /* Add segmenter for Japanese */\n case \"ja\":\n scripts.push(`${base}/tinyseg.js`)\n break\n\n /* Add segmenter for Hindi and Thai */\n case \"hi\":\n case \"th\":\n scripts.push(`${base}/wordcut.js`)\n break\n }\n\n /* Add language support */\n if (lang !== \"en\")\n scripts.push(`${base}/min/lunr.${lang}.min.js`)\n }\n\n /* Add multi-language support */\n if (config.lang.length > 1)\n scripts.push(`${base}/min/lunr.multi.min.js`)\n\n /* Load scripts synchronously */\n if (scripts.length)\n await importScripts(\n `${base}/min/lunr.stemmer.support.min.js`,\n ...scripts\n )\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Message handler\n *\n * @param message - Source message\n *\n * @returns Target message\n */\nexport async function handler(\n message: SearchMessage\n): Promise {\n switch (message.type) {\n\n /* Search setup message */\n case SearchMessageType.SETUP:\n await setupSearchLanguages(message.data.config)\n index = new Search(message.data)\n return {\n type: SearchMessageType.READY\n }\n\n /* Search query message */\n case SearchMessageType.QUERY:\n const query = message.data\n try {\n return {\n type: SearchMessageType.RESULT,\n data: index.search(query)\n }\n\n /* Return empty result in case of error */\n } catch (err) {\n console.warn(`Invalid query: ${query} \u2013 see https://bit.ly/2s3ChXG`)\n console.warn(err)\n return {\n type: SearchMessageType.RESULT,\n data: { items: [] }\n }\n }\n\n /* All other messages */\n default:\n throw new TypeError(\"Invalid message type\")\n }\n}\n\n/* ----------------------------------------------------------------------------\n * Worker\n * ------------------------------------------------------------------------- */\n\n/* Expose Lunr.js in global scope, or stemmers won't work */\nself.lunr = lunr\n\n/* Handle messages */\naddEventListener(\"message\", async ev => {\n postMessage(await handler(ev.data))\n})\n", "/*\n * Copyright (c) 2016-2024 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Retrieve all elements matching the query selector\n *\n * @template T - Element type\n *\n * @param selector - Query selector\n * @param node - Node of reference\n *\n * @returns Elements\n */\nexport function getElements(\n selector: T, node?: ParentNode\n): HTMLElementTagNameMap[T][]\n\nexport function getElements(\n selector: string, node?: ParentNode\n): T[]\n\nexport function getElements(\n selector: string, node: ParentNode = document\n): T[] {\n return Array.from(node.querySelectorAll(selector))\n}\n\n/**\n * Retrieve an element matching a query selector or throw a reference error\n *\n * Note that this function assumes that the element is present. If unsure if an\n * element is existent, use the `getOptionalElement` function instead.\n *\n * @template T - Element type\n *\n * @param selector - Query selector\n * @param node - Node of reference\n *\n * @returns Element\n */\nexport function getElement(\n selector: T, node?: ParentNode\n): HTMLElementTagNameMap[T]\n\nexport function getElement(\n selector: string, node?: ParentNode\n): T\n\nexport function getElement(\n selector: string, node: ParentNode = document\n): T {\n const el = getOptionalElement(selector, node)\n if (typeof el === \"undefined\")\n throw new ReferenceError(\n `Missing element: expected \"${selector}\" to be present`\n )\n\n /* Return element */\n return el\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Retrieve an optional element matching the query selector\n *\n * @template T - Element type\n *\n * @param selector - Query selector\n * @param node - Node of reference\n *\n * @returns Element or nothing\n */\nexport function getOptionalElement(\n selector: T, node?: ParentNode\n): HTMLElementTagNameMap[T] | undefined\n\nexport function getOptionalElement(\n selector: string, node?: ParentNode\n): T | undefined\n\nexport function getOptionalElement(\n selector: string, node: ParentNode = document\n): T | undefined {\n return node.querySelector(selector) || undefined\n}\n\n/**\n * Retrieve the currently active element\n *\n * @returns Element or nothing\n */\nexport function getActiveElement(): HTMLElement | undefined {\n return (\n document.activeElement?.shadowRoot?.activeElement as HTMLElement ??\n document.activeElement as HTMLElement ??\n undefined\n )\n}\n", "/*\n * Copyright (c) 2016-2024 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\n/* ----------------------------------------------------------------------------\n * Polyfills\n * ------------------------------------------------------------------------- */\n\n/* Polyfill `Object.entries` */\nif (!Object.entries)\n Object.entries = function (obj: object) {\n const data: [string, string][] = []\n for (const key of Object.keys(obj))\n // @ts-expect-error - ignore property access warning\n data.push([key, obj[key]])\n\n /* Return entries */\n return data\n }\n\n/* Polyfill `Object.values` */\nif (!Object.values)\n Object.values = function (obj: object) {\n const data: string[] = []\n for (const key of Object.keys(obj))\n // @ts-expect-error - ignore property access warning\n data.push(obj[key])\n\n /* Return values */\n return data\n }\n\n/* ------------------------------------------------------------------------- */\n\n/* Polyfills for `Element` */\nif (typeof Element !== \"undefined\") {\n\n /* Polyfill `Element.scrollTo` */\n if (!Element.prototype.scrollTo)\n Element.prototype.scrollTo = function (\n x?: ScrollToOptions | number, y?: number\n ): void {\n if (typeof x === \"object\") {\n this.scrollLeft = x.left!\n this.scrollTop = x.top!\n } else {\n this.scrollLeft = x!\n this.scrollTop = y!\n }\n }\n\n /* Polyfill `Element.replaceWith` */\n if (!Element.prototype.replaceWith)\n Element.prototype.replaceWith = function (\n ...nodes: Array\n ): void {\n const parent = this.parentNode\n if (parent) {\n if (nodes.length === 0)\n parent.removeChild(this)\n\n /* Replace children and create text nodes */\n for (let i = nodes.length - 1; i >= 0; i--) {\n let node = nodes[i]\n if (typeof node === \"string\")\n node = document.createTextNode(node)\n else if (node.parentNode)\n node.parentNode.removeChild(node)\n\n /* Replace child or insert before previous sibling */\n if (!i)\n parent.replaceChild(node, this)\n else\n parent.insertBefore(this.previousSibling!, node)\n }\n }\n }\n}\n", "/*\n * Copyright (c) 2016-2024 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Search configuration\n */\nexport interface SearchConfig {\n lang: string[] /* Search languages */\n separator: string /* Search separator */\n pipeline: SearchPipelineFn[] /* Search pipeline */\n}\n\n/**\n * Search document\n */\nexport interface SearchDocument {\n location: string /* Document location */\n title: string /* Document title */\n text: string /* Document text */\n tags?: string[] /* Document tags */\n boost?: number /* Document boost */\n parent?: SearchDocument /* Document parent */\n}\n\n/**\n * Search options\n */\nexport interface SearchOptions {\n suggest: boolean /* Search suggestions */\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Search index\n */\nexport interface SearchIndex {\n config: SearchConfig /* Search configuration */\n docs: SearchDocument[] /* Search documents */\n options: SearchOptions /* Search options */\n}\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Search pipeline function\n */\ntype SearchPipelineFn =\n | \"trimmer\" /* Trimmer */\n | \"stopWordFilter\" /* Stop word filter */\n | \"stemmer\" /* Stemmer */\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Create a search document map\n *\n * This function creates a mapping of URLs (including anchors) to the actual\n * articles and sections. It relies on the invariant that the search index is\n * ordered with the main article appearing before all sections with anchors.\n * If this is not the case, the logic music be changed.\n *\n * @param docs - Search documents\n *\n * @returns Search document map\n */\nexport function setupSearchDocumentMap(\n docs: SearchDocument[]\n): Map {\n const map = new Map()\n for (const doc of docs) {\n const [path] = doc.location.split(\"#\")\n\n /* Add document article */\n const article = map.get(path)\n if (typeof article === \"undefined\") {\n map.set(path, doc)\n\n /* Add document section */\n } else {\n map.set(doc.location, doc)\n doc.parent = article\n }\n }\n\n /* Return search document map */\n return map\n}\n", "/*\n * Copyright (c) 2016-2024 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Visitor function\n *\n * @param start - Start offset\n * @param end - End offset\n */\ntype VisitorFn = (\n start: number, end: number\n) => void\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Split a string using the given separator\n *\n * @param input - Input value\n * @param separator - Separator\n * @param fn - Visitor function\n */\nexport function split(\n input: string, separator: RegExp, fn: VisitorFn\n): void {\n separator = new RegExp(separator, \"g\")\n\n /* Split string using separator */\n let match: RegExpExecArray | null\n let index = 0\n do {\n match = separator.exec(input)\n\n /* Emit non-empty range */\n const until = match?.index ?? input.length\n if (index < until)\n fn(index, until)\n\n /* Update last index */\n if (match) {\n const [term] = match\n index = match.index + term.length\n\n /* Support zero-length lookaheads */\n if (term.length === 0)\n separator.lastIndex = match.index + 1\n }\n } while (match)\n}\n", "/*\n * Copyright (c) 2016-2024 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Extraction type\n *\n * This type defines the possible values that are encoded into the first two\n * bits of a section that is part of the blocks of a tokenization table. There\n * are three types of interest: HTML opening and closing tags, as well as the\n * actual text content we need to extract for indexing.\n */\nexport const enum Extract {\n TAG_OPEN = 0, /* HTML opening tag */\n TEXT = 1, /* Text content */\n TAG_CLOSE = 2 /* HTML closing tag */\n}\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Visitor function\n *\n * @param block - Block index\n * @param type - Extraction type\n * @param start - Start offset\n * @param end - End offset\n */\ntype VisitorFn = (\n block: number, type: Extract, start: number, end: number\n) => void\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Split a string into markup and text sections\n *\n * This function scans a string and divides it up into sections of markup and\n * text. For each section, it invokes the given visitor function with the block\n * index, extraction type, as well as start and end offsets. Using a visitor\n * function (= streaming data) is ideal for minimizing pressure on the GC.\n *\n * @param input - Input value\n * @param fn - Visitor function\n */\nexport function extract(\n input: string, fn: VisitorFn\n): void {\n\n let block = 0 /* Current block */\n let start = 0 /* Current start offset */\n let end = 0 /* Current end offset */\n\n /* Split string into sections */\n for (let stack = 0; end < input.length; end++) {\n\n /* Opening tag after non-empty section */\n if (input.charAt(end) === \"<\" && end > start) {\n fn(block, Extract.TEXT, start, start = end)\n\n /* Closing tag */\n } else if (input.charAt(end) === \">\") {\n if (input.charAt(start + 1) === \"/\") {\n if (--stack === 0)\n fn(block++, Extract.TAG_CLOSE, start, end + 1)\n\n /* Tag is not self-closing */\n } else if (input.charAt(end - 1) !== \"/\") {\n if (stack++ === 0)\n fn(block, Extract.TAG_OPEN, start, end + 1)\n }\n\n /* New section */\n start = end + 1\n }\n }\n\n /* Add trailing section */\n if (end > start)\n fn(block, Extract.TEXT, start, end)\n}\n", "/*\n * Copyright (c) 2016-2024 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Position table\n */\nexport type PositionTable = number[][]\n\n/**\n * Position\n */\nexport type Position = number\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Highlight all occurrences in a string\n *\n * This function receives a field's value (e.g. like `title` or `text`), it's\n * position table that was generated during indexing, and the positions found\n * when executing the query. It then highlights all occurrences, and returns\n * their concatenation. In case of multiple blocks, two are returned.\n *\n * @param input - Input value\n * @param table - Table for indexing\n * @param positions - Occurrences\n * @param full - Full results\n *\n * @returns Highlighted string value\n */\nexport function highlight(\n input: string, table: PositionTable, positions: Position[], full = false\n): string {\n return highlightAll([input], table, positions, full).pop()!\n}\n\n/**\n * Highlight all occurrences in a set of strings\n *\n * @param inputs - Input values\n * @param table - Table for indexing\n * @param positions - Occurrences\n * @param full - Full results\n *\n * @returns Highlighted string values\n */\nexport function highlightAll(\n inputs: string[], table: PositionTable, positions: Position[], full = false\n): string[] {\n\n /* Map blocks to input values */\n const mapping = [0]\n for (let t = 1; t < table.length; t++) {\n const prev = table[t - 1]\n const next = table[t]\n\n /* Check if table points to new block */\n const p = prev[prev.length - 1] >>> 2 & 0x3FF\n const q = next[0] >>> 12\n\n /* Add block to mapping */\n mapping.push(+(p > q) + mapping[mapping.length - 1])\n }\n\n /* Highlight strings one after another */\n return inputs.map((input, i) => {\n let cursor = 0\n\n /* Map occurrences to blocks */\n const blocks = new Map()\n for (const p of positions.sort((a, b) => a - b)) {\n const index = p & 0xFFFFF\n const block = p >>> 20\n if (mapping[block] !== i)\n continue\n\n /* Ensure presence of block group */\n let group = blocks.get(block)\n if (typeof group === \"undefined\")\n blocks.set(block, group = [])\n\n /* Add index to group */\n group.push(index)\n }\n\n /* Just return string, if no occurrences */\n if (blocks.size === 0)\n return input\n\n /* Compute slices */\n const slices: string[] = []\n for (const [block, indexes] of blocks) {\n const t = table[block]\n\n /* Extract positions and length */\n const start = t[0] >>> 12\n const end = t[t.length - 1] >>> 12\n const length = t[t.length - 1] >>> 2 & 0x3FF\n\n /* Add prefix, if full results are desired */\n if (full && start > cursor)\n slices.push(input.slice(cursor, start))\n\n /* Extract and highlight slice */\n let slice = input.slice(start, end + length)\n for (const j of indexes.sort((a, b) => b - a)) {\n\n /* Retrieve offset and length of match */\n const p = (t[j] >>> 12) - start\n const q = (t[j] >>> 2 & 0x3FF) + p\n\n /* Wrap occurrence */\n slice = [\n slice.slice(0, p),\n \"\",\n slice.slice(p, q),\n \"\",\n slice.slice(q)\n ].join(\"\")\n }\n\n /* Update cursor */\n cursor = end + length\n\n /* Append slice and abort if we have two */\n if (slices.push(slice) === 2)\n break\n }\n\n /* Add suffix, if full results are desired */\n if (full && cursor < input.length)\n slices.push(input.slice(cursor))\n\n /* Return highlighted slices */\n return slices.join(\"\")\n })\n}\n", "/*\n * Copyright (c) 2016-2024 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { split } from \"../_\"\nimport {\n Extract,\n extract\n} from \"../extract\"\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Split a string or set of strings into tokens\n *\n * This tokenizer supersedes the default tokenizer that is provided by Lunr.js,\n * as it is aware of HTML tags and allows for multi-character splitting.\n *\n * It takes the given inputs, splits each of them into markup and text sections,\n * tokenizes and segments (if necessary) each of them, and then indexes them in\n * a table by using a compact bit representation. Bitwise techniques are used\n * to write and read from the table during indexing and querying.\n *\n * @see https://bit.ly/3W3Xw4J - Search: better, faster, smaller\n *\n * @param input - Input value(s)\n *\n * @returns Tokens\n */\nexport function tokenize(\n input?: string | string[]\n): lunr.Token[] {\n const tokens: lunr.Token[] = []\n if (typeof input === \"undefined\")\n return tokens\n\n /* Tokenize strings one after another */\n const inputs = Array.isArray(input) ? input : [input]\n for (let i = 0; i < inputs.length; i++) {\n const table = lunr.tokenizer.table\n const total = table.length\n\n /* Split string into sections and tokenize content blocks */\n extract(inputs[i], (block, type, start, end) => {\n table[block += total] ||= []\n switch (type) {\n\n /* Handle markup */\n case Extract.TAG_OPEN:\n case Extract.TAG_CLOSE:\n table[block].push(\n start << 12 |\n end - start << 2 |\n type\n )\n break\n\n /* Handle text content */\n case Extract.TEXT:\n const section = inputs[i].slice(start, end)\n split(section, lunr.tokenizer.separator, (index, until) => {\n\n /**\n * Apply segmenter after tokenization. Note that the segmenter will\n * also split words at word boundaries, which is not what we want,\n * so we need to check if we can somehow mitigate this behavior.\n */\n if (typeof lunr.segmenter !== \"undefined\") {\n const subsection = section.slice(index, until)\n if (/^[MHIK]$/.test(lunr.segmenter.ctype_(subsection))) {\n const segments = lunr.segmenter.segment(subsection)\n for (let s = 0, l = 0; s < segments.length; s++) {\n\n /* Add block to section */\n table[block] ||= []\n table[block].push(\n start + index + l << 12 |\n segments[s].length << 2 |\n type\n )\n\n /* Add token with position */\n tokens.push(new lunr.Token(\n segments[s].toLowerCase(), {\n position: block << 20 | table[block].length - 1\n }\n ))\n\n /* Keep track of length */\n l += segments[s].length\n }\n return\n }\n }\n\n /* Add block to section */\n table[block].push(\n start + index << 12 |\n until - index << 2 |\n type\n )\n\n /* Add token with position */\n tokens.push(new lunr.Token(\n section.slice(index, until).toLowerCase(), {\n position: block << 20 | table[block].length - 1\n }\n ))\n })\n }\n })\n }\n\n /* Return tokens */\n return tokens\n}\n", "/*\n * Copyright (c) 2016-2024 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Visitor function\n *\n * @param value - String value\n *\n * @returns String term(s)\n */\ntype VisitorFn = (\n value: string\n) => string | string[]\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Default transformation function\n *\n * 1. Trim excess whitespace from left and right.\n *\n * 2. Search for parts in quotation marks and prepend a `+` modifier to denote\n * that the resulting document must contain all parts, converting the query\n * to an `AND` query (as opposed to the default `OR` behavior). While users\n * may expect parts enclosed in quotation marks to map to span queries, i.e.\n * for which order is important, Lunr.js doesn't support them, so the best\n * we can do is to convert the parts to an `AND` query.\n *\n * 3. Replace control characters which are not located at the beginning of the\n * query or preceded by white space, or are not followed by a non-whitespace\n * character or are at the end of the query string. Furthermore, filter\n * unmatched quotation marks.\n *\n * 4. Split the query string at whitespace, then pass each part to the visitor\n * function for tokenization, and append a wildcard to every resulting term\n * that is not explicitly marked with a `+`, `-`, `~` or `^` modifier, since\n * it ensures consistent and stable ranking when multiple terms are entered.\n * Also, if a fuzzy or boost modifier are given, but no numeric value has\n * been entered, default to 1 to not induce a query error.\n *\n * @param query - Query value\n * @param fn - Visitor function\n *\n * @returns Transformed query value\n */\nexport function transform(\n query: string, fn: VisitorFn = term => term\n): string {\n return query\n\n /* => 1 */\n .trim()\n\n /* => 2 */\n .split(/\"([^\"]+)\"/g)\n .map((parts, index) => index & 1\n ? parts.replace(/^\\b|^(?![^\\x00-\\x7F]|$)|\\s+/g, \" +\")\n : parts\n )\n .join(\"\")\n\n /* => 3 */\n .replace(/\"|(?:^|\\s+)[*+\\-:^~]+(?=\\s+|$)/g, \"\")\n\n /* => 4 */\n .split(/\\s+/g)\n .reduce((prev, term) => {\n const next = fn(term)\n return [...prev, ...Array.isArray(next) ? next : [next]]\n }, [] as string[])\n .map(term => /([~^]$)/.test(term) ? `${term}1` : term)\n .map(term => /(^[+-]|[~^]\\d+$)/.test(term) ? term : `${term}*`)\n .join(\" \")\n}\n", "/*\n * Copyright (c) 2016-2024 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { split } from \"../../internal\"\nimport { transform } from \"../transform\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Search query clause\n */\nexport interface SearchQueryClause {\n presence: lunr.Query.presence /* Clause presence */\n term: string /* Clause term */\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Search query terms\n */\nexport type SearchQueryTerms = Record\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Transform search query\n *\n * This function lexes the given search query and applies the transformation\n * function to each term, preserving markup like `+` and `-` modifiers.\n *\n * @param query - Search query\n *\n * @returns Search query\n */\nexport function transformSearchQuery(\n query: string\n): string {\n\n /* Split query terms with tokenizer */\n return transform(query, part => {\n const terms: string[] = []\n\n /* Initialize lexer and analyze part */\n const lexer = new lunr.QueryLexer(part)\n lexer.run()\n\n /* Extract and tokenize term from lexeme */\n for (const { type, str: term, start, end } of lexer.lexemes)\n switch (type) {\n\n /* Hack: remove colon - see https://bit.ly/3wD3T3I */\n case \"FIELD\":\n if (![\"title\", \"text\", \"tags\"].includes(term))\n part = [\n part.slice(0, end),\n \" \",\n part.slice(end + 1)\n ].join(\"\")\n break\n\n /* Tokenize term */\n case \"TERM\":\n split(term, lunr.tokenizer.separator, (...range) => {\n terms.push([\n part.slice(0, start),\n term.slice(...range),\n part.slice(end)\n ].join(\"\"))\n })\n }\n\n /* Return terms */\n return terms\n })\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Parse a search query for analysis\n *\n * Lunr.js itself has a bug where it doesn't detect or remove wildcards for\n * query clauses, so we must do this here.\n *\n * @see https://bit.ly/3DpTGtz - GitHub issue\n *\n * @param value - Query value\n *\n * @returns Search query clauses\n */\nexport function parseSearchQuery(\n value: string\n): SearchQueryClause[] {\n const query = new lunr.Query([\"title\", \"text\", \"tags\"])\n const parser = new lunr.QueryParser(value, query)\n\n /* Parse Search query */\n parser.parse()\n for (const clause of query.clauses) {\n clause.usePipeline = true\n\n /* Handle leading wildcard */\n if (clause.term.startsWith(\"*\")) {\n clause.wildcard = lunr.Query.wildcard.LEADING\n clause.term = clause.term.slice(1)\n }\n\n /* Handle trailing wildcard */\n if (clause.term.endsWith(\"*\")) {\n clause.wildcard = lunr.Query.wildcard.TRAILING\n clause.term = clause.term.slice(0, -1)\n }\n }\n\n /* Return query clauses */\n return query.clauses\n}\n\n/**\n * Analyze the search query clauses in regard to the search terms found\n *\n * @param query - Search query clauses\n * @param terms - Search terms\n *\n * @returns Search query terms\n */\nexport function getSearchQueryTerms(\n query: SearchQueryClause[], terms: string[]\n): SearchQueryTerms {\n const clauses = new Set(query)\n\n /* Match query clauses against terms */\n const result: SearchQueryTerms = {}\n for (let t = 0; t < terms.length; t++)\n for (const clause of clauses)\n if (terms[t].startsWith(clause.term)) {\n result[clause.term] = true\n clauses.delete(clause)\n }\n\n /* Annotate unmatched non-stopword query clauses */\n for (const clause of clauses)\n if (lunr.stopWordFilter?.(clause.term))\n result[clause.term] = false\n\n /* Return query terms */\n return result\n}\n", "/*\n * Copyright (c) 2016-2024 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Segment a search query using the inverted index\n *\n * This function implements a clever approach to text segmentation for Asian\n * languages, as it used the information already available in the search index.\n * The idea is to greedily segment the search query based on the tokens that are\n * already part of the index, as described in the linked issue.\n *\n * @see https://bit.ly/3lwjrk7 - GitHub issue\n *\n * @param query - Query value\n * @param index - Inverted index\n *\n * @returns Segmented query value\n */\nexport function segment(\n query: string, index: object\n): Iterable {\n const segments = new Set()\n\n /* Segment search query */\n const wordcuts = new Uint16Array(query.length)\n for (let i = 0; i < query.length; i++)\n for (let j = i + 1; j < query.length; j++) {\n const value = query.slice(i, j)\n if (value in index)\n wordcuts[i] = j - i\n }\n\n /* Compute longest matches with minimum overlap */\n const stack = [0]\n for (let s = stack.length; s > 0;) {\n const p = stack[--s]\n for (let q = 1; q < wordcuts[p]; q++)\n if (wordcuts[p + q] > wordcuts[p] - q) {\n segments.add(query.slice(p, p + q))\n stack[s++] = p + q\n }\n\n /* Continue at end of query string */\n const q = p + wordcuts[p]\n if (wordcuts[q] && q < query.length - 1)\n stack[s++] = q\n\n /* Add current segment */\n segments.add(query.slice(p, q))\n }\n\n // @todo fix this case in the code block above, this is a hotfix\n if (segments.has(\"\"))\n return new Set([query])\n\n /* Return segmented query value */\n return segments\n}\n", "/*\n * Copyright (c) 2016-2024 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n SearchDocument,\n SearchIndex,\n SearchOptions,\n setupSearchDocumentMap\n} from \"../config\"\nimport {\n Position,\n PositionTable,\n highlight,\n highlightAll,\n tokenize\n} from \"../internal\"\nimport {\n SearchQueryTerms,\n getSearchQueryTerms,\n parseSearchQuery,\n segment,\n transformSearchQuery\n} from \"../query\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Search item\n */\nexport interface SearchItem\n extends SearchDocument\n{\n score: number /* Score (relevance) */\n terms: SearchQueryTerms /* Search query terms */\n}\n\n/**\n * Search result\n */\nexport interface SearchResult {\n items: SearchItem[][] /* Search items */\n suggest?: string[] /* Search suggestions */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Create field extractor factory\n *\n * @param table - Position table map\n *\n * @returns Extractor factory\n */\nfunction extractor(table: Map) {\n return (name: keyof SearchDocument) => {\n return (doc: SearchDocument) => {\n if (typeof doc[name] === \"undefined\")\n return undefined\n\n /* Compute identifier and initialize table */\n const id = [doc.location, name].join(\":\")\n table.set(id, lunr.tokenizer.table = [])\n\n /* Return field value */\n return doc[name]\n }\n }\n}\n\n/**\n * Compute the difference of two lists of strings\n *\n * @param a - 1st list of strings\n * @param b - 2nd list of strings\n *\n * @returns Difference\n */\nfunction difference(a: string[], b: string[]): string[] {\n const [x, y] = [new Set(a), new Set(b)]\n return [\n ...new Set([...x].filter(value => !y.has(value)))\n ]\n}\n\n/* ----------------------------------------------------------------------------\n * Class\n * ------------------------------------------------------------------------- */\n\n/**\n * Search index\n */\nexport class Search {\n\n /**\n * Search document map\n */\n protected map: Map\n\n /**\n * Search options\n */\n protected options: SearchOptions\n\n /**\n * The underlying Lunr.js search index\n */\n protected index: lunr.Index\n\n /**\n * Internal position table map\n */\n protected table: Map\n\n /**\n * Create the search integration\n *\n * @param data - Search index\n */\n public constructor({ config, docs, options }: SearchIndex) {\n const field = extractor(this.table = new Map())\n\n /* Set up document map and options */\n this.map = setupSearchDocumentMap(docs)\n this.options = options\n\n /* Set up document index */\n this.index = lunr(function () {\n this.metadataWhitelist = [\"position\"]\n this.b(0)\n\n /* Set up (multi-)language support */\n if (config.lang.length === 1 && config.lang[0] !== \"en\") {\n // @ts-expect-error - namespace indexing not supported\n this.use(lunr[config.lang[0]])\n } else if (config.lang.length > 1) {\n this.use(lunr.multiLanguage(...config.lang))\n }\n\n /* Set up custom tokenizer (must be after language setup) */\n this.tokenizer = tokenize as typeof lunr.tokenizer\n lunr.tokenizer.separator = new RegExp(config.separator)\n\n /* Set up custom segmenter, if loaded */\n lunr.segmenter = \"TinySegmenter\" in lunr\n ? new lunr.TinySegmenter()\n : undefined\n\n /* Compute functions to be removed from the pipeline */\n const fns = difference([\n \"trimmer\", \"stopWordFilter\", \"stemmer\"\n ], config.pipeline)\n\n /* Remove functions from the pipeline for registered languages */\n for (const lang of config.lang.map(language => (\n // @ts-expect-error - namespace indexing not supported\n language === \"en\" ? lunr : lunr[language]\n )))\n for (const fn of fns) {\n this.pipeline.remove(lang[fn])\n this.searchPipeline.remove(lang[fn])\n }\n\n /* Set up index reference */\n this.ref(\"location\")\n\n /* Set up index fields */\n this.field(\"title\", { boost: 1e3, extractor: field(\"title\") })\n this.field(\"text\", { boost: 1e0, extractor: field(\"text\") })\n this.field(\"tags\", { boost: 1e6, extractor: field(\"tags\") })\n\n /* Add documents to index */\n for (const doc of docs)\n this.add(doc, { boost: doc.boost })\n })\n }\n\n /**\n * Search for matching documents\n *\n * @param query - Search query\n *\n * @returns Search result\n */\n public search(query: string): SearchResult {\n\n // Experimental Chinese segmentation\n query = query.replace(/\\p{sc=Han}+/gu, value => {\n return [...segment(value, this.index.invertedIndex)]\n .join(\"* \")\n })\n\n // @todo: move segmenter (above) into transformSearchQuery\n query = transformSearchQuery(query)\n if (!query)\n return { items: [] }\n\n /* Parse query to extract clauses for analysis */\n const clauses = parseSearchQuery(query)\n .filter(clause => (\n clause.presence !== lunr.Query.presence.PROHIBITED\n ))\n\n /* Perform search and post-process results */\n const groups = this.index.search(query)\n\n /* Apply post-query boosts based on title and search query terms */\n .reduce((item, { ref, score, matchData }) => {\n let doc = this.map.get(ref)\n if (typeof doc !== \"undefined\") {\n\n /* Shallow copy document */\n doc = { ...doc }\n if (doc.tags)\n doc.tags = [...doc.tags]\n\n /* Compute and analyze search query terms */\n const terms = getSearchQueryTerms(\n clauses,\n Object.keys(matchData.metadata)\n )\n\n /* Highlight matches in fields */\n for (const field of this.index.fields) {\n if (typeof doc[field] === \"undefined\")\n continue\n\n /* Collect positions from matches */\n const positions: Position[] = []\n for (const match of Object.values(matchData.metadata))\n if (typeof match[field] !== \"undefined\")\n positions.push(...match[field].position)\n\n /* Skip highlighting, if no positions were collected */\n if (!positions.length)\n continue\n\n /* Load table and determine highlighting method */\n const table = this.table.get([doc.location, field].join(\":\"))!\n const fn = Array.isArray(doc[field])\n ? highlightAll\n : highlight\n\n // @ts-expect-error - stop moaning, TypeScript!\n doc[field] = fn(doc[field], table, positions, field !== \"text\")\n }\n\n /* Highlight title and text and apply post-query boosts */\n const boost = +!doc.parent +\n Object.values(terms)\n .filter(t => t).length /\n Object.keys(terms).length\n\n /* Append item */\n item.push({\n ...doc,\n score: score * (1 + boost ** 2),\n terms\n })\n }\n return item\n }, [])\n\n /* Sort search results again after applying boosts */\n .sort((a, b) => b.score - a.score)\n\n /* Group search results by article */\n .reduce((items, result) => {\n const doc = this.map.get(result.location)\n if (typeof doc !== \"undefined\") {\n const ref = doc.parent\n ? doc.parent.location\n : doc.location\n items.set(ref, [...items.get(ref) || [], result])\n }\n return items\n }, new Map())\n\n /* Ensure that every item set has an article */\n for (const [ref, items] of groups)\n if (!items.find(item => item.location === ref)) {\n const doc = this.map.get(ref)!\n items.push({ ...doc, score: 0, terms: {} })\n }\n\n /* Generate search suggestions, if desired */\n let suggest: string[] | undefined\n if (this.options.suggest) {\n const titles = this.index.query(builder => {\n for (const clause of clauses)\n builder.term(clause.term, {\n fields: [\"title\"],\n presence: lunr.Query.presence.REQUIRED,\n wildcard: lunr.Query.wildcard.TRAILING\n })\n })\n\n /* Retrieve suggestions for best match */\n suggest = titles.length\n ? Object.keys(titles[0].matchData.metadata)\n : []\n }\n\n /* Return search result */\n return {\n items: [...groups.values()],\n ...typeof suggest !== \"undefined\" && { suggest }\n }\n }\n}\n"], - "mappings": "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", + "sourcesContent": ["/**\n * lunr - http://lunrjs.com - A bit like Solr, but much smaller and not as bright - 2.3.9\n * Copyright (C) 2020 Oliver Nightingale\n * @license MIT\n */\n\n;(function(){\n\n/**\n * A convenience function for configuring and constructing\n * a new lunr Index.\n *\n * A lunr.Builder instance is created and the pipeline setup\n * with a trimmer, stop word filter and stemmer.\n *\n * This builder object is yielded to the configuration function\n * that is passed as a parameter, allowing the list of fields\n * and other builder parameters to be customised.\n *\n * All documents _must_ be added within the passed config function.\n *\n * @example\n * var idx = lunr(function () {\n * this.field('title')\n * this.field('body')\n * this.ref('id')\n *\n * documents.forEach(function (doc) {\n * this.add(doc)\n * }, this)\n * })\n *\n * @see {@link lunr.Builder}\n * @see {@link lunr.Pipeline}\n * @see {@link lunr.trimmer}\n * @see {@link lunr.stopWordFilter}\n * @see {@link lunr.stemmer}\n * @namespace {function} lunr\n */\nvar lunr = function (config) {\n var builder = new lunr.Builder\n\n builder.pipeline.add(\n lunr.trimmer,\n lunr.stopWordFilter,\n lunr.stemmer\n )\n\n builder.searchPipeline.add(\n lunr.stemmer\n )\n\n config.call(builder, builder)\n return builder.build()\n}\n\nlunr.version = \"2.3.9\"\n/*!\n * lunr.utils\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * A namespace containing utils for the rest of the lunr library\n * @namespace lunr.utils\n */\nlunr.utils = {}\n\n/**\n * Print a warning message to the console.\n *\n * @param {String} message The message to be printed.\n * @memberOf lunr.utils\n * @function\n */\nlunr.utils.warn = (function (global) {\n /* eslint-disable no-console */\n return function (message) {\n if (global.console && console.warn) {\n console.warn(message)\n }\n }\n /* eslint-enable no-console */\n})(this)\n\n/**\n * Convert an object to a string.\n *\n * In the case of `null` and `undefined` the function returns\n * the empty string, in all other cases the result of calling\n * `toString` on the passed object is returned.\n *\n * @param {Any} obj The object to convert to a string.\n * @return {String} string representation of the passed object.\n * @memberOf lunr.utils\n */\nlunr.utils.asString = function (obj) {\n if (obj === void 0 || obj === null) {\n return \"\"\n } else {\n return obj.toString()\n }\n}\n\n/**\n * Clones an object.\n *\n * Will create a copy of an existing object such that any mutations\n * on the copy cannot affect the original.\n *\n * Only shallow objects are supported, passing a nested object to this\n * function will cause a TypeError.\n *\n * Objects with primitives, and arrays of primitives are supported.\n *\n * @param {Object} obj The object to clone.\n * @return {Object} a clone of the passed object.\n * @throws {TypeError} when a nested object is passed.\n * @memberOf Utils\n */\nlunr.utils.clone = function (obj) {\n if (obj === null || obj === undefined) {\n return obj\n }\n\n var clone = Object.create(null),\n keys = Object.keys(obj)\n\n for (var i = 0; i < keys.length; i++) {\n var key = keys[i],\n val = obj[key]\n\n if (Array.isArray(val)) {\n clone[key] = val.slice()\n continue\n }\n\n if (typeof val === 'string' ||\n typeof val === 'number' ||\n typeof val === 'boolean') {\n clone[key] = val\n continue\n }\n\n throw new TypeError(\"clone is not deep and does not support nested objects\")\n }\n\n return clone\n}\nlunr.FieldRef = function (docRef, fieldName, stringValue) {\n this.docRef = docRef\n this.fieldName = fieldName\n this._stringValue = stringValue\n}\n\nlunr.FieldRef.joiner = \"/\"\n\nlunr.FieldRef.fromString = function (s) {\n var n = s.indexOf(lunr.FieldRef.joiner)\n\n if (n === -1) {\n throw \"malformed field ref string\"\n }\n\n var fieldRef = s.slice(0, n),\n docRef = s.slice(n + 1)\n\n return new lunr.FieldRef (docRef, fieldRef, s)\n}\n\nlunr.FieldRef.prototype.toString = function () {\n if (this._stringValue == undefined) {\n this._stringValue = this.fieldName + lunr.FieldRef.joiner + this.docRef\n }\n\n return this._stringValue\n}\n/*!\n * lunr.Set\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * A lunr set.\n *\n * @constructor\n */\nlunr.Set = function (elements) {\n this.elements = Object.create(null)\n\n if (elements) {\n this.length = elements.length\n\n for (var i = 0; i < this.length; i++) {\n this.elements[elements[i]] = true\n }\n } else {\n this.length = 0\n }\n}\n\n/**\n * A complete set that contains all elements.\n *\n * @static\n * @readonly\n * @type {lunr.Set}\n */\nlunr.Set.complete = {\n intersect: function (other) {\n return other\n },\n\n union: function () {\n return this\n },\n\n contains: function () {\n return true\n }\n}\n\n/**\n * An empty set that contains no elements.\n *\n * @static\n * @readonly\n * @type {lunr.Set}\n */\nlunr.Set.empty = {\n intersect: function () {\n return this\n },\n\n union: function (other) {\n return other\n },\n\n contains: function () {\n return false\n }\n}\n\n/**\n * Returns true if this set contains the specified object.\n *\n * @param {object} object - Object whose presence in this set is to be tested.\n * @returns {boolean} - True if this set contains the specified object.\n */\nlunr.Set.prototype.contains = function (object) {\n return !!this.elements[object]\n}\n\n/**\n * Returns a new set containing only the elements that are present in both\n * this set and the specified set.\n *\n * @param {lunr.Set} other - set to intersect with this set.\n * @returns {lunr.Set} a new set that is the intersection of this and the specified set.\n */\n\nlunr.Set.prototype.intersect = function (other) {\n var a, b, elements, intersection = []\n\n if (other === lunr.Set.complete) {\n return this\n }\n\n if (other === lunr.Set.empty) {\n return other\n }\n\n if (this.length < other.length) {\n a = this\n b = other\n } else {\n a = other\n b = this\n }\n\n elements = Object.keys(a.elements)\n\n for (var i = 0; i < elements.length; i++) {\n var element = elements[i]\n if (element in b.elements) {\n intersection.push(element)\n }\n }\n\n return new lunr.Set (intersection)\n}\n\n/**\n * Returns a new set combining the elements of this and the specified set.\n *\n * @param {lunr.Set} other - set to union with this set.\n * @return {lunr.Set} a new set that is the union of this and the specified set.\n */\n\nlunr.Set.prototype.union = function (other) {\n if (other === lunr.Set.complete) {\n return lunr.Set.complete\n }\n\n if (other === lunr.Set.empty) {\n return this\n }\n\n return new lunr.Set(Object.keys(this.elements).concat(Object.keys(other.elements)))\n}\n/**\n * A function to calculate the inverse document frequency for\n * a posting. This is shared between the builder and the index\n *\n * @private\n * @param {object} posting - The posting for a given term\n * @param {number} documentCount - The total number of documents.\n */\nlunr.idf = function (posting, documentCount) {\n var documentsWithTerm = 0\n\n for (var fieldName in posting) {\n if (fieldName == '_index') continue // Ignore the term index, its not a field\n documentsWithTerm += Object.keys(posting[fieldName]).length\n }\n\n var x = (documentCount - documentsWithTerm + 0.5) / (documentsWithTerm + 0.5)\n\n return Math.log(1 + Math.abs(x))\n}\n\n/**\n * A token wraps a string representation of a token\n * as it is passed through the text processing pipeline.\n *\n * @constructor\n * @param {string} [str=''] - The string token being wrapped.\n * @param {object} [metadata={}] - Metadata associated with this token.\n */\nlunr.Token = function (str, metadata) {\n this.str = str || \"\"\n this.metadata = metadata || {}\n}\n\n/**\n * Returns the token string that is being wrapped by this object.\n *\n * @returns {string}\n */\nlunr.Token.prototype.toString = function () {\n return this.str\n}\n\n/**\n * A token update function is used when updating or optionally\n * when cloning a token.\n *\n * @callback lunr.Token~updateFunction\n * @param {string} str - The string representation of the token.\n * @param {Object} metadata - All metadata associated with this token.\n */\n\n/**\n * Applies the given function to the wrapped string token.\n *\n * @example\n * token.update(function (str, metadata) {\n * return str.toUpperCase()\n * })\n *\n * @param {lunr.Token~updateFunction} fn - A function to apply to the token string.\n * @returns {lunr.Token}\n */\nlunr.Token.prototype.update = function (fn) {\n this.str = fn(this.str, this.metadata)\n return this\n}\n\n/**\n * Creates a clone of this token. Optionally a function can be\n * applied to the cloned token.\n *\n * @param {lunr.Token~updateFunction} [fn] - An optional function to apply to the cloned token.\n * @returns {lunr.Token}\n */\nlunr.Token.prototype.clone = function (fn) {\n fn = fn || function (s) { return s }\n return new lunr.Token (fn(this.str, this.metadata), this.metadata)\n}\n/*!\n * lunr.tokenizer\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * A function for splitting a string into tokens ready to be inserted into\n * the search index. Uses `lunr.tokenizer.separator` to split strings, change\n * the value of this property to change how strings are split into tokens.\n *\n * This tokenizer will convert its parameter to a string by calling `toString` and\n * then will split this string on the character in `lunr.tokenizer.separator`.\n * Arrays will have their elements converted to strings and wrapped in a lunr.Token.\n *\n * Optional metadata can be passed to the tokenizer, this metadata will be cloned and\n * added as metadata to every token that is created from the object to be tokenized.\n *\n * @static\n * @param {?(string|object|object[])} obj - The object to convert into tokens\n * @param {?object} metadata - Optional metadata to associate with every token\n * @returns {lunr.Token[]}\n * @see {@link lunr.Pipeline}\n */\nlunr.tokenizer = function (obj, metadata) {\n if (obj == null || obj == undefined) {\n return []\n }\n\n if (Array.isArray(obj)) {\n return obj.map(function (t) {\n return new lunr.Token(\n lunr.utils.asString(t).toLowerCase(),\n lunr.utils.clone(metadata)\n )\n })\n }\n\n var str = obj.toString().toLowerCase(),\n len = str.length,\n tokens = []\n\n for (var sliceEnd = 0, sliceStart = 0; sliceEnd <= len; sliceEnd++) {\n var char = str.charAt(sliceEnd),\n sliceLength = sliceEnd - sliceStart\n\n if ((char.match(lunr.tokenizer.separator) || sliceEnd == len)) {\n\n if (sliceLength > 0) {\n var tokenMetadata = lunr.utils.clone(metadata) || {}\n tokenMetadata[\"position\"] = [sliceStart, sliceLength]\n tokenMetadata[\"index\"] = tokens.length\n\n tokens.push(\n new lunr.Token (\n str.slice(sliceStart, sliceEnd),\n tokenMetadata\n )\n )\n }\n\n sliceStart = sliceEnd + 1\n }\n\n }\n\n return tokens\n}\n\n/**\n * The separator used to split a string into tokens. Override this property to change the behaviour of\n * `lunr.tokenizer` behaviour when tokenizing strings. By default this splits on whitespace and hyphens.\n *\n * @static\n * @see lunr.tokenizer\n */\nlunr.tokenizer.separator = /[\\s\\-]+/\n/*!\n * lunr.Pipeline\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * lunr.Pipelines maintain an ordered list of functions to be applied to all\n * tokens in documents entering the search index and queries being ran against\n * the index.\n *\n * An instance of lunr.Index created with the lunr shortcut will contain a\n * pipeline with a stop word filter and an English language stemmer. Extra\n * functions can be added before or after either of these functions or these\n * default functions can be removed.\n *\n * When run the pipeline will call each function in turn, passing a token, the\n * index of that token in the original list of all tokens and finally a list of\n * all the original tokens.\n *\n * The output of functions in the pipeline will be passed to the next function\n * in the pipeline. To exclude a token from entering the index the function\n * should return undefined, the rest of the pipeline will not be called with\n * this token.\n *\n * For serialisation of pipelines to work, all functions used in an instance of\n * a pipeline should be registered with lunr.Pipeline. Registered functions can\n * then be loaded. If trying to load a serialised pipeline that uses functions\n * that are not registered an error will be thrown.\n *\n * If not planning on serialising the pipeline then registering pipeline functions\n * is not necessary.\n *\n * @constructor\n */\nlunr.Pipeline = function () {\n this._stack = []\n}\n\nlunr.Pipeline.registeredFunctions = Object.create(null)\n\n/**\n * A pipeline function maps lunr.Token to lunr.Token. A lunr.Token contains the token\n * string as well as all known metadata. A pipeline function can mutate the token string\n * or mutate (or add) metadata for a given token.\n *\n * A pipeline function can indicate that the passed token should be discarded by returning\n * null, undefined or an empty string. This token will not be passed to any downstream pipeline\n * functions and will not be added to the index.\n *\n * Multiple tokens can be returned by returning an array of tokens. Each token will be passed\n * to any downstream pipeline functions and all will returned tokens will be added to the index.\n *\n * Any number of pipeline functions may be chained together using a lunr.Pipeline.\n *\n * @interface lunr.PipelineFunction\n * @param {lunr.Token} token - A token from the document being processed.\n * @param {number} i - The index of this token in the complete list of tokens for this document/field.\n * @param {lunr.Token[]} tokens - All tokens for this document/field.\n * @returns {(?lunr.Token|lunr.Token[])}\n */\n\n/**\n * Register a function with the pipeline.\n *\n * Functions that are used in the pipeline should be registered if the pipeline\n * needs to be serialised, or a serialised pipeline needs to be loaded.\n *\n * Registering a function does not add it to a pipeline, functions must still be\n * added to instances of the pipeline for them to be used when running a pipeline.\n *\n * @param {lunr.PipelineFunction} fn - The function to check for.\n * @param {String} label - The label to register this function with\n */\nlunr.Pipeline.registerFunction = function (fn, label) {\n if (label in this.registeredFunctions) {\n lunr.utils.warn('Overwriting existing registered function: ' + label)\n }\n\n fn.label = label\n lunr.Pipeline.registeredFunctions[fn.label] = fn\n}\n\n/**\n * Warns if the function is not registered as a Pipeline function.\n *\n * @param {lunr.PipelineFunction} fn - The function to check for.\n * @private\n */\nlunr.Pipeline.warnIfFunctionNotRegistered = function (fn) {\n var isRegistered = fn.label && (fn.label in this.registeredFunctions)\n\n if (!isRegistered) {\n lunr.utils.warn('Function is not registered with pipeline. This may cause problems when serialising the index.\\n', fn)\n }\n}\n\n/**\n * Loads a previously serialised pipeline.\n *\n * All functions to be loaded must already be registered with lunr.Pipeline.\n * If any function from the serialised data has not been registered then an\n * error will be thrown.\n *\n * @param {Object} serialised - The serialised pipeline to load.\n * @returns {lunr.Pipeline}\n */\nlunr.Pipeline.load = function (serialised) {\n var pipeline = new lunr.Pipeline\n\n serialised.forEach(function (fnName) {\n var fn = lunr.Pipeline.registeredFunctions[fnName]\n\n if (fn) {\n pipeline.add(fn)\n } else {\n throw new Error('Cannot load unregistered function: ' + fnName)\n }\n })\n\n return pipeline\n}\n\n/**\n * Adds new functions to the end of the pipeline.\n *\n * Logs a warning if the function has not been registered.\n *\n * @param {lunr.PipelineFunction[]} functions - Any number of functions to add to the pipeline.\n */\nlunr.Pipeline.prototype.add = function () {\n var fns = Array.prototype.slice.call(arguments)\n\n fns.forEach(function (fn) {\n lunr.Pipeline.warnIfFunctionNotRegistered(fn)\n this._stack.push(fn)\n }, this)\n}\n\n/**\n * Adds a single function after a function that already exists in the\n * pipeline.\n *\n * Logs a warning if the function has not been registered.\n *\n * @param {lunr.PipelineFunction} existingFn - A function that already exists in the pipeline.\n * @param {lunr.PipelineFunction} newFn - The new function to add to the pipeline.\n */\nlunr.Pipeline.prototype.after = function (existingFn, newFn) {\n lunr.Pipeline.warnIfFunctionNotRegistered(newFn)\n\n var pos = this._stack.indexOf(existingFn)\n if (pos == -1) {\n throw new Error('Cannot find existingFn')\n }\n\n pos = pos + 1\n this._stack.splice(pos, 0, newFn)\n}\n\n/**\n * Adds a single function before a function that already exists in the\n * pipeline.\n *\n * Logs a warning if the function has not been registered.\n *\n * @param {lunr.PipelineFunction} existingFn - A function that already exists in the pipeline.\n * @param {lunr.PipelineFunction} newFn - The new function to add to the pipeline.\n */\nlunr.Pipeline.prototype.before = function (existingFn, newFn) {\n lunr.Pipeline.warnIfFunctionNotRegistered(newFn)\n\n var pos = this._stack.indexOf(existingFn)\n if (pos == -1) {\n throw new Error('Cannot find existingFn')\n }\n\n this._stack.splice(pos, 0, newFn)\n}\n\n/**\n * Removes a function from the pipeline.\n *\n * @param {lunr.PipelineFunction} fn The function to remove from the pipeline.\n */\nlunr.Pipeline.prototype.remove = function (fn) {\n var pos = this._stack.indexOf(fn)\n if (pos == -1) {\n return\n }\n\n this._stack.splice(pos, 1)\n}\n\n/**\n * Runs the current list of functions that make up the pipeline against the\n * passed tokens.\n *\n * @param {Array} tokens The tokens to run through the pipeline.\n * @returns {Array}\n */\nlunr.Pipeline.prototype.run = function (tokens) {\n var stackLength = this._stack.length\n\n for (var i = 0; i < stackLength; i++) {\n var fn = this._stack[i]\n var memo = []\n\n for (var j = 0; j < tokens.length; j++) {\n var result = fn(tokens[j], j, tokens)\n\n if (result === null || result === void 0 || result === '') continue\n\n if (Array.isArray(result)) {\n for (var k = 0; k < result.length; k++) {\n memo.push(result[k])\n }\n } else {\n memo.push(result)\n }\n }\n\n tokens = memo\n }\n\n return tokens\n}\n\n/**\n * Convenience method for passing a string through a pipeline and getting\n * strings out. This method takes care of wrapping the passed string in a\n * token and mapping the resulting tokens back to strings.\n *\n * @param {string} str - The string to pass through the pipeline.\n * @param {?object} metadata - Optional metadata to associate with the token\n * passed to the pipeline.\n * @returns {string[]}\n */\nlunr.Pipeline.prototype.runString = function (str, metadata) {\n var token = new lunr.Token (str, metadata)\n\n return this.run([token]).map(function (t) {\n return t.toString()\n })\n}\n\n/**\n * Resets the pipeline by removing any existing processors.\n *\n */\nlunr.Pipeline.prototype.reset = function () {\n this._stack = []\n}\n\n/**\n * Returns a representation of the pipeline ready for serialisation.\n *\n * Logs a warning if the function has not been registered.\n *\n * @returns {Array}\n */\nlunr.Pipeline.prototype.toJSON = function () {\n return this._stack.map(function (fn) {\n lunr.Pipeline.warnIfFunctionNotRegistered(fn)\n\n return fn.label\n })\n}\n/*!\n * lunr.Vector\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * A vector is used to construct the vector space of documents and queries. These\n * vectors support operations to determine the similarity between two documents or\n * a document and a query.\n *\n * Normally no parameters are required for initializing a vector, but in the case of\n * loading a previously dumped vector the raw elements can be provided to the constructor.\n *\n * For performance reasons vectors are implemented with a flat array, where an elements\n * index is immediately followed by its value. E.g. [index, value, index, value]. This\n * allows the underlying array to be as sparse as possible and still offer decent\n * performance when being used for vector calculations.\n *\n * @constructor\n * @param {Number[]} [elements] - The flat list of element index and element value pairs.\n */\nlunr.Vector = function (elements) {\n this._magnitude = 0\n this.elements = elements || []\n}\n\n\n/**\n * Calculates the position within the vector to insert a given index.\n *\n * This is used internally by insert and upsert. If there are duplicate indexes then\n * the position is returned as if the value for that index were to be updated, but it\n * is the callers responsibility to check whether there is a duplicate at that index\n *\n * @param {Number} insertIdx - The index at which the element should be inserted.\n * @returns {Number}\n */\nlunr.Vector.prototype.positionForIndex = function (index) {\n // For an empty vector the tuple can be inserted at the beginning\n if (this.elements.length == 0) {\n return 0\n }\n\n var start = 0,\n end = this.elements.length / 2,\n sliceLength = end - start,\n pivotPoint = Math.floor(sliceLength / 2),\n pivotIndex = this.elements[pivotPoint * 2]\n\n while (sliceLength > 1) {\n if (pivotIndex < index) {\n start = pivotPoint\n }\n\n if (pivotIndex > index) {\n end = pivotPoint\n }\n\n if (pivotIndex == index) {\n break\n }\n\n sliceLength = end - start\n pivotPoint = start + Math.floor(sliceLength / 2)\n pivotIndex = this.elements[pivotPoint * 2]\n }\n\n if (pivotIndex == index) {\n return pivotPoint * 2\n }\n\n if (pivotIndex > index) {\n return pivotPoint * 2\n }\n\n if (pivotIndex < index) {\n return (pivotPoint + 1) * 2\n }\n}\n\n/**\n * Inserts an element at an index within the vector.\n *\n * Does not allow duplicates, will throw an error if there is already an entry\n * for this index.\n *\n * @param {Number} insertIdx - The index at which the element should be inserted.\n * @param {Number} val - The value to be inserted into the vector.\n */\nlunr.Vector.prototype.insert = function (insertIdx, val) {\n this.upsert(insertIdx, val, function () {\n throw \"duplicate index\"\n })\n}\n\n/**\n * Inserts or updates an existing index within the vector.\n *\n * @param {Number} insertIdx - The index at which the element should be inserted.\n * @param {Number} val - The value to be inserted into the vector.\n * @param {function} fn - A function that is called for updates, the existing value and the\n * requested value are passed as arguments\n */\nlunr.Vector.prototype.upsert = function (insertIdx, val, fn) {\n this._magnitude = 0\n var position = this.positionForIndex(insertIdx)\n\n if (this.elements[position] == insertIdx) {\n this.elements[position + 1] = fn(this.elements[position + 1], val)\n } else {\n this.elements.splice(position, 0, insertIdx, val)\n }\n}\n\n/**\n * Calculates the magnitude of this vector.\n *\n * @returns {Number}\n */\nlunr.Vector.prototype.magnitude = function () {\n if (this._magnitude) return this._magnitude\n\n var sumOfSquares = 0,\n elementsLength = this.elements.length\n\n for (var i = 1; i < elementsLength; i += 2) {\n var val = this.elements[i]\n sumOfSquares += val * val\n }\n\n return this._magnitude = Math.sqrt(sumOfSquares)\n}\n\n/**\n * Calculates the dot product of this vector and another vector.\n *\n * @param {lunr.Vector} otherVector - The vector to compute the dot product with.\n * @returns {Number}\n */\nlunr.Vector.prototype.dot = function (otherVector) {\n var dotProduct = 0,\n a = this.elements, b = otherVector.elements,\n aLen = a.length, bLen = b.length,\n aVal = 0, bVal = 0,\n i = 0, j = 0\n\n while (i < aLen && j < bLen) {\n aVal = a[i], bVal = b[j]\n if (aVal < bVal) {\n i += 2\n } else if (aVal > bVal) {\n j += 2\n } else if (aVal == bVal) {\n dotProduct += a[i + 1] * b[j + 1]\n i += 2\n j += 2\n }\n }\n\n return dotProduct\n}\n\n/**\n * Calculates the similarity between this vector and another vector.\n *\n * @param {lunr.Vector} otherVector - The other vector to calculate the\n * similarity with.\n * @returns {Number}\n */\nlunr.Vector.prototype.similarity = function (otherVector) {\n return this.dot(otherVector) / this.magnitude() || 0\n}\n\n/**\n * Converts the vector to an array of the elements within the vector.\n *\n * @returns {Number[]}\n */\nlunr.Vector.prototype.toArray = function () {\n var output = new Array (this.elements.length / 2)\n\n for (var i = 1, j = 0; i < this.elements.length; i += 2, j++) {\n output[j] = this.elements[i]\n }\n\n return output\n}\n\n/**\n * A JSON serializable representation of the vector.\n *\n * @returns {Number[]}\n */\nlunr.Vector.prototype.toJSON = function () {\n return this.elements\n}\n/* eslint-disable */\n/*!\n * lunr.stemmer\n * Copyright (C) 2020 Oliver Nightingale\n * Includes code from - http://tartarus.org/~martin/PorterStemmer/js.txt\n */\n\n/**\n * lunr.stemmer is an english language stemmer, this is a JavaScript\n * implementation of the PorterStemmer taken from http://tartarus.org/~martin\n *\n * @static\n * @implements {lunr.PipelineFunction}\n * @param {lunr.Token} token - The string to stem\n * @returns {lunr.Token}\n * @see {@link lunr.Pipeline}\n * @function\n */\nlunr.stemmer = (function(){\n var step2list = {\n \"ational\" : \"ate\",\n \"tional\" : \"tion\",\n \"enci\" : \"ence\",\n \"anci\" : \"ance\",\n \"izer\" : \"ize\",\n \"bli\" : \"ble\",\n \"alli\" : \"al\",\n \"entli\" : \"ent\",\n \"eli\" : \"e\",\n \"ousli\" : \"ous\",\n \"ization\" : \"ize\",\n \"ation\" : \"ate\",\n \"ator\" : \"ate\",\n \"alism\" : \"al\",\n \"iveness\" : \"ive\",\n \"fulness\" : \"ful\",\n \"ousness\" : \"ous\",\n \"aliti\" : \"al\",\n \"iviti\" : \"ive\",\n \"biliti\" : \"ble\",\n \"logi\" : \"log\"\n },\n\n step3list = {\n \"icate\" : \"ic\",\n \"ative\" : \"\",\n \"alize\" : \"al\",\n \"iciti\" : \"ic\",\n \"ical\" : \"ic\",\n \"ful\" : \"\",\n \"ness\" : \"\"\n },\n\n c = \"[^aeiou]\", // consonant\n v = \"[aeiouy]\", // vowel\n C = c + \"[^aeiouy]*\", // consonant sequence\n V = v + \"[aeiou]*\", // vowel sequence\n\n mgr0 = \"^(\" + C + \")?\" + V + C, // [C]VC... is m>0\n meq1 = \"^(\" + C + \")?\" + V + C + \"(\" + V + \")?$\", // [C]VC[V] is m=1\n mgr1 = \"^(\" + C + \")?\" + V + C + V + C, // [C]VCVC... is m>1\n s_v = \"^(\" + C + \")?\" + v; // vowel in stem\n\n var re_mgr0 = new RegExp(mgr0);\n var re_mgr1 = new RegExp(mgr1);\n var re_meq1 = new RegExp(meq1);\n var re_s_v = new RegExp(s_v);\n\n var re_1a = /^(.+?)(ss|i)es$/;\n var re2_1a = /^(.+?)([^s])s$/;\n var re_1b = /^(.+?)eed$/;\n var re2_1b = /^(.+?)(ed|ing)$/;\n var re_1b_2 = /.$/;\n var re2_1b_2 = /(at|bl|iz)$/;\n var re3_1b_2 = new RegExp(\"([^aeiouylsz])\\\\1$\");\n var re4_1b_2 = new RegExp(\"^\" + C + v + \"[^aeiouwxy]$\");\n\n var re_1c = /^(.+?[^aeiou])y$/;\n var re_2 = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/;\n\n var re_3 = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/;\n\n var re_4 = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/;\n var re2_4 = /^(.+?)(s|t)(ion)$/;\n\n var re_5 = /^(.+?)e$/;\n var re_5_1 = /ll$/;\n var re3_5 = new RegExp(\"^\" + C + v + \"[^aeiouwxy]$\");\n\n var porterStemmer = function porterStemmer(w) {\n var stem,\n suffix,\n firstch,\n re,\n re2,\n re3,\n re4;\n\n if (w.length < 3) { return w; }\n\n firstch = w.substr(0,1);\n if (firstch == \"y\") {\n w = firstch.toUpperCase() + w.substr(1);\n }\n\n // Step 1a\n re = re_1a\n re2 = re2_1a;\n\n if (re.test(w)) { w = w.replace(re,\"$1$2\"); }\n else if (re2.test(w)) { w = w.replace(re2,\"$1$2\"); }\n\n // Step 1b\n re = re_1b;\n re2 = re2_1b;\n if (re.test(w)) {\n var fp = re.exec(w);\n re = re_mgr0;\n if (re.test(fp[1])) {\n re = re_1b_2;\n w = w.replace(re,\"\");\n }\n } else if (re2.test(w)) {\n var fp = re2.exec(w);\n stem = fp[1];\n re2 = re_s_v;\n if (re2.test(stem)) {\n w = stem;\n re2 = re2_1b_2;\n re3 = re3_1b_2;\n re4 = re4_1b_2;\n if (re2.test(w)) { w = w + \"e\"; }\n else if (re3.test(w)) { re = re_1b_2; w = w.replace(re,\"\"); }\n else if (re4.test(w)) { w = w + \"e\"; }\n }\n }\n\n // Step 1c - replace suffix y or Y by i if preceded by a non-vowel which is not the first letter of the word (so cry -> cri, by -> by, say -> say)\n re = re_1c;\n if (re.test(w)) {\n var fp = re.exec(w);\n stem = fp[1];\n w = stem + \"i\";\n }\n\n // Step 2\n re = re_2;\n if (re.test(w)) {\n var fp = re.exec(w);\n stem = fp[1];\n suffix = fp[2];\n re = re_mgr0;\n if (re.test(stem)) {\n w = stem + step2list[suffix];\n }\n }\n\n // Step 3\n re = re_3;\n if (re.test(w)) {\n var fp = re.exec(w);\n stem = fp[1];\n suffix = fp[2];\n re = re_mgr0;\n if (re.test(stem)) {\n w = stem + step3list[suffix];\n }\n }\n\n // Step 4\n re = re_4;\n re2 = re2_4;\n if (re.test(w)) {\n var fp = re.exec(w);\n stem = fp[1];\n re = re_mgr1;\n if (re.test(stem)) {\n w = stem;\n }\n } else if (re2.test(w)) {\n var fp = re2.exec(w);\n stem = fp[1] + fp[2];\n re2 = re_mgr1;\n if (re2.test(stem)) {\n w = stem;\n }\n }\n\n // Step 5\n re = re_5;\n if (re.test(w)) {\n var fp = re.exec(w);\n stem = fp[1];\n re = re_mgr1;\n re2 = re_meq1;\n re3 = re3_5;\n if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) {\n w = stem;\n }\n }\n\n re = re_5_1;\n re2 = re_mgr1;\n if (re.test(w) && re2.test(w)) {\n re = re_1b_2;\n w = w.replace(re,\"\");\n }\n\n // and turn initial Y back to y\n\n if (firstch == \"y\") {\n w = firstch.toLowerCase() + w.substr(1);\n }\n\n return w;\n };\n\n return function (token) {\n return token.update(porterStemmer);\n }\n})();\n\nlunr.Pipeline.registerFunction(lunr.stemmer, 'stemmer')\n/*!\n * lunr.stopWordFilter\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * lunr.generateStopWordFilter builds a stopWordFilter function from the provided\n * list of stop words.\n *\n * The built in lunr.stopWordFilter is built using this generator and can be used\n * to generate custom stopWordFilters for applications or non English languages.\n *\n * @function\n * @param {Array} token The token to pass through the filter\n * @returns {lunr.PipelineFunction}\n * @see lunr.Pipeline\n * @see lunr.stopWordFilter\n */\nlunr.generateStopWordFilter = function (stopWords) {\n var words = stopWords.reduce(function (memo, stopWord) {\n memo[stopWord] = stopWord\n return memo\n }, {})\n\n return function (token) {\n if (token && words[token.toString()] !== token.toString()) return token\n }\n}\n\n/**\n * lunr.stopWordFilter is an English language stop word list filter, any words\n * contained in the list will not be passed through the filter.\n *\n * This is intended to be used in the Pipeline. If the token does not pass the\n * filter then undefined will be returned.\n *\n * @function\n * @implements {lunr.PipelineFunction}\n * @params {lunr.Token} token - A token to check for being a stop word.\n * @returns {lunr.Token}\n * @see {@link lunr.Pipeline}\n */\nlunr.stopWordFilter = lunr.generateStopWordFilter([\n 'a',\n 'able',\n 'about',\n 'across',\n 'after',\n 'all',\n 'almost',\n 'also',\n 'am',\n 'among',\n 'an',\n 'and',\n 'any',\n 'are',\n 'as',\n 'at',\n 'be',\n 'because',\n 'been',\n 'but',\n 'by',\n 'can',\n 'cannot',\n 'could',\n 'dear',\n 'did',\n 'do',\n 'does',\n 'either',\n 'else',\n 'ever',\n 'every',\n 'for',\n 'from',\n 'get',\n 'got',\n 'had',\n 'has',\n 'have',\n 'he',\n 'her',\n 'hers',\n 'him',\n 'his',\n 'how',\n 'however',\n 'i',\n 'if',\n 'in',\n 'into',\n 'is',\n 'it',\n 'its',\n 'just',\n 'least',\n 'let',\n 'like',\n 'likely',\n 'may',\n 'me',\n 'might',\n 'most',\n 'must',\n 'my',\n 'neither',\n 'no',\n 'nor',\n 'not',\n 'of',\n 'off',\n 'often',\n 'on',\n 'only',\n 'or',\n 'other',\n 'our',\n 'own',\n 'rather',\n 'said',\n 'say',\n 'says',\n 'she',\n 'should',\n 'since',\n 'so',\n 'some',\n 'than',\n 'that',\n 'the',\n 'their',\n 'them',\n 'then',\n 'there',\n 'these',\n 'they',\n 'this',\n 'tis',\n 'to',\n 'too',\n 'twas',\n 'us',\n 'wants',\n 'was',\n 'we',\n 'were',\n 'what',\n 'when',\n 'where',\n 'which',\n 'while',\n 'who',\n 'whom',\n 'why',\n 'will',\n 'with',\n 'would',\n 'yet',\n 'you',\n 'your'\n])\n\nlunr.Pipeline.registerFunction(lunr.stopWordFilter, 'stopWordFilter')\n/*!\n * lunr.trimmer\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * lunr.trimmer is a pipeline function for trimming non word\n * characters from the beginning and end of tokens before they\n * enter the index.\n *\n * This implementation may not work correctly for non latin\n * characters and should either be removed or adapted for use\n * with languages with non-latin characters.\n *\n * @static\n * @implements {lunr.PipelineFunction}\n * @param {lunr.Token} token The token to pass through the filter\n * @returns {lunr.Token}\n * @see lunr.Pipeline\n */\nlunr.trimmer = function (token) {\n return token.update(function (s) {\n return s.replace(/^\\W+/, '').replace(/\\W+$/, '')\n })\n}\n\nlunr.Pipeline.registerFunction(lunr.trimmer, 'trimmer')\n/*!\n * lunr.TokenSet\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * A token set is used to store the unique list of all tokens\n * within an index. Token sets are also used to represent an\n * incoming query to the index, this query token set and index\n * token set are then intersected to find which tokens to look\n * up in the inverted index.\n *\n * A token set can hold multiple tokens, as in the case of the\n * index token set, or it can hold a single token as in the\n * case of a simple query token set.\n *\n * Additionally token sets are used to perform wildcard matching.\n * Leading, contained and trailing wildcards are supported, and\n * from this edit distance matching can also be provided.\n *\n * Token sets are implemented as a minimal finite state automata,\n * where both common prefixes and suffixes are shared between tokens.\n * This helps to reduce the space used for storing the token set.\n *\n * @constructor\n */\nlunr.TokenSet = function () {\n this.final = false\n this.edges = {}\n this.id = lunr.TokenSet._nextId\n lunr.TokenSet._nextId += 1\n}\n\n/**\n * Keeps track of the next, auto increment, identifier to assign\n * to a new tokenSet.\n *\n * TokenSets require a unique identifier to be correctly minimised.\n *\n * @private\n */\nlunr.TokenSet._nextId = 1\n\n/**\n * Creates a TokenSet instance from the given sorted array of words.\n *\n * @param {String[]} arr - A sorted array of strings to create the set from.\n * @returns {lunr.TokenSet}\n * @throws Will throw an error if the input array is not sorted.\n */\nlunr.TokenSet.fromArray = function (arr) {\n var builder = new lunr.TokenSet.Builder\n\n for (var i = 0, len = arr.length; i < len; i++) {\n builder.insert(arr[i])\n }\n\n builder.finish()\n return builder.root\n}\n\n/**\n * Creates a token set from a query clause.\n *\n * @private\n * @param {Object} clause - A single clause from lunr.Query.\n * @param {string} clause.term - The query clause term.\n * @param {number} [clause.editDistance] - The optional edit distance for the term.\n * @returns {lunr.TokenSet}\n */\nlunr.TokenSet.fromClause = function (clause) {\n if ('editDistance' in clause) {\n return lunr.TokenSet.fromFuzzyString(clause.term, clause.editDistance)\n } else {\n return lunr.TokenSet.fromString(clause.term)\n }\n}\n\n/**\n * Creates a token set representing a single string with a specified\n * edit distance.\n *\n * Insertions, deletions, substitutions and transpositions are each\n * treated as an edit distance of 1.\n *\n * Increasing the allowed edit distance will have a dramatic impact\n * on the performance of both creating and intersecting these TokenSets.\n * It is advised to keep the edit distance less than 3.\n *\n * @param {string} str - The string to create the token set from.\n * @param {number} editDistance - The allowed edit distance to match.\n * @returns {lunr.Vector}\n */\nlunr.TokenSet.fromFuzzyString = function (str, editDistance) {\n var root = new lunr.TokenSet\n\n var stack = [{\n node: root,\n editsRemaining: editDistance,\n str: str\n }]\n\n while (stack.length) {\n var frame = stack.pop()\n\n // no edit\n if (frame.str.length > 0) {\n var char = frame.str.charAt(0),\n noEditNode\n\n if (char in frame.node.edges) {\n noEditNode = frame.node.edges[char]\n } else {\n noEditNode = new lunr.TokenSet\n frame.node.edges[char] = noEditNode\n }\n\n if (frame.str.length == 1) {\n noEditNode.final = true\n }\n\n stack.push({\n node: noEditNode,\n editsRemaining: frame.editsRemaining,\n str: frame.str.slice(1)\n })\n }\n\n if (frame.editsRemaining == 0) {\n continue\n }\n\n // insertion\n if (\"*\" in frame.node.edges) {\n var insertionNode = frame.node.edges[\"*\"]\n } else {\n var insertionNode = new lunr.TokenSet\n frame.node.edges[\"*\"] = insertionNode\n }\n\n if (frame.str.length == 0) {\n insertionNode.final = true\n }\n\n stack.push({\n node: insertionNode,\n editsRemaining: frame.editsRemaining - 1,\n str: frame.str\n })\n\n // deletion\n // can only do a deletion if we have enough edits remaining\n // and if there are characters left to delete in the string\n if (frame.str.length > 1) {\n stack.push({\n node: frame.node,\n editsRemaining: frame.editsRemaining - 1,\n str: frame.str.slice(1)\n })\n }\n\n // deletion\n // just removing the last character from the str\n if (frame.str.length == 1) {\n frame.node.final = true\n }\n\n // substitution\n // can only do a substitution if we have enough edits remaining\n // and if there are characters left to substitute\n if (frame.str.length >= 1) {\n if (\"*\" in frame.node.edges) {\n var substitutionNode = frame.node.edges[\"*\"]\n } else {\n var substitutionNode = new lunr.TokenSet\n frame.node.edges[\"*\"] = substitutionNode\n }\n\n if (frame.str.length == 1) {\n substitutionNode.final = true\n }\n\n stack.push({\n node: substitutionNode,\n editsRemaining: frame.editsRemaining - 1,\n str: frame.str.slice(1)\n })\n }\n\n // transposition\n // can only do a transposition if there are edits remaining\n // and there are enough characters to transpose\n if (frame.str.length > 1) {\n var charA = frame.str.charAt(0),\n charB = frame.str.charAt(1),\n transposeNode\n\n if (charB in frame.node.edges) {\n transposeNode = frame.node.edges[charB]\n } else {\n transposeNode = new lunr.TokenSet\n frame.node.edges[charB] = transposeNode\n }\n\n if (frame.str.length == 1) {\n transposeNode.final = true\n }\n\n stack.push({\n node: transposeNode,\n editsRemaining: frame.editsRemaining - 1,\n str: charA + frame.str.slice(2)\n })\n }\n }\n\n return root\n}\n\n/**\n * Creates a TokenSet from a string.\n *\n * The string may contain one or more wildcard characters (*)\n * that will allow wildcard matching when intersecting with\n * another TokenSet.\n *\n * @param {string} str - The string to create a TokenSet from.\n * @returns {lunr.TokenSet}\n */\nlunr.TokenSet.fromString = function (str) {\n var node = new lunr.TokenSet,\n root = node\n\n /*\n * Iterates through all characters within the passed string\n * appending a node for each character.\n *\n * When a wildcard character is found then a self\n * referencing edge is introduced to continually match\n * any number of any characters.\n */\n for (var i = 0, len = str.length; i < len; i++) {\n var char = str[i],\n final = (i == len - 1)\n\n if (char == \"*\") {\n node.edges[char] = node\n node.final = final\n\n } else {\n var next = new lunr.TokenSet\n next.final = final\n\n node.edges[char] = next\n node = next\n }\n }\n\n return root\n}\n\n/**\n * Converts this TokenSet into an array of strings\n * contained within the TokenSet.\n *\n * This is not intended to be used on a TokenSet that\n * contains wildcards, in these cases the results are\n * undefined and are likely to cause an infinite loop.\n *\n * @returns {string[]}\n */\nlunr.TokenSet.prototype.toArray = function () {\n var words = []\n\n var stack = [{\n prefix: \"\",\n node: this\n }]\n\n while (stack.length) {\n var frame = stack.pop(),\n edges = Object.keys(frame.node.edges),\n len = edges.length\n\n if (frame.node.final) {\n /* In Safari, at this point the prefix is sometimes corrupted, see:\n * https://github.com/olivernn/lunr.js/issues/279 Calling any\n * String.prototype method forces Safari to \"cast\" this string to what\n * it's supposed to be, fixing the bug. */\n frame.prefix.charAt(0)\n words.push(frame.prefix)\n }\n\n for (var i = 0; i < len; i++) {\n var edge = edges[i]\n\n stack.push({\n prefix: frame.prefix.concat(edge),\n node: frame.node.edges[edge]\n })\n }\n }\n\n return words\n}\n\n/**\n * Generates a string representation of a TokenSet.\n *\n * This is intended to allow TokenSets to be used as keys\n * in objects, largely to aid the construction and minimisation\n * of a TokenSet. As such it is not designed to be a human\n * friendly representation of the TokenSet.\n *\n * @returns {string}\n */\nlunr.TokenSet.prototype.toString = function () {\n // NOTE: Using Object.keys here as this.edges is very likely\n // to enter 'hash-mode' with many keys being added\n //\n // avoiding a for-in loop here as it leads to the function\n // being de-optimised (at least in V8). From some simple\n // benchmarks the performance is comparable, but allowing\n // V8 to optimize may mean easy performance wins in the future.\n\n if (this._str) {\n return this._str\n }\n\n var str = this.final ? '1' : '0',\n labels = Object.keys(this.edges).sort(),\n len = labels.length\n\n for (var i = 0; i < len; i++) {\n var label = labels[i],\n node = this.edges[label]\n\n str = str + label + node.id\n }\n\n return str\n}\n\n/**\n * Returns a new TokenSet that is the intersection of\n * this TokenSet and the passed TokenSet.\n *\n * This intersection will take into account any wildcards\n * contained within the TokenSet.\n *\n * @param {lunr.TokenSet} b - An other TokenSet to intersect with.\n * @returns {lunr.TokenSet}\n */\nlunr.TokenSet.prototype.intersect = function (b) {\n var output = new lunr.TokenSet,\n frame = undefined\n\n var stack = [{\n qNode: b,\n output: output,\n node: this\n }]\n\n while (stack.length) {\n frame = stack.pop()\n\n // NOTE: As with the #toString method, we are using\n // Object.keys and a for loop instead of a for-in loop\n // as both of these objects enter 'hash' mode, causing\n // the function to be de-optimised in V8\n var qEdges = Object.keys(frame.qNode.edges),\n qLen = qEdges.length,\n nEdges = Object.keys(frame.node.edges),\n nLen = nEdges.length\n\n for (var q = 0; q < qLen; q++) {\n var qEdge = qEdges[q]\n\n for (var n = 0; n < nLen; n++) {\n var nEdge = nEdges[n]\n\n if (nEdge == qEdge || qEdge == '*') {\n var node = frame.node.edges[nEdge],\n qNode = frame.qNode.edges[qEdge],\n final = node.final && qNode.final,\n next = undefined\n\n if (nEdge in frame.output.edges) {\n // an edge already exists for this character\n // no need to create a new node, just set the finality\n // bit unless this node is already final\n next = frame.output.edges[nEdge]\n next.final = next.final || final\n\n } else {\n // no edge exists yet, must create one\n // set the finality bit and insert it\n // into the output\n next = new lunr.TokenSet\n next.final = final\n frame.output.edges[nEdge] = next\n }\n\n stack.push({\n qNode: qNode,\n output: next,\n node: node\n })\n }\n }\n }\n }\n\n return output\n}\nlunr.TokenSet.Builder = function () {\n this.previousWord = \"\"\n this.root = new lunr.TokenSet\n this.uncheckedNodes = []\n this.minimizedNodes = {}\n}\n\nlunr.TokenSet.Builder.prototype.insert = function (word) {\n var node,\n commonPrefix = 0\n\n if (word < this.previousWord) {\n throw new Error (\"Out of order word insertion\")\n }\n\n for (var i = 0; i < word.length && i < this.previousWord.length; i++) {\n if (word[i] != this.previousWord[i]) break\n commonPrefix++\n }\n\n this.minimize(commonPrefix)\n\n if (this.uncheckedNodes.length == 0) {\n node = this.root\n } else {\n node = this.uncheckedNodes[this.uncheckedNodes.length - 1].child\n }\n\n for (var i = commonPrefix; i < word.length; i++) {\n var nextNode = new lunr.TokenSet,\n char = word[i]\n\n node.edges[char] = nextNode\n\n this.uncheckedNodes.push({\n parent: node,\n char: char,\n child: nextNode\n })\n\n node = nextNode\n }\n\n node.final = true\n this.previousWord = word\n}\n\nlunr.TokenSet.Builder.prototype.finish = function () {\n this.minimize(0)\n}\n\nlunr.TokenSet.Builder.prototype.minimize = function (downTo) {\n for (var i = this.uncheckedNodes.length - 1; i >= downTo; i--) {\n var node = this.uncheckedNodes[i],\n childKey = node.child.toString()\n\n if (childKey in this.minimizedNodes) {\n node.parent.edges[node.char] = this.minimizedNodes[childKey]\n } else {\n // Cache the key for this node since\n // we know it can't change anymore\n node.child._str = childKey\n\n this.minimizedNodes[childKey] = node.child\n }\n\n this.uncheckedNodes.pop()\n }\n}\n/*!\n * lunr.Index\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * An index contains the built index of all documents and provides a query interface\n * to the index.\n *\n * Usually instances of lunr.Index will not be created using this constructor, instead\n * lunr.Builder should be used to construct new indexes, or lunr.Index.load should be\n * used to load previously built and serialized indexes.\n *\n * @constructor\n * @param {Object} attrs - The attributes of the built search index.\n * @param {Object} attrs.invertedIndex - An index of term/field to document reference.\n * @param {Object} attrs.fieldVectors - Field vectors\n * @param {lunr.TokenSet} attrs.tokenSet - An set of all corpus tokens.\n * @param {string[]} attrs.fields - The names of indexed document fields.\n * @param {lunr.Pipeline} attrs.pipeline - The pipeline to use for search terms.\n */\nlunr.Index = function (attrs) {\n this.invertedIndex = attrs.invertedIndex\n this.fieldVectors = attrs.fieldVectors\n this.tokenSet = attrs.tokenSet\n this.fields = attrs.fields\n this.pipeline = attrs.pipeline\n}\n\n/**\n * A result contains details of a document matching a search query.\n * @typedef {Object} lunr.Index~Result\n * @property {string} ref - The reference of the document this result represents.\n * @property {number} score - A number between 0 and 1 representing how similar this document is to the query.\n * @property {lunr.MatchData} matchData - Contains metadata about this match including which term(s) caused the match.\n */\n\n/**\n * Although lunr provides the ability to create queries using lunr.Query, it also provides a simple\n * query language which itself is parsed into an instance of lunr.Query.\n *\n * For programmatically building queries it is advised to directly use lunr.Query, the query language\n * is best used for human entered text rather than program generated text.\n *\n * At its simplest queries can just be a single term, e.g. `hello`, multiple terms are also supported\n * and will be combined with OR, e.g `hello world` will match documents that contain either 'hello'\n * or 'world', though those that contain both will rank higher in the results.\n *\n * Wildcards can be included in terms to match one or more unspecified characters, these wildcards can\n * be inserted anywhere within the term, and more than one wildcard can exist in a single term. Adding\n * wildcards will increase the number of documents that will be found but can also have a negative\n * impact on query performance, especially with wildcards at the beginning of a term.\n *\n * Terms can be restricted to specific fields, e.g. `title:hello`, only documents with the term\n * hello in the title field will match this query. Using a field not present in the index will lead\n * to an error being thrown.\n *\n * Modifiers can also be added to terms, lunr supports edit distance and boost modifiers on terms. A term\n * boost will make documents matching that term score higher, e.g. `foo^5`. Edit distance is also supported\n * to provide fuzzy matching, e.g. 'hello~2' will match documents with hello with an edit distance of 2.\n * Avoid large values for edit distance to improve query performance.\n *\n * Each term also supports a presence modifier. By default a term's presence in document is optional, however\n * this can be changed to either required or prohibited. For a term's presence to be required in a document the\n * term should be prefixed with a '+', e.g. `+foo bar` is a search for documents that must contain 'foo' and\n * optionally contain 'bar'. Conversely a leading '-' sets the terms presence to prohibited, i.e. it must not\n * appear in a document, e.g. `-foo bar` is a search for documents that do not contain 'foo' but may contain 'bar'.\n *\n * To escape special characters the backslash character '\\' can be used, this allows searches to include\n * characters that would normally be considered modifiers, e.g. `foo\\~2` will search for a term \"foo~2\" instead\n * of attempting to apply a boost of 2 to the search term \"foo\".\n *\n * @typedef {string} lunr.Index~QueryString\n * @example Simple single term query\n * hello\n * @example Multiple term query\n * hello world\n * @example term scoped to a field\n * title:hello\n * @example term with a boost of 10\n * hello^10\n * @example term with an edit distance of 2\n * hello~2\n * @example terms with presence modifiers\n * -foo +bar baz\n */\n\n/**\n * Performs a search against the index using lunr query syntax.\n *\n * Results will be returned sorted by their score, the most relevant results\n * will be returned first. For details on how the score is calculated, please see\n * the {@link https://lunrjs.com/guides/searching.html#scoring|guide}.\n *\n * For more programmatic querying use lunr.Index#query.\n *\n * @param {lunr.Index~QueryString} queryString - A string containing a lunr query.\n * @throws {lunr.QueryParseError} If the passed query string cannot be parsed.\n * @returns {lunr.Index~Result[]}\n */\nlunr.Index.prototype.search = function (queryString) {\n return this.query(function (query) {\n var parser = new lunr.QueryParser(queryString, query)\n parser.parse()\n })\n}\n\n/**\n * A query builder callback provides a query object to be used to express\n * the query to perform on the index.\n *\n * @callback lunr.Index~queryBuilder\n * @param {lunr.Query} query - The query object to build up.\n * @this lunr.Query\n */\n\n/**\n * Performs a query against the index using the yielded lunr.Query object.\n *\n * If performing programmatic queries against the index, this method is preferred\n * over lunr.Index#search so as to avoid the additional query parsing overhead.\n *\n * A query object is yielded to the supplied function which should be used to\n * express the query to be run against the index.\n *\n * Note that although this function takes a callback parameter it is _not_ an\n * asynchronous operation, the callback is just yielded a query object to be\n * customized.\n *\n * @param {lunr.Index~queryBuilder} fn - A function that is used to build the query.\n * @returns {lunr.Index~Result[]}\n */\nlunr.Index.prototype.query = function (fn) {\n // for each query clause\n // * process terms\n // * expand terms from token set\n // * find matching documents and metadata\n // * get document vectors\n // * score documents\n\n var query = new lunr.Query(this.fields),\n matchingFields = Object.create(null),\n queryVectors = Object.create(null),\n termFieldCache = Object.create(null),\n requiredMatches = Object.create(null),\n prohibitedMatches = Object.create(null)\n\n /*\n * To support field level boosts a query vector is created per\n * field. An empty vector is eagerly created to support negated\n * queries.\n */\n for (var i = 0; i < this.fields.length; i++) {\n queryVectors[this.fields[i]] = new lunr.Vector\n }\n\n fn.call(query, query)\n\n for (var i = 0; i < query.clauses.length; i++) {\n /*\n * Unless the pipeline has been disabled for this term, which is\n * the case for terms with wildcards, we need to pass the clause\n * term through the search pipeline. A pipeline returns an array\n * of processed terms. Pipeline functions may expand the passed\n * term, which means we may end up performing multiple index lookups\n * for a single query term.\n */\n var clause = query.clauses[i],\n terms = null,\n clauseMatches = lunr.Set.empty\n\n if (clause.usePipeline) {\n terms = this.pipeline.runString(clause.term, {\n fields: clause.fields\n })\n } else {\n terms = [clause.term]\n }\n\n for (var m = 0; m < terms.length; m++) {\n var term = terms[m]\n\n /*\n * Each term returned from the pipeline needs to use the same query\n * clause object, e.g. the same boost and or edit distance. The\n * simplest way to do this is to re-use the clause object but mutate\n * its term property.\n */\n clause.term = term\n\n /*\n * From the term in the clause we create a token set which will then\n * be used to intersect the indexes token set to get a list of terms\n * to lookup in the inverted index\n */\n var termTokenSet = lunr.TokenSet.fromClause(clause),\n expandedTerms = this.tokenSet.intersect(termTokenSet).toArray()\n\n /*\n * If a term marked as required does not exist in the tokenSet it is\n * impossible for the search to return any matches. We set all the field\n * scoped required matches set to empty and stop examining any further\n * clauses.\n */\n if (expandedTerms.length === 0 && clause.presence === lunr.Query.presence.REQUIRED) {\n for (var k = 0; k < clause.fields.length; k++) {\n var field = clause.fields[k]\n requiredMatches[field] = lunr.Set.empty\n }\n\n break\n }\n\n for (var j = 0; j < expandedTerms.length; j++) {\n /*\n * For each term get the posting and termIndex, this is required for\n * building the query vector.\n */\n var expandedTerm = expandedTerms[j],\n posting = this.invertedIndex[expandedTerm],\n termIndex = posting._index\n\n for (var k = 0; k < clause.fields.length; k++) {\n /*\n * For each field that this query term is scoped by (by default\n * all fields are in scope) we need to get all the document refs\n * that have this term in that field.\n *\n * The posting is the entry in the invertedIndex for the matching\n * term from above.\n */\n var field = clause.fields[k],\n fieldPosting = posting[field],\n matchingDocumentRefs = Object.keys(fieldPosting),\n termField = expandedTerm + \"/\" + field,\n matchingDocumentsSet = new lunr.Set(matchingDocumentRefs)\n\n /*\n * if the presence of this term is required ensure that the matching\n * documents are added to the set of required matches for this clause.\n *\n */\n if (clause.presence == lunr.Query.presence.REQUIRED) {\n clauseMatches = clauseMatches.union(matchingDocumentsSet)\n\n if (requiredMatches[field] === undefined) {\n requiredMatches[field] = lunr.Set.complete\n }\n }\n\n /*\n * if the presence of this term is prohibited ensure that the matching\n * documents are added to the set of prohibited matches for this field,\n * creating that set if it does not yet exist.\n */\n if (clause.presence == lunr.Query.presence.PROHIBITED) {\n if (prohibitedMatches[field] === undefined) {\n prohibitedMatches[field] = lunr.Set.empty\n }\n\n prohibitedMatches[field] = prohibitedMatches[field].union(matchingDocumentsSet)\n\n /*\n * Prohibited matches should not be part of the query vector used for\n * similarity scoring and no metadata should be extracted so we continue\n * to the next field\n */\n continue\n }\n\n /*\n * The query field vector is populated using the termIndex found for\n * the term and a unit value with the appropriate boost applied.\n * Using upsert because there could already be an entry in the vector\n * for the term we are working with. In that case we just add the scores\n * together.\n */\n queryVectors[field].upsert(termIndex, clause.boost, function (a, b) { return a + b })\n\n /**\n * If we've already seen this term, field combo then we've already collected\n * the matching documents and metadata, no need to go through all that again\n */\n if (termFieldCache[termField]) {\n continue\n }\n\n for (var l = 0; l < matchingDocumentRefs.length; l++) {\n /*\n * All metadata for this term/field/document triple\n * are then extracted and collected into an instance\n * of lunr.MatchData ready to be returned in the query\n * results\n */\n var matchingDocumentRef = matchingDocumentRefs[l],\n matchingFieldRef = new lunr.FieldRef (matchingDocumentRef, field),\n metadata = fieldPosting[matchingDocumentRef],\n fieldMatch\n\n if ((fieldMatch = matchingFields[matchingFieldRef]) === undefined) {\n matchingFields[matchingFieldRef] = new lunr.MatchData (expandedTerm, field, metadata)\n } else {\n fieldMatch.add(expandedTerm, field, metadata)\n }\n\n }\n\n termFieldCache[termField] = true\n }\n }\n }\n\n /**\n * If the presence was required we need to update the requiredMatches field sets.\n * We do this after all fields for the term have collected their matches because\n * the clause terms presence is required in _any_ of the fields not _all_ of the\n * fields.\n */\n if (clause.presence === lunr.Query.presence.REQUIRED) {\n for (var k = 0; k < clause.fields.length; k++) {\n var field = clause.fields[k]\n requiredMatches[field] = requiredMatches[field].intersect(clauseMatches)\n }\n }\n }\n\n /**\n * Need to combine the field scoped required and prohibited\n * matching documents into a global set of required and prohibited\n * matches\n */\n var allRequiredMatches = lunr.Set.complete,\n allProhibitedMatches = lunr.Set.empty\n\n for (var i = 0; i < this.fields.length; i++) {\n var field = this.fields[i]\n\n if (requiredMatches[field]) {\n allRequiredMatches = allRequiredMatches.intersect(requiredMatches[field])\n }\n\n if (prohibitedMatches[field]) {\n allProhibitedMatches = allProhibitedMatches.union(prohibitedMatches[field])\n }\n }\n\n var matchingFieldRefs = Object.keys(matchingFields),\n results = [],\n matches = Object.create(null)\n\n /*\n * If the query is negated (contains only prohibited terms)\n * we need to get _all_ fieldRefs currently existing in the\n * index. This is only done when we know that the query is\n * entirely prohibited terms to avoid any cost of getting all\n * fieldRefs unnecessarily.\n *\n * Additionally, blank MatchData must be created to correctly\n * populate the results.\n */\n if (query.isNegated()) {\n matchingFieldRefs = Object.keys(this.fieldVectors)\n\n for (var i = 0; i < matchingFieldRefs.length; i++) {\n var matchingFieldRef = matchingFieldRefs[i]\n var fieldRef = lunr.FieldRef.fromString(matchingFieldRef)\n matchingFields[matchingFieldRef] = new lunr.MatchData\n }\n }\n\n for (var i = 0; i < matchingFieldRefs.length; i++) {\n /*\n * Currently we have document fields that match the query, but we\n * need to return documents. The matchData and scores are combined\n * from multiple fields belonging to the same document.\n *\n * Scores are calculated by field, using the query vectors created\n * above, and combined into a final document score using addition.\n */\n var fieldRef = lunr.FieldRef.fromString(matchingFieldRefs[i]),\n docRef = fieldRef.docRef\n\n if (!allRequiredMatches.contains(docRef)) {\n continue\n }\n\n if (allProhibitedMatches.contains(docRef)) {\n continue\n }\n\n var fieldVector = this.fieldVectors[fieldRef],\n score = queryVectors[fieldRef.fieldName].similarity(fieldVector),\n docMatch\n\n if ((docMatch = matches[docRef]) !== undefined) {\n docMatch.score += score\n docMatch.matchData.combine(matchingFields[fieldRef])\n } else {\n var match = {\n ref: docRef,\n score: score,\n matchData: matchingFields[fieldRef]\n }\n matches[docRef] = match\n results.push(match)\n }\n }\n\n /*\n * Sort the results objects by score, highest first.\n */\n return results.sort(function (a, b) {\n return b.score - a.score\n })\n}\n\n/**\n * Prepares the index for JSON serialization.\n *\n * The schema for this JSON blob will be described in a\n * separate JSON schema file.\n *\n * @returns {Object}\n */\nlunr.Index.prototype.toJSON = function () {\n var invertedIndex = Object.keys(this.invertedIndex)\n .sort()\n .map(function (term) {\n return [term, this.invertedIndex[term]]\n }, this)\n\n var fieldVectors = Object.keys(this.fieldVectors)\n .map(function (ref) {\n return [ref, this.fieldVectors[ref].toJSON()]\n }, this)\n\n return {\n version: lunr.version,\n fields: this.fields,\n fieldVectors: fieldVectors,\n invertedIndex: invertedIndex,\n pipeline: this.pipeline.toJSON()\n }\n}\n\n/**\n * Loads a previously serialized lunr.Index\n *\n * @param {Object} serializedIndex - A previously serialized lunr.Index\n * @returns {lunr.Index}\n */\nlunr.Index.load = function (serializedIndex) {\n var attrs = {},\n fieldVectors = {},\n serializedVectors = serializedIndex.fieldVectors,\n invertedIndex = Object.create(null),\n serializedInvertedIndex = serializedIndex.invertedIndex,\n tokenSetBuilder = new lunr.TokenSet.Builder,\n pipeline = lunr.Pipeline.load(serializedIndex.pipeline)\n\n if (serializedIndex.version != lunr.version) {\n lunr.utils.warn(\"Version mismatch when loading serialised index. Current version of lunr '\" + lunr.version + \"' does not match serialized index '\" + serializedIndex.version + \"'\")\n }\n\n for (var i = 0; i < serializedVectors.length; i++) {\n var tuple = serializedVectors[i],\n ref = tuple[0],\n elements = tuple[1]\n\n fieldVectors[ref] = new lunr.Vector(elements)\n }\n\n for (var i = 0; i < serializedInvertedIndex.length; i++) {\n var tuple = serializedInvertedIndex[i],\n term = tuple[0],\n posting = tuple[1]\n\n tokenSetBuilder.insert(term)\n invertedIndex[term] = posting\n }\n\n tokenSetBuilder.finish()\n\n attrs.fields = serializedIndex.fields\n\n attrs.fieldVectors = fieldVectors\n attrs.invertedIndex = invertedIndex\n attrs.tokenSet = tokenSetBuilder.root\n attrs.pipeline = pipeline\n\n return new lunr.Index(attrs)\n}\n/*!\n * lunr.Builder\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * lunr.Builder performs indexing on a set of documents and\n * returns instances of lunr.Index ready for querying.\n *\n * All configuration of the index is done via the builder, the\n * fields to index, the document reference, the text processing\n * pipeline and document scoring parameters are all set on the\n * builder before indexing.\n *\n * @constructor\n * @property {string} _ref - Internal reference to the document reference field.\n * @property {string[]} _fields - Internal reference to the document fields to index.\n * @property {object} invertedIndex - The inverted index maps terms to document fields.\n * @property {object} documentTermFrequencies - Keeps track of document term frequencies.\n * @property {object} documentLengths - Keeps track of the length of documents added to the index.\n * @property {lunr.tokenizer} tokenizer - Function for splitting strings into tokens for indexing.\n * @property {lunr.Pipeline} pipeline - The pipeline performs text processing on tokens before indexing.\n * @property {lunr.Pipeline} searchPipeline - A pipeline for processing search terms before querying the index.\n * @property {number} documentCount - Keeps track of the total number of documents indexed.\n * @property {number} _b - A parameter to control field length normalization, setting this to 0 disabled normalization, 1 fully normalizes field lengths, the default value is 0.75.\n * @property {number} _k1 - A parameter to control how quickly an increase in term frequency results in term frequency saturation, the default value is 1.2.\n * @property {number} termIndex - A counter incremented for each unique term, used to identify a terms position in the vector space.\n * @property {array} metadataWhitelist - A list of metadata keys that have been whitelisted for entry in the index.\n */\nlunr.Builder = function () {\n this._ref = \"id\"\n this._fields = Object.create(null)\n this._documents = Object.create(null)\n this.invertedIndex = Object.create(null)\n this.fieldTermFrequencies = {}\n this.fieldLengths = {}\n this.tokenizer = lunr.tokenizer\n this.pipeline = new lunr.Pipeline\n this.searchPipeline = new lunr.Pipeline\n this.documentCount = 0\n this._b = 0.75\n this._k1 = 1.2\n this.termIndex = 0\n this.metadataWhitelist = []\n}\n\n/**\n * Sets the document field used as the document reference. Every document must have this field.\n * The type of this field in the document should be a string, if it is not a string it will be\n * coerced into a string by calling toString.\n *\n * The default ref is 'id'.\n *\n * The ref should _not_ be changed during indexing, it should be set before any documents are\n * added to the index. Changing it during indexing can lead to inconsistent results.\n *\n * @param {string} ref - The name of the reference field in the document.\n */\nlunr.Builder.prototype.ref = function (ref) {\n this._ref = ref\n}\n\n/**\n * A function that is used to extract a field from a document.\n *\n * Lunr expects a field to be at the top level of a document, if however the field\n * is deeply nested within a document an extractor function can be used to extract\n * the right field for indexing.\n *\n * @callback fieldExtractor\n * @param {object} doc - The document being added to the index.\n * @returns {?(string|object|object[])} obj - The object that will be indexed for this field.\n * @example Extracting a nested field\n * function (doc) { return doc.nested.field }\n */\n\n/**\n * Adds a field to the list of document fields that will be indexed. Every document being\n * indexed should have this field. Null values for this field in indexed documents will\n * not cause errors but will limit the chance of that document being retrieved by searches.\n *\n * All fields should be added before adding documents to the index. Adding fields after\n * a document has been indexed will have no effect on already indexed documents.\n *\n * Fields can be boosted at build time. This allows terms within that field to have more\n * importance when ranking search results. Use a field boost to specify that matches within\n * one field are more important than other fields.\n *\n * @param {string} fieldName - The name of a field to index in all documents.\n * @param {object} attributes - Optional attributes associated with this field.\n * @param {number} [attributes.boost=1] - Boost applied to all terms within this field.\n * @param {fieldExtractor} [attributes.extractor] - Function to extract a field from a document.\n * @throws {RangeError} fieldName cannot contain unsupported characters '/'\n */\nlunr.Builder.prototype.field = function (fieldName, attributes) {\n if (/\\//.test(fieldName)) {\n throw new RangeError (\"Field '\" + fieldName + \"' contains illegal character '/'\")\n }\n\n this._fields[fieldName] = attributes || {}\n}\n\n/**\n * A parameter to tune the amount of field length normalisation that is applied when\n * calculating relevance scores. A value of 0 will completely disable any normalisation\n * and a value of 1 will fully normalise field lengths. The default is 0.75. Values of b\n * will be clamped to the range 0 - 1.\n *\n * @param {number} number - The value to set for this tuning parameter.\n */\nlunr.Builder.prototype.b = function (number) {\n if (number < 0) {\n this._b = 0\n } else if (number > 1) {\n this._b = 1\n } else {\n this._b = number\n }\n}\n\n/**\n * A parameter that controls the speed at which a rise in term frequency results in term\n * frequency saturation. The default value is 1.2. Setting this to a higher value will give\n * slower saturation levels, a lower value will result in quicker saturation.\n *\n * @param {number} number - The value to set for this tuning parameter.\n */\nlunr.Builder.prototype.k1 = function (number) {\n this._k1 = number\n}\n\n/**\n * Adds a document to the index.\n *\n * Before adding fields to the index the index should have been fully setup, with the document\n * ref and all fields to index already having been specified.\n *\n * The document must have a field name as specified by the ref (by default this is 'id') and\n * it should have all fields defined for indexing, though null or undefined values will not\n * cause errors.\n *\n * Entire documents can be boosted at build time. Applying a boost to a document indicates that\n * this document should rank higher in search results than other documents.\n *\n * @param {object} doc - The document to add to the index.\n * @param {object} attributes - Optional attributes associated with this document.\n * @param {number} [attributes.boost=1] - Boost applied to all terms within this document.\n */\nlunr.Builder.prototype.add = function (doc, attributes) {\n var docRef = doc[this._ref],\n fields = Object.keys(this._fields)\n\n this._documents[docRef] = attributes || {}\n this.documentCount += 1\n\n for (var i = 0; i < fields.length; i++) {\n var fieldName = fields[i],\n extractor = this._fields[fieldName].extractor,\n field = extractor ? extractor(doc) : doc[fieldName],\n tokens = this.tokenizer(field, {\n fields: [fieldName]\n }),\n terms = this.pipeline.run(tokens),\n fieldRef = new lunr.FieldRef (docRef, fieldName),\n fieldTerms = Object.create(null)\n\n this.fieldTermFrequencies[fieldRef] = fieldTerms\n this.fieldLengths[fieldRef] = 0\n\n // store the length of this field for this document\n this.fieldLengths[fieldRef] += terms.length\n\n // calculate term frequencies for this field\n for (var j = 0; j < terms.length; j++) {\n var term = terms[j]\n\n if (fieldTerms[term] == undefined) {\n fieldTerms[term] = 0\n }\n\n fieldTerms[term] += 1\n\n // add to inverted index\n // create an initial posting if one doesn't exist\n if (this.invertedIndex[term] == undefined) {\n var posting = Object.create(null)\n posting[\"_index\"] = this.termIndex\n this.termIndex += 1\n\n for (var k = 0; k < fields.length; k++) {\n posting[fields[k]] = Object.create(null)\n }\n\n this.invertedIndex[term] = posting\n }\n\n // add an entry for this term/fieldName/docRef to the invertedIndex\n if (this.invertedIndex[term][fieldName][docRef] == undefined) {\n this.invertedIndex[term][fieldName][docRef] = Object.create(null)\n }\n\n // store all whitelisted metadata about this token in the\n // inverted index\n for (var l = 0; l < this.metadataWhitelist.length; l++) {\n var metadataKey = this.metadataWhitelist[l],\n metadata = term.metadata[metadataKey]\n\n if (this.invertedIndex[term][fieldName][docRef][metadataKey] == undefined) {\n this.invertedIndex[term][fieldName][docRef][metadataKey] = []\n }\n\n this.invertedIndex[term][fieldName][docRef][metadataKey].push(metadata)\n }\n }\n\n }\n}\n\n/**\n * Calculates the average document length for this index\n *\n * @private\n */\nlunr.Builder.prototype.calculateAverageFieldLengths = function () {\n\n var fieldRefs = Object.keys(this.fieldLengths),\n numberOfFields = fieldRefs.length,\n accumulator = {},\n documentsWithField = {}\n\n for (var i = 0; i < numberOfFields; i++) {\n var fieldRef = lunr.FieldRef.fromString(fieldRefs[i]),\n field = fieldRef.fieldName\n\n documentsWithField[field] || (documentsWithField[field] = 0)\n documentsWithField[field] += 1\n\n accumulator[field] || (accumulator[field] = 0)\n accumulator[field] += this.fieldLengths[fieldRef]\n }\n\n var fields = Object.keys(this._fields)\n\n for (var i = 0; i < fields.length; i++) {\n var fieldName = fields[i]\n accumulator[fieldName] = accumulator[fieldName] / documentsWithField[fieldName]\n }\n\n this.averageFieldLength = accumulator\n}\n\n/**\n * Builds a vector space model of every document using lunr.Vector\n *\n * @private\n */\nlunr.Builder.prototype.createFieldVectors = function () {\n var fieldVectors = {},\n fieldRefs = Object.keys(this.fieldTermFrequencies),\n fieldRefsLength = fieldRefs.length,\n termIdfCache = Object.create(null)\n\n for (var i = 0; i < fieldRefsLength; i++) {\n var fieldRef = lunr.FieldRef.fromString(fieldRefs[i]),\n fieldName = fieldRef.fieldName,\n fieldLength = this.fieldLengths[fieldRef],\n fieldVector = new lunr.Vector,\n termFrequencies = this.fieldTermFrequencies[fieldRef],\n terms = Object.keys(termFrequencies),\n termsLength = terms.length\n\n\n var fieldBoost = this._fields[fieldName].boost || 1,\n docBoost = this._documents[fieldRef.docRef].boost || 1\n\n for (var j = 0; j < termsLength; j++) {\n var term = terms[j],\n tf = termFrequencies[term],\n termIndex = this.invertedIndex[term]._index,\n idf, score, scoreWithPrecision\n\n if (termIdfCache[term] === undefined) {\n idf = lunr.idf(this.invertedIndex[term], this.documentCount)\n termIdfCache[term] = idf\n } else {\n idf = termIdfCache[term]\n }\n\n score = idf * ((this._k1 + 1) * tf) / (this._k1 * (1 - this._b + this._b * (fieldLength / this.averageFieldLength[fieldName])) + tf)\n score *= fieldBoost\n score *= docBoost\n scoreWithPrecision = Math.round(score * 1000) / 1000\n // Converts 1.23456789 to 1.234.\n // Reducing the precision so that the vectors take up less\n // space when serialised. Doing it now so that they behave\n // the same before and after serialisation. Also, this is\n // the fastest approach to reducing a number's precision in\n // JavaScript.\n\n fieldVector.insert(termIndex, scoreWithPrecision)\n }\n\n fieldVectors[fieldRef] = fieldVector\n }\n\n this.fieldVectors = fieldVectors\n}\n\n/**\n * Creates a token set of all tokens in the index using lunr.TokenSet\n *\n * @private\n */\nlunr.Builder.prototype.createTokenSet = function () {\n this.tokenSet = lunr.TokenSet.fromArray(\n Object.keys(this.invertedIndex).sort()\n )\n}\n\n/**\n * Builds the index, creating an instance of lunr.Index.\n *\n * This completes the indexing process and should only be called\n * once all documents have been added to the index.\n *\n * @returns {lunr.Index}\n */\nlunr.Builder.prototype.build = function () {\n this.calculateAverageFieldLengths()\n this.createFieldVectors()\n this.createTokenSet()\n\n return new lunr.Index({\n invertedIndex: this.invertedIndex,\n fieldVectors: this.fieldVectors,\n tokenSet: this.tokenSet,\n fields: Object.keys(this._fields),\n pipeline: this.searchPipeline\n })\n}\n\n/**\n * Applies a plugin to the index builder.\n *\n * A plugin is a function that is called with the index builder as its context.\n * Plugins can be used to customise or extend the behaviour of the index\n * in some way. A plugin is just a function, that encapsulated the custom\n * behaviour that should be applied when building the index.\n *\n * The plugin function will be called with the index builder as its argument, additional\n * arguments can also be passed when calling use. The function will be called\n * with the index builder as its context.\n *\n * @param {Function} plugin The plugin to apply.\n */\nlunr.Builder.prototype.use = function (fn) {\n var args = Array.prototype.slice.call(arguments, 1)\n args.unshift(this)\n fn.apply(this, args)\n}\n/**\n * Contains and collects metadata about a matching document.\n * A single instance of lunr.MatchData is returned as part of every\n * lunr.Index~Result.\n *\n * @constructor\n * @param {string} term - The term this match data is associated with\n * @param {string} field - The field in which the term was found\n * @param {object} metadata - The metadata recorded about this term in this field\n * @property {object} metadata - A cloned collection of metadata associated with this document.\n * @see {@link lunr.Index~Result}\n */\nlunr.MatchData = function (term, field, metadata) {\n var clonedMetadata = Object.create(null),\n metadataKeys = Object.keys(metadata || {})\n\n // Cloning the metadata to prevent the original\n // being mutated during match data combination.\n // Metadata is kept in an array within the inverted\n // index so cloning the data can be done with\n // Array#slice\n for (var i = 0; i < metadataKeys.length; i++) {\n var key = metadataKeys[i]\n clonedMetadata[key] = metadata[key].slice()\n }\n\n this.metadata = Object.create(null)\n\n if (term !== undefined) {\n this.metadata[term] = Object.create(null)\n this.metadata[term][field] = clonedMetadata\n }\n}\n\n/**\n * An instance of lunr.MatchData will be created for every term that matches a\n * document. However only one instance is required in a lunr.Index~Result. This\n * method combines metadata from another instance of lunr.MatchData with this\n * objects metadata.\n *\n * @param {lunr.MatchData} otherMatchData - Another instance of match data to merge with this one.\n * @see {@link lunr.Index~Result}\n */\nlunr.MatchData.prototype.combine = function (otherMatchData) {\n var terms = Object.keys(otherMatchData.metadata)\n\n for (var i = 0; i < terms.length; i++) {\n var term = terms[i],\n fields = Object.keys(otherMatchData.metadata[term])\n\n if (this.metadata[term] == undefined) {\n this.metadata[term] = Object.create(null)\n }\n\n for (var j = 0; j < fields.length; j++) {\n var field = fields[j],\n keys = Object.keys(otherMatchData.metadata[term][field])\n\n if (this.metadata[term][field] == undefined) {\n this.metadata[term][field] = Object.create(null)\n }\n\n for (var k = 0; k < keys.length; k++) {\n var key = keys[k]\n\n if (this.metadata[term][field][key] == undefined) {\n this.metadata[term][field][key] = otherMatchData.metadata[term][field][key]\n } else {\n this.metadata[term][field][key] = this.metadata[term][field][key].concat(otherMatchData.metadata[term][field][key])\n }\n\n }\n }\n }\n}\n\n/**\n * Add metadata for a term/field pair to this instance of match data.\n *\n * @param {string} term - The term this match data is associated with\n * @param {string} field - The field in which the term was found\n * @param {object} metadata - The metadata recorded about this term in this field\n */\nlunr.MatchData.prototype.add = function (term, field, metadata) {\n if (!(term in this.metadata)) {\n this.metadata[term] = Object.create(null)\n this.metadata[term][field] = metadata\n return\n }\n\n if (!(field in this.metadata[term])) {\n this.metadata[term][field] = metadata\n return\n }\n\n var metadataKeys = Object.keys(metadata)\n\n for (var i = 0; i < metadataKeys.length; i++) {\n var key = metadataKeys[i]\n\n if (key in this.metadata[term][field]) {\n this.metadata[term][field][key] = this.metadata[term][field][key].concat(metadata[key])\n } else {\n this.metadata[term][field][key] = metadata[key]\n }\n }\n}\n/**\n * A lunr.Query provides a programmatic way of defining queries to be performed\n * against a {@link lunr.Index}.\n *\n * Prefer constructing a lunr.Query using the {@link lunr.Index#query} method\n * so the query object is pre-initialized with the right index fields.\n *\n * @constructor\n * @property {lunr.Query~Clause[]} clauses - An array of query clauses.\n * @property {string[]} allFields - An array of all available fields in a lunr.Index.\n */\nlunr.Query = function (allFields) {\n this.clauses = []\n this.allFields = allFields\n}\n\n/**\n * Constants for indicating what kind of automatic wildcard insertion will be used when constructing a query clause.\n *\n * This allows wildcards to be added to the beginning and end of a term without having to manually do any string\n * concatenation.\n *\n * The wildcard constants can be bitwise combined to select both leading and trailing wildcards.\n *\n * @constant\n * @default\n * @property {number} wildcard.NONE - The term will have no wildcards inserted, this is the default behaviour\n * @property {number} wildcard.LEADING - Prepend the term with a wildcard, unless a leading wildcard already exists\n * @property {number} wildcard.TRAILING - Append a wildcard to the term, unless a trailing wildcard already exists\n * @see lunr.Query~Clause\n * @see lunr.Query#clause\n * @see lunr.Query#term\n * @example query term with trailing wildcard\n * query.term('foo', { wildcard: lunr.Query.wildcard.TRAILING })\n * @example query term with leading and trailing wildcard\n * query.term('foo', {\n * wildcard: lunr.Query.wildcard.LEADING | lunr.Query.wildcard.TRAILING\n * })\n */\n\nlunr.Query.wildcard = new String (\"*\")\nlunr.Query.wildcard.NONE = 0\nlunr.Query.wildcard.LEADING = 1\nlunr.Query.wildcard.TRAILING = 2\n\n/**\n * Constants for indicating what kind of presence a term must have in matching documents.\n *\n * @constant\n * @enum {number}\n * @see lunr.Query~Clause\n * @see lunr.Query#clause\n * @see lunr.Query#term\n * @example query term with required presence\n * query.term('foo', { presence: lunr.Query.presence.REQUIRED })\n */\nlunr.Query.presence = {\n /**\n * Term's presence in a document is optional, this is the default value.\n */\n OPTIONAL: 1,\n\n /**\n * Term's presence in a document is required, documents that do not contain\n * this term will not be returned.\n */\n REQUIRED: 2,\n\n /**\n * Term's presence in a document is prohibited, documents that do contain\n * this term will not be returned.\n */\n PROHIBITED: 3\n}\n\n/**\n * A single clause in a {@link lunr.Query} contains a term and details on how to\n * match that term against a {@link lunr.Index}.\n *\n * @typedef {Object} lunr.Query~Clause\n * @property {string[]} fields - The fields in an index this clause should be matched against.\n * @property {number} [boost=1] - Any boost that should be applied when matching this clause.\n * @property {number} [editDistance] - Whether the term should have fuzzy matching applied, and how fuzzy the match should be.\n * @property {boolean} [usePipeline] - Whether the term should be passed through the search pipeline.\n * @property {number} [wildcard=lunr.Query.wildcard.NONE] - Whether the term should have wildcards appended or prepended.\n * @property {number} [presence=lunr.Query.presence.OPTIONAL] - The terms presence in any matching documents.\n */\n\n/**\n * Adds a {@link lunr.Query~Clause} to this query.\n *\n * Unless the clause contains the fields to be matched all fields will be matched. In addition\n * a default boost of 1 is applied to the clause.\n *\n * @param {lunr.Query~Clause} clause - The clause to add to this query.\n * @see lunr.Query~Clause\n * @returns {lunr.Query}\n */\nlunr.Query.prototype.clause = function (clause) {\n if (!('fields' in clause)) {\n clause.fields = this.allFields\n }\n\n if (!('boost' in clause)) {\n clause.boost = 1\n }\n\n if (!('usePipeline' in clause)) {\n clause.usePipeline = true\n }\n\n if (!('wildcard' in clause)) {\n clause.wildcard = lunr.Query.wildcard.NONE\n }\n\n if ((clause.wildcard & lunr.Query.wildcard.LEADING) && (clause.term.charAt(0) != lunr.Query.wildcard)) {\n clause.term = \"*\" + clause.term\n }\n\n if ((clause.wildcard & lunr.Query.wildcard.TRAILING) && (clause.term.slice(-1) != lunr.Query.wildcard)) {\n clause.term = \"\" + clause.term + \"*\"\n }\n\n if (!('presence' in clause)) {\n clause.presence = lunr.Query.presence.OPTIONAL\n }\n\n this.clauses.push(clause)\n\n return this\n}\n\n/**\n * A negated query is one in which every clause has a presence of\n * prohibited. These queries require some special processing to return\n * the expected results.\n *\n * @returns boolean\n */\nlunr.Query.prototype.isNegated = function () {\n for (var i = 0; i < this.clauses.length; i++) {\n if (this.clauses[i].presence != lunr.Query.presence.PROHIBITED) {\n return false\n }\n }\n\n return true\n}\n\n/**\n * Adds a term to the current query, under the covers this will create a {@link lunr.Query~Clause}\n * to the list of clauses that make up this query.\n *\n * The term is used as is, i.e. no tokenization will be performed by this method. Instead conversion\n * to a token or token-like string should be done before calling this method.\n *\n * The term will be converted to a string by calling `toString`. Multiple terms can be passed as an\n * array, each term in the array will share the same options.\n *\n * @param {object|object[]} term - The term(s) to add to the query.\n * @param {object} [options] - Any additional properties to add to the query clause.\n * @returns {lunr.Query}\n * @see lunr.Query#clause\n * @see lunr.Query~Clause\n * @example adding a single term to a query\n * query.term(\"foo\")\n * @example adding a single term to a query and specifying search fields, term boost and automatic trailing wildcard\n * query.term(\"foo\", {\n * fields: [\"title\"],\n * boost: 10,\n * wildcard: lunr.Query.wildcard.TRAILING\n * })\n * @example using lunr.tokenizer to convert a string to tokens before using them as terms\n * query.term(lunr.tokenizer(\"foo bar\"))\n */\nlunr.Query.prototype.term = function (term, options) {\n if (Array.isArray(term)) {\n term.forEach(function (t) { this.term(t, lunr.utils.clone(options)) }, this)\n return this\n }\n\n var clause = options || {}\n clause.term = term.toString()\n\n this.clause(clause)\n\n return this\n}\nlunr.QueryParseError = function (message, start, end) {\n this.name = \"QueryParseError\"\n this.message = message\n this.start = start\n this.end = end\n}\n\nlunr.QueryParseError.prototype = new Error\nlunr.QueryLexer = function (str) {\n this.lexemes = []\n this.str = str\n this.length = str.length\n this.pos = 0\n this.start = 0\n this.escapeCharPositions = []\n}\n\nlunr.QueryLexer.prototype.run = function () {\n var state = lunr.QueryLexer.lexText\n\n while (state) {\n state = state(this)\n }\n}\n\nlunr.QueryLexer.prototype.sliceString = function () {\n var subSlices = [],\n sliceStart = this.start,\n sliceEnd = this.pos\n\n for (var i = 0; i < this.escapeCharPositions.length; i++) {\n sliceEnd = this.escapeCharPositions[i]\n subSlices.push(this.str.slice(sliceStart, sliceEnd))\n sliceStart = sliceEnd + 1\n }\n\n subSlices.push(this.str.slice(sliceStart, this.pos))\n this.escapeCharPositions.length = 0\n\n return subSlices.join('')\n}\n\nlunr.QueryLexer.prototype.emit = function (type) {\n this.lexemes.push({\n type: type,\n str: this.sliceString(),\n start: this.start,\n end: this.pos\n })\n\n this.start = this.pos\n}\n\nlunr.QueryLexer.prototype.escapeCharacter = function () {\n this.escapeCharPositions.push(this.pos - 1)\n this.pos += 1\n}\n\nlunr.QueryLexer.prototype.next = function () {\n if (this.pos >= this.length) {\n return lunr.QueryLexer.EOS\n }\n\n var char = this.str.charAt(this.pos)\n this.pos += 1\n return char\n}\n\nlunr.QueryLexer.prototype.width = function () {\n return this.pos - this.start\n}\n\nlunr.QueryLexer.prototype.ignore = function () {\n if (this.start == this.pos) {\n this.pos += 1\n }\n\n this.start = this.pos\n}\n\nlunr.QueryLexer.prototype.backup = function () {\n this.pos -= 1\n}\n\nlunr.QueryLexer.prototype.acceptDigitRun = function () {\n var char, charCode\n\n do {\n char = this.next()\n charCode = char.charCodeAt(0)\n } while (charCode > 47 && charCode < 58)\n\n if (char != lunr.QueryLexer.EOS) {\n this.backup()\n }\n}\n\nlunr.QueryLexer.prototype.more = function () {\n return this.pos < this.length\n}\n\nlunr.QueryLexer.EOS = 'EOS'\nlunr.QueryLexer.FIELD = 'FIELD'\nlunr.QueryLexer.TERM = 'TERM'\nlunr.QueryLexer.EDIT_DISTANCE = 'EDIT_DISTANCE'\nlunr.QueryLexer.BOOST = 'BOOST'\nlunr.QueryLexer.PRESENCE = 'PRESENCE'\n\nlunr.QueryLexer.lexField = function (lexer) {\n lexer.backup()\n lexer.emit(lunr.QueryLexer.FIELD)\n lexer.ignore()\n return lunr.QueryLexer.lexText\n}\n\nlunr.QueryLexer.lexTerm = function (lexer) {\n if (lexer.width() > 1) {\n lexer.backup()\n lexer.emit(lunr.QueryLexer.TERM)\n }\n\n lexer.ignore()\n\n if (lexer.more()) {\n return lunr.QueryLexer.lexText\n }\n}\n\nlunr.QueryLexer.lexEditDistance = function (lexer) {\n lexer.ignore()\n lexer.acceptDigitRun()\n lexer.emit(lunr.QueryLexer.EDIT_DISTANCE)\n return lunr.QueryLexer.lexText\n}\n\nlunr.QueryLexer.lexBoost = function (lexer) {\n lexer.ignore()\n lexer.acceptDigitRun()\n lexer.emit(lunr.QueryLexer.BOOST)\n return lunr.QueryLexer.lexText\n}\n\nlunr.QueryLexer.lexEOS = function (lexer) {\n if (lexer.width() > 0) {\n lexer.emit(lunr.QueryLexer.TERM)\n }\n}\n\n// This matches the separator used when tokenising fields\n// within a document. These should match otherwise it is\n// not possible to search for some tokens within a document.\n//\n// It is possible for the user to change the separator on the\n// tokenizer so it _might_ clash with any other of the special\n// characters already used within the search string, e.g. :.\n//\n// This means that it is possible to change the separator in\n// such a way that makes some words unsearchable using a search\n// string.\nlunr.QueryLexer.termSeparator = lunr.tokenizer.separator\n\nlunr.QueryLexer.lexText = function (lexer) {\n while (true) {\n var char = lexer.next()\n\n if (char == lunr.QueryLexer.EOS) {\n return lunr.QueryLexer.lexEOS\n }\n\n // Escape character is '\\'\n if (char.charCodeAt(0) == 92) {\n lexer.escapeCharacter()\n continue\n }\n\n if (char == \":\") {\n return lunr.QueryLexer.lexField\n }\n\n if (char == \"~\") {\n lexer.backup()\n if (lexer.width() > 0) {\n lexer.emit(lunr.QueryLexer.TERM)\n }\n return lunr.QueryLexer.lexEditDistance\n }\n\n if (char == \"^\") {\n lexer.backup()\n if (lexer.width() > 0) {\n lexer.emit(lunr.QueryLexer.TERM)\n }\n return lunr.QueryLexer.lexBoost\n }\n\n // \"+\" indicates term presence is required\n // checking for length to ensure that only\n // leading \"+\" are considered\n if (char == \"+\" && lexer.width() === 1) {\n lexer.emit(lunr.QueryLexer.PRESENCE)\n return lunr.QueryLexer.lexText\n }\n\n // \"-\" indicates term presence is prohibited\n // checking for length to ensure that only\n // leading \"-\" are considered\n if (char == \"-\" && lexer.width() === 1) {\n lexer.emit(lunr.QueryLexer.PRESENCE)\n return lunr.QueryLexer.lexText\n }\n\n if (char.match(lunr.QueryLexer.termSeparator)) {\n return lunr.QueryLexer.lexTerm\n }\n }\n}\n\nlunr.QueryParser = function (str, query) {\n this.lexer = new lunr.QueryLexer (str)\n this.query = query\n this.currentClause = {}\n this.lexemeIdx = 0\n}\n\nlunr.QueryParser.prototype.parse = function () {\n this.lexer.run()\n this.lexemes = this.lexer.lexemes\n\n var state = lunr.QueryParser.parseClause\n\n while (state) {\n state = state(this)\n }\n\n return this.query\n}\n\nlunr.QueryParser.prototype.peekLexeme = function () {\n return this.lexemes[this.lexemeIdx]\n}\n\nlunr.QueryParser.prototype.consumeLexeme = function () {\n var lexeme = this.peekLexeme()\n this.lexemeIdx += 1\n return lexeme\n}\n\nlunr.QueryParser.prototype.nextClause = function () {\n var completedClause = this.currentClause\n this.query.clause(completedClause)\n this.currentClause = {}\n}\n\nlunr.QueryParser.parseClause = function (parser) {\n var lexeme = parser.peekLexeme()\n\n if (lexeme == undefined) {\n return\n }\n\n switch (lexeme.type) {\n case lunr.QueryLexer.PRESENCE:\n return lunr.QueryParser.parsePresence\n case lunr.QueryLexer.FIELD:\n return lunr.QueryParser.parseField\n case lunr.QueryLexer.TERM:\n return lunr.QueryParser.parseTerm\n default:\n var errorMessage = \"expected either a field or a term, found \" + lexeme.type\n\n if (lexeme.str.length >= 1) {\n errorMessage += \" with value '\" + lexeme.str + \"'\"\n }\n\n throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end)\n }\n}\n\nlunr.QueryParser.parsePresence = function (parser) {\n var lexeme = parser.consumeLexeme()\n\n if (lexeme == undefined) {\n return\n }\n\n switch (lexeme.str) {\n case \"-\":\n parser.currentClause.presence = lunr.Query.presence.PROHIBITED\n break\n case \"+\":\n parser.currentClause.presence = lunr.Query.presence.REQUIRED\n break\n default:\n var errorMessage = \"unrecognised presence operator'\" + lexeme.str + \"'\"\n throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end)\n }\n\n var nextLexeme = parser.peekLexeme()\n\n if (nextLexeme == undefined) {\n var errorMessage = \"expecting term or field, found nothing\"\n throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end)\n }\n\n switch (nextLexeme.type) {\n case lunr.QueryLexer.FIELD:\n return lunr.QueryParser.parseField\n case lunr.QueryLexer.TERM:\n return lunr.QueryParser.parseTerm\n default:\n var errorMessage = \"expecting term or field, found '\" + nextLexeme.type + \"'\"\n throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end)\n }\n}\n\nlunr.QueryParser.parseField = function (parser) {\n var lexeme = parser.consumeLexeme()\n\n if (lexeme == undefined) {\n return\n }\n\n if (parser.query.allFields.indexOf(lexeme.str) == -1) {\n var possibleFields = parser.query.allFields.map(function (f) { return \"'\" + f + \"'\" }).join(', '),\n errorMessage = \"unrecognised field '\" + lexeme.str + \"', possible fields: \" + possibleFields\n\n throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end)\n }\n\n parser.currentClause.fields = [lexeme.str]\n\n var nextLexeme = parser.peekLexeme()\n\n if (nextLexeme == undefined) {\n var errorMessage = \"expecting term, found nothing\"\n throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end)\n }\n\n switch (nextLexeme.type) {\n case lunr.QueryLexer.TERM:\n return lunr.QueryParser.parseTerm\n default:\n var errorMessage = \"expecting term, found '\" + nextLexeme.type + \"'\"\n throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end)\n }\n}\n\nlunr.QueryParser.parseTerm = function (parser) {\n var lexeme = parser.consumeLexeme()\n\n if (lexeme == undefined) {\n return\n }\n\n parser.currentClause.term = lexeme.str.toLowerCase()\n\n if (lexeme.str.indexOf(\"*\") != -1) {\n parser.currentClause.usePipeline = false\n }\n\n var nextLexeme = parser.peekLexeme()\n\n if (nextLexeme == undefined) {\n parser.nextClause()\n return\n }\n\n switch (nextLexeme.type) {\n case lunr.QueryLexer.TERM:\n parser.nextClause()\n return lunr.QueryParser.parseTerm\n case lunr.QueryLexer.FIELD:\n parser.nextClause()\n return lunr.QueryParser.parseField\n case lunr.QueryLexer.EDIT_DISTANCE:\n return lunr.QueryParser.parseEditDistance\n case lunr.QueryLexer.BOOST:\n return lunr.QueryParser.parseBoost\n case lunr.QueryLexer.PRESENCE:\n parser.nextClause()\n return lunr.QueryParser.parsePresence\n default:\n var errorMessage = \"Unexpected lexeme type '\" + nextLexeme.type + \"'\"\n throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end)\n }\n}\n\nlunr.QueryParser.parseEditDistance = function (parser) {\n var lexeme = parser.consumeLexeme()\n\n if (lexeme == undefined) {\n return\n }\n\n var editDistance = parseInt(lexeme.str, 10)\n\n if (isNaN(editDistance)) {\n var errorMessage = \"edit distance must be numeric\"\n throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end)\n }\n\n parser.currentClause.editDistance = editDistance\n\n var nextLexeme = parser.peekLexeme()\n\n if (nextLexeme == undefined) {\n parser.nextClause()\n return\n }\n\n switch (nextLexeme.type) {\n case lunr.QueryLexer.TERM:\n parser.nextClause()\n return lunr.QueryParser.parseTerm\n case lunr.QueryLexer.FIELD:\n parser.nextClause()\n return lunr.QueryParser.parseField\n case lunr.QueryLexer.EDIT_DISTANCE:\n return lunr.QueryParser.parseEditDistance\n case lunr.QueryLexer.BOOST:\n return lunr.QueryParser.parseBoost\n case lunr.QueryLexer.PRESENCE:\n parser.nextClause()\n return lunr.QueryParser.parsePresence\n default:\n var errorMessage = \"Unexpected lexeme type '\" + nextLexeme.type + \"'\"\n throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end)\n }\n}\n\nlunr.QueryParser.parseBoost = function (parser) {\n var lexeme = parser.consumeLexeme()\n\n if (lexeme == undefined) {\n return\n }\n\n var boost = parseInt(lexeme.str, 10)\n\n if (isNaN(boost)) {\n var errorMessage = \"boost must be numeric\"\n throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end)\n }\n\n parser.currentClause.boost = boost\n\n var nextLexeme = parser.peekLexeme()\n\n if (nextLexeme == undefined) {\n parser.nextClause()\n return\n }\n\n switch (nextLexeme.type) {\n case lunr.QueryLexer.TERM:\n parser.nextClause()\n return lunr.QueryParser.parseTerm\n case lunr.QueryLexer.FIELD:\n parser.nextClause()\n return lunr.QueryParser.parseField\n case lunr.QueryLexer.EDIT_DISTANCE:\n return lunr.QueryParser.parseEditDistance\n case lunr.QueryLexer.BOOST:\n return lunr.QueryParser.parseBoost\n case lunr.QueryLexer.PRESENCE:\n parser.nextClause()\n return lunr.QueryParser.parsePresence\n default:\n var errorMessage = \"Unexpected lexeme type '\" + nextLexeme.type + \"'\"\n throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end)\n }\n}\n\n /**\n * export the module via AMD, CommonJS or as a browser global\n * Export code from https://github.com/umdjs/umd/blob/master/returnExports.js\n */\n ;(function (root, factory) {\n if (typeof define === 'function' && define.amd) {\n // AMD. Register as an anonymous module.\n define(factory)\n } else if (typeof exports === 'object') {\n /**\n * Node. Does not work with strict CommonJS, but\n * only CommonJS-like enviroments that support module.exports,\n * like Node.\n */\n module.exports = factory()\n } else {\n // Browser globals (root is window)\n root.lunr = factory()\n }\n }(this, function () {\n /**\n * Just return a value to define the module export.\n * This example returns an object, but the module\n * can return a function as the exported value.\n */\n return lunr\n }))\n})();\n", "/*\n * Copyright (c) 2016-2024 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A RTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport lunr from \"lunr\"\n\nimport { getElement } from \"~/browser/element/_\"\nimport \"~/polyfills\"\n\nimport { Search } from \"../../_\"\nimport { SearchConfig } from \"../../config\"\nimport {\n SearchMessage,\n SearchMessageType\n} from \"../message\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Add support for `iframe-worker` shim\n *\n * While `importScripts` is synchronous when executed inside of a web worker,\n * it's not possible to provide a synchronous shim implementation. The cool\n * thing is that awaiting a non-Promise will convert it into a Promise, so\n * extending the type definition to return a `Promise` shouldn't break anything.\n *\n * @see https://bit.ly/2PjDnXi - GitHub comment\n *\n * @param urls - Scripts to load\n *\n * @returns Promise resolving with no result\n */\ndeclare global {\n function importScripts(...urls: string[]): Promise | void\n}\n\n/* ----------------------------------------------------------------------------\n * Data\n * ------------------------------------------------------------------------- */\n\n/**\n * Search index\n */\nlet index: Search\n\n/* ----------------------------------------------------------------------------\n * Helper functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Fetch (= import) multi-language support through `lunr-languages`\n *\n * This function automatically imports the stemmers necessary to process the\n * languages which are defined as part of the search configuration.\n *\n * If the worker runs inside of an `iframe` (when using `iframe-worker` as\n * a shim), the base URL for the stemmers to be loaded must be determined by\n * searching for the first `script` element with a `src` attribute, which will\n * contain the contents of this script.\n *\n * @param config - Search configuration\n *\n * @returns Promise resolving with no result\n */\nasync function setupSearchLanguages(\n config: SearchConfig\n): Promise {\n let base = \"../lunr\"\n\n /* Detect `iframe-worker` and fix base URL */\n if (typeof parent !== \"undefined\" && \"IFrameWorker\" in parent) {\n const worker = getElement(\"script[src]\")\n const [path] = worker.src.split(\"/worker\")\n\n /* Prefix base with path */\n base = base.replace(\"..\", path)\n }\n\n /* Add scripts for languages */\n const scripts = []\n for (const lang of config.lang) {\n switch (lang) {\n\n /* Add segmenter for Japanese */\n case \"ja\":\n scripts.push(`${base}/tinyseg.js`)\n break\n\n /* Add segmenter for Hindi and Thai */\n case \"hi\":\n case \"th\":\n scripts.push(`${base}/wordcut.js`)\n break\n }\n\n /* Add language support */\n if (lang !== \"en\")\n scripts.push(`${base}/min/lunr.${lang}.min.js`)\n }\n\n /* Add multi-language support */\n if (config.lang.length > 1)\n scripts.push(`${base}/min/lunr.multi.min.js`)\n\n /* Load scripts synchronously */\n if (scripts.length)\n await importScripts(\n `${base}/min/lunr.stemmer.support.min.js`,\n ...scripts\n )\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Message handler\n *\n * @param message - Source message\n *\n * @returns Target message\n */\nexport async function handler(\n message: SearchMessage\n): Promise {\n switch (message.type) {\n\n /* Search setup message */\n case SearchMessageType.SETUP:\n await setupSearchLanguages(message.data.config)\n index = new Search(message.data)\n return {\n type: SearchMessageType.READY\n }\n\n /* Search query message */\n case SearchMessageType.QUERY:\n const query = message.data\n try {\n return {\n type: SearchMessageType.RESULT,\n data: index.search(query)\n }\n\n /* Return empty result in case of error */\n } catch (err) {\n console.warn(`Invalid query: ${query} \u2013 see https://bit.ly/2s3ChXG`)\n console.warn(err)\n return {\n type: SearchMessageType.RESULT,\n data: { items: [] }\n }\n }\n\n /* All other messages */\n default:\n throw new TypeError(\"Invalid message type\")\n }\n}\n\n/* ----------------------------------------------------------------------------\n * Worker\n * ------------------------------------------------------------------------- */\n\n/* Expose Lunr.js in global scope, or stemmers won't work */\nself.lunr = lunr\n\n/* Monkey-patch Lunr.js to mitigate https://t.ly/68TLq */\nlunr.utils.warn = console.warn\n\n/* Handle messages */\naddEventListener(\"message\", async ev => {\n postMessage(await handler(ev.data))\n})\n", "/*\n * Copyright (c) 2016-2024 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Retrieve all elements matching the query selector\n *\n * @template T - Element type\n *\n * @param selector - Query selector\n * @param node - Node of reference\n *\n * @returns Elements\n */\nexport function getElements(\n selector: T, node?: ParentNode\n): HTMLElementTagNameMap[T][]\n\nexport function getElements(\n selector: string, node?: ParentNode\n): T[]\n\nexport function getElements(\n selector: string, node: ParentNode = document\n): T[] {\n return Array.from(node.querySelectorAll(selector))\n}\n\n/**\n * Retrieve an element matching a query selector or throw a reference error\n *\n * Note that this function assumes that the element is present. If unsure if an\n * element is existent, use the `getOptionalElement` function instead.\n *\n * @template T - Element type\n *\n * @param selector - Query selector\n * @param node - Node of reference\n *\n * @returns Element\n */\nexport function getElement(\n selector: T, node?: ParentNode\n): HTMLElementTagNameMap[T]\n\nexport function getElement(\n selector: string, node?: ParentNode\n): T\n\nexport function getElement(\n selector: string, node: ParentNode = document\n): T {\n const el = getOptionalElement(selector, node)\n if (typeof el === \"undefined\")\n throw new ReferenceError(\n `Missing element: expected \"${selector}\" to be present`\n )\n\n /* Return element */\n return el\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Retrieve an optional element matching the query selector\n *\n * @template T - Element type\n *\n * @param selector - Query selector\n * @param node - Node of reference\n *\n * @returns Element or nothing\n */\nexport function getOptionalElement(\n selector: T, node?: ParentNode\n): HTMLElementTagNameMap[T] | undefined\n\nexport function getOptionalElement(\n selector: string, node?: ParentNode\n): T | undefined\n\nexport function getOptionalElement(\n selector: string, node: ParentNode = document\n): T | undefined {\n return node.querySelector(selector) || undefined\n}\n\n/**\n * Retrieve the currently active element\n *\n * @returns Element or nothing\n */\nexport function getActiveElement(): HTMLElement | undefined {\n return (\n document.activeElement?.shadowRoot?.activeElement as HTMLElement ??\n document.activeElement as HTMLElement ??\n undefined\n )\n}\n", "/*\n * Copyright (c) 2016-2024 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\n/* ----------------------------------------------------------------------------\n * Polyfills\n * ------------------------------------------------------------------------- */\n\n/* Polyfill `Object.entries` */\nif (!Object.entries)\n Object.entries = function (obj: object) {\n const data: [string, string][] = []\n for (const key of Object.keys(obj))\n // @ts-expect-error - ignore property access warning\n data.push([key, obj[key]])\n\n /* Return entries */\n return data\n }\n\n/* Polyfill `Object.values` */\nif (!Object.values)\n Object.values = function (obj: object) {\n const data: string[] = []\n for (const key of Object.keys(obj))\n // @ts-expect-error - ignore property access warning\n data.push(obj[key])\n\n /* Return values */\n return data\n }\n\n/* ------------------------------------------------------------------------- */\n\n/* Polyfills for `Element` */\nif (typeof Element !== \"undefined\") {\n\n /* Polyfill `Element.scrollTo` */\n if (!Element.prototype.scrollTo)\n Element.prototype.scrollTo = function (\n x?: ScrollToOptions | number, y?: number\n ): void {\n if (typeof x === \"object\") {\n this.scrollLeft = x.left!\n this.scrollTop = x.top!\n } else {\n this.scrollLeft = x!\n this.scrollTop = y!\n }\n }\n\n /* Polyfill `Element.replaceWith` */\n if (!Element.prototype.replaceWith)\n Element.prototype.replaceWith = function (\n ...nodes: Array\n ): void {\n const parent = this.parentNode\n if (parent) {\n if (nodes.length === 0)\n parent.removeChild(this)\n\n /* Replace children and create text nodes */\n for (let i = nodes.length - 1; i >= 0; i--) {\n let node = nodes[i]\n if (typeof node === \"string\")\n node = document.createTextNode(node)\n else if (node.parentNode)\n node.parentNode.removeChild(node)\n\n /* Replace child or insert before previous sibling */\n if (!i)\n parent.replaceChild(node, this)\n else\n parent.insertBefore(this.previousSibling!, node)\n }\n }\n }\n}\n", "/*\n * Copyright (c) 2016-2024 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Search configuration\n */\nexport interface SearchConfig {\n lang: string[] /* Search languages */\n separator: string /* Search separator */\n pipeline: SearchPipelineFn[] /* Search pipeline */\n}\n\n/**\n * Search document\n */\nexport interface SearchDocument {\n location: string /* Document location */\n title: string /* Document title */\n text: string /* Document text */\n tags?: string[] /* Document tags */\n boost?: number /* Document boost */\n parent?: SearchDocument /* Document parent */\n}\n\n/**\n * Search options\n */\nexport interface SearchOptions {\n suggest: boolean /* Search suggestions */\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Search index\n */\nexport interface SearchIndex {\n config: SearchConfig /* Search configuration */\n docs: SearchDocument[] /* Search documents */\n options: SearchOptions /* Search options */\n}\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Search pipeline function\n */\ntype SearchPipelineFn =\n | \"trimmer\" /* Trimmer */\n | \"stopWordFilter\" /* Stop word filter */\n | \"stemmer\" /* Stemmer */\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Create a search document map\n *\n * This function creates a mapping of URLs (including anchors) to the actual\n * articles and sections. It relies on the invariant that the search index is\n * ordered with the main article appearing before all sections with anchors.\n * If this is not the case, the logic music be changed.\n *\n * @param docs - Search documents\n *\n * @returns Search document map\n */\nexport function setupSearchDocumentMap(\n docs: SearchDocument[]\n): Map {\n const map = new Map()\n for (const doc of docs) {\n const [path] = doc.location.split(\"#\")\n\n /* Add document article */\n const article = map.get(path)\n if (typeof article === \"undefined\") {\n map.set(path, doc)\n\n /* Add document section */\n } else {\n map.set(doc.location, doc)\n doc.parent = article\n }\n }\n\n /* Return search document map */\n return map\n}\n", "/*\n * Copyright (c) 2016-2024 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Visitor function\n *\n * @param start - Start offset\n * @param end - End offset\n */\ntype VisitorFn = (\n start: number, end: number\n) => void\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Split a string using the given separator\n *\n * @param input - Input value\n * @param separator - Separator\n * @param fn - Visitor function\n */\nexport function split(\n input: string, separator: RegExp, fn: VisitorFn\n): void {\n separator = new RegExp(separator, \"g\")\n\n /* Split string using separator */\n let match: RegExpExecArray | null\n let index = 0\n do {\n match = separator.exec(input)\n\n /* Emit non-empty range */\n const until = match?.index ?? input.length\n if (index < until)\n fn(index, until)\n\n /* Update last index */\n if (match) {\n const [term] = match\n index = match.index + term.length\n\n /* Support zero-length lookaheads */\n if (term.length === 0)\n separator.lastIndex = match.index + 1\n }\n } while (match)\n}\n", "/*\n * Copyright (c) 2016-2024 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Extraction type\n *\n * This type defines the possible values that are encoded into the first two\n * bits of a section that is part of the blocks of a tokenization table. There\n * are three types of interest: HTML opening and closing tags, as well as the\n * actual text content we need to extract for indexing.\n */\nexport const enum Extract {\n TAG_OPEN = 0, /* HTML opening tag */\n TEXT = 1, /* Text content */\n TAG_CLOSE = 2 /* HTML closing tag */\n}\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Visitor function\n *\n * @param block - Block index\n * @param type - Extraction type\n * @param start - Start offset\n * @param end - End offset\n */\ntype VisitorFn = (\n block: number, type: Extract, start: number, end: number\n) => void\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Split a string into markup and text sections\n *\n * This function scans a string and divides it up into sections of markup and\n * text. For each section, it invokes the given visitor function with the block\n * index, extraction type, as well as start and end offsets. Using a visitor\n * function (= streaming data) is ideal for minimizing pressure on the GC.\n *\n * @param input - Input value\n * @param fn - Visitor function\n */\nexport function extract(\n input: string, fn: VisitorFn\n): void {\n\n let block = 0 /* Current block */\n let start = 0 /* Current start offset */\n let end = 0 /* Current end offset */\n\n /* Split string into sections */\n for (let stack = 0; end < input.length; end++) {\n\n /* Opening tag after non-empty section */\n if (input.charAt(end) === \"<\" && end > start) {\n fn(block, Extract.TEXT, start, start = end)\n\n /* Closing tag */\n } else if (input.charAt(end) === \">\") {\n if (input.charAt(start + 1) === \"/\") {\n if (--stack === 0)\n fn(block++, Extract.TAG_CLOSE, start, end + 1)\n\n /* Tag is not self-closing */\n } else if (input.charAt(end - 1) !== \"/\") {\n if (stack++ === 0)\n fn(block, Extract.TAG_OPEN, start, end + 1)\n }\n\n /* New section */\n start = end + 1\n }\n }\n\n /* Add trailing section */\n if (end > start)\n fn(block, Extract.TEXT, start, end)\n}\n", "/*\n * Copyright (c) 2016-2024 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Position table\n */\nexport type PositionTable = number[][]\n\n/**\n * Position\n */\nexport type Position = number\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Highlight all occurrences in a string\n *\n * This function receives a field's value (e.g. like `title` or `text`), it's\n * position table that was generated during indexing, and the positions found\n * when executing the query. It then highlights all occurrences, and returns\n * their concatenation. In case of multiple blocks, two are returned.\n *\n * @param input - Input value\n * @param table - Table for indexing\n * @param positions - Occurrences\n * @param full - Full results\n *\n * @returns Highlighted string value\n */\nexport function highlight(\n input: string, table: PositionTable, positions: Position[], full = false\n): string {\n return highlightAll([input], table, positions, full).pop()!\n}\n\n/**\n * Highlight all occurrences in a set of strings\n *\n * @param inputs - Input values\n * @param table - Table for indexing\n * @param positions - Occurrences\n * @param full - Full results\n *\n * @returns Highlighted string values\n */\nexport function highlightAll(\n inputs: string[], table: PositionTable, positions: Position[], full = false\n): string[] {\n\n /* Map blocks to input values */\n const mapping = [0]\n for (let t = 1; t < table.length; t++) {\n const prev = table[t - 1]\n const next = table[t]\n\n /* Check if table points to new block */\n const p = prev[prev.length - 1] >>> 2 & 0x3FF\n const q = next[0] >>> 12\n\n /* Add block to mapping */\n mapping.push(+(p > q) + mapping[mapping.length - 1])\n }\n\n /* Highlight strings one after another */\n return inputs.map((input, i) => {\n let cursor = 0\n\n /* Map occurrences to blocks */\n const blocks = new Map()\n for (const p of positions.sort((a, b) => a - b)) {\n const index = p & 0xFFFFF\n const block = p >>> 20\n if (mapping[block] !== i)\n continue\n\n /* Ensure presence of block group */\n let group = blocks.get(block)\n if (typeof group === \"undefined\")\n blocks.set(block, group = [])\n\n /* Add index to group */\n group.push(index)\n }\n\n /* Just return string, if no occurrences */\n if (blocks.size === 0)\n return input\n\n /* Compute slices */\n const slices: string[] = []\n for (const [block, indexes] of blocks) {\n const t = table[block]\n\n /* Extract positions and length */\n const start = t[0] >>> 12\n const end = t[t.length - 1] >>> 12\n const length = t[t.length - 1] >>> 2 & 0x3FF\n\n /* Add prefix, if full results are desired */\n if (full && start > cursor)\n slices.push(input.slice(cursor, start))\n\n /* Extract and highlight slice */\n let slice = input.slice(start, end + length)\n for (const j of indexes.sort((a, b) => b - a)) {\n\n /* Retrieve offset and length of match */\n const p = (t[j] >>> 12) - start\n const q = (t[j] >>> 2 & 0x3FF) + p\n\n /* Wrap occurrence */\n slice = [\n slice.slice(0, p),\n \"\",\n slice.slice(p, q),\n \"\",\n slice.slice(q)\n ].join(\"\")\n }\n\n /* Update cursor */\n cursor = end + length\n\n /* Append slice and abort if we have two */\n if (slices.push(slice) === 2)\n break\n }\n\n /* Add suffix, if full results are desired */\n if (full && cursor < input.length)\n slices.push(input.slice(cursor))\n\n /* Return highlighted slices */\n return slices.join(\"\")\n })\n}\n", "/*\n * Copyright (c) 2016-2024 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { split } from \"../_\"\nimport {\n Extract,\n extract\n} from \"../extract\"\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Split a string or set of strings into tokens\n *\n * This tokenizer supersedes the default tokenizer that is provided by Lunr.js,\n * as it is aware of HTML tags and allows for multi-character splitting.\n *\n * It takes the given inputs, splits each of them into markup and text sections,\n * tokenizes and segments (if necessary) each of them, and then indexes them in\n * a table by using a compact bit representation. Bitwise techniques are used\n * to write and read from the table during indexing and querying.\n *\n * @see https://bit.ly/3W3Xw4J - Search: better, faster, smaller\n *\n * @param input - Input value(s)\n *\n * @returns Tokens\n */\nexport function tokenize(\n input?: string | string[]\n): lunr.Token[] {\n const tokens: lunr.Token[] = []\n if (typeof input === \"undefined\")\n return tokens\n\n /* Tokenize strings one after another */\n const inputs = Array.isArray(input) ? input : [input]\n for (let i = 0; i < inputs.length; i++) {\n const table = lunr.tokenizer.table\n const total = table.length\n\n /* Split string into sections and tokenize content blocks */\n extract(inputs[i], (block, type, start, end) => {\n table[block += total] ||= []\n switch (type) {\n\n /* Handle markup */\n case Extract.TAG_OPEN:\n case Extract.TAG_CLOSE:\n table[block].push(\n start << 12 |\n end - start << 2 |\n type\n )\n break\n\n /* Handle text content */\n case Extract.TEXT:\n const section = inputs[i].slice(start, end)\n split(section, lunr.tokenizer.separator, (index, until) => {\n\n /**\n * Apply segmenter after tokenization. Note that the segmenter will\n * also split words at word boundaries, which is not what we want,\n * so we need to check if we can somehow mitigate this behavior.\n */\n if (typeof lunr.segmenter !== \"undefined\") {\n const subsection = section.slice(index, until)\n if (/^[MHIK]$/.test(lunr.segmenter.ctype_(subsection))) {\n const segments = lunr.segmenter.segment(subsection)\n for (let s = 0, l = 0; s < segments.length; s++) {\n\n /* Add block to section */\n table[block] ||= []\n table[block].push(\n start + index + l << 12 |\n segments[s].length << 2 |\n type\n )\n\n /* Add token with position */\n tokens.push(new lunr.Token(\n segments[s].toLowerCase(), {\n position: block << 20 | table[block].length - 1\n }\n ))\n\n /* Keep track of length */\n l += segments[s].length\n }\n return\n }\n }\n\n /* Add block to section */\n table[block].push(\n start + index << 12 |\n until - index << 2 |\n type\n )\n\n /* Add token with position */\n tokens.push(new lunr.Token(\n section.slice(index, until).toLowerCase(), {\n position: block << 20 | table[block].length - 1\n }\n ))\n })\n }\n })\n }\n\n /* Return tokens */\n return tokens\n}\n", "/*\n * Copyright (c) 2016-2024 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Visitor function\n *\n * @param value - String value\n *\n * @returns String term(s)\n */\ntype VisitorFn = (\n value: string\n) => string | string[]\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Default transformation function\n *\n * 1. Trim excess whitespace from left and right.\n *\n * 2. Search for parts in quotation marks and prepend a `+` modifier to denote\n * that the resulting document must contain all parts, converting the query\n * to an `AND` query (as opposed to the default `OR` behavior). While users\n * may expect parts enclosed in quotation marks to map to span queries, i.e.\n * for which order is important, Lunr.js doesn't support them, so the best\n * we can do is to convert the parts to an `AND` query.\n *\n * 3. Replace control characters which are not located at the beginning of the\n * query or preceded by white space, or are not followed by a non-whitespace\n * character or are at the end of the query string. Furthermore, filter\n * unmatched quotation marks.\n *\n * 4. Split the query string at whitespace, then pass each part to the visitor\n * function for tokenization, and append a wildcard to every resulting term\n * that is not explicitly marked with a `+`, `-`, `~` or `^` modifier, since\n * it ensures consistent and stable ranking when multiple terms are entered.\n * Also, if a fuzzy or boost modifier are given, but no numeric value has\n * been entered, default to 1 to not induce a query error.\n *\n * @param query - Query value\n * @param fn - Visitor function\n *\n * @returns Transformed query value\n */\nexport function transform(\n query: string, fn: VisitorFn = term => term\n): string {\n return query\n\n /* => 1 */\n .trim()\n\n /* => 2 */\n .split(/\"([^\"]+)\"/g)\n .map((parts, index) => index & 1\n ? parts.replace(/^\\b|^(?![^\\x00-\\x7F]|$)|\\s+/g, \" +\")\n : parts\n )\n .join(\"\")\n\n /* => 3 */\n .replace(/\"|(?:^|\\s+)[*+\\-:^~]+(?=\\s+|$)/g, \"\")\n\n /* => 4 */\n .split(/\\s+/g)\n .reduce((prev, term) => {\n const next = fn(term)\n return [...prev, ...Array.isArray(next) ? next : [next]]\n }, [] as string[])\n .map(term => /([~^]$)/.test(term) ? `${term}1` : term)\n .map(term => /(^[+-]|[~^]\\d+$)/.test(term) ? term : `${term}*`)\n .join(\" \")\n}\n", "/*\n * Copyright (c) 2016-2024 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { split } from \"../../internal\"\nimport { transform } from \"../transform\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Search query clause\n */\nexport interface SearchQueryClause {\n presence: lunr.Query.presence /* Clause presence */\n term: string /* Clause term */\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Search query terms\n */\nexport type SearchQueryTerms = Record\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Transform search query\n *\n * This function lexes the given search query and applies the transformation\n * function to each term, preserving markup like `+` and `-` modifiers.\n *\n * @param query - Search query\n *\n * @returns Search query\n */\nexport function transformSearchQuery(\n query: string\n): string {\n\n /* Split query terms with tokenizer */\n return transform(query, part => {\n const terms: string[] = []\n\n /* Initialize lexer and analyze part */\n const lexer = new lunr.QueryLexer(part)\n lexer.run()\n\n /* Extract and tokenize term from lexeme */\n for (const { type, str: term, start, end } of lexer.lexemes)\n switch (type) {\n\n /* Hack: remove colon - see https://bit.ly/3wD3T3I */\n case \"FIELD\":\n if (![\"title\", \"text\", \"tags\"].includes(term))\n part = [\n part.slice(0, end),\n \" \",\n part.slice(end + 1)\n ].join(\"\")\n break\n\n /* Tokenize term */\n case \"TERM\":\n split(term, lunr.tokenizer.separator, (...range) => {\n terms.push([\n part.slice(0, start),\n term.slice(...range),\n part.slice(end)\n ].join(\"\"))\n })\n }\n\n /* Return terms */\n return terms\n })\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Parse a search query for analysis\n *\n * Lunr.js itself has a bug where it doesn't detect or remove wildcards for\n * query clauses, so we must do this here.\n *\n * @see https://bit.ly/3DpTGtz - GitHub issue\n *\n * @param value - Query value\n *\n * @returns Search query clauses\n */\nexport function parseSearchQuery(\n value: string\n): SearchQueryClause[] {\n const query = new lunr.Query([\"title\", \"text\", \"tags\"])\n const parser = new lunr.QueryParser(value, query)\n\n /* Parse Search query */\n parser.parse()\n for (const clause of query.clauses) {\n clause.usePipeline = true\n\n /* Handle leading wildcard */\n if (clause.term.startsWith(\"*\")) {\n clause.wildcard = lunr.Query.wildcard.LEADING\n clause.term = clause.term.slice(1)\n }\n\n /* Handle trailing wildcard */\n if (clause.term.endsWith(\"*\")) {\n clause.wildcard = lunr.Query.wildcard.TRAILING\n clause.term = clause.term.slice(0, -1)\n }\n }\n\n /* Return query clauses */\n return query.clauses\n}\n\n/**\n * Analyze the search query clauses in regard to the search terms found\n *\n * @param query - Search query clauses\n * @param terms - Search terms\n *\n * @returns Search query terms\n */\nexport function getSearchQueryTerms(\n query: SearchQueryClause[], terms: string[]\n): SearchQueryTerms {\n const clauses = new Set(query)\n\n /* Match query clauses against terms */\n const result: SearchQueryTerms = {}\n for (let t = 0; t < terms.length; t++)\n for (const clause of clauses)\n if (terms[t].startsWith(clause.term)) {\n result[clause.term] = true\n clauses.delete(clause)\n }\n\n /* Annotate unmatched non-stopword query clauses */\n for (const clause of clauses)\n if (lunr.stopWordFilter?.(clause.term))\n result[clause.term] = false\n\n /* Return query terms */\n return result\n}\n", "/*\n * Copyright (c) 2016-2024 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Segment a search query using the inverted index\n *\n * This function implements a clever approach to text segmentation for Asian\n * languages, as it used the information already available in the search index.\n * The idea is to greedily segment the search query based on the tokens that are\n * already part of the index, as described in the linked issue.\n *\n * @see https://bit.ly/3lwjrk7 - GitHub issue\n *\n * @param query - Query value\n * @param index - Inverted index\n *\n * @returns Segmented query value\n */\nexport function segment(\n query: string, index: object\n): Iterable {\n const segments = new Set()\n\n /* Segment search query */\n const wordcuts = new Uint16Array(query.length)\n for (let i = 0; i < query.length; i++)\n for (let j = i + 1; j < query.length; j++) {\n const value = query.slice(i, j)\n if (value in index)\n wordcuts[i] = j - i\n }\n\n /* Compute longest matches with minimum overlap */\n const stack = [0]\n for (let s = stack.length; s > 0;) {\n const p = stack[--s]\n for (let q = 1; q < wordcuts[p]; q++)\n if (wordcuts[p + q] > wordcuts[p] - q) {\n segments.add(query.slice(p, p + q))\n stack[s++] = p + q\n }\n\n /* Continue at end of query string */\n const q = p + wordcuts[p]\n if (wordcuts[q] && q < query.length - 1)\n stack[s++] = q\n\n /* Add current segment */\n segments.add(query.slice(p, q))\n }\n\n // @todo fix this case in the code block above, this is a hotfix\n if (segments.has(\"\"))\n return new Set([query])\n\n /* Return segmented query value */\n return segments\n}\n", "/*\n * Copyright (c) 2016-2024 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n SearchDocument,\n SearchIndex,\n SearchOptions,\n setupSearchDocumentMap\n} from \"../config\"\nimport {\n Position,\n PositionTable,\n highlight,\n highlightAll,\n tokenize\n} from \"../internal\"\nimport {\n SearchQueryTerms,\n getSearchQueryTerms,\n parseSearchQuery,\n segment,\n transformSearchQuery\n} from \"../query\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Search item\n */\nexport interface SearchItem\n extends SearchDocument\n{\n score: number /* Score (relevance) */\n terms: SearchQueryTerms /* Search query terms */\n}\n\n/**\n * Search result\n */\nexport interface SearchResult {\n items: SearchItem[][] /* Search items */\n suggest?: string[] /* Search suggestions */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Create field extractor factory\n *\n * @param table - Position table map\n *\n * @returns Extractor factory\n */\nfunction extractor(table: Map) {\n return (name: keyof SearchDocument) => {\n return (doc: SearchDocument) => {\n if (typeof doc[name] === \"undefined\")\n return undefined\n\n /* Compute identifier and initialize table */\n const id = [doc.location, name].join(\":\")\n table.set(id, lunr.tokenizer.table = [])\n\n /* Return field value */\n return doc[name]\n }\n }\n}\n\n/**\n * Compute the difference of two lists of strings\n *\n * @param a - 1st list of strings\n * @param b - 2nd list of strings\n *\n * @returns Difference\n */\nfunction difference(a: string[], b: string[]): string[] {\n const [x, y] = [new Set(a), new Set(b)]\n return [\n ...new Set([...x].filter(value => !y.has(value)))\n ]\n}\n\n/* ----------------------------------------------------------------------------\n * Class\n * ------------------------------------------------------------------------- */\n\n/**\n * Search index\n */\nexport class Search {\n\n /**\n * Search document map\n */\n protected map: Map\n\n /**\n * Search options\n */\n protected options: SearchOptions\n\n /**\n * The underlying Lunr.js search index\n */\n protected index: lunr.Index\n\n /**\n * Internal position table map\n */\n protected table: Map\n\n /**\n * Create the search integration\n *\n * @param data - Search index\n */\n public constructor({ config, docs, options }: SearchIndex) {\n const field = extractor(this.table = new Map())\n\n /* Set up document map and options */\n this.map = setupSearchDocumentMap(docs)\n this.options = options\n\n /* Set up document index */\n this.index = lunr(function () {\n this.metadataWhitelist = [\"position\"]\n this.b(0)\n\n /* Set up (multi-)language support */\n if (config.lang.length === 1 && config.lang[0] !== \"en\") {\n // @ts-expect-error - namespace indexing not supported\n this.use(lunr[config.lang[0]])\n } else if (config.lang.length > 1) {\n this.use(lunr.multiLanguage(...config.lang))\n }\n\n /* Set up custom tokenizer (must be after language setup) */\n this.tokenizer = tokenize as typeof lunr.tokenizer\n lunr.tokenizer.separator = new RegExp(config.separator)\n\n /* Set up custom segmenter, if loaded */\n lunr.segmenter = \"TinySegmenter\" in lunr\n ? new lunr.TinySegmenter()\n : undefined\n\n /* Compute functions to be removed from the pipeline */\n const fns = difference([\n \"trimmer\", \"stopWordFilter\", \"stemmer\"\n ], config.pipeline)\n\n /* Remove functions from the pipeline for registered languages */\n for (const lang of config.lang.map(language => (\n // @ts-expect-error - namespace indexing not supported\n language === \"en\" ? lunr : lunr[language]\n )))\n for (const fn of fns) {\n this.pipeline.remove(lang[fn])\n this.searchPipeline.remove(lang[fn])\n }\n\n /* Set up index reference */\n this.ref(\"location\")\n\n /* Set up index fields */\n this.field(\"title\", { boost: 1e3, extractor: field(\"title\") })\n this.field(\"text\", { boost: 1e0, extractor: field(\"text\") })\n this.field(\"tags\", { boost: 1e6, extractor: field(\"tags\") })\n\n /* Add documents to index */\n for (const doc of docs)\n this.add(doc, { boost: doc.boost })\n })\n }\n\n /**\n * Search for matching documents\n *\n * @param query - Search query\n *\n * @returns Search result\n */\n public search(query: string): SearchResult {\n\n // Experimental Chinese segmentation\n query = query.replace(/\\p{sc=Han}+/gu, value => {\n return [...segment(value, this.index.invertedIndex)]\n .join(\"* \")\n })\n\n // @todo: move segmenter (above) into transformSearchQuery\n query = transformSearchQuery(query)\n if (!query)\n return { items: [] }\n\n /* Parse query to extract clauses for analysis */\n const clauses = parseSearchQuery(query)\n .filter(clause => (\n clause.presence !== lunr.Query.presence.PROHIBITED\n ))\n\n /* Perform search and post-process results */\n const groups = this.index.search(query)\n\n /* Apply post-query boosts based on title and search query terms */\n .reduce((item, { ref, score, matchData }) => {\n let doc = this.map.get(ref)\n if (typeof doc !== \"undefined\") {\n\n /* Shallow copy document */\n doc = { ...doc }\n if (doc.tags)\n doc.tags = [...doc.tags]\n\n /* Compute and analyze search query terms */\n const terms = getSearchQueryTerms(\n clauses,\n Object.keys(matchData.metadata)\n )\n\n /* Highlight matches in fields */\n for (const field of this.index.fields) {\n if (typeof doc[field] === \"undefined\")\n continue\n\n /* Collect positions from matches */\n const positions: Position[] = []\n for (const match of Object.values(matchData.metadata))\n if (typeof match[field] !== \"undefined\")\n positions.push(...match[field].position)\n\n /* Skip highlighting, if no positions were collected */\n if (!positions.length)\n continue\n\n /* Load table and determine highlighting method */\n const table = this.table.get([doc.location, field].join(\":\"))!\n const fn = Array.isArray(doc[field])\n ? highlightAll\n : highlight\n\n // @ts-expect-error - stop moaning, TypeScript!\n doc[field] = fn(doc[field], table, positions, field !== \"text\")\n }\n\n /* Highlight title and text and apply post-query boosts */\n const boost = +!doc.parent +\n Object.values(terms)\n .filter(t => t).length /\n Object.keys(terms).length\n\n /* Append item */\n item.push({\n ...doc,\n score: score * (1 + boost ** 2),\n terms\n })\n }\n return item\n }, [])\n\n /* Sort search results again after applying boosts */\n .sort((a, b) => b.score - a.score)\n\n /* Group search results by article */\n .reduce((items, result) => {\n const doc = this.map.get(result.location)\n if (typeof doc !== \"undefined\") {\n const ref = doc.parent\n ? doc.parent.location\n : doc.location\n items.set(ref, [...items.get(ref) || [], result])\n }\n return items\n }, new Map())\n\n /* Ensure that every item set has an article */\n for (const [ref, items] of groups)\n if (!items.find(item => item.location === ref)) {\n const doc = this.map.get(ref)!\n items.push({ ...doc, score: 0, terms: {} })\n }\n\n /* Generate search suggestions, if desired */\n let suggest: string[] | undefined\n if (this.options.suggest) {\n const titles = this.index.query(builder => {\n for (const clause of clauses)\n builder.term(clause.term, {\n fields: [\"title\"],\n presence: lunr.Query.presence.REQUIRED,\n wildcard: lunr.Query.wildcard.TRAILING\n })\n })\n\n /* Retrieve suggestions for best match */\n suggest = titles.length\n ? Object.keys(titles[0].matchData.metadata)\n : []\n }\n\n /* Return search result */\n return {\n items: [...groups.values()],\n ...typeof suggest !== \"undefined\" && { suggest }\n }\n }\n}\n"], + "mappings": "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", "names": ["require_lunr", "__commonJSMin", "exports", "module", "lunr", "config", "builder", "global", "message", "obj", "clone", "keys", "key", "val", "docRef", "fieldName", "stringValue", "s", "n", "fieldRef", "elements", "i", "other", "object", "a", "b", "intersection", "element", "posting", "documentCount", "documentsWithTerm", "x", "str", "metadata", "fn", "t", "len", "tokens", "sliceEnd", "sliceStart", "char", "sliceLength", "tokenMetadata", "label", "isRegistered", "serialised", "pipeline", "fnName", "fns", "existingFn", "newFn", "pos", "stackLength", "memo", "j", "result", "k", "token", "index", "start", "end", "pivotPoint", "pivotIndex", "insertIdx", "position", "sumOfSquares", "elementsLength", "otherVector", "dotProduct", "aLen", "bLen", "aVal", "bVal", "output", "step2list", "step3list", "c", "v", "C", "V", "mgr0", "meq1", "mgr1", "s_v", "re_mgr0", "re_mgr1", "re_meq1", "re_s_v", "re_1a", "re2_1a", "re_1b", "re2_1b", "re_1b_2", "re2_1b_2", "re3_1b_2", "re4_1b_2", "re_1c", "re_2", "re_3", "re_4", "re2_4", "re_5", "re_5_1", "re3_5", "porterStemmer", "w", "stem", "suffix", "firstch", "re", "re2", "re3", "re4", "fp", "stopWords", "words", "stopWord", "arr", "clause", "editDistance", "root", "stack", "frame", "noEditNode", "insertionNode", "substitutionNode", "charA", "charB", "transposeNode", "node", "final", "next", "edges", "edge", "labels", "qEdges", "qLen", "nEdges", "nLen", "q", "qEdge", "nEdge", "qNode", "word", "commonPrefix", "nextNode", "downTo", "childKey", "attrs", "queryString", "query", "parser", "matchingFields", "queryVectors", "termFieldCache", "requiredMatches", "prohibitedMatches", "terms", "clauseMatches", "m", "term", "termTokenSet", "expandedTerms", "field", "expandedTerm", "termIndex", "fieldPosting", "matchingDocumentRefs", "termField", "matchingDocumentsSet", "l", "matchingDocumentRef", "matchingFieldRef", "fieldMatch", "allRequiredMatches", "allProhibitedMatches", "matchingFieldRefs", "results", "matches", "fieldVector", "score", "docMatch", "match", "invertedIndex", "fieldVectors", "ref", "serializedIndex", "serializedVectors", "serializedInvertedIndex", "tokenSetBuilder", "tuple", "attributes", "number", "doc", "fields", "extractor", "fieldTerms", "metadataKey", "fieldRefs", "numberOfFields", "accumulator", "documentsWithField", "fieldRefsLength", "termIdfCache", "fieldLength", "termFrequencies", "termsLength", "fieldBoost", "docBoost", "tf", "idf", "scoreWithPrecision", "args", "clonedMetadata", "metadataKeys", "otherMatchData", "allFields", "options", "state", "subSlices", "type", "charCode", "lexer", "lexeme", "completedClause", "errorMessage", "nextLexeme", "possibleFields", "f", "boost", "factory", "import_lunr", "getElement", "selector", "node", "el", "getOptionalElement", "obj", "data", "key", "x", "y", "nodes", "parent", "i", "node", "setupSearchDocumentMap", "docs", "map", "doc", "path", "article", "split", "input", "separator", "fn", "_a", "match", "index", "until", "term", "extract", "input", "fn", "block", "start", "end", "stack", "highlight", "input", "table", "positions", "full", "highlightAll", "inputs", "mapping", "t", "prev", "next", "p", "q", "i", "cursor", "blocks", "a", "b", "index", "block", "group", "slices", "indexes", "start", "end", "length", "slice", "j", "tokenize", "input", "tokens", "inputs", "i", "table", "total", "extract", "block", "type", "start", "end", "_a", "section", "split", "index", "until", "subsection", "segments", "s", "l", "transform", "query", "fn", "term", "parts", "index", "prev", "next", "transformSearchQuery", "query", "transform", "part", "terms", "lexer", "type", "term", "start", "end", "split", "range", "parseSearchQuery", "value", "clause", "getSearchQueryTerms", "_a", "clauses", "result", "t", "segment", "query", "index", "segments", "wordcuts", "i", "j", "stack", "p", "q", "extractor", "table", "name", "doc", "id", "difference", "a", "b", "x", "y", "value", "Search", "config", "docs", "options", "field", "setupSearchDocumentMap", "tokenize", "fns", "lang", "language", "fn", "query", "segment", "transformSearchQuery", "clauses", "parseSearchQuery", "clause", "groups", "item", "ref", "score", "matchData", "__spreadValues", "terms", "getSearchQueryTerms", "positions", "match", "highlightAll", "highlight", "boost", "t", "__spreadProps", "__pow", "items", "result", "suggest", "titles", "builder", "index", "setupSearchLanguages", "config", "__async", "base", "worker", "getElement", "path", "scripts", "lang", "handler", "message", "Search", "query", "err", "lunr", "ev"] } diff --git a/assets/torw2020/presentation/index.html b/assets/torw2020/presentation/index.html index 712ddb1..1ce1d01 100644 --- a/assets/torw2020/presentation/index.html +++ b/assets/torw2020/presentation/index.html @@ -18,7 +18,7 @@ - + @@ -1762,7 +1762,7 @@

Questions?{"base": "../../..", "features": ["tabs"], "search": "../../javascripts/workers/search.07f07601.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + diff --git a/community/CODE_OF_CONDUCT/index.html b/community/CODE_OF_CONDUCT/index.html index b1c538f..8213316 100644 --- a/community/CODE_OF_CONDUCT/index.html +++ b/community/CODE_OF_CONDUCT/index.html @@ -22,7 +22,7 @@ - + @@ -1081,7 +1081,7 @@

Attribution - + diff --git a/community/CONTRIBUTING/index.html b/community/CONTRIBUTING/index.html index 7d7c27c..68454ab 100644 --- a/community/CONTRIBUTING/index.html +++ b/community/CONTRIBUTING/index.html @@ -22,7 +22,7 @@ - + @@ -1476,7 +1476,7 @@

Thank you!{"base": "../..", "features": ["tabs"], "search": "../../assets/javascripts/workers/search.07f07601.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + diff --git a/community/features/index.html b/community/features/index.html index 4cdd71b..0861289 100644 --- a/community/features/index.html +++ b/community/features/index.html @@ -22,7 +22,7 @@ - + @@ -1124,7 +1124,7 @@

How the int - + diff --git a/community/index.html b/community/index.html index 8feeb32..ed8ea4b 100644 --- a/community/index.html +++ b/community/index.html @@ -22,7 +22,7 @@ - + @@ -1077,7 +1077,7 @@

Current members of the GitHu - + diff --git a/community/licensing/index.html b/community/licensing/index.html index 04e11f1..3e48dd9 100644 --- a/community/licensing/index.html +++ b/community/licensing/index.html @@ -22,7 +22,7 @@ - + @@ -1396,7 +1396,7 @@

Licensing of Docker and Sing - + diff --git a/community/members/index.html b/community/members/index.html index e707e92..7258f55 100644 --- a/community/members/index.html +++ b/community/members/index.html @@ -22,7 +22,7 @@ - + @@ -971,7 +971,7 @@

Contributors - + diff --git a/devs/devenv/index.html b/devs/devenv/index.html index fb7589a..f8c159b 100644 --- a/devs/devenv/index.html +++ b/devs/devenv/index.html @@ -22,7 +22,7 @@ - + @@ -1134,7 +1134,7 @@

Code-Server Development En - + diff --git a/devs/releases/index.html b/devs/releases/index.html index e3ee695..e7c71fe 100644 --- a/devs/releases/index.html +++ b/devs/releases/index.html @@ -20,7 +20,7 @@ - + @@ -1202,7 +1202,7 @@

Long-term support series - + diff --git a/devs/versions/index.html b/devs/versions/index.html index 1e7009a..b66910c 100644 --- a/devs/versions/index.html +++ b/devs/versions/index.html @@ -22,7 +22,7 @@ - + @@ -1102,7 +1102,7 @@

MRIQC{"base": "../..", "features": ["tabs"], "search": "../../assets/javascripts/workers/search.07f07601.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + diff --git a/index.html b/index.html index 66dd943..3cd4fb6 100644 --- a/index.html +++ b/index.html @@ -20,7 +20,7 @@ - + @@ -950,7 +950,7 @@

Building on the success story - + diff --git a/intro/nipreps/index.html b/intro/nipreps/index.html index 87f6552..cbb1bcb 100644 --- a/intro/nipreps/index.html +++ b/intro/nipreps/index.html @@ -22,7 +22,7 @@ - + @@ -1040,7 +1040,7 @@

Early-stage projects{"base": "../..", "features": ["tabs"], "search": "../../assets/javascripts/workers/search.07f07601.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + diff --git a/intro/transparency/index.html b/intro/transparency/index.html index 9c2eda8..b68ca5d 100644 --- a/intro/transparency/index.html +++ b/intro/transparency/index.html @@ -22,7 +22,7 @@ - + @@ -987,7 +987,7 @@

Citation boilerplates{"base": "../..", "features": ["tabs"], "search": "../../assets/javascripts/workers/search.07f07601.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + diff --git a/news/index.html b/news/index.html index f963101..b0e59a5 100644 --- a/news/index.html +++ b/news/index.html @@ -22,7 +22,7 @@ - + @@ -961,7 +961,7 @@

NiPreps Roundups {"base": "..", "features": ["tabs"], "search": "../assets/javascripts/workers/search.07f07601.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + diff --git a/search/search_index.json b/search/search_index.json index 0ee33da..18d2c60 100644 --- a/search/search_index.json +++ b/search/search_index.json @@ -1 +1 @@ -{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"NeuroImaging PREProcessing toolS (NiPreps)","text":"

NiPreps augment the scanner to produce data directly consumable by analyses.

We refer to data directly consumable by analyses as analysis-grade data by analogy with the concept of \"sushi-grade (or sashimi-grade) fish\" in that both are products that have been:

  • minimally preprocessed, but are
  • safe to consume directly.

"},{"location":"#building-on-the-success-story-of-fmriprep","title":"Building on the success story of fMRIPrep","text":"

NiPreps were conceived as a generalization of fMRIPrep across new modalities, populations, cohorts, and species. fMRIPrep is widely adopted, as our telemetry with Sentry (and now, in-house with migas) shows:

fMRIPrep is executed an average of 9,500 times every week, of which, around 7,000 times it finishes successfully (72.9% success rate). The average number of executions started includes debug and dry runs where researchers do not intend actually process data. Therefore, the effective (that is, discarding test runs) success ratio of fMRIPrep is likely higher."},{"location":"apps/docker/","title":"Executing with Docker","text":"

Summary

Here, we describe how to run NiPreps with Docker containers. To illustrate the process, we will show the execution of fMRIPrep, but these guidelines extend to any other end-user NiPrep.

"},{"location":"apps/docker/#before-you-start-install-docker","title":"Before you start: install Docker","text":"

Probably, the most popular framework to execute containers is Docker. If you are to run a NiPrep on your PC/laptop, this is the RECOMMENDED way of execution. Please make sure you follow the Docker installation instructions. You can check your Docker Runtime installation running their hello-world image:

$ docker run --rm hello-world\n

If you have a functional installation, then you should obtain the following output:

Hello from Docker!\nThis message shows that your installation appears to be working correctly.\n\nTo generate this message, Docker took the following steps:\n 1. The Docker client contacted the Docker daemon.\n 2. The Docker daemon pulled the \"hello-world\" image from the Docker Hub.\n    (amd64)\n 3. The Docker daemon created a new container from that image which runs the\n    executable that produces the output you are currently reading.\n 4. The Docker daemon streamed that output to the Docker client, which sent it\n    to your terminal.\n\nTo try something more ambitious, you can run an Ubuntu container with:\n $ docker run -it ubuntu bash\n\nShare images, automate workflows, and more with a free Docker ID:\n https://hub.docker.com/\n\nFor more examples and ideas, visit:\n https://docs.docker.com/get-started/\n

After checking your Docker Engine is capable of running Docker images, you are ready to pull your first NiPreps container image.

"},{"location":"apps/docker/#docker-images","title":"Docker images","text":"

For every new version of the particular NiPrep app that is released, a corresponding Docker image is generated. The Docker image becomes a container when the execution engine loads the image and adds an extra layer that makes it runnable. In order to run NiPreps Docker images, the Docker Runtime must be installed.

Taking fMRIPrep to illustrate the usage, first you might want to make sure of the exact version of the tool to be used:

$ docker pull nipreps/fmriprep:<latest-version>\n

You can run NiPreps interacting directly with the Docker Engine via the docker run interface.

"},{"location":"apps/docker/#running-a-niprep-with-a-lightweight-wrapper","title":"Running a NiPrep with a lightweight wrapper","text":"

Some NiPreps include a lightweight wrapper script for convenience. That is the case of fMRIPrep and its fmriprep-docker wrapper. Before starting, make sure you have the wrapper installed. When you run fmriprep-docker, it will generate a Docker command line for you, print it out for reporting purposes, and then execute it without further action needed, e.g.:

$ fmriprep-docker /path/to/data/dir /path/to/output/dir participant\nRUNNING: docker run --rm -it -v /path/to/data/dir:/data:ro \\\n    -v /path/to_output/dir:/out nipreps/fmriprep:20.2.2 \\\n    /data /out participant\n...\n

fmriprep-docker implements the unified command-line interface of BIDS Apps, and automatically translates directories into Docker mount points for you.

We have published a step-by-step tutorial illustrating how to run fmriprep-docker. This tutorial also provides valuable troubleshooting insights and advice on what to do after fMRIPrep has run.

"},{"location":"apps/docker/#running-a-niprep-directly-interacting-with-the-docker-engine","title":"Running a NiPrep directly interacting with the Docker Engine","text":"

If you need a finer control over the container execution, or you feel comfortable with the Docker Engine, avoiding the extra software layer of the wrapper might be a good decision.

Accessing filesystems in the host within the container: Containers are confined in a sandbox, so they can't access the host in any ways unless you explicitly prescribe acceptable accesses to the host. The Docker Engine provides mounting filesystems into the container with the -v argument and the following syntax: -v some/path/in/host:/absolute/path/within/container:ro, where the trailing :ro specifies that the mount is read-only. The mount permissions modifiers can be omitted, which means the mount will have read-write permissions. In general, you'll want to at least provide two mount-points: one set in read-only mode for the input data and one read/write to store the outputs. Potentially, you'll want to provide one or two more mount-points: one for the working directory, in case you need to debug some issue or reuse pre-cached results; and a TemplateFlow folder to preempt the download of your favorite templates in every run.

Running containers as a user: By default, Docker will run the container as root. Some share systems my limit this feature and only allow running containers as a user. When the container is run as root, files written out to filesystems mounted from the host will have the user id 1000 by default. In other words, you'll need to be able to run as root in the host to change permissions or manage these files. Alternatively, running as a user allows preempting these permissions issues. It is possible to run as a user with the -u argument. In general, we will want to use the same user ID as the running user in the host to ensure the ownership of files written during the container execution. Therefore, you will generally run the container with -u $( id -u ).

You may also invoke docker directly:

$ docker run -ti --rm \\\n    -v path/to/data:/data:ro \\\n    -v path/to/output:/out \\\n    nipreps/fmriprep:<latest-version> \\\n    /data /out/out \\\n    participant\n

For example: :

$ docker run -ti --rm \\\n    -v $HOME/ds005:/data:ro \\\n    -v $HOME/ds005/derivatives:/out \\\n    -v $HOME/tmp/ds005-workdir:/work \\\n    nipreps/fmriprep:<latest-version> \\\n    /data /out/fmriprep-<latest-version> \\\n    participant \\\n    -w /work\n

Once the Docker Engine arguments are written, the remainder of the command line follows the usage. In other words, the first section of the command line is all equivalent to the fmriprep executable in a bare-metal installation: :

$ docker run -ti --rm \\                      # These lines\n    -v $HOME/ds005:/data:ro \\                # are equivalent to\n    -v $HOME/ds005/derivatives:/out \\        # a call to the App's\n    -v $HOME/tmp/ds005-workdir:/work \\       # entry-point.\n    nipreps/fmriprep:<latest-version> \\  #\n    \\\n    /data /out/fmriprep-<latest-version> \\   # These lines correspond\n    participant \\                            # to the particular BIDS\n    -w /work                                 # App arguments.\n
"},{"location":"apps/framework/","title":"Introduction","text":""},{"location":"apps/framework/#what-is-bids","title":"What is BIDS?","text":"

The Brain Imaging Data Structure (BIDS) is a standard for organizing and describing brain datasets, including MRI. The common naming convention and folder structure allow researchers to easily reuse BIDS datasets, re-apply analysis protocols, and run standardized automatic data preprocessing pipelines (and particularly, BIDS Apps). The BIDS starter-kit contains a wide collection of educational resources. Validity of the structure can be assessed with the online BIDS-Validator. The tree of a typical, valid (BIDS-compliant) dataset is shown below:

ds000003/\n \u251c\u2500 CHANGES\n \u251c\u2500 dataset_description.json\n \u251c\u2500 participants.tsv\n \u251c\u2500 README\n \u251c\u2500 sub-01/\n \u2502 \u251c\u2500 anat/\n \u2502 \u2502 \u251c\u2500 sub-01_inplaneT2.nii.gz\n \u2502 \u2502 \u2514\u2500 sub-01_T1w.nii.gz\n \u2502 \u2514\u2500 func/\n \u2502 \u251c\u2500 sub-01_task-rhymejudgment_bold.nii.gz\n \u2502 \u2514\u2500 sub-01_task-rhymejudgment_events.tsv\n \u251c\u2500 sub-02/\n \u251c\u2500 sub-03/\n
"},{"location":"apps/framework/#what-is-a-bids-app","title":"What is a BIDS App?","text":"

(Taken from the BIDS Apps paper)

A BIDS App is a container image capturing a neuroimaging pipeline that takes a BIDS-formatted dataset as input. Since the input is a whole dataset, apps are able to combine multiple modalities, sessions, and/or subjects, but at the same time need to implement ways to query input datasets. Each BIDS App has the same core set of command-line arguments, making them easy to run and integrate into automated platforms. BIDS Apps are constructed in a way that does not depend on any software outside of the container image other than the container engine.

BIDS Apps rely upon two technologies for container computing:

  1. Docker \u2014 for building, hosting as well as running containers on local hardware (running Windows, Mac OS X or Linux) or in the cloud.
  2. Singularity \u2014 for running containers on HPCs (high-performance computing).

BIDS Apps are deposited in the Docker Hub repository, making them openly accessible. Each app is versioned and all of the historical versions are available to download. By reporting the BIDS App name and version in a manuscript, authors can provide others with the ability to exactly replicate their analysis workflow.

Docker is used for its excellent documentation, maturity, and the Docker Hub service for storage and distribution of the images. Docker containers are easily run on personal computers and cloud services. However, the Docker Runtime was originally designed to run different components of web services (HTTP servers, databases etc.) using cloud resources. Docker thus requires root or root-like permissions, as well as modern versions of Linux kernel (to perform user mapping and management of network resources); though this is not a problem in context of renting cloud resources (which are not shared with other users), it makes it difficult or impossible to use in a multi-tenant environment such as an HPC system, which is often the most cost-effective computational resource available to researchers.

Singularity, on the other hand, is a unique container technology designed from the ground up with the encapsulation of binary dependencies and HPC use in mind. Its main advantage over Docker is that it does not require root access for container execution and thus is safe to use on multi-tenant systems. In addition, it does not require recent Linux kernel functionalities (such as namespaces, cgroups and capabilities), making it easy to install on legacy systems.

"},{"location":"apps/framework/#analysis-levels","title":"Analysis levels","text":"

BIDS Apps decouple the individual level analysis (processing of independent subjects) from group-level analyses aggregating participants. For the analysis of individual subjects, Apps need to understand the BIDS structure of the input dataset, so that the required inputs for the designated subject are found. Apps are designed to easily process derivatives generated by the participant-level or other Apps. The overall workflow has an entry-point and an end-point responsible of setting-up the map-reduce tasks and the tear-down including organizing the outputs for its archiving, respectively. Each App may implement multiple map and reduce steps.

"},{"location":"apps/framework/#a-unified-command-line-interface","title":"A unified command-line interface","text":"

To improve user experience and ability to integrate BIDS Apps into various computational platforms, each App follows a set of core command-line arguments:

$ <entrypoint> <bids_dataset> <output_path> <analysis_level>\n

For instance, to run fMRIPrep on a dataset located in /data/bids_root and write the outputs to /data/bids_root/derivatives/:

$ fmriprep /data/bids_root /data/bids_root/derivatives/ participant\n

In this case, we have selected to run the participant level (to process individual subjects). fMRIPrep does not have a group level, but other BIDS Apps may have. For instance, MRIQC generates group-level reports with the following command-line:

$ mriqc /data/bids_root /data/bids_root/derivatives/ group\n
"},{"location":"apps/framework/#what-are-bids-derivatives","title":"What are BIDS Derivatives?","text":"

NiPreps generate derivatives of the original data, and they fulfill the BIDS specification for the results of Apps that are created for subsequent consumption by other BIDS-Apps. These derivatives must follow the BIDS Derivatives specification (draft). An example of BIDS Derivatives filesystem tree, generated with fMRIPrep 1.5:

derivatives/\n\u251c\u2500\u2500 fmriprep/\n\u2502 \u251c\u2500\u2500 dataset_description.json\n\u2502 \u251c\u2500\u2500 logs\n\u2502 \u251c\u2500\u2500 sub-01.html\n\u2502 \u251c\u2500\u2500 sub-01/\n\u2502 \u2502 \u251c\u2500\u2500 anat/\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_desc-brain_mask.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_dseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_label-GM_probseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_label-WM_probseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_label-CSF_probseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_desc-preproc_T1w.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_space-MNI152_desc-brain_mask.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_space-MNI152_dseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_space-MNI152_label-GM_probseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_space-MNI152_label-WM_probseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_space-MNI152_label-CSF_probseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_space-MNI152_desc-preproc_T1w.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_from-MNI152_to-T1w_mode-image_xfm.h5\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_from-T1w_to-MNI152_mode-image_xfm.h5\n\u2502 \u2502 \u2502 \u2514\u2500\u2500 sub-01_from-orig_to-T1w_mode-image_xfm.txt\n\u2502 \u2502 \u251c\u2500\u2500 figures/\n\u2502 \u2502 \u2514\u2500\u2500 func/\n\u2502 \u2502   \u251c\u2500\u2500 sub-01_task-rhymejudgment_space-MNI152_boldref.nii.gz\n\u2502 \u2502   \u251c\u2500\u2500 sub-01_task-rhymejudgment_space-MNI152_desc-preproc_bold.nii.gz\n\u2502 \u2502   \u251c\u2500\u2500 sub-01_task-rhymejudgment_space-MNI152_desc-confounds_regressors.nii.gz\n\u2502 \u2502   \u2514\u2500\u2500 sub-01_task-rhymejudgment_space-MNI152_desc-brain_mask.nii.gz\n\u2502 \u251c\u2500\u2500 sub-02.html\n\u2502 \u251c\u2500\u2500 sub-02/\n\u2502 \u251c\u2500\u2500 sub-03.html\n\u2502 \u2514\u2500\u2500 sub-03/\n

"},{"location":"apps/singularity/","title":"Executing with Singularity","text":"

Summary

Here, we describe how to run NiPreps with Singularity containers. To illustrate the process, we will show the execution of fMRIPrep, but these guidelines extend to any other end-user NiPrep.

"},{"location":"apps/singularity/#preparing-a-singularity-image","title":"Preparing a Singularity image","text":"

Singularity version >= 2.5: If the version of Singularity installed on your HPC (High-Performance Computing) system is modern enough you can create Singularity image directly on the system. This is as simple as:

$ singularity build /my_images/fmriprep-<version>.simg \\\n                    docker://nipreps/fmriprep:<version>\n

where <version> should be replaced with the desired version of fMRIPrep that you want to download.

Singularity version < 2.5: In this case, start with a machine (e.g., your personal computer) with Docker installed. Use docker2singularity to create a singularity image. You will need an active internet connection and some time:

$ docker run --privileged -t --rm \\\n    -v /var/run/docker.sock:/var/run/docker.sock \\\n    -v D:\\host\\path\\where\\to\\output\\singularity\\image:/output \\\n    singularityware/docker2singularity \\\n    nipreps/fmriprep:<version>\n

Where <version> should be replaced with the desired version of fMRIPrep that you want to download.

Beware of the back slashes, expected for Windows systems. For *nix users the command translates as follows: :

$ docker run --privileged -t --rm \\\n    -v /var/run/docker.sock:/var/run/docker.sock \\\n    -v /absolute/path/to/output/folder:/output \\\n    singularityware/docker2singularity \\\n    nipreps/fmriprep:<version>\n

Transfer the resulting Singularity image to the HPC, for example, using scp or rsync:

$ scp nipreps_fmriprep*.img user@hcpserver.edu:/my_images\n
"},{"location":"apps/singularity/#running-a-singularity-image","title":"Running a Singularity Image","text":"

If the data to be preprocessed is also on the HPC, you are ready to run the NiPrep:

$ singularity run --cleanenv fmriprep.simg \\\n    path/to/data/dir path/to/output/dir \\\n    participant \\\n    --participant-label label\n
"},{"location":"apps/singularity/#handling-environment-variables","title":"Handling environment variables","text":"

Singularity by default exposes all environment variables from the host inside the container. Because of this, your host libraries (e.g., NiPype or a Python environment) could be accidentally used instead of the ones inside the container. To avoid such a situation, we strongly recommend using the --cleanenv argument in all scenarios. For example:

$ singularity run --cleanenv fmriprep.simg \\\n  /work/04168/asdf/lonestar/ $WORK/lonestar/output \\\n  participant \\\n  --participant-label 387 --nthreads 16 -w $WORK/lonestar/work \\\n  --omp-nthreads 16\n

Alternatively, conflicts might be preempted and some problems mitigated by unsetting potentially problematic settings, such as the PYTHONPATH variable, before running:

$ unset PYTHONPATH; singularity run fmriprep.simg \\\n  /work/04168/asdf/lonestar/ $WORK/lonestar/output \\\n  participant \\\n  --participant-label 387 --nthreads 16 -w $WORK/lonestar/work \\\n  --omp-nthreads 16\n

It is possible to define environment variables scoped within the container by using the SINGULARITYENV_* magic, in combination with --cleanenv. For example, we can set the FreeSurfer license variable (see fMRIPrep's documentation on this) as follows: :

$ export SINGULARITYENV_FS_LICENSE=$HOME/.freesurfer.txt\n$ singularity exec --cleanenv fmriprep.simg env | grep FS_LICENSE\nFS_LICENSE=/home/users/oesteban/.freesurfer.txt\n

As we can see, the export in the first line tells Singularity to set a corresponding environment variable of the same name after dropping the prefix SINGULARITYENV_.

"},{"location":"apps/singularity/#accessing-the-hosts-filesystem","title":"Accessing the host's filesystem","text":"

Depending on how Singularity is configured on your cluster it might or might not automatically bind (mount or expose) host's folders to the container (e.g., /scratch, or $HOME). This is particularly relevant because, if you can't run Singularity in privileged mode (which is almost certainly true in all the scenarios), Singularity containers are read only. This is to say that you won't be able to write anything unless Singularity can access the host's filesystem in write mode.

By default, Singularity automatically binds (mounts) the user's home directory and a scratch directory. In addition, Singularity generally allows binding the necessary folders with the -B <host_folder>:<container_folder>[:<permissions>] Singularity argument. For example:

$ singularity run --cleanenv -B /work:/work fmriprep.simg \\\n  /work/my_dataset/ /work/my_dataset/derivatives/fmriprep \\\n  participant \\\n  --participant-label 387 --nthreads 16 \\\n  --omp-nthreads 16\n

Warning

If your Singularity installation doesn't allow you to bind non-existent bind points, you'll get an error saying WARNING: Skipping user bind, non existent bind point (directory) in container. In this scenario, you can either try to bind things onto some other bind point you know it exists in the image or rebuild your singularity image with docker2singularity as follows:

$ docker run --privileged -ti --rm -v /var/run/docker.sock:/var/run/docker.sock \\\n         -v $PWD:/output singularityware/docker2singularity \\\n         -m \"/gpfs /scratch /work /share /lscratch /opt/templateflow\"\n

In the example above, the following bind points are created: /gpfs, /scratch, /work, /share, /opt/templateflow.

Important

One great feature of containers is their confinement or isolation from the host system. Binding mount points breaks this principle, as the container has now access to create changes in the host. Therefore, it is generally recommended to use binding scarcely and granting very limited access to the minimum necessary resources. In other words, it is preferred to bind just one subdirectory of $HOME than the full $HOME directory of the host (see nipreps/fmriprep#1778 (comment)).

Relevant aspects of the $HOME directory within the container: By default, Singularity will bind the user's $HOME directory in the host into the /home/$USER (or equivalent) in the container. Most of the times, it will also redefine the $HOME environment variable and update it to point to the corresponding mount point in /home/$USER. However, these defaults can be overwritten in your system. It is recommended to check your settings with your system's administrators. If your Singularity installation allows it, you can workaround the $HOME specification combining the bind mounts argument (-B) with the home overwrite argument (--home) as follows:

$ singularity run -B $HOME:/home/fmriprep --home /home/fmriprep \\\n      --cleanenv fmriprep.simg <fmriprep arguments>\n
"},{"location":"apps/singularity/#templateflow-and-singularity","title":"TemplateFlow and Singularity","text":"

TemplateFlow is a helper tool that allows neuroimaging workflows to programmatically access a repository of standard neuroimaging templates. In other words, TemplateFlow allows NiPreps to dynamically change the templates that are used, e.g., in the atlas-based brain extraction step or spatial normalization.

Default settings in the Singularity image should get along with the Singularity installation of your system. However, deviations from the default configurations of your installation may break this compatibility. A particularly problematic case arises when the home directory is mounted in the container, but the $HOME environment variable is not correspondingly updated. Typically, you will experience errors like OSError: [Errno 30] Read-only file system or FileNotFoundError: [Errno 2] No such file or directory: '/home/fmriprep/.cache'.

If it is not explicitly forbidden in your installation, the first attempt to overcome this issue is manually setting the $HOME directory as follows:

$ singularity run --home $HOME --cleanenv fmriprep.simg <fmriprep arguments>\n

If the user's home directory is not automatically bound, then the second step would include manually binding it as in the section above: :

$ singularity run -B $HOME:/home/fmriprep --home /home/fmriprep \\\n      --cleanenv fmriprep.simg <fmriprep arguments>\n

Finally, if the --home argument cannot be used, you'll need to provide the container with writable filesystems where TemplateFlow's files can be downloaded. In addition, you will need to indicate fMRIPrep to update the default paths with the new mount points setting the SINGULARITYENV_TEMPLATEFLOW_HOME variable. :

# Tell the NiPrep where TemplateFlow will place downloads\n$ export SINGULARITYENV_TEMPLATEFLOW_HOME=/opt/templateflow\n$ singularity run -B <writable-path-on-host>:/opt/templateflow \\\n      --cleanenv fmriprep.simg <fmriprep arguments>\n
"},{"location":"apps/singularity/#restricted-internet-access","title":"Restricted Internet access","text":"

We have identified several conditions in which running NiPreps might fail because of spotty or impossible access to Internet.

If your compute node cannot have access to Internet, then you'll need to pull down from TemplateFlow all the resources that will be necessary ahead of run-time.

If that is not the case (i.e., you should be able to hit HTTP/s endpoints), then you can try the following:

  • VerifiedHTTPSConnection ... Failed to establish a new connection: [Errno 110] Connection timed out. If you encounter an error like this, probably you'll need to set up an http proxy exporting SINGULARITYENV_http_proxy (see nipreps/fmriprep#1778 (comment). For example:

    $ export SINGULARITYENV_https_proxy=http://<ip or proxy name>:<port>\n
  • requests.exceptions.SSLError: HTTPSConnectionPool .... In this case, your container seems to be able to reach the Internet, but unable to use SSL encription. There are two potential solutions to the issue. The recommended one is setting REQUESTS_CA_BUNDLE to the appropriate path, and/or binding the appropriate filesystem:

    $ export SINGULARITYENV_REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt\n$ singularity run -B <path-to-certs-folder>:/etc/ssl/certs \\\n      --cleanenv fmriprep.simg <fmriprep arguments>\n
    Otherwise, some users have succeeded pre-fetching the necessary templates onto the TemplateFlow directory to then bind the folder at execution:

    $ export TEMPLATEFLOW_HOME=/path/to/keep/templateflow\n$ python -m pip install -U templateflow  # Install the client\n$ python\n>>> import templateflow.api\n>>> templateflow.api.TF_S3_ROOT = 'http://templateflow.s3.amazonaws.com'\n>>> api.get(\u2018MNI152NLin6Asym\u2019)\n

Finally, run the singularity image binding the appropriate folder:

$ export SINGULARITYENV_TEMPLATEFLOW_HOME=/templateflow\n$ singularity run -B ${TEMPLATEFLOW_HOME:-$HOME/.cache/templateflow}:/templateflow \\\n      --cleanenv fmriprep.simg <fmriprep arguments>\n
"},{"location":"apps/singularity/#troubleshooting","title":"Troubleshooting","text":"

Setting up a functional execution framework with Singularity might be tricky in some HPC (high-performance computing) systems. Please make sure you have read the relevant documentation of Singularity, and checked all the defaults and configuration in your system. The next step is checking the environment and access to fMRIPrep resources, using singularity shell.

  1. Check access to input data folder, and BIDS validity:

    $ singularity shell -B path/to/data:/data fmriprep.simg\nSingularity fmriprep.simg:~> ls /data\nCHANGES  README  dataset_description.json  participants.tsv  sub-01  sub-02  sub-03  sub-04  sub-05  sub-06  sub-07  sub-08  sub-09  sub-10  sub-11  sub-12  sub-13  sub-14  sub-15  sub-16  task-balloonanalogrisktask_bold.json\nSingularity fmriprep.simg:~> bids-validator /data\n   1: [WARN] You should define 'SliceTiming' for this file. If you don't provide this information slice time correction will not be possible. (code: 13 - SLICE_TIMING_NOT_DEFINED)\n          ./sub-01/func/sub-01_task-balloonanalogrisktask_run-01_bold.nii.gz\n          ./sub-01/func/sub-01_task-balloonanalogrisktask_run-02_bold.nii.gz\n          ./sub-01/func/sub-01_task-balloonanalogrisktask_run-03_bold.nii.gz\n          ./sub-02/func/sub-02_task-balloonanalogrisktask_run-01_bold.nii.gz\n          ./sub-02/func/sub-02_task-balloonanalogrisktask_run-02_bold.nii.gz\n          ./sub-02/func/sub-02_task-balloonanalogrisktask_run-03_bold.nii.gz\n          ./sub-03/func/sub-03_task-balloonanalogrisktask_run-01_bold.nii.gz\n          ./sub-03/func/sub-03_task-balloonanalogrisktask_run-02_bold.nii.gz\n          ./sub-03/func/sub-03_task-balloonanalogrisktask_run-03_bold.nii.gz\n          ./sub-04/func/sub-04_task-balloonanalogrisktask_run-01_bold.nii.gz\n          ... and 38 more files having this issue (Use --verbose to see them all).\n  Please visit https://neurostars.org/search?q=SLICE_TIMING_NOT_DEFINED for existing conversations about this issue.\n

  2. Check access to output data folder, and whether you have write permissions

    $ singularity shell -B path/to/data/derivatives/fmriprep-1.5.0:/out fmriprep.simg\nSingularity fmriprep.simg:~> ls /out\nSingularity fmriprep.simg:~> touch /out/test\nSingularity fmriprep.simg:~> rm /out/test\n

  3. Check access and permissions to $HOME:

    $ singularity shell fmriprep.simg\nSingularity fmriprep.simg:~> mkdir -p $HOME/.cache/testfolder\nSingularity fmriprep.simg:~> rmdir $HOME/.cache/testfolder\n

  4. Check TemplateFlow operation:

    $ singularity shell -B path/to/templateflow:/templateflow fmriprep.simg\nSingularity fmriprep.simg:~> echo ${TEMPLATEFLOW_HOME:-$HOME/.cache/templateflow}\n/home/users/oesteban/.cache/templateflow\nSingularity fmriprep.simg:~> python -c \"from templateflow.api import get; get(['MNI152NLin2009cAsym', 'MNI152NLin6Asym', 'OASIS30ANTs', 'MNIPediatricAsym', 'MNIInfant'])\"\n   Downloading https://templateflow.s3.amazonaws.com/tpl-MNI152NLin6Asym/tpl-MNI152NLin6Asym_res-01_atlas-HOCPA_desc-th0_dseg.nii.gz\n   304B [00:00, 1.28kB/s]\n   Downloading https://templateflow.s3.amazonaws.com/tpl-MNI152NLin6Asym/tpl-MNI152NLin6Asym_res-01_atlas-HOCPA_desc-th25_dseg.nii.gz\n   261B [00:00, 1.04kB/s]\n   Downloading https://templateflow.s3.amazonaws.com/tpl-MNI152NLin6Asym/tpl-MNI152NLin6Asym_res-01_atlas-HOCPA_desc-th50_dseg.nii.gz\n   219B [00:00, 867B/s]\n   ...\n

"},{"location":"apps/singularity/#running-singularity-on-a-slurm-system","title":"Running Singularity on a SLURM system","text":"

An example of sbatch script to run fMRIPrep on a SLURM system1 is given below. The submission script will generate one task per subject using a job array.

#!/bin/bash\n#\n#SBATCH -J fmriprep\n#SBATCH --time=48:00:00\n#SBATCH -n 1\n#SBATCH --cpus-per-task=16\n#SBATCH --mem-per-cpu=4G\n#SBATCH -p normal,mygroup  # Queue names you can submit to\n# Outputs ----------------------------------\n#SBATCH -o log/%x-%A-%a.out\n#SBATCH -e log/%x-%A-%a.err\n#SBATCH --mail-user=%u@domain.tld\n#SBATCH --mail-type=ALL\n# ------------------------------------------\n\nBIDS_DIR=\"$STUDY/data\"\nDERIVS_DIR=\"derivatives/fmriprep-20.2.2\"\nLOCAL_FREESURFER_DIR=\"$STUDY/data/derivatives/freesurfer-6.0.1\"\n\n# Prepare some writeable bind-mount points.\nTEMPLATEFLOW_HOST_HOME=$HOME/.cache/templateflow\nFMRIPREP_HOST_CACHE=$HOME/.cache/fmriprep\nmkdir -p ${TEMPLATEFLOW_HOST_HOME}\nmkdir -p ${FMRIPREP_HOST_CACHE}\n\n# Prepare derivatives folder\nmkdir -p ${BIDS_DIR}/${DERIVS_DIR}\n\n# Make sure FS_LICENSE is defined in the container.\nexport SINGULARITYENV_FS_LICENSE=$HOME/.freesurfer.txt\n\n# Designate a templateflow bind-mount point\nexport SINGULARITYENV_TEMPLATEFLOW_HOME=\"/templateflow\"\nSINGULARITY_CMD=\"singularity run --cleanenv -B $BIDS_DIR:/data -B ${TEMPLATEFLOW_HOST_HOME}:${SINGULARITYENV_TEMPLATEFLOW_HOME} -B $L_SCRATCH:/work -B ${LOCAL_FREESURFER_DIR}:/fsdir $STUDY/images/fmriprep_20.2.2.simg\"\n\n# Parse the participants.tsv file and extract one subject ID from the line corresponding to this SLURM task.\nsubject=$( sed -n -E \"$((${SLURM_ARRAY_TASK_ID} + 1))s/sub-(\\S*)\\>.*/\\1/gp\" ${BIDS_DIR}/participants.tsv )\n\n# Remove IsRunning files from FreeSurfer\nfind ${LOCAL_FREESURFER_DIR}/sub-$subject/ -name \"*IsRunning*\" -type f -delete\n\n# Compose the command line\ncmd=\"${SINGULARITY_CMD} /data /data/${DERIVS_DIR} participant --participant-label $subject -w /work/ -vv --omp-nthreads 8 --nthreads 12 --mem_mb 30000 --output-spaces MNI152NLin2009cAsym:res-2 anat fsnative fsaverage5 --use-aroma --fs-subjects-dir /fsdir\"\n\n# Setup done, run the command\necho Running task ${SLURM_ARRAY_TASK_ID}\necho Commandline: $cmd\neval $cmd\nexitcode=$?\n\n# Output results to a table\necho \"sub-$subject   ${SLURM_ARRAY_TASK_ID}    $exitcode\" \\\n      >> ${SLURM_JOB_NAME}.${SLURM_ARRAY_JOB_ID}.tsv\necho Finished tasks ${SLURM_ARRAY_TASK_ID} with exit code $exitcode\nexit $exitcode\n
Submission is then as easy as:

$ export STUDY=/path/to/some/folder\n$ sbatch --array=1-$(( $( wc -l $STUDY/data/participants.tsv | cut -f1 -d' ' ) - 1 )) fmriprep.slurm\n
  1. assuming that job arrays and Singularity are available\u00a0\u21a9

"},{"location":"assets/ORN-Workshop/presentation/","title":"Presentation","text":"

layout: false count: false

.middle.center[

"},{"location":"assets/ORN-Workshop/presentation/#building-communities-around-reproducible-workflows","title":"Building communities around reproducible workflows","text":""},{"location":"assets/ORN-Workshop/presentation/#o-esteban","title":"O. Esteban","text":""},{"location":"assets/ORN-Workshop/presentation/#chuv-lausanne-university-hospital","title":"CHUV | Lausanne University Hospital","text":""},{"location":"assets/ORN-Workshop/presentation/#wwwniprepsorg","title":"www.nipreps.org","text":"

]

layout: false count: false

.middle.center[

"},{"location":"assets/ORN-Workshop/presentation/#building-communities-around-reproducible-workflows_1","title":"Building communities around reproducible workflows","text":""},{"location":"assets/ORN-Workshop/presentation/#o-esteban_1","title":"O. Esteban","text":""},{"location":"assets/ORN-Workshop/presentation/#chuv-lausanne-university-hospital_1","title":"CHUV | Lausanne University Hospital","text":""},{"location":"assets/ORN-Workshop/presentation/#wwwniprepsorg_1","title":"www.nipreps.org","text":"

]

???

"},{"location":"assets/ORN-Workshop/presentation/#im-going-to-talk-about-how-we-are-building-a-framework-of-preprocessing-pipelines-for-neuroimaging-called-nipreps-based-on-the-fmriprep-experience","title":"I'm going to talk about how we are building a framework of preprocessing pipelines for neuroimaging called NiPreps, based on the fMRIPrep experience.","text":"

name: newsection layout: true class: section-separator

.perma-sidebar[

"},{"location":"assets/ORN-Workshop/presentation/#data-processing","title":"Data Processing","text":""},{"location":"assets/ORN-Workshop/presentation/#day-2-15h-cet","title":"(Day 2, 15h CET)","text":""},{"location":"assets/ORN-Workshop/presentation/#workflows","title":"Workflows","text":"

]

name: sidebar layout: true

.perma-sidebar[

"},{"location":"assets/ORN-Workshop/presentation/#data-processing_1","title":"Data Processing","text":""},{"location":"assets/ORN-Workshop/presentation/#day-2-15h-cet_1","title":"(Day 2, 15h CET)","text":""},{"location":"assets/ORN-Workshop/presentation/#workflows_1","title":"Workflows","text":"

]

template: sidebar

"},{"location":"assets/ORN-Workshop/presentation/#neuroimaging-is-now-mature","title":"Neuroimaging is now mature","text":"
  • many excellent tools available (from specialized to foundational)
  • large toolboxes (AFNI, ANTs/ITK, FreeSurfer, FSL, Nilearn, SPM, etc.)
  • workflow software (Nipype, Shellscripts, Nextflow, CWL)
  • container technology, CI/CD

  • a wealth of prior knowledge (esp. about humans)

  • LOTS of data acquired everyday

"},{"location":"assets/ORN-Workshop/presentation/#workflows-games-on","title":"Workflows - game's on!","text":"
  • although many neuroimaging areas are still in search of methodological breakthroughs,

  • challenges have moved on to the workflows:

  • workflows within traditional toolboxes - usually not flexible to adapt to new data
  • BIDS and BIDS-Apps.

???

  • researchers have a large portfolio of image processing components readily available
  • toolboxes with great support and active maintenance:
"},{"location":"assets/ORN-Workshop/presentation/#new-questions-changing-the-focus","title":"New questions changing the focus:","text":""},{"location":"assets/ORN-Workshop/presentation/#-validity-does-the-workflow-actually-work-out","title":"- validity (does the workflow actually work out?)","text":""},{"location":"assets/ORN-Workshop/presentation/#-transparency-is-it-a-black-box-how-precise-is-reporting","title":"- transparency (is it a black-box? how precise is reporting?)","text":""},{"location":"assets/ORN-Workshop/presentation/#-vibration-how-each-tool-choice-parameters-affect-overall","title":"- vibration (how each tool choice & parameters affect overall?)","text":""},{"location":"assets/ORN-Workshop/presentation/#-throughput-how-much-datatime-can-it-possible-take","title":"- throughput (how much data/time can it possible take?)","text":""},{"location":"assets/ORN-Workshop/presentation/#-robustness-can-i-use-it-on-diverse-studies","title":"- robustness (can I use it on diverse studies?)","text":""},{"location":"assets/ORN-Workshop/presentation/#-evaluation-what-is-it-unique-about-the-workflow-wrt-existing-alternatives","title":"- evaluation (what is it unique about the workflow, w.r.t. existing alternatives?)","text":""},{"location":"assets/ORN-Workshop/presentation/#the-garden-of-forking-paths","title":"The garden of forking paths","text":"

(Botvinik-Nezer et al., 2020)

Around 50% of teams used fMRIPrep'ed inputs.

"},{"location":"assets/ORN-Workshop/presentation/#the-fmriprep-story","title":"The fMRIPrep story","text":""},{"location":"assets/ORN-Workshop/presentation/#fmriprep-produces-analysis-ready-data-from-diverse-data","title":"fMRIPrep produces analysis-ready data from diverse data","text":"
  • minimal requirements (BIDS-compliant);
  • agnostic to downstream steps of the workflow
  • produces BIDS-Derivatives;
  • robust against inhomogeneity of data across studies

???

fMRIPrep takes in a task-based or resting-state functional MRI dataset in BIDS-format and returns preprocessed data ready for analysis.

Preprocessed data can be used for a broad range of analysis, and they are formatted following BIDS-Derivatives to maximize compatibility with: * major software packages (AFNI, FSL, SPM*, etc.) * further temporal filtering and denoising: fMRIDenoise * any BIDS-Derivatives compliant tool (e.g., FitLins).

--

"},{"location":"assets/ORN-Workshop/presentation/#fmriprep-is-a-bids-app-gorgolewski-et-al-2017","title":"fMRIPrep is a BIDS-App (Gorgolewski, et al. 2017)","text":"
  • adhered to modern software-engineering standards (CI/CD, containers)
  • compatible interface with other BIDS-Apps
  • optimized for automatic execution

???

fMRIPrep adopts the BIDS-App specifications. That means the software is tested with every change to the codebase, it also means that packaging, containerization, and deployment are also automated and require tests to be passing. BIDS-Apps are inter-operable (via BIDS-Derivatives), and optimized for execution in HPC, Cloud, etc.

--

"},{"location":"assets/ORN-Workshop/presentation/#minimizes-human-intervention","title":"Minimizes human intervention","text":"
  • avoid error-prone parameters settings (read them from BIDS)
  • adapts the workflow to the actual data available
  • while remaining flexible to some design choices (e.g., whether or not reconstructing surfaces or customizing target normalized standard spaces)

???

fMRIPrep minimizes human intervention because the user does not need to fiddle with any parameters - they are obtained from the BIDS structure. However, fMRIPrep does allow some flexibility to ensure the preprocessing meets the requirements of the intended analyses.

"},{"location":"assets/ORN-Workshop/presentation/#fmriprep-was-not-originally-envisioned-as-a-community-project","title":"fMRIPrep was not originally envisioned as a community project ...","text":"

(we just wanted a robust tool to automatically preprocess incoming data of OpenNeuro.org)

--

"},{"location":"assets/ORN-Workshop/presentation/#but-a-community-built-up-quickly-around-it","title":"... but a community built up quickly around it","text":"

--

.pull-left[

"},{"location":"assets/ORN-Workshop/presentation/#why","title":"Why?","text":"
  • Preprocessing of fMRI was in need for division of labor.

  • Obsession with transparency made early-adopters confident of the recipes they were applying.

  • Responsiveness to feedback. ]

.pull-right[

]

???

Preprocessing is a time-consuming effort, requires expertise converging imaging foundations & CS, typically addressed with legacy in-house pipelines.

On the right-hand side, you'll find the chart of unique visitors to fmriprep.org, which is the documentation website.

"},{"location":"assets/ORN-Workshop/presentation/#key-aspect-credit-all-direct-contributors","title":"Key aspect: credit all direct contributors","text":"

--

"},{"location":"assets/ORN-Workshop/presentation/#and-indirect-citation-boilerplate","title":".. and indirect: citation boilerplate.","text":""},{"location":"assets/ORN-Workshop/presentation/#researchers-want-to-spend-more-time-on-those-areas-most-relevant-to-them","title":"Researchers want to spend more time on those areas most relevant to them","text":"

(probably not preprocessing...)

???

With the development of fMRIPrep we understood that researchers don't want to waste their time on preprocessing (except for researchers developing new preprocessing techniques).

--

"},{"location":"assets/ORN-Workshop/presentation/#writing-fmriprep-required-a-team-of-several-experts-in-processing-methods-for-neuroimaging-with-a-solid-base-on-computer-science","title":"Writing fMRIPrep required a team of several experts in processing methods for neuroimaging, with a solid base on Computer Science.","text":"

(research programs just can't cover the neuroscience and the engineering of the whole workflow - we need to divide the labor)

???

The current neuroimaging workflow requires extensive knowledge in sometimes orthogonal fields such as neuroscience and computer science. Dividing the labor in labs, communities or individuals with the necessary expertise is the fundamental for the advance of the whole field.

--

"},{"location":"assets/ORN-Workshop/presentation/#transparency-helps-against-the-risk-of-super-easy-tools","title":"Transparency helps against the risk of super-easy tools","text":"

(easy-to-use tools are risky because they might get a researcher very far with no idea whatsoever of what they've done)

???

There is an implicit risk in making things too easy to operate:

For instance, imagine someone who runs fMRIPrep on diffusion data by tricking the BIDS naming into an apparently functional MRI dataset. If fMRIPrep reached the end at all, the garbage at the output could be fed into further tools, in a sort of a snowballing problem.

When researchers have access to the guts of the software and are given an opportunity to understand what's going on, the risk of misuse dips.

--

"},{"location":"assets/ORN-Workshop/presentation/#established-toolboxes-do-not-have-incentives-for-compatibility","title":"Established toolboxes do not have incentives for compatibility","text":"

(and to some extent this is not necessarily bad, as long as they are kept well-tested and they embrace/help-develop some minimal standards)

???

AFNI, ANTs, FSL, FreeSurfer, SPM, etc. have comprehensive software validation tests, methodological validation tests, stress tests, etc. - which pushed up their quality and made them fundamental for the field.

Therefore, it is better to keep things that way (although some minimal efforts towards convergence in compatibility are of course welcome)

"},{"location":"assets/ORN-Workshop/presentation/#the-dmriprep-story","title":"The dMRIPrep story","text":"

After the success of fMRIPrep, some neuroimagers asked \"when a diffussion MRI fMRIPrep?\"

"},{"location":"assets/ORN-Workshop/presentation/#neurostarsorg","title":"NeuroStars.org","text":"

(please note this down)

--

Same situation in the field of diffusion MRI:

"},{"location":"assets/ORN-Workshop/presentation/#image-processing-possible-guidelines-for-the-standardization-clinical-applications-j-veraart","title":"Image Processing: Possible Guidelines for the Standardization & Clinical Applications (J. Veraart)","text":"

(https://www.ismrm.org/19/program_files/MIS15.htm)

--

"},{"location":"assets/ORN-Workshop/presentation/#please-join","title":"Please join!","text":"

Joseph, M.; Pisner, D.; Richie-Halford, A.; Lerma-Usabiaga, G.; Keshavan, A.; Kent, JD.; Cieslak, M.; Poldrack, RA.; Rokem, A.; Esteban, O.

template: newsection layout: false

.middle.center[

"},{"location":"assets/ORN-Workshop/presentation/#wwwniprepsorg_2","title":"www.nipreps.org","text":""},{"location":"assets/ORN-Workshop/presentation/#nipreps-neuroimaging-preprocessing-tools","title":"(NiPreps == NeuroImaging PREProcessing toolS)","text":"

]

???

The enormous success of fMRIPrep led us to propose its generalization to other MRI and non-MRI modalities, as well as nonhuman species (for instance, rodents), and particular populations currently unsupported by fMRIPrep such as infants.

"},{"location":"assets/ORN-Workshop/presentation/#augmenting-scanners-to-produce-analysis-grade-data","title":"Augmenting scanners to produce \"analysis-grade\" data","text":""},{"location":"assets/ORN-Workshop/presentation/#data-directly-consumable-by-analyses","title":"(data directly consumable by analyses)","text":"

.pull-left[

Analysis-grade data is an analogy to the concept of \"sushi-grade (or sashimi-grade) fish\" in that both are:

.large[minimally preprocessed,]

and

.large[safe to consume directly.] ]

.pull-right[ ]

???

The goal, therefore, of NiPreps is to extend the scanner so that, in a way, they produce data ready for analysis.

We liken these analysis-grade data to sushi-grade fish, because in both cases the product is minimally preprocessed and at the same time safe to consume as is.

template: newsection layout: false

.middle.center[

"},{"location":"assets/ORN-Workshop/presentation/#deconstructing-fmriprep","title":"Deconstructing fMRIPrep","text":"

]

???

For the last two years we've been decomposing the architecture of fMRIPrep, spinning off its constituent parts that are valuable in other applications.

This process of decoupling (to use a proper CS term) has been greatly facilitated by the modular nature of the code since its inception.

???

The processing elements extracted from fMRIPrep can be mapped to three regimes of responsibility:

  • Software infrastructure composed by tools ensuring the collaboration and the most basic tooling.
  • Middleware utilities, which build more advanced tooling based on the foundational infrastructure
  • And at the top of the stack end-user applications - namely fMRIPrep, dMRIPrep, sMRIPrep and MRIQC.

As we can see, the boundaries of these three architectural layers are soft and tools such as TemplateFlow may stand in between.

Only projects enclosed in the brain shape pertain to the NiPreps community. NiPype, NiBabel and BIDS are so deeply embedded as dependencies that NiPreps can't be understood without them.

  • BIDS provides a standard, guaranteeing I/O agreements:

  • Allows workflows to self-adapt to the inputs

  • Ensures the shareability of the results

  • PyBIDS: a Python tool to query BIDS datasets (Yarkoni et al., 2019):

>>> from bids import BIDSLayout\n\n# Point PyBIDS to the dataset's path\n>>> layout = BIDSLayout(\"/data/coolproject\")\n\n# List the participant IDs of present subjects\n>>> layout.get_subjects()\n['01', '02', '03', '04', '05']\n\n# List session identifiers, if present\n>>> layout.get_sessions()\n['01', '02']\n\n# List functional MRI tasks\n>>> layout.get_tasks()\n['rest', 'nback']\n

???

BIDS is one of the keys to success for fMRIPrep and consequently, a strategic element of NiPreps.

Because the tools so far are written in Python, PyBIDS is a powerful tool to index and query inputs and outputs.

The code snippet illustrates the ease to find out the subject identifiers available in the dataset, sessions, and tasks.

"},{"location":"assets/ORN-Workshop/presentation/#bids-derivatives","title":"BIDS Derivatives","text":"

.cut-right[

derivatives/\n\u251c\u2500\u2500 fmriprep/\n\u2502 \u251c\u2500\u2500 dataset_description.json\n\u2502 \u251c\u2500\u2500 logs\n\u2502 \u251c\u2500\u2500 sub-01.html\n\u2502 \u251c\u2500\u2500 sub-01/\n\u2502 \u2502 \u251c\u2500\u2500 anat/\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_desc-brain_mask.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_dseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_label-GM_probseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_label-WM_probseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_label-CSF_probseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_desc-preproc_T1w.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_space-MNI152_desc-brain_mask.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_space-MNI152_dseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_space-MNI152_label-GM_probseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_space-MNI152_label-WM_probseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_space-MNI152_label-CSF_probseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_space-MNI152_desc-preproc_T1w.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_from-MNI152_to-T1w_mode-image_xfm.h5\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_from-T1w_to-MNI152_mode-image_xfm.h5\n\u2502 \u2502 \u2502 \u2514\u2500\u2500 sub-01_from-orig_to-T1w_mode-image_xfm.txt\n\u2502 \u2502 \u251c\u2500\u2500 figures/\n\u2502 \u2502 \u2514\u2500\u2500 func/\n\u2502 \u2502   \u251c\u2500\u2500 sub-01_task-rhymejudgment_space-MNI152_boldref.nii.gz\n\u2502 \u2502   \u251c\u2500\u2500 sub-01_task-rhymejudgment_space-MNI152_desc-preproc_bold.nii.gz\n\u2502 \u2502   \u251c\u2500\u2500 sub-01_task-rhymejudgment_space-MNI152_desc-confounds_regressors.nii.gz\n\u2502 \u2502   \u2514\u2500\u2500 sub-01_task-rhymejudgment_space-MNI152_desc-brain_mask.nii.gz\n
]

???

All NiPreps must write out BIDS-Derivatives. As illustrated in the example, the outputs of fMRIPrep are very similar to the BIDS standard for acquired data.

"},{"location":"assets/ORN-Workshop/presentation/#bids-apps","title":"BIDS-Apps","text":"
  • BIDS-Apps proposes a workflow structure model:
  • Use of containers & CI/CD

  • Uniform interface: .cut-right[

    fmriprep /data /data/derivatives/fmriprep-20.1.1 participant [+OPTIONS]\n
    ]

???

All end-user applications in NiPreps must conform to the BIDS-Apps specifications.

The BIDS-Apps paper identified a common pattern in neuroimaging studies, where individual participants (and runs) are processed first individually, and then based on the outcomes, further levels of data aggregation are executed.

For this reason, BIDS-Apps define two major levels of execution: participant and group level.

Finally, the paper also stresses the importance of containerizing applications to ensure long-term preservation of run-to-run repeatability and proposes a common command line interface as described at the bottom:

  • first the name of the BIDS-Apps (fmriprep, in this case)
  • followed by input and output directories (respectively),
  • to finally indicate the analysis level (always participant, for the case of fmriprep)

.pull-left[

from nipype.interfaces.fsl import BET\nbrain_extract = BET(\n  in_file=\"/data/coolproject/sub-01/ses-01/anat/sub-01_ses-01_T1w.nii\",\n  out_file=\"/out/sub-01/ses-01/anat/sub-01_ses-01_desc-brain_T1w.nii\"\n)\nbrain_extract.run()\n

Nipype is the gateway to mix-and-match from AFNI, ANTs, Dipy, FreeSurfer, FSL, MRTrix, SPM, etc. ]

.pull-right[

]

???

Nipype is the glue stitching together all the underlying neuroimaging toolboxes and provides the execution framework.

The snippet shows how the widely known BET tool from FSL can be executed using NiPype. This is a particular example instance of interfaces - which provide uniform access to the tooling with Python.

Finally, combining these interfaces we generate processing workflows to fulfill higher level processing tasks.

???

For instance, we may have a look into fMRIPrep's functional processing block.

Nipype helps understand (and opens windows in the black box) generating these graph representation of the workflow.

\"\"\"Fix the affine of a rodent dataset, imposing 0.2x0.2x0.2 [mm].\"\"\"\nimport numpy as np\nimport nibabel as nb\n\n# Open the file\nimg = nb.load(\"sub-25_MGE_MouseBrain_3D_MGE_150.nii.gz\")\n\n# New (correct) affine\naff = np.diag((-0.2, -0.2, 0.2, 1.0))\n\n# Use nibabel to reorient to canonical\ncard = nb.as_closest_canonical(nb.Nifti1Image(\n    img.dataobj,\n    np.diag((-0.2, -0.2, 0.2, 1.0)),\n    None\n))\n\n# Save to disk\ncard.to_filename(\"sub-25_T2star.nii.gz\")\n

???

NiBabel allows Python to easily access neuroimaging data formats such as NIfTI, GIFTI and CIFTI2.

Although this might be a trivial task, the proliferation of neuroimaging software has led to some sort of Wild West of formats, and sometimes interoperation is not ensured.

"},{"location":"assets/ORN-Workshop/presentation/#in-the-snippet-we-can-see-how-we-can-manipulate-the-orientation-headers-of-a-nifti-volume-in-particular-a-rodent-image-with-incorrect-affine-information","title":"In the snippet, we can see how we can manipulate the orientation headers of a NIfTI volume, in particular a rodent image with incorrect affine information.","text":"

.pull-left[

Transforms typically are the outcome of image registration methodologies

The proliferation of software implementations of image registration methodologies has resulted in a spread of data structures and file formats used to preserve and communicate transforms.

(Esteban et al., 2020) ]

.pull-right[

]

???

NiTransforms is a super-interesting toy project where we are exercising our finest coding skills. It completes NiBabel in the effort of making spatial transforms calculated by neuroimaging software tools interoperable.

When it goes beyond the alpha state, it is expected to be merged into NiBabel.

At the moment, NiTransforms is already integrated in fMRIPrep +20.1 to concatenate LTA (linear affine transforms) transforms obtained with FreeSurfer, ITK transforms obtained with ANTs, and motion parameters estimated with FSL.

Compatibility across formats is hard due to the many arbitrary decisions in establishing the mathematical framework of the transform and the intrinsic confusion of applying a transform.

While intuitively we understand applying a transform as \"transforming the moving image so that I can represent it overlaid or fused with the reference image and both should look aligned\", in reality, we only transform coordinates from the reference image into the moving image's space (step 1 on the right).

Once we know where the center of every voxel of the reference image falls in the moving image coordinate system, we read in the information (in other words, a value) from the moving image. Because the location will probably be off-grid, we interpolate such a value from the neighboring voxels (step 2).

Finally (step 3) we generate a new image object with the structure of the reference image and the data interpolated from the moving information. This new image object is the moving image \"moved\" on to the reference image space and thus, both look aligned.

.pull-left[

  • The Archive (right) is a repository of templates and atlases
  • The Python Client (bottom) provides easy access (with lazy-loading) to the Archive
>>> from templateflow import api as tflow\n>>> tflow.get(\n...     'MNI152NLin6Asym',\n...     desc=None,\n...     resolution=1,\n...     suffix='T1w',\n...     extension='nii.gz'\n... )\nPosixPath('/templateflow_home/tpl-MNI152NLin6Asym/tpl-MNI152NLin6Asym_res-01_T1w.nii.gz')\n

.large[www.templateflow.org] ]

.pull-right[

]

???

One of the most ancient feature requests received from fMRIPrep early adopters was improving the flexibility of spatial normalization to standard templates other than fMRIPrep's default.

For instance, infant templates.

TemplateFlow offers an Archive of templates where they are stored, maintained and re-distributed;

and a Python client that helps accessing them.

On the right hand side, an screenshot of the TemplateFlow browser shows some of the templates currently available in the repository. The browser can be reached at www.templateflow.org.

The tool is based on PyBIDS, and the snippet will surely remind you of it. In this case the example shows how to obtain the T1w template corresponding to FSL's MNI space, at the highest resolution.

If the files requested are not in TemplateFlow's cache, they will be pulled down and kept for further utilization.

"},{"location":"assets/ORN-Workshop/presentation/#templateflow-archive","title":"TemplateFlow - Archive","text":"

.small[(Ciric et al. 2020, in prep)]

???

The Archive allows a rich range of data and metadata to be stored with the template.

Datatypes in the repository cover:

  • images containing population-average templates,
  • masks (for instance brain masks),
  • atlases (including parcellations and segmentations)
  • transform files between templates

Metadata can be stored with the usual BIDS options.

Finally, templates allow having multiple cohorts, in a similar encoding to that of multi-session BIDS datasets.

Multiple cohorts are useful, for instance, in infant templates with averages at several gestational ages.

NiWorkflows is a miscellaneous mixture of tooling used by downstream NiPreps:

???

NiWorkflows is, historically, the first component detached from fMRIPrep.

For that reason, its scope and vision has very fuzzy boundaries as compared to the other tools.

The most relevant utilities incorporated within NiWorkflows are:

--

  • The reportlet aggregation and individual report generation system

???

First, the individual report system which aggregates the visual elements or the reports (which we call \"reportlets\") and generates the final HTML document.

Also, most of the engineering behind the generation of these reportlets and their integration within NiPype are part of NiWorkflows

--

  • Custom extensions to NiPype interfaces

???

Beyond the extension of NiPype to generate a reportlet from any given interface, NiWorkflows is the test bed for many utilities that are then upstreamed to nipype.

Also, special interfaces with a limited scope that should not be included in nipype are maintained here.

--

  • Workflows useful across applications

???

Finally, NiWorkflows indeed offers workflows that can be used by end-user NiPreps. For instance atlas-based brain extraction of anatomical images, based on ANTs.

???

Echo-planar imaging (EPI) are typically affected by distortions along the phase encoding axis, caused by the perturbation of the magnetic field at tissue interfaces.

Looking at the reportlet, we can see how in the \"before\" panel, the image is warped.

The distortion is most obvious in the coronal view (middle row) because this image has posterior-anterior phase encoding.

Focusing on the changes between \"before\" and \"after\" correction in this coronal view, we can see how the blue contours delineating the corpus callosum fit better the dark shade in the data after correction.

"},{"location":"assets/ORN-Workshop/presentation/#upcoming-new-utilities","title":"Upcoming new utilities","text":""},{"location":"assets/ORN-Workshop/presentation/#nibabies-fmriprep-babies","title":"NiBabies | fMRIPrep-babies","text":"
  • Mathias Goncalves
"},{"location":"assets/ORN-Workshop/presentation/#nirodents-fmriprep-rodents","title":"NiRodents | fMRIPrep-rodents","text":"
  • Eilidh MacNicol

???

So, what's coming up next?

NiBabies is some sort of NiWorkflows equivalent for the preprocessing of infant imaging. At the moment, only atlas-based brain extraction using ANTs (and adapted from NiWorkflows) is in active developments.

Next steps include brain tissue segmentation.

Similarly, NiRodents is the NiWorkflows parallel for the prepocessing of rodent preclinical imaging. Again, only atlas-based brain extraction adapted from NiWorkflows is being developed.

"},{"location":"assets/ORN-Workshop/presentation/#nipreps-is-a-framework-for-the-development-of-preprocessing-workflows","title":"NiPreps is a framework for the development of preprocessing workflows","text":"
  • Principled design, with BIDS as an strategic component
  • Leveraging existing, widely used software
  • Using NiPype as a foundation

???

To wrap-up, I've presented NiPreps, a framework for developing preprocessing workflows inspired by fMRIPrep.

The framework is heavily principle and tags along BIDS as a foundational component

NiPreps should not reinvent any wheel, trying to reuse as much as possible of the widely used and tested existing software.

Nipype serves as a glue components to orchestrate workflows.

--

"},{"location":"assets/ORN-Workshop/presentation/#why-preprocessing","title":"Why preprocessing?","text":"
  • We propose to consider preprocessing as part of the image acquisition and reconstruction
  • When setting the boundaries that way, it seems sensible to pursue some standardization in the preprocessing:
  • Less experimental degrees of freedom for the researcher
  • Researchers can focus on the analysis
  • More homogeneous data at the output (e.g., for machine learning)
  • How:
  • Transparency is key to success: individual reports and documentation (open source is implicit).
  • Best engineering practices (e.g., containers and CI/CD)

???

But why just preprocessing, with a very strict scope?

We propose to think about preprocessing as part of the image acquisition and reconstruction process (in other words, scanning), rather than part of the analysis workflow.

This decoupling from analysis comes with several upshots:

First, there are less moving parts to play with for researchers in the attempt to fit their methods to the data (instead of fitting data with their methods).

Second, such division of labor allows the researcher to use their time in the analysis.

Finally, two preprocessed datasets from two different studies and scanning sites should be more homogeneous when processed with the same instruments, in comparison to processing them with idiosyncratic, lab-managed, preprocessing workflows.

However, for NiPreps to work we need to make sure the tools are transparent.

Not just with the individual reports and thorough documentation, also because of the community driven development. For instance, the peer-review process that goes around large incremental changes is fundamental to ensure the quality of the tool.

In addition, best engineering practices suggested in the BIDS-Apps paper, along with those we have been including with fMRIPrep, are necessary to ensure the quality of the final product.

--

"},{"location":"assets/ORN-Workshop/presentation/#challenges","title":"Challenges","text":"
  • Testing / Validation!

???

As an open problem, validating the results of the tool remains extremely challenging for the lack in gold standard datasets that can tell us the best possible outcome.

"},{"location":"assets/ORN-Workshop/presentation/#the-nmind-story","title":"The NMiND story","text":"

NMiND = NeverMIND, this Neuroimaging Method Is Not Duplicated

"},{"location":"assets/ORN-Workshop/presentation/#pis-worried-about-methodological-duplicity","title":"PIs worried about methodological duplicity","text":"

M. Milham, D. Fair, T. Satterthwaite, S. Ghosh, R. Poldrack, etc.

--

"},{"location":"assets/ORN-Workshop/presentation/#nminds-workgroups","title":"NMiND's workgroups","text":"

nosology group, coding standards & patterns, sharing standards, testing standards, crediting contributors, funding strategy, benchmarking datasets.

"},{"location":"assets/ORN-Workshop/presentation/#nminds-nosology-goals","title":"NMiND's nosology goals","text":"
  • Consensus glossary of terms
  • Landscape the portfolio of methodological solutions along several experimental and computational dimensions
  • Organize and document a taxonomy along those dimensions
  • Index existing software (from unit methods to workflows) in the taxonomy

Please Join!

template: newsection layout: false

.middle.center[

"},{"location":"assets/ORN-Workshop/presentation/#thanks","title":"Thanks!","text":""},{"location":"assets/ORN-Workshop/presentation/#questions","title":"Questions?","text":"

]

"},{"location":"assets/bhd2020/presentation/","title":"Presentation","text":"

layout: false count: false

.middle.center[

"},{"location":"assets/bhd2020/presentation/#nipreps-neuroimaging-preprocessing-tools","title":"NiPreps | NeuroImaging PREProcessing toolS","text":""},{"location":"assets/bhd2020/presentation/#o-esteban","title":"O. Esteban","text":""},{"location":"assets/bhd2020/presentation/#chuv-lausanne-university-hospital","title":"CHUV | Lausanne University Hospital","text":""},{"location":"assets/bhd2020/presentation/#wwwniprepsorgassetsbhd2020","title":"www.nipreps.org/assets/bhd2020","text":"

]

layout: false count: false

.middle.center[

"},{"location":"assets/bhd2020/presentation/#nipreps-neuroimaging-preprocessing-tools_1","title":"NiPreps | NeuroImaging PREProcessing toolS","text":""},{"location":"assets/bhd2020/presentation/#o-esteban_1","title":"O. Esteban","text":""},{"location":"assets/bhd2020/presentation/#chuv-lausanne-university-hospital_1","title":"CHUV | Lausanne University Hospital","text":""},{"location":"assets/bhd2020/presentation/#wwwniprepsorgassetsbhd2020_1","title":"www.nipreps.org/assets/bhd2020","text":"

]

???

"},{"location":"assets/bhd2020/presentation/#im-going-to-talk-about-how-we-are-building-a-framework-of-preprocessing-pipelines-for-neuroimaging-called-nipreps-based-on-the-fmriprep-experience","title":"I'm going to talk about how we are building a framework of preprocessing pipelines for neuroimaging called NiPreps, based on the fMRIPrep experience.","text":"

name: newsection layout: true class: section-separator

.perma-sidebar[

"},{"location":"assets/bhd2020/presentation/#bhd2020","title":"BHD2020","text":""},{"location":"assets/bhd2020/presentation/#day-2-14h-cet","title":"(Day 2, 14h CET)","text":""},{"location":"assets/bhd2020/presentation/#nipreps","title":"NiPreps","text":"

]

name: sidebar layout: true

.perma-sidebar[

"},{"location":"assets/bhd2020/presentation/#bhd2020_1","title":"BHD2020","text":""},{"location":"assets/bhd2020/presentation/#day-2-14h-cet_1","title":"(Day 2, 14h CET)","text":""},{"location":"assets/bhd2020/presentation/#nipreps_1","title":"NiPreps","text":"

]

template: sidebar

"},{"location":"assets/bhd2020/presentation/#outlook","title":"Outlook","text":""},{"location":"assets/bhd2020/presentation/#1-understand-what-preprocessing-is-from-fmri","title":"1. Understand what preprocessing is - from fMRI","text":""},{"location":"assets/bhd2020/presentation/#2-the-fmriprep-experience","title":"2. The fMRIPrep experience","text":""},{"location":"assets/bhd2020/presentation/#3-the-dmriprep-experience","title":"3. The dMRIPrep experience","text":""},{"location":"assets/bhd2020/presentation/#4-importance-of-the-visual-reports","title":"4. Importance of the visual reports","text":""},{"location":"assets/bhd2020/presentation/#5-introducing-nipreps","title":"5. Introducing NiPreps","text":""},{"location":"assets/bhd2020/presentation/#6-open-forum-first-steps-and-contributing","title":"6. Open forum: first steps and contributing","text":""},{"location":"assets/bhd2020/presentation/#the-research-workflow-of-functional-mri-nowadays","title":"The research workflow of functional MRI (nowadays)","text":"

(source: next slide)

"},{"location":"assets/bhd2020/presentation/#the-research-workflow-of-functional-mri-2006","title":"The research workflow of functional MRI (2006)","text":"

(Strother, 2006; 10.1109/MEMB.2006.1607667)

"},{"location":"assets/bhd2020/presentation/#the-research-workflow-of-functional-mri-ab","title":"The research workflow of functional MRI (a.B.*)","text":"

Adapted (Strother, 2006)

*a.B. = after BIDS (Brain Imaging Data Structure; Gorgolewski et al. (2016))

"},{"location":"assets/bhd2020/presentation/#neuroimaging-is-now-mature","title":"Neuroimaging is now mature","text":"
  • many excellent tools available (from specialized to foundational)
  • large toolboxes (AFNI, ANTs/ITK, FreeSurfer, FSL, Nilearn, SPM, etc.)
  • workflow software (Nipype, Shellscripts, Nextflow, CWL)
  • container technology, CI/CD

  • a wealth of prior knowledge (esp. about humans)

  • LOTS of data acquired everyday

"},{"location":"assets/bhd2020/presentation/#bids-a-thrust-of-technology-driven-development","title":"BIDS - A thrust of technology-driven development","text":"
  • A uniform and complete interface to data:

  • Uniform: enables the workflow adapt to the data

  • Complete: enables validation and minimizes human-intervention

  • Extensible reproducibility:

  • BIDS-Derivatives

  • BIDS-Apps (Gorgolewski et al., 2017)

???

  • researchers have a large portfolio of image processing components readily available
  • toolboxes with great support and active maintenance:
"},{"location":"assets/bhd2020/presentation/#new-questions-changing-the-focus","title":"New questions changing the focus:","text":""},{"location":"assets/bhd2020/presentation/#-validity-does-the-workflow-actually-work-out","title":"- validity (does the workflow actually work out?)","text":""},{"location":"assets/bhd2020/presentation/#-transparency-is-it-a-black-box-how-precise-is-reporting","title":"- transparency (is it a black-box? how precise is reporting?)","text":""},{"location":"assets/bhd2020/presentation/#-vibration-how-each-tool-choice-parameters-affect-overall","title":"- vibration (how each tool choice & parameters affect overall?)","text":""},{"location":"assets/bhd2020/presentation/#-throughput-how-much-datatime-can-it-possible-take","title":"- throughput (how much data/time can it possible take?)","text":""},{"location":"assets/bhd2020/presentation/#-robustness-can-i-use-it-on-diverse-studies","title":"- robustness (can I use it on diverse studies?)","text":""},{"location":"assets/bhd2020/presentation/#-evaluation-what-is-it-unique-about-the-workflow-wrt-existing-alternatives","title":"- evaluation (what is it unique about the workflow, w.r.t. existing alternatives?)","text":""},{"location":"assets/bhd2020/presentation/#the-garden-of-forking-paths","title":"The garden of forking paths","text":"

(Botvinik-Nezer et al., 2020)

Around 50% of teams used fMRIPrep'ed inputs.

"},{"location":"assets/bhd2020/presentation/#the-fmriprep-story","title":"The fMRIPrep story","text":""},{"location":"assets/bhd2020/presentation/#fmriprep-produces-analysis-ready-data-from-diverse-data","title":"fMRIPrep produces analysis-ready data from diverse data","text":"
  • minimal requirements (BIDS-compliant);
  • agnostic to downstream steps of the workflow
  • produces BIDS-Derivatives;
  • robust against inhomogeneity of data across studies

???

fMRIPrep takes in a task-based or resting-state functional MRI dataset in BIDS-format and returns preprocessed data ready for analysis.

Preprocessed data can be used for a broad range of analysis, and they are formatted following BIDS-Derivatives to maximize compatibility with: * major software packages (AFNI, FSL, SPM*, etc.) * further temporal filtering and denoising: fMRIDenoise * any BIDS-Derivatives compliant tool (e.g., FitLins).

--

"},{"location":"assets/bhd2020/presentation/#fmriprep-is-a-bids-app-gorgolewski-et-al-2017","title":"fMRIPrep is a BIDS-App (Gorgolewski, et al. 2017)","text":"
  • adhered to modern software-engineering standards (CI/CD, containers)
  • compatible interface with other BIDS-Apps
  • optimized for automatic execution

???

fMRIPrep adopts the BIDS-App specifications. That means the software is tested with every change to the codebase, it also means that packaging, containerization, and deployment are also automated and require tests to be passing. BIDS-Apps are inter-operable (via BIDS-Derivatives), and optimized for execution in HPC, Cloud, etc.

--

"},{"location":"assets/bhd2020/presentation/#minimizes-human-intervention","title":"Minimizes human intervention","text":"
  • avoid error-prone parameters settings (read them from BIDS)
  • adapts the workflow to the actual data available
  • while remaining flexible to some design choices (e.g., whether or not reconstructing surfaces or customizing target normalized standard spaces)

???

fMRIPrep minimizes human intervention because the user does not need to fiddle with any parameters - they are obtained from the BIDS structure. However, fMRIPrep does allow some flexibility to ensure the preprocessing meets the requirements of the intended analyses.

"},{"location":"assets/bhd2020/presentation/#fmriprep-was-not-originally-envisioned-as-a-community-project","title":"fMRIPrep was not originally envisioned as a community project ...","text":"

(we just wanted a robust tool to automatically preprocess incoming data of OpenNeuro.org)

--

"},{"location":"assets/bhd2020/presentation/#but-a-community-built-up-quickly-around-it","title":"... but a community built up quickly around it","text":"

--

.pull-left[

"},{"location":"assets/bhd2020/presentation/#why","title":"Why?","text":"
  • Preprocessing of fMRI was in need for division of labor.

  • Obsession with transparency made early-adopters confident of the recipes they were applying.

  • Responsiveness to feedback. ]

.pull-right[

]

???

Preprocessing is a time-consuming effort, requires expertise converging imaging foundations & CS, typically addressed with legacy in-house pipelines.

On the right-hand side, you'll find the chart of unique visitors to fmriprep.org, which is the documentation website.

"},{"location":"assets/bhd2020/presentation/#key-aspect-credit-all-direct-contributors","title":"Key aspect: credit all direct contributors","text":"

--

"},{"location":"assets/bhd2020/presentation/#and-indirect-citation-boilerplate","title":".. and indirect: citation boilerplate.","text":""},{"location":"assets/bhd2020/presentation/#researchers-want-to-spend-more-time-on-those-areas-most-relevant-to-them","title":"Researchers want to spend more time on those areas most relevant to them","text":"

(probably not preprocessing...)

???

With the development of fMRIPrep we understood that researchers don't want to waste their time on preprocessing (except for researchers developing new preprocessing techniques).

--

"},{"location":"assets/bhd2020/presentation/#writing-fmriprep-required-a-team-of-several-experts-in-processing-methods-for-neuroimaging-with-a-solid-base-on-computer-science","title":"Writing fMRIPrep required a team of several experts in processing methods for neuroimaging, with a solid base on Computer Science.","text":"

(research programs just can't cover the neuroscience and the engineering of the whole workflow - we need to divide the labor)

???

The current neuroimaging workflow requires extensive knowledge in sometimes orthogonal fields such as neuroscience and computer science. Dividing the labor in labs, communities or individuals with the necessary expertise is the fundamental for the advance of the whole field.

--

"},{"location":"assets/bhd2020/presentation/#transparency-helps-against-the-risk-of-super-easy-tools","title":"Transparency helps against the risk of super-easy tools","text":"

(easy-to-use tools are risky because they might get a researcher very far with no idea whatsoever of what they've done)

???

There is an implicit risk in making things too easy to operate:

For instance, imagine someone who runs fMRIPrep on diffusion data by tricking the BIDS naming into an apparently functional MRI dataset. If fMRIPrep reached the end at all, the garbage at the output could be fed into further tools, in a sort of a snowballing problem.

When researchers have access to the guts of the software and are given an opportunity to understand what's going on, the risk of misuse dips.

--

"},{"location":"assets/bhd2020/presentation/#established-toolboxes-do-not-have-incentives-for-compatibility","title":"Established toolboxes do not have incentives for compatibility","text":"

(and to some extent this is not necessarily bad, as long as they are kept well-tested and they embrace/help-develop some minimal standards)

???

AFNI, ANTs, FSL, FreeSurfer, SPM, etc. have comprehensive software validation tests, methodological validation tests, stress tests, etc. - which pushed up their quality and made them fundamental for the field.

Therefore, it is better to keep things that way (although some minimal efforts towards convergence in compatibility are of course welcome)

(Esteban et al., 2019)

"},{"location":"assets/bhd2020/presentation/#the-dmriprep-story","title":"The dMRIPrep story","text":"

After the success of fMRIPrep, Dr. A. Keshavan asked \"when a dMRIPrep?\"

"},{"location":"assets/bhd2020/presentation/#neurostarsorg","title":"NeuroStars.org","text":"

(please note this down)

"},{"location":"assets/bhd2020/presentation/#the-dmriprep-story_1","title":"The dMRIPrep story","text":"

After the success of fMRIPrep, Dr. A. Keshavan asked \"when a dMRIPrep?\"

Image Processing: Possible Guidelines for the Standardization & Clinical Applications

(Veraart, 2019)

"},{"location":"assets/bhd2020/presentation/#please-join","title":"Please join!","text":"

Joseph, M.; Pisner, D.; Richie-Halford, A.; Lerma-Usabiaga, G.; Keshavan, A.; Kent, JD.; Veraart, J.; Cieslak, M.; Poldrack, RA.; Rokem, A.; Esteban, O.

template: newsection layout: false

.middle.center[

"},{"location":"assets/bhd2020/presentation/#understanding-what-preprocessing-is-with-visual-reports","title":"Understanding what preprocessing is with visual reports","text":"

]

"},{"location":"assets/bhd2020/presentation/#the-individual-report","title":"The individual report","text":"

???

Let's walk through one example of report. Reports have several sections, starting with a summary indicating the particularities of this dataset and workflow choices made based on the input data.

The anatomical section follows with several visualizations to assess the anatomical processing steps mentioned before, spatial normalization to template spaces (the flickering panel helps assess alignment) and finally surface reconstruction.

Then, all functional runs are concatenated, and all show the same structure. After an initial summary of this particular run, the alignment to the same subject's anatomical image is presented, with contours of the white and pial surfaces as cues. Next panel shows the brain mask and ROIs utilized by the CompCor denoising. For each run we then find some visualizations to assess the generated confounding signals.

After all functional runs are presented, the About section keeps information to aid reproducibility of results, such as the software's version, or the exact command line run.

The boilerplate is found next, with a text version shown by default and tabs to convert to Markdown and LaTeX.

Reports conclude with a list of encountered errors (if any).

"},{"location":"assets/bhd2020/presentation/#reports-are-a-crucial-element-to-ensure-transparency","title":"Reports are a crucial element to ensure transparency","text":"

.pull-left[

]

.pull-right[

.distribute[ fMRIPrep generates one participant-wide report after execution.

Reports describe the data as found, and the steps applied (providing .blue[visual support to look inside the box]):

  1. show researchers their data;

  2. show how fMRIPrep interpreted the data (describing the actual preprocessing steps);

  3. quality control of results, facilitating early error detection. ] ]

???

Therefore, reports have become a fundamental feature of fMRIPrep because they not only allow assessing the quality of the processing, but also provide an insight about the logic supporting such processing.

In other words, reports help respond to the what was done and the why was it done in addition to the how well it did.

template: newsection layout: false

.middle.center[

"},{"location":"assets/bhd2020/presentation/#wwwniprepsorg","title":"www.nipreps.org","text":""},{"location":"assets/bhd2020/presentation/#nipreps-neuroimaging-preprocessing-tools_2","title":"(NiPreps == NeuroImaging PREProcessing toolS)","text":"

]

???

The enormous success of fMRIPrep led us to propose its generalization to other MRI and non-MRI modalities, as well as nonhuman species (for instance, rodents), and particular populations currently unsupported by fMRIPrep such as infants.

"},{"location":"assets/bhd2020/presentation/#augmenting-scanners-to-produce-analysis-grade-data","title":"Augmenting scanners to produce \"analysis-grade\" data","text":""},{"location":"assets/bhd2020/presentation/#data-directly-consumable-by-analyses","title":"(data directly consumable by analyses)","text":"

.pull-left[

Analysis-grade data is an analogy to the concept of \"sushi-grade (or sashimi-grade) fish\" in that both are:

.large[minimally preprocessed,]

and

.large[safe to consume directly.] ]

.pull-right[ ]

???

The goal, therefore, of NiPreps is to extend the scanner so that, in a way, they produce data ready for analysis.

We liken these analysis-grade data to sushi-grade fish, because in both cases the product is minimally preprocessed and at the same time safe to consume as is.

template: newsection layout: false

.middle.center[

"},{"location":"assets/bhd2020/presentation/#deconstructing-fmriprep","title":"Deconstructing fMRIPrep","text":"

]

???

For the last two years we've been decomposing the architecture of fMRIPrep, spinning off its constituent parts that are valuable in other applications.

This process of decoupling (to use a proper CS term) has been greatly facilitated by the modular nature of the code since its inception.

???

The processing elements extracted from fMRIPrep can be mapped to three regimes of responsibility:

  • Software infrastructure composed by tools ensuring the collaboration and the most basic tooling.
  • Middleware utilities, which build more advanced tooling based on the foundational infrastructure
  • And at the top of the stack end-user applications - namely fMRIPrep, dMRIPrep, sMRIPrep and MRIQC.

As we can see, the boundaries of these three architectural layers are soft and tools such as TemplateFlow may stand in between.

Only projects enclosed in the brain shape pertain to the NiPreps community. NiPype, NiBabel and BIDS are so deeply embedded as dependencies that NiPreps can't be understood without them.

  • BIDS provides a standard, guaranteeing I/O agreements:

  • Allows workflows to self-adapt to the inputs

  • Ensures the shareability of the results

  • PyBIDS: a Python tool to query BIDS datasets (Yarkoni et al., 2019):

>>> from bids import BIDSLayout\n\n# Point PyBIDS to the dataset's path\n>>> layout = BIDSLayout(\"/data/coolproject\")\n\n# List the participant IDs of present subjects\n>>> layout.get_subjects()\n['01', '02', '03', '04', '05']\n\n# List session identifiers, if present\n>>> layout.get_sessions()\n['01', '02']\n\n# List functional MRI tasks\n>>> layout.get_tasks()\n['rest', 'nback']\n

???

BIDS is one of the keys to success for fMRIPrep and consequently, a strategic element of NiPreps.

Because the tools so far are written in Python, PyBIDS is a powerful tool to index and query inputs and outputs.

The code snippet illustrates the ease to find out the subject identifiers available in the dataset, sessions, and tasks.

"},{"location":"assets/bhd2020/presentation/#bids-derivatives","title":"BIDS Derivatives","text":"

.cut-right[

derivatives/\n\u251c\u2500\u2500 fmriprep/\n\u2502 \u251c\u2500\u2500 dataset_description.json\n\u2502 \u251c\u2500\u2500 logs\n\u2502 \u251c\u2500\u2500 sub-01.html\n\u2502 \u251c\u2500\u2500 sub-01/\n\u2502 \u2502 \u251c\u2500\u2500 anat/\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_desc-brain_mask.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_dseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_label-GM_probseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_label-WM_probseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_label-CSF_probseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_desc-preproc_T1w.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_space-MNI152_desc-brain_mask.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_space-MNI152_dseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_space-MNI152_label-GM_probseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_space-MNI152_label-WM_probseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_space-MNI152_label-CSF_probseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_space-MNI152_desc-preproc_T1w.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_from-MNI152_to-T1w_mode-image_xfm.h5\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_from-T1w_to-MNI152_mode-image_xfm.h5\n\u2502 \u2502 \u2502 \u2514\u2500\u2500 sub-01_from-orig_to-T1w_mode-image_xfm.txt\n\u2502 \u2502 \u251c\u2500\u2500 figures/\n\u2502 \u2502 \u2514\u2500\u2500 func/\n\u2502 \u2502   \u251c\u2500\u2500 sub-01_task-rhymejudgment_space-MNI152_boldref.nii.gz\n\u2502 \u2502   \u251c\u2500\u2500 sub-01_task-rhymejudgment_space-MNI152_desc-preproc_bold.nii.gz\n\u2502 \u2502   \u251c\u2500\u2500 sub-01_task-rhymejudgment_space-MNI152_desc-confounds_regressors.nii.gz\n\u2502 \u2502   \u2514\u2500\u2500 sub-01_task-rhymejudgment_space-MNI152_desc-brain_mask.nii.gz\n
]

???

All NiPreps must write out BIDS-Derivatives. As illustrated in the example, the outputs of fMRIPrep are very similar to the BIDS standard for acquired data.

"},{"location":"assets/bhd2020/presentation/#bids-apps","title":"BIDS-Apps","text":"
  • BIDS-Apps proposes a workflow structure model:
  • Use of containers & CI/CD

  • Uniform interface: .cut-right[

    fmriprep /data /data/derivatives/fmriprep-20.1.1 participant [+OPTIONS]\n
    ]

???

All end-user applications in NiPreps must conform to the BIDS-Apps specifications.

The BIDS-Apps paper identified a common pattern in neuroimaging studies, where individual participants (and runs) are processed first individually, and then based on the outcomes, further levels of data aggregation are executed.

For this reason, BIDS-Apps define two major levels of execution: participant and group level.

Finally, the paper also stresses the importance of containerizing applications to ensure long-term preservation of run-to-run repeatability and proposes a common command line interface as described at the bottom:

  • first the name of the BIDS-Apps (fmriprep, in this case)
  • followed by input and output directories (respectively),
  • to finally indicate the analysis level (always participant, for the case of fmriprep)

.pull-left[

from nipype.interfaces.fsl import BET\nbrain_extract = BET(\n  in_file=\"/data/coolproject/sub-01/ses-01/anat/sub-01_ses-01_T1w.nii\",\n  out_file=\"/out/sub-01/ses-01/anat/sub-01_ses-01_desc-brain_T1w.nii\"\n)\nbrain_extract.run()\n

Nipype is the gateway to mix-and-match from AFNI, ANTs, Dipy, FreeSurfer, FSL, MRTrix, SPM, etc. ]

.pull-right[

]

???

Nipype is the glue stitching together all the underlying neuroimaging toolboxes and provides the execution framework.

The snippet shows how the widely known BET tool from FSL can be executed using NiPype. This is a particular example instance of interfaces - which provide uniform access to the tooling with Python.

Finally, combining these interfaces we generate processing workflows to fulfill higher level processing tasks.

???

For instance, we may have a look into fMRIPrep's functional processing block.

Nipype helps understand (and opens windows in the black box) generating these graph representation of the workflow.

\"\"\"Fix the affine of a rodent dataset, imposing 0.2x0.2x0.2 [mm].\"\"\"\nimport numpy as np\nimport nibabel as nb\n\n# Open the file\nimg = nb.load(\"sub-25_MGE_MouseBrain_3D_MGE_150.nii.gz\")\n\n# New (correct) affine\naff = np.diag((-0.2, -0.2, 0.2, 1.0))\n\n# Use nibabel to reorient to canonical\ncard = nb.as_closest_canonical(nb.Nifti1Image(\n    img.dataobj,\n    np.diag((-0.2, -0.2, 0.2, 1.0)),\n    None\n))\n\n# Save to disk\ncard.to_filename(\"sub-25_T2star.nii.gz\")\n

???

NiBabel allows Python to easily access neuroimaging data formats such as NIfTI, GIFTI and CIFTI2.

Although this might be a trivial task, the proliferation of neuroimaging software has led to some sort of Wild West of formats, and sometimes interoperation is not ensured.

"},{"location":"assets/bhd2020/presentation/#in-the-snippet-we-can-see-how-we-can-manipulate-the-orientation-headers-of-a-nifti-volume-in-particular-a-rodent-image-with-incorrect-affine-information","title":"In the snippet, we can see how we can manipulate the orientation headers of a NIfTI volume, in particular a rodent image with incorrect affine information.","text":"

.pull-left[

Transforms typically are the outcome of image registration methodologies

The proliferation of software implementations of image registration methodologies has resulted in a spread of data structures and file formats used to preserve and communicate transforms.

(Esteban et al., 2020) ]

.pull-right[

]

???

NiTransforms is a super-interesting toy project where we are exercising our finest coding skills. It completes NiBabel in the effort of making spatial transforms calculated by neuroimaging software tools interoperable.

When it goes beyond the alpha state, it is expected to be merged into NiBabel.

At the moment, NiTransforms is already integrated in fMRIPrep +20.1 to concatenate LTA (linear affine transforms) transforms obtained with FreeSurfer, ITK transforms obtained with ANTs, and motion parameters estimated with FSL.

Compatibility across formats is hard due to the many arbitrary decisions in establishing the mathematical framework of the transform and the intrinsic confusion of applying a transform.

While intuitively we understand applying a transform as \"transforming the moving image so that I can represent it overlaid or fused with the reference image and both should look aligned\", in reality, we only transform coordinates from the reference image into the moving image's space (step 1 on the right).

Once we know where the center of every voxel of the reference image falls in the moving image coordinate system, we read in the information (in other words, a value) from the moving image. Because the location will probably be off-grid, we interpolate such a value from the neighboring voxels (step 2).

Finally (step 3) we generate a new image object with the structure of the reference image and the data interpolated from the moving information. This new image object is the moving image \"moved\" on to the reference image space and thus, both look aligned.

.pull-left[

  • The Archive (right) is a repository of templates and atlases
  • The Python Client (bottom) provides easy access (with lazy-loading) to the Archive
>>> from templateflow import api as tflow\n>>> tflow.get(\n...     'MNI152NLin6Asym',\n...     desc=None,\n...     resolution=1,\n...     suffix='T1w',\n...     extension='nii.gz'\n... )\nPosixPath('/templateflow_home/tpl-MNI152NLin6Asym/tpl-MNI152NLin6Asym_res-01_T1w.nii.gz')\n

.large[www.templateflow.org] ]

.pull-right[

]

???

One of the most ancient feature requests received from fMRIPrep early adopters was improving the flexibility of spatial normalization to standard templates other than fMRIPrep's default.

For instance, infant templates.

TemplateFlow offers an Archive of templates where they are stored, maintained and re-distributed;

and a Python client that helps accessing them.

On the right hand side, an screenshot of the TemplateFlow browser shows some of the templates currently available in the repository. The browser can be reached at www.templateflow.org.

The tool is based on PyBIDS, and the snippet will surely remind you of it. In this case the example shows how to obtain the T1w template corresponding to FSL's MNI space, at the highest resolution.

If the files requested are not in TemplateFlow's cache, they will be pulled down and kept for further utilization.

"},{"location":"assets/bhd2020/presentation/#templateflow-archive","title":"TemplateFlow - Archive","text":"

.small[(Ciric et al. 2020, in prep)]

???

The Archive allows a rich range of data and metadata to be stored with the template.

Datatypes in the repository cover:

  • images containing population-average templates,
  • masks (for instance brain masks),
  • atlases (including parcellations and segmentations)
  • transform files between templates

Metadata can be stored with the usual BIDS options.

Finally, templates allow having multiple cohorts, in a similar encoding to that of multi-session BIDS datasets.

Multiple cohorts are useful, for instance, in infant templates with averages at several gestational ages.

NiWorkflows is a miscellaneous mixture of tooling used by downstream NiPreps:

???

NiWorkflows is, historically, the first component detached from fMRIPrep.

For that reason, its scope and vision has very fuzzy boundaries as compared to the other tools.

The most relevant utilities incorporated within NiWorkflows are:

--

  • The reportlet aggregation and individual report generation system

???

First, the individual report system which aggregates the visual elements or the reports (which we call \"reportlets\") and generates the final HTML document.

Also, most of the engineering behind the generation of these reportlets and their integration within NiPype are part of NiWorkflows

--

  • Custom extensions to NiPype interfaces

???

Beyond the extension of NiPype to generate a reportlet from any given interface, NiWorkflows is the test bed for many utilities that are then upstreamed to nipype.

Also, special interfaces with a limited scope that should not be included in nipype are maintained here.

--

  • Workflows useful across applications

???

Finally, NiWorkflows indeed offers workflows that can be used by end-user NiPreps. For instance atlas-based brain extraction of anatomical images, based on ANTs.

???

Echo-planar imaging (EPI) are typically affected by distortions along the phase encoding axis, caused by the perturbation of the magnetic field at tissue interfaces.

Looking at the reportlet, we can see how in the \"before\" panel, the image is warped.

The distortion is most obvious in the coronal view (middle row) because this image has posterior-anterior phase encoding.

Focusing on the changes between \"before\" and \"after\" correction in this coronal view, we can see how the blue contours delineating the corpus callosum fit better the dark shade in the data after correction.

"},{"location":"assets/bhd2020/presentation/#upcoming-new-utilities","title":"Upcoming new utilities","text":""},{"location":"assets/bhd2020/presentation/#nibabies-fmriprep-babies","title":"NiBabies | fMRIPrep-babies","text":"
  • Mathias Goncalves
"},{"location":"assets/bhd2020/presentation/#nirodents-fmriprep-rodents","title":"NiRodents | fMRIPrep-rodents","text":"
  • Eilidh MacNicol

???

So, what's coming up next?

NiBabies is some sort of NiWorkflows equivalent for the preprocessing of infant imaging. At the moment, only atlas-based brain extraction using ANTs (and adapted from NiWorkflows) is in active developments.

Next steps include brain tissue segmentation.

Similarly, NiRodents is the NiWorkflows parallel for the prepocessing of rodent preclinical imaging. Again, only atlas-based brain extraction adapted from NiWorkflows is being developed.

"},{"location":"assets/bhd2020/presentation/#nipreps-is-a-framework-for-the-development-of-preprocessing-workflows","title":"NiPreps is a framework for the development of preprocessing workflows","text":"
  • Principled design, with BIDS as an strategic component
  • Leveraging existing, widely used software
  • Using NiPype as a foundation

???

To wrap-up, I've presented NiPreps, a framework for developing preprocessing workflows inspired by fMRIPrep.

The framework is heavily principle and tags along BIDS as a foundational component

NiPreps should not reinvent any wheel, trying to reuse as much as possible of the widely used and tested existing software.

Nipype serves as a glue components to orchestrate workflows.

--

"},{"location":"assets/bhd2020/presentation/#why-preprocessing","title":"Why preprocessing?","text":"
  • We propose to consider preprocessing as part of the image acquisition and reconstruction
  • When setting the boundaries that way, it seems sensible to pursue some standardization in the preprocessing:
  • Less experimental degrees of freedom for the researcher
  • Researchers can focus on the analysis
  • More homogeneous data at the output (e.g., for machine learning)
  • How:
  • Transparency is key to success: individual reports and documentation (open source is implicit).
  • Best engineering practices (e.g., containers and CI/CD)

???

But why just preprocessing, with a very strict scope?

We propose to think about preprocessing as part of the image acquisition and reconstruction process (in other words, scanning), rather than part of the analysis workflow.

This decoupling from analysis comes with several upshots:

First, there are less moving parts to play with for researchers in the attempt to fit their methods to the data (instead of fitting data with their methods).

Second, such division of labor allows the researcher to use their time in the analysis.

Finally, two preprocessed datasets from two different studies and scanning sites should be more homogeneous when processed with the same instruments, in comparison to processing them with idiosyncratic, lab-managed, preprocessing workflows.

However, for NiPreps to work we need to make sure the tools are transparent.

Not just with the individual reports and thorough documentation, also because of the community driven development. For instance, the peer-review process that goes around large incremental changes is fundamental to ensure the quality of the tool.

In addition, best engineering practices suggested in the BIDS-Apps paper, along with those we have been including with fMRIPrep, are necessary to ensure the quality of the final product.

--

"},{"location":"assets/bhd2020/presentation/#challenges","title":"Challenges","text":"
  • Testing / Validation!

???

As an open problem, validating the results of the tool remains extremely challenging for the lack in gold standard datasets that can tell us the best possible outcome.

template: newsection layout: false

.middle.center[

"},{"location":"assets/bhd2020/presentation/#where-to-start","title":"Where to start?","text":""},{"location":"assets/bhd2020/presentation/#wwwniprepsorg_1","title":"www.nipreps.org","text":""},{"location":"assets/bhd2020/presentation/#githubcomnipreps","title":"github.com/nipreps","text":"

]

template: newsection layout: false

.middle.center[

"},{"location":"assets/bhd2020/presentation/#thanks","title":"Thanks!","text":""},{"location":"assets/bhd2020/presentation/#questions","title":"Questions?","text":"

]

"},{"location":"assets/torw2020/presentation/","title":"Presentation","text":"

layout: false count: false

.middle.center[

"},{"location":"assets/torw2020/presentation/#building-next-generation-preprocessing-pipelines","title":"Building next-generation preprocessing pipelines:","text":""},{"location":"assets/torw2020/presentation/#the-fmriprep-experience","title":"the fMRIPrep experience","text":""},{"location":"assets/torw2020/presentation/#o-esteban","title":"O. Esteban","text":""},{"location":"assets/torw2020/presentation/#center-for-reproducible-neuroscience","title":"Center for Reproducible Neuroscience","text":""},{"location":"assets/torw2020/presentation/#stanford-university","title":"Stanford University","text":""},{"location":"assets/torw2020/presentation/#wwwniprepsorg","title":"www.nipreps.org","text":"

]

layout: false count: false

.middle.center[

"},{"location":"assets/torw2020/presentation/#building-next-generation-preprocessing-pipelines_1","title":"Building next-generation preprocessing pipelines:","text":""},{"location":"assets/torw2020/presentation/#the-fmriprep-experience_1","title":"the fMRIPrep experience","text":""},{"location":"assets/torw2020/presentation/#o-esteban_1","title":"O. Esteban","text":""},{"location":"assets/torw2020/presentation/#center-for-reproducible-neuroscience_1","title":"Center for Reproducible Neuroscience","text":""},{"location":"assets/torw2020/presentation/#stanford-university_1","title":"Stanford University","text":""},{"location":"assets/torw2020/presentation/#wwwniprepsorg_1","title":"www.nipreps.org","text":"

]

???

"},{"location":"assets/torw2020/presentation/#im-going-to-talk-about-how-we-are-building-a-framework-of-preprocessing-pipelines-for-neuroimaging-called-nipreps-based-on-the-fmriprep-experience","title":"I'm going to talk about how we are building a framework of preprocessing pipelines for neuroimaging called NiPreps, based on the fMRIPrep experience.","text":"

name: newsection layout: true class: section-separator

.perma-sidebar[

"},{"location":"assets/torw2020/presentation/#torw2020","title":"TORW2020","text":""},{"location":"assets/torw2020/presentation/#talk-12","title":"Talk 12","text":""},{"location":"assets/torw2020/presentation/#nipreps","title":"NiPreps","text":"

]

template: newsection layout: false

.middle.center[

"},{"location":"assets/torw2020/presentation/#introducing-fmriprep","title":"Introducing fMRIPrep","text":"

]

???

Let's begin with some of the history behind fMRIPrep.

name: sidebar layout: true

.perma-sidebar[

"},{"location":"assets/torw2020/presentation/#torw2020_1","title":"TORW2020","text":""},{"location":"assets/torw2020/presentation/#talk-12_1","title":"Talk 12","text":""},{"location":"assets/torw2020/presentation/#nipreps_1","title":"NiPreps","text":"

]

template: sidebar

"},{"location":"assets/torw2020/presentation/#fmriprep-produces-analysis-ready-data-from-acquired-fmri-data","title":"fMRIPrep produces analysis-ready data from acquired (fMRI) data","text":"
  • minimal requirements (BIDS-compliant);
  • agnostic to downstream steps of the workflow
  • produces BIDS-Derivatives;

???

fMRIPrep takes in a task-based or resting-state functional MRI dataset in BIDS-format and returns preprocessed data ready for analysis.

Preprocessed data can be used for a broad range of analysis, and they are formatted following BIDS-Derivatives to maximize compatibility with: * major software packages (AFNI, FSL, SPM*, etc.) * further temporal filtering and denoising: fMRIDenoise * any BIDS-Derivatives compliant tool (e.g., FitLins).

--

"},{"location":"assets/torw2020/presentation/#fmriprep-is-a-bids-app-gorgolewski-et-al-2017","title":"fMRIPrep is a BIDS-App (Gorgolewski, et al. 2017)","text":"
  • adhered to modern software-engineering standards (CI/CD, containers)
  • compatible interface with other BIDS-Apps
  • optimized for automatic execution

???

fMRIPrep adopts the BIDS-App specifications. That means the software is tested with every change to the codebase, it also means that packaging, containerization, and deployment are also automated and require tests to be passing. BIDS-Apps are inter-operable (via BIDS-Derivatives), and optimized for execution in HPC, Cloud, etc.

--

"},{"location":"assets/torw2020/presentation/#minimizes-human-intervention","title":"Minimizes human intervention","text":"
  • avoid error-prone parameters settings (read them from BIDS)
  • adapts the workflow to the actual data available
  • while remaining flexible to some design choices (e.g., whether or not reconstructing surfaces or customizing target normalized standard spaces)

???

fMRIPrep minimizes human intervention because the user does not need to fiddle with any parameters - they are obtained from the BIDS structure. However, fMRIPrep does allow some flexibility to ensure the preprocessing meets the requirements of the intended analyses.

--

"},{"location":"assets/torw2020/presentation/#fmriprep-bundles-many-tools-afni-fsl-freesurfer-nilearn-etc","title":"fMRIPrep bundles many tools (AFNI, FSL, FreeSurfer, Nilearn, etc.)","text":"
  • (do not reinvent the wheel)

???

Finally, fMRIPrep sits on top of giants' shoulders: AFNI, FSL, FreeSurfer, Nilearn, etc. all implement methods very well backed-up and are thoroughly tested on their own.

"},{"location":"assets/torw2020/presentation/#we-started-fmriprep-in-february-2016","title":"We started fMRIPrep in February 2016","text":""},{"location":"assets/torw2020/presentation/#objectives","title":"Objectives:","text":"
  • Develop an fMRI preprocessing tool enforcing BIDS for the inputs
  • Automatically executable within OpenNeuro
"},{"location":"assets/torw2020/presentation/#initially-inspired-by-hcp-pipelines","title":"Initially inspired by HCP Pipelines","text":"
  • Problem: robustness vs. the wide variability of inputs

???

We began working on fMRIPrep back in 2016 with much more humble expectations: - We needed to develop an fMRI preprocessing tool leveraging BIDS - smart enough to adapt the workflow for the input dataset, - and the tool should be executable in OpenNeuro without human intervention.

Please note that at the time, the BIDS-Apps specification didn't exist yet.

We started out with an eye on HCP Pipelines, and soon identified that datasets in OpenNeuro varied extremely in terms of acquisition protocols and imaging parameters, which is definitely not a problem for HCP Pipelines, which has very specific requirements for the inputs.

"},{"location":"assets/torw2020/presentation/#fmriprep-adoption-and-popularization-brought-new-challenges","title":"fMRIPrep adoption and popularization brought new challenges","text":"

.pull-right[

]

???

With the fast adoption and popularization of fMRIPrep, new challenges surfaced.

On the right-hand side, you'll find the chart of unique visitors to fmriprep.org, which is the documentation website.

--

.pull-left[

"},{"location":"assets/torw2020/presentation/#transparency-was-addressed-with","title":"Transparency was addressed with:","text":"
  • the individual reports;
  • the thorough documentation; and
  • the citation boilerplate. ]

???

We realized that transparency is indeed a very hard problem. The first leg of our solution was the creation of a solid report system. fMRIPrep generates one individual report per participant, containing information not just to quality control the results, but also to understand the processing flow.

We also strived for a comprehensive, thorough documentation.

Finally, the so-called citation boilerplate appended to the individual reports describe the actual workflow that has been run, noting all the software that was applied including their versions and references.

--

.pull-left[

"},{"location":"assets/torw2020/presentation/#run-to-run-repeatability-is-an-open-issue","title":"Run-to-run repeatability is an open issue:","text":"
  • computational precautions (e.g., unpredictable float truncation/rounding)
  • keep track of all random seeds (version +20.1) ]

???

Reproducibility in terms of run-to-run repeatability of results become as a more apparent problem, and we are always trying to minimize the vibration caused by computational factors, software versions, etc.

--

.pull-left[

"},{"location":"assets/torw2020/presentation/#overwhelming-feedback","title":"Overwhelming feedback:","text":"
  • massive amounts of bug reports, questioning the robustness
  • organic emergence of fMRIPrep enthusiasts (thanks to E. DuPre, JD. Kent) ]

???

We always maintained close attention to all the feedback channels. At some point we were washed over with bug reports that we needed to address. We also started to doubt the robustness against the variability of inputs, and set a thorough stress-test plan using data from OpenNeuro (reported in our Nat Meth paper). Among this feedback flooding, some external friends started to emerge and lent their shoulders in answering questions, fixing bugs, etc.

In particular, I want to thank Elizabeth DuPre (McGill) and James Kent (Univ. of Iowa) for being the earliest adopters and contributors.

"},{"location":"assets/torw2020/presentation/#fmriprep-is-stable-today-although-unfinished","title":"fMRIPrep is stable today, although unfinished","text":"

(Esteban et al., 2019)

???

These developments resulted in the following default processing workflow.

At the highest level, anatomical preprocessing (left-hand block) and functional preprocessing (right-hand block) can be clearly identified as the largest workflow units.

fMRIPrep combines all the anatomical images at the input in one anatomical reference, removes the intensity non-uniformity, delineates brain tissues, reconstructs surfaces, spatially normalizes the anatomical reference to one or more standard spaces.

On the functional pathway, a reference is calculated for further processes, then head-motion parameters are estimated (please note head-motion is accounted for in the last resampling step, in combination with other transforms), slice-timing correction is applied if requested.

Then, susceptibility distortion is estimated, if sufficient information (in terms of acquisition and metadata) is found in the BIDS structure.

Finally, data are mapped to the same individual's anatomical reference and outputs in the several output spaces requested are generated, along with a file gathering time-series of nuisance signals.

"},{"location":"assets/torw2020/presentation/#the-individual-report","title":"The individual report","text":"

???

Let's walk through one example of report. Reports have several sections, starting with a summary indicating the particularities of this dataset and workflow choices made based on the input data.

The anatomical section follows with several visualizations to assess the anatomical processing steps mentioned before, spatial normalization to template spaces (the flickering panel helps assess alignment) and finally surface reconstruction.

Then, all functional runs are concatenated, and all show the same structure. After an initial summary of this particular run, the alignment to the same subject's anatomical image is presented, with contours of the white and pial surfaces as cues. Next panel shows the brain mask and ROIs utilized by the CompCor denoising. For each run we then find some visualizations to assess the generated confounding signals.

After all functional runs are presented, the About section keeps information to aid reproducibility of results, such as the software's version, or the exact command line run.

The boilerplate is found next, with a text version shown by default and tabs to convert to Markdown and LaTeX.

Reports conclude with a list of encountered errors (if any).

"},{"location":"assets/torw2020/presentation/#reports-are-a-crucial-element-to-ensure-transparency","title":"Reports are a crucial element to ensure transparency","text":"

.pull-left[

]

.pull-right[

.distribute[ fMRIPrep generates one participant-wide report after execution.

Reports describe the data as found, and the steps applied (providing .blue[visual support to look inside the box]):

  1. show researchers their data;

  2. show how fMRIPrep interpreted the data (describing the actual preprocessing steps);

  3. quality control of results, facilitating early error detection. ] ]

???

Therefore, reports have become a fundamental feature of fMRIPrep because they not only allow assessing the quality of the processing, but also provide an insight about the logic supporting such processing.

In other words, reports help respond to the what was done and the why was it done in addition to the how well it did.

"},{"location":"assets/torw2020/presentation/#documentation-as-a-second-leg-of-transparency-fmripreporg","title":"Documentation as a second leg of transparency (fmriprep.org)","text":"
  • Hackathons & docu-sprints

  • the CompCor documentation example

.large[fmriprep.org]

???

We promptly identified the need for a very comprehensive documentation. The website at fmriprep.org covers a substantial area of how the tool works under the hood and how to best operate it.

The documentation turned out to be a great ice breaker for contributors, who have pushed forward fundamental sections of it.

Most of the largest increments in documentation are the result of discussions in hackathons, docusprints, neurostars, github, etc. A hallmark example was pull request 1877 by Karolina Finc, who gathered together a massive amount of knowledge from many contributors. Now this is up and open in our documentation website.

"},{"location":"assets/torw2020/presentation/#fmriprep-is-more-of-a-community-driven-project-every-day","title":"fMRIPrep is more of a community-driven project every day","text":"
  • Bug-fixes: we ensured that open feedback channels were attended (GitHub, NeuroStars, mailing list, etc.);

  • users began also proposing new features (some including code!);

  • with NiPreps we are working towards handling the project over to the community.

???

To ensure the future sustainability of the project (what some developers call Bus factor), we are transitioning the tool to NiPreps, transferring the large community nurtured over the past four years with it.

--

"},{"location":"assets/torw2020/presentation/#how-does-fmriprep-compensate-its-contributors","title":"How does fMRIPrep compensate its contributors?","text":"
  • Contributors are invited to coauthor relevant publications about fMRIPrep.
  • Anyone who helps with documentation, code or relevant discussions is a contributor.

.pull-left[

]

.pull-right[

]

???

In return, beyond the rewards of being part of an open source project, fMRIPrep gives some scientific credit back in the form of publications.

  • All contributors are invited to coauthor these publications.
  • Anything that helps the project is considered a sufficient contribution.
"},{"location":"assets/torw2020/presentation/#lessons-learned","title":"Lessons learned","text":""},{"location":"assets/torw2020/presentation/#researchers-want-to-spend-more-time-on-those-areas-most-relevant-to-them","title":"Researchers want to spend more time on those areas most relevant to them","text":"

(probably not preprocessing...)

???

With the development of fMRIPrep we understood that researchers don't want to waste their time on preprocessing (except for researchers developing new preprocessing techniques).

--

"},{"location":"assets/torw2020/presentation/#writing-fmriprep-required-a-team-of-several-experts-in-processing-methods-for-neuroimaging-with-a-solid-base-on-computer-science","title":"Writing fMRIPrep required a team of several experts in processing methods for neuroimaging, with a solid base on Computer Science.","text":"

(research programs just can't cover the neuroscience and the engineering of the whole workflow - we need to divide the labor)

???

The current neuroimaging workflow requires extensive knowledge in sometimes orthogonal fields such as neuroscience and computer science. Dividing the labor in labs, communities or individuals with the necessary expertise is the fundamental for the advance of the whole field.

--

"},{"location":"assets/torw2020/presentation/#transparency-helps-against-the-risk-of-super-easy-tools","title":"Transparency helps against the risk of super-easy tools","text":"

(easy-to-use tools are risky because they might get a researcher very far with no idea whatsoever of what they've done)

???

There is an implicit risk in making things too easy to operate:

For instance, imagine someone who runs fMRIPrep on diffusion data by tricking the BIDS naming into an apparently functional MRI dataset. If fMRIPrep reached the end at all, the garbage at the output could be fed into further tools, in a sort of a snowballing problem.

When researchers have access to the guts of the software and are given an opportunity to understand what's going on, the risk of misuse dips.

--

"},{"location":"assets/torw2020/presentation/#established-toolboxes-do-not-have-incentives-for-compatibility","title":"Established toolboxes do not have incentives for compatibility","text":"

(and to some extent this is not necessarily bad, as long as they are kept well-tested and they embrace/help-develop some minimal standards)

???

AFNI, ANTs, FSL, FreeSurfer, SPM, etc. have comprehensive software validation tests, methodological validation tests, stress tests, etc. - which pushed up their quality and made them fundamental for the field.

Therefore, it is better to keep things that way (although some minimal efforts towards convergence in compatibility are of course welcome)

template: newsection layout: false

.middle.center[

"},{"location":"assets/torw2020/presentation/#wwwniprepsorg_2","title":"www.nipreps.org","text":""},{"location":"assets/torw2020/presentation/#nipreps-neuroimaging-preprocessing-tools","title":"(NiPreps == NeuroImaging PREProcessing toolS)","text":"

]

???

The enormous success of fMRIPrep led us to propose its generalization to other MRI and non-MRI modalities, as well as nonhuman species (for instance, rodents), and particular populations currently unsupported by fMRIPrep such as infants.

"},{"location":"assets/torw2020/presentation/#augmenting-scanners-to-produce-analysis-grade-data","title":"Augmenting scanners to produce \"analysis-grade\" data","text":""},{"location":"assets/torw2020/presentation/#data-directly-consumable-by-analyses","title":"(data directly consumable by analyses)","text":"

.pull-left[

Analysis-grade data is an analogy to the concept of \"sushi-grade (or sashimi-grade) fish\" in that both are:

.large[minimally preprocessed,]

and

.large[safe to consume directly.] ]

.pull-right[ ]

???

The goal, therefore, of NiPreps is to extend the scanner so that, in a way, they produce data ready for analysis.

We liken these analysis-grade data to sushi-grade fish, because in both cases the product is minimally preprocessed and at the same time safe to consume as is.

template: newsection layout: false

.middle.center[

"},{"location":"assets/torw2020/presentation/#deconstructing-fmriprep","title":"Deconstructing fMRIPrep","text":"

]

???

For the last two years we've been decomposing the architecture of fMRIPrep, spinning off its constituent parts that are valuable in other applications.

This process of decoupling (to use a proper CS term) has been greatly facilitated by the modular nature of the code since its inception.

???

The processing elements extracted from fMRIPrep can be mapped to three regimes of responsibility:

  • Software infrastructure composed by tools ensuring the collaboration and the most basic tooling.
  • Middleware utilities, which build more advanced tooling based on the foundational infrastructure
  • And at the top of the stack end-user applications - namely fMRIPrep, dMRIPrep, sMRIPrep and MRIQC.

As we can see, the boundaries of these three architectural layers are soft and tools such as TemplateFlow may stand in between.

Only projects enclosed in the brain shape pertain to the NiPreps community. NiPype, NiBabel and BIDS are so deeply embedded as dependencies that NiPreps can't be understood without them.

  • BIDS provides a standard, guaranteeing I/O agreements:

  • Allows workflows to self-adapt to the inputs

  • Ensures the shareability of the results

  • PyBIDS: a Python tool to query BIDS datasets (Yarkoni et al., 2019):

>>> from bids import BIDSLayout\n\n# Point PyBIDS to the dataset's path\n>>> layout = BIDSLayout(\"/data/coolproject\")\n\n# List the participant IDs of present subjects\n>>> layout.get_subjects()\n['01', '02', '03', '04', '05']\n\n# List session identifiers, if present\n>>> layout.get_sessions()\n['01', '02']\n\n# List functional MRI tasks\n>>> layout.get_tasks()\n['rest', 'nback']\n

???

BIDS is one of the keys to success for fMRIPrep and consequently, a strategic element of NiPreps.

Because the tools so far are written in Python, PyBIDS is a powerful tool to index and query inputs and outputs.

The code snippet illustrates the ease to find out the subject identifiers available in the dataset, sessions, and tasks.

"},{"location":"assets/torw2020/presentation/#bids-derivatives","title":"BIDS Derivatives","text":"

.cut-right[

derivatives/\n\u251c\u2500\u2500 fmriprep/\n\u2502 \u251c\u2500\u2500 dataset_description.json\n\u2502 \u251c\u2500\u2500 logs\n\u2502 \u251c\u2500\u2500 sub-01.html\n\u2502 \u251c\u2500\u2500 sub-01/\n\u2502 \u2502 \u251c\u2500\u2500 anat/\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_desc-brain_mask.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_dseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_label-GM_probseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_label-WM_probseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_label-CSF_probseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_desc-preproc_T1w.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_space-MNI152_desc-brain_mask.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_space-MNI152_dseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_space-MNI152_label-GM_probseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_space-MNI152_label-WM_probseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_space-MNI152_label-CSF_probseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_space-MNI152_desc-preproc_T1w.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_from-MNI152_to-T1w_mode-image_xfm.h5\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_from-T1w_to-MNI152_mode-image_xfm.h5\n\u2502 \u2502 \u2502 \u2514\u2500\u2500 sub-01_from-orig_to-T1w_mode-image_xfm.txt\n\u2502 \u2502 \u251c\u2500\u2500 figures/\n\u2502 \u2502 \u2514\u2500\u2500 func/\n\u2502 \u2502   \u251c\u2500\u2500 sub-01_task-rhymejudgment_space-MNI152_boldref.nii.gz\n\u2502 \u2502   \u251c\u2500\u2500 sub-01_task-rhymejudgment_space-MNI152_desc-preproc_bold.nii.gz\n\u2502 \u2502   \u251c\u2500\u2500 sub-01_task-rhymejudgment_space-MNI152_desc-confounds_regressors.nii.gz\n\u2502 \u2502   \u2514\u2500\u2500 sub-01_task-rhymejudgment_space-MNI152_desc-brain_mask.nii.gz\n
]

???

All NiPreps must write out BIDS-Derivatives. As illustrated in the example, the outputs of fMRIPrep are very similar to the BIDS standard for acquired data.

"},{"location":"assets/torw2020/presentation/#bids-apps","title":"BIDS-Apps","text":"
  • BIDS-Apps proposes a workflow structure model:
  • Use of containers & CI/CD

  • Uniform interface: .cut-right[

    fmriprep /data /data/derivatives/fmriprep-20.1.1 participant [+OPTIONS]\n
    ]

???

All end-user applications in NiPreps must conform to the BIDS-Apps specifications.

The BIDS-Apps paper identified a common pattern in neuroimaging studies, where individual participants (and runs) are processed first individually, and then based on the outcomes, further levels of data aggregation are executed.

For this reason, BIDS-Apps define two major levels of execution: participant and group level.

Finally, the paper also stresses the importance of containerizing applications to ensure long-term preservation of run-to-run repeatability and proposes a common command line interface as described at the bottom:

  • first the name of the BIDS-Apps (fmriprep, in this case)
  • followed by input and output directories (respectively),
  • to finally indicate the analysis level (always participant, for the case of fmriprep)

.pull-left[

from nipype.interfaces.fsl import BET\nbrain_extract = BET(\n  in_file=\"/data/coolproject/sub-01/ses-01/anat/sub-01_ses-01_T1w.nii\",\n  out_file=\"/out/sub-01/ses-01/anat/sub-01_ses-01_desc-brain_T1w.nii\"\n)\nbrain_extract.run()\n

Nipype is the gateway to mix-and-match from AFNI, ANTs, Dipy, FreeSurfer, FSL, MRTrix, SPM, etc. ]

.pull-right[

]

???

Nipype is the glue stitching together all the underlying neuroimaging toolboxes and provides the execution framework.

The snippet shows how the widely known BET tool from FSL can be executed using NiPype. This is a particular example instance of interfaces - which provide uniform access to the tooling with Python.

Finally, combining these interfaces we generate processing workflows to fulfill higher level processing tasks.

???

For instance, we may have a look into fMRIPrep's functional processing block.

Nipype helps understand (and opens windows in the black box) generating these graph representation of the workflow.

\"\"\"Fix the affine of a rodent dataset, imposing 0.2x0.2x0.2 [mm].\"\"\"\nimport numpy as np\nimport nibabel as nb\n\n# Open the file\nimg = nb.load(\"sub-25_MGE_MouseBrain_3D_MGE_150.nii.gz\")\n\n# New (correct) affine\naff = np.diag((-0.2, -0.2, 0.2, 1.0))\n\n# Use nibabel to reorient to canonical\ncard = nb.as_closest_canonical(nb.Nifti1Image(\n    img.dataobj,\n    np.diag((-0.2, -0.2, 0.2, 1.0)),\n    None\n))\n\n# Save to disk\ncard.to_filename(\"sub-25_T2star.nii.gz\")\n

???

NiBabel allows Python to easily access neuroimaging data formats such as NIfTI, GIFTI and CIFTI2.

Although this might be a trivial task, the proliferation of neuroimaging software has led to some sort of Wild West of formats, and sometimes interoperation is not ensured.

"},{"location":"assets/torw2020/presentation/#in-the-snippet-we-can-see-how-we-can-manipulate-the-orientation-headers-of-a-nifti-volume-in-particular-a-rodent-image-with-incorrect-affine-information","title":"In the snippet, we can see how we can manipulate the orientation headers of a NIfTI volume, in particular a rodent image with incorrect affine information.","text":"

.pull-left[

Transforms typically are the outcome of image registration methodologies

The proliferation of software implementations of image registration methodologies has resulted in a spread of data structures and file formats used to preserve and communicate transforms.

(Esteban et al., 2020) ]

.pull-right[

]

???

NiTransforms is a super-interesting toy project where we are exercising our finest coding skills. It completes NiBabel in the effort of making spatial transforms calculated by neuroimaging software tools interoperable.

When it goes beyond the alpha state, it is expected to be merged into NiBabel.

At the moment, NiTransforms is already integrated in fMRIPrep +20.1 to concatenate LTA (linear affine transforms) transforms obtained with FreeSurfer, ITK transforms obtained with ANTs, and motion parameters estimated with FSL.

Compatibility across formats is hard due to the many arbitrary decisions in establishing the mathematical framework of the transform and the intrinsic confusion of applying a transform.

While intuitively we understand applying a transform as \"transforming the moving image so that I can represent it overlaid or fused with the reference image and both should look aligned\", in reality, we only transform coordinates from the reference image into the moving image's space (step 1 on the right).

Once we know where the center of every voxel of the reference image falls in the moving image coordinate system, we read in the information (in other words, a value) from the moving image. Because the location will probably be off-grid, we interpolate such a value from the neighboring voxels (step 2).

Finally (step 3) we generate a new image object with the structure of the reference image and the data interpolated from the moving information. This new image object is the moving image \"moved\" on to the reference image space and thus, both look aligned.

.pull-left[

  • The Archive (right) is a repository of templates and atlases
  • The Python Client (bottom) provides easy access (with lazy-loading) to the Archive
>>> from templateflow import api as tflow\n>>> tflow.get(\n...     'MNI152NLin6Asym',\n...     desc=None,\n...     resolution=1,\n...     suffix='T1w',\n...     extension='nii.gz'\n... )\nPosixPath('/templateflow_home/tpl-MNI152NLin6Asym/tpl-MNI152NLin6Asym_res-01_T1w.nii.gz')\n

.large[www.templateflow.org] ]

.pull-right[

]

???

One of the most ancient feature requests received from fMRIPrep early adopters was improving the flexibility of spatial normalization to standard templates other than fMRIPrep's default.

For instance, infant templates.

TemplateFlow offers an Archive of templates where they are stored, maintained and re-distributed;

and a Python client that helps accessing them.

On the right hand side, an screenshot of the TemplateFlow browser shows some of the templates currently available in the repository. The browser can be reached at www.templateflow.org.

The tool is based on PyBIDS, and the snippet will surely remind you of it. In this case the example shows how to obtain the T1w template corresponding to FSL's MNI space, at the highest resolution.

If the files requested are not in TemplateFlow's cache, they will be pulled down and kept for further utilization.

"},{"location":"assets/torw2020/presentation/#templateflow-archive","title":"TemplateFlow - Archive","text":"

.small[(Ciric et al. 2020, in prep)]

???

The Archive allows a rich range of data and metadata to be stored with the template.

Datatypes in the repository cover:

  • images containing population-average templates,
  • masks (for instance brain masks),
  • atlases (including parcellations and segmentations)
  • transform files between templates

Metadata can be stored with the usual BIDS options.

Finally, templates allow having multiple cohorts, in a similar encoding to that of multi-session BIDS datasets.

Multiple cohorts are useful, for instance, in infant templates with averages at several gestational ages.

NiWorkflows is a miscellaneous mixture of tooling used by downstream NiPreps:

???

NiWorkflows is, historically, the first component detached from fMRIPrep.

For that reason, its scope and vision has very fuzzy boundaries as compared to the other tools.

The most relevant utilities incorporated within NiWorkflows are:

--

  • The reportlet aggregation and individual report generation system

???

First, the individual report system which aggregates the visual elements or the reports (which we call \"reportlets\") and generates the final HTML document.

Also, most of the engineering behind the generation of these reportlets and their integration within NiPype are part of NiWorkflows

--

  • Custom extensions to NiPype interfaces

???

Beyond the extension of NiPype to generate a reportlet from any given interface, NiWorkflows is the test bed for many utilities that are then upstreamed to nipype.

Also, special interfaces with a limited scope that should not be included in nipype are maintained here.

--

  • Workflows useful across applications

???

Finally, NiWorkflows indeed offers workflows that can be used by end-user NiPreps. For instance atlas-based brain extraction of anatomical images, based on ANTs.

???

Echo-planar imaging (EPI) are typically affected by distortions along the phase encoding axis, caused by the perturbation of the magnetic field at tissue interfaces.

Looking at the reportlet, we can see how in the \"before\" panel, the image is warped.

The distortion is most obvious in the coronal view (middle row) because this image has posterior-anterior phase encoding.

Focusing on the changes between \"before\" and \"after\" correction in this coronal view, we can see how the blue contours delineating the corpus callosum fit better the dark shade in the data after correction.

"},{"location":"assets/torw2020/presentation/#sdcflows-as-integrated-in-fmriprep","title":"SDCFlows, as integrated in fMRIPrep","text":"

.left-column3[

]

.right-column3[ * Hierarchy of SDC methods: 1. PE-Polar 2. Fieldmap 3. Fieldmap-less

  • Arguments:
  • --use-syn-sdc
  • --force-syn
  • --ignore fieldmaps

  • REQUIRES (opts. 1 or 2): setting the IntendedFor metadata field of fieldmaps. ]

???

With SDCFlows, fMRIPrep implements a rather sophisticated pipeline for the estimation of susceptibility distortions.

Depending on whether the input dataset contains EPI images with opposed phase encoding polarities (the so-called PE-Polar correction), fieldmaps (as Gradient Recalled Echo sequences) or the fieldmap-less estimation is requested,

then SDCFlows establishes a hierarchy of corrections.

After correction, we are interested in assessing that low-frequency distortions have been accounted for and that high-frequency (with extreme regions suffering severe drop-outs) are not excessively present.

.pull-left[

] .pull-right[

]

???

sMRIPrep corresponds to the split of the anatomical preprocessing workflow originally proposed with fMRIPrep.

With the support of TemplateFlow, the tool now supports spatial normalization to one or more templates found in the TemplateFlow Archive.

It also supports the use of custom templates, whenever they are correctly installed in the templateflow's cache folder.

???

dMRIPrep and fMRIPrep are, of course the tip of the iceberg.

dMRIPrep is still in an alpha state, steadily progressing through the path fMRIPrep has delineated for NiPreps.

Hopefully, at this point of the talk fMRIPrep doesn't need further description.

template: newsection layout: false

.middle.center[

"},{"location":"assets/torw2020/presentation/#other-components-of-nipreps","title":"Other components of NiPreps","text":"

]

???

Some additional components of NiPreps were never part of fMRIPrep's codebase, or they have been started recently.

???

Such is the case of the quality control tools.

MRIQC produces visual reports for the efficient screening of acquired (meaning, unprocessed) data - in particular anatomical and functional MRI of the human brain.

CrowdMRI is an internet service where anonimized quality control metrics are uploaded automatically as they are computed by MRIQC.

The endgoal is to gather enough data to describe the normative distribution of these metrics across image parameters and scanning devices and sites.

Finally, MRIQCnets encloses several machine learning projects regarding the quality of acquired images.

"},{"location":"assets/torw2020/presentation/#upcoming-new-utilities","title":"Upcoming new utilities","text":""},{"location":"assets/torw2020/presentation/#nibabies","title":"NiBabies","text":"
  • Recently started, covering infant MRI brain-extraction for now (Mathias Goncalves)
"},{"location":"assets/torw2020/presentation/#nirodents","title":"NiRodents","text":"
  • Recently started, covering rodent MRI brain-extraction for now (Eilidh MacNicol)

???

So, what's coming up next?

NiBabies is some sort of NiWorkflows equivalent for the preprocessing of infant imaging. At the moment, only atlas-based brain extraction using ANTs (and adapted from NiWorkflows) is in active developments.

Next steps include brain tissue segmentation.

Similarly, NiRodents is the NiWorkflows parallel for the prepocessing of rodent preclinical imaging. Again, only atlas-based brain extraction adapted from NiWorkflows is being developed.

--

"},{"location":"assets/torw2020/presentation/#future-lines","title":"Future lines","text":"
  • fMRIPrep-babies

  • fMRIPrep-rodents

  • MolPrep / PETPrep ?

???

In a mid-term future, both NiBabies and NiRodents should allow the extension of fMRIPrep to these new two idiosyncratic data families.

In additions, plans for a molecular imaging or PET preprocessing NiPrep are being designed.

"},{"location":"assets/torw2020/presentation/#conclusion","title":"Conclusion","text":""},{"location":"assets/torw2020/presentation/#nipreps-is-a-framework-for-the-development-of-preprocessing-workflows","title":"NiPreps is a framework for the development of preprocessing workflows","text":"
  • Principled design, with BIDS as an strategic component
  • Leveraging existing, widely used software
  • Using NiPype as a foundation

???

To wrap-up, I've presented NiPreps, a framework for developing preprocessing workflows inspired by fMRIPrep.

The framework is heavily principle and tags along BIDS as a foundational component

NiPreps should not reinvent any wheel, trying to reuse as much as possible of the widely used and tested existing software.

Nipype serves as a glue components to orchestrate workflows.

--

"},{"location":"assets/torw2020/presentation/#why-preprocessing","title":"Why preprocessing?","text":"
  • We propose to consider preprocessing as part of the image acquisition and reconstruction
  • When setting the boundaries that way, it seems sensible to pursue some standardization in the preprocessing:
  • Less experimental degrees of freedom for the researcher
  • Researchers can focus on the analysis
  • More homogeneous data at the output (e.g., for machine learning)
  • How:
  • Transparency is key to success: individual reports and documentation (open source is implicit).
  • Best engineering practices (e.g., containers and CI/CD)

???

But why just preprocessing, with a very strict scope?

We propose to think about preprocessing as part of the image acquisition and reconstruction process (in other words, scanning), rather than part of the analysis workflow.

This decoupling from analysis comes with several upshots:

First, there are less moving parts to play with for researchers in the attempt to fit their methods to the data (instead of fitting data with their methods).

Second, such division of labor allows the researcher to use their time in the analysis.

Finally, two preprocessed datasets from two different studies and scanning sites should be more homogeneous when processed with the same instruments, in comparison to processing them with idiosyncratic, lab-managed, preprocessing workflows.

However, for NiPreps to work we need to make sure the tools are transparent.

Not just with the individual reports and thorough documentation, also because of the community driven development. For instance, the peer-review process that goes around large incremental changes is fundamental to ensure the quality of the tool.

In addition, best engineering practices suggested in the BIDS-Apps paper, along with those we have been including with fMRIPrep, are necessary to ensure the quality of the final product.

--

"},{"location":"assets/torw2020/presentation/#challenges","title":"Challenges","text":"
  • Testing / Validation!

???

As an open problem, validating the results of the tool remains extremely challenging for the lack in gold standard datasets that can tell us the best possible outcome.

template: newsection layout: false

.middle.center[

"},{"location":"assets/torw2020/presentation/#open-phd-student-position","title":"Open PhD student position!","text":"

]

template: newsection layout: false

.middle.center[

"},{"location":"assets/torw2020/presentation/#thanks","title":"Thanks!","text":""},{"location":"assets/torw2020/presentation/#questions","title":"Questions?","text":"

]

"},{"location":"community/","title":"Join the NiPreps Community","text":"

One of the pillars of fMRIPrep, the seed project for NiPreps, has been nurturing an open-source community. Building Welcoming Communities is crucial for open-source software because of several reasons:

  • Engaging users and contributors (in a very liberal sense, not just with code) helps establish a development road-map:

    • In the case of fMRIPrep, many users have reported bugs via our issue tracker and Neurostars.org. Even though testing is one of the primary focuses for fMRIPrep, without these bug-report contributions the tool would have never reached the dependability level it requires to serve its purpose.
    • Users identify and propose new features, often illuminating shady areas the most involved developers did not find time or the right context to explore.
  • The community exposes the software and also increases the externality of the software. The neuroimaging discussion supported by Neurostars.org has been a key factor for the adoption of fMRIPrep.

  • Users always give back, and it is not uncommon to see elaborate responses to bug-reports and questions about fMRIPrep on Neurostars.org by users who had similar questions previously.

Because of the scientific purpose of NiPreps, there is one more fundamental reason to grow a (scientific) community around the tools: rigor/scrutiny. As one reviews a few of the most discussed pull-requests to fMRIPrep, very soon they realize that we don't just need to get the code right. We strive for integrating high-quality code, but even more importantly, that code must get the scientific method it implements right. This is particularly difficult because in most of the cases there aren't test oracles (in software engineering terms) or gold-standards (in scientific terms) to efficiently evaluate the validity of new features (even to exercise a minuscule area of the domain of inputs). The redundancy of expert eyes looking at our code has only helped make it better.

"},{"location":"community/#current-members-of-the-github-organization","title":"Current members of the GitHub organization","text":"

A total of 100 neuroimagers have already joined us. Becoming a member will give you access to additional forums for discussion, subscribing notifications for events and meetings, etc. You can request you are added to the organization by creating a new issue here.

"},{"location":"community/CODE_OF_CONDUCT/","title":"NiPreps Code of Conduct","text":""},{"location":"community/CODE_OF_CONDUCT/#our-pledge","title":"Our Pledge","text":"

In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.

"},{"location":"community/CODE_OF_CONDUCT/#our-standards","title":"Our Standards","text":"

Examples of behavior that contributes to creating a positive environment include:

  • Using welcoming and inclusive language
  • Being respectful of differing viewpoints and experiences
  • Gracefully accepting constructive criticism
  • Focusing on what is best for the community
  • Showing empathy towards other community members

Examples of unacceptable behavior by participants include:

  • The use of sexualized language or imagery and unwelcome sexual attention or advances
  • Trolling, insulting/derogatory comments, and personal or political attacks
  • Public or private harassment
  • Publishing others' private information, such as a physical or electronic address, without explicit permission
  • Other conduct which could reasonably be considered inappropriate in a professional setting
"},{"location":"community/CODE_OF_CONDUCT/#our-responsibilities","title":"Our Responsibilities","text":"

Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.

Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.

"},{"location":"community/CODE_OF_CONDUCT/#scope","title":"Scope","text":"

This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.

"},{"location":"community/CODE_OF_CONDUCT/#enforcement","title":"Enforcement","text":"

Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting Oscar Esteban at oesteban@stanford.edu or Chris Markiewicz at markiewicz@stanford.edu, two members of the project team. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.

Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.

"},{"location":"community/CODE_OF_CONDUCT/#attribution","title":"Attribution","text":"

This Code of Conduct is adapted from the Contributor Covenant, version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html

For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq

"},{"location":"community/CONTRIBUTING/","title":"Contributing Guidelines","text":"

Welcome to the NiPreps project! We're excited you're here and want to contribute.

Imposter's syndrome disclaimer

Imposter's syndrome disclaimer1: We want your help. No, really.

There may be a little voice inside your head that is telling you that you're not ready to be an open-source contributor; that your skills aren't nearly good enough to contribute. What could you possibly offer a project like this one?

We assure you - the little voice in your head is wrong. If you can write code at all, you can contribute code to open-source. Contributing to open-source projects is a fantastic way to advance one's coding skills. Writing perfect code isn't the measure of a good developer (that would disqualify all of us!); it's trying to create something, making mistakes, and learning from those mistakes. That's how we all improve, and we are happy to help others learn.

Being an open-source contributor doesn't just mean writing code, either. You can help out by writing documentation, tests, or even giving feedback about the project (and yes - that includes giving feedback about the contribution process). Some of these contributions may be the most valuable to the project as a whole, because you're coming to the project with fresh eyes, so you can see the errors and assumptions that seasoned contributors have glossed over.

"},{"location":"community/CONTRIBUTING/#driving-principles","title":"Driving principles","text":"

NiPreps are built around three overarching principles:

  1. Robustness - The pipeline adapts the preprocessing steps depending on the input dataset and should provide results as good as possible independently of scanner make, scanning parameters or presence of additional correction scans (such as fieldmaps).
  2. Ease of use - Thanks to dependence on the BIDS standard, manual parameter input is reduced to a minimum, allowing the pipeline to run in an automatic fashion.
  3. \"Glass box\" philosophy - Automation should not mean that one should not visually inspect the results or understand the methods. Thus, NiPreps provides visual reports for each subject, detailing the accuracy of the most important processing steps. This, combined with the documentation, can help researchers to understand the process and decide which subjects should be kept for the group level analysis.

These principles distill some design and organizational foundations:

  1. NiPreps only and fully support BIDS and BIDS-Derivatives for the input and output data.
  2. NiPreps are packaged as a fully-compliant BIDS-Apps, not just in its user interface, but also in the continuous integration, testing, and delivery.
  3. The scope of NiPreps is strictly limited to preprocessing tasks.
  4. NiPreps are agnostic to subsequent analysis, i.e., any software supporting BIDS-Derivatives for its inputs should be amenable to analyze data preprocessed with them.
  5. NiPreps are thoroughly and transparently documented (including the generation of individual, visual reports with a consistent format that serve as scaffolds for understanding the underpinnings and design decisions).
  6. NiPreps are community-driven, and contributors (in any sense) always get credited with authorship within relevant publications.
  7. NiPreps are modular, reliant on widely-used tools such as AFNI, ANTs, FreeSurfer, FSL, NiLearn, or DIPY [7-12] and extensible via plug-ins.
"},{"location":"community/CONTRIBUTING/#practical-guide-to-submitting-your-contribution","title":"Practical guide to submitting your contribution","text":"

These guidelines are designed to make it as easy as possible to get involved. If you have any questions that aren't discussed below, please let us know by opening an issue!

Before you start, you'll need to set up a free GitHub account and sign in. Here are some instructions.

Already know what you're looking for in this guide? Jump to the following sections:

  • Joining the conversation
  • Contributing through Github
  • Understanding issues
  • Making a change
  • Structuring contributions
  • Licensing
  • Recognizing contributors
"},{"location":"community/CONTRIBUTING/#joining-the-conversation","title":"Joining the conversation","text":"

NiPreps is maintained by a growing group of enthusiastic developers\u2014 and we're excited to have you join! Most of our discussions will take place on open issues.

We also encourage users to report any difficulties they encounter on NeuroStars, a community platform for discussing neuroimaging.

We actively monitor both spaces and look forward to hearing from you in either venue!

"},{"location":"community/CONTRIBUTING/#contributing-through-github","title":"Contributing through GitHub","text":"

git is a really useful tool for version control. GitHub sits on top of git and supports collaborative and distributed working.

If you're not yet familiar with git, there are lots of great resources to help you git started! Some of our favorites include the git Handbook and the Software Carpentry introduction to git.

On GitHub, You'll use Markdown to chat in issues and pull requests. You can think of Markdown as a few little symbols around your text that will allow GitHub to render the text with a little bit of formatting. For example, you could write words as bold (**bold**), or in italics (*italics*), or as a link ([link](https://youtu.be/dQw4w9WgXcQ)) to another webpage.

GitHub has a really helpful page for getting started with writing and formatting Markdown on GitHub.

"},{"location":"community/CONTRIBUTING/#understanding-issues","title":"Understanding issues","text":"

Every project on GitHub uses issues slightly differently.

The following outlines how the NiPreps developers think about these tools.

  • Issues are individual pieces of work that need to be completed to move the project forward. A general guideline: if you find yourself tempted to write a great big issue that is difficult to describe as one unit of work, please consider splitting it into two or more issues.

    Issues are assigned labels which explain how they relate to the overall project's goals and immediate next steps.

"},{"location":"community/CONTRIBUTING/#issue-labels","title":"Issue Labels","text":"

The current list of issue labels are here and include:

  • These issues contain a task that is amenable to new contributors because it doesn't entail a steep learning curve.

    If you feel that you can contribute to one of these issues, we especially encourage you to do so!

  • These issues point to problems in the project.

    If you find new a bug, please give as much detail as possible in your issue, including steps to recreate the error. If you experience the same bug as one already listed, please add any additional information that you have as a comment.

  • These issues are asking for new features and improvements to be considered by the project.

    Please try to make sure that your requested feature is distinct from any others that have already been requested or implemented. If you find one that's similar but there are subtle differences, please reference the other request in your issue.

In order to define priorities and directions in the development roadmap, we have two sets of special labels:

Label Description Estimation of the downstream impact the proposed feature/bugfix will have. Estimation of effort required to implement the requested feature or fix the reported bug.

One way to understand these labels is to consider how they would apply to an imaginary issue. For example, if -- after a release -- a bug is identified that re-introduces a previously solved issue (i.e., its regresses the code outputs to some undesired behavior), we might assign it the following labels: . Its development priority would then be \"high\", since it is a low-effort, high-impact change.

Long-term goals may be labelled as a combination of: and or since they will have a high-impact on the code-base, but require a medium or high amount of effort. Of note, issues with the labels: or are less likely to be addressed because they are less likely to impact the code-base, or because they will require a very high activation energy to do so.

"},{"location":"community/CONTRIBUTING/#making-a-change","title":"Making a change","text":"

We appreciate all contributions to NiPreps, but those accepted fastest will follow a workflow similar to the following:

  1. Comment on an existing issue or open a new issue referencing your addition. This allows other members of the NiPreps development team to confirm that you aren't overlapping with work that's currently underway and that everyone is on the same page with the goal of the work you're going to carry out. This blog is a nice explanation of why putting this work in up front is so useful to everyone involved.

  2. Fork the particular NiPrep repository (e.g., fMRIPrep) with your GitHub user. This is now your own unique copy of that particular NiPreps component. Changes here won't affect anyone else's work, so it's a safe space to explore edits to the code!

  3. Clone your forked NiPreps repository to your machine/computer. While you can edit files directly on github, sometimes the changes you want to make will be complex and you will want to use a text editor that you have installed on your local machine/computer. (One great text editor is vscode). In order to work on the code locally, you must clone your forked repository. To keep up with changes in the NiPreps repository, add the \"upstream\" NiPreps repository as a remote to your locally cloned repository.

    git remote add upstream https://github.com/nipreps/fmriprep.git\n
    Make sure to keep your fork up to date with the upstream repository. For example, to update your master branch on your local cloned repository:
    git fetch upstream\ngit checkout master\ngit merge upstream/master\n

  4. Create a new branch to develop and maintain the proposed code changes. For example:

    git fetch upstream  # Always start with an updated upstream\ngit checkout -b fix/bug-1222 upstream/master\n
    Please consider using appropriate branch names as those listed below, and mind that some of them are special (e.g., doc/ and docs/):

    • fix/<some-identifier>: for bugfixes
    • enh/<feature-name>: for new features
    • doc/<some-identifier>: for documentation improvements. You should name all your documentation branches with the prefix doc/ or docs/ as that will preempt triggering the full battery of continuous integration tests.
  5. Make the changes you've discussed, following the NiPreps coding style guide. Try to keep the changes focused: it is generally easy to review changes that address one feature or bug at a time. It can also be helpful to test your changes locally, using a NiPreps development environment. Once you are satisfied with your local changes, add/commit/push them to the branch on your forked repository.

  6. Submit a pull request. A member of the development team will review your changes to confirm that they can be merged into the main code base. Pull request titles should begin with a descriptive prefix (for example, ENH: Support for SB-reference in multi-band datasets):

    • ENH: enhancements or new features (example)
    • FIX: bug fixes (example)
    • TST: new or updated tests (example)
    • DOC: new or updated documentation (example)
    • STY: style changes (example)
    • REF: refactoring existing code (example)
    • CI: updates to continous integration infrastructure (example)
    • MAINT: general maintenance (example)
    • For works-in-progress, add the WIP tag in addition to the descriptive prefix. Pull-requests tagged with WIP: will not be merged until the tag is removed.
  7. Have your PR reviewed by the developers team, and update your changes accordingly in your branch. The reviewers will take special care in assisting you address their comments, as well as dealing with conflicts and other tricky situations that could emerge from distributed development.

"},{"location":"community/CONTRIBUTING/#nipreps-coding-style-guide","title":"NiPreps coding style guide","text":"

Whenever possible, instances of Nipype Nodes and Workflows should use the same names as the variables they are assigned to. This makes it easier to relate the content of the working directory to the code that generated it when debugging.

Workflow variables should end in _wf to indicate that they refer to Workflows and not Nodes. For instance, a workflow whose basename is myworkflow might be defined as follows:

from nipype.pipeline import engine as pe\n\nmyworkflow_wf = pe.Workflow(name='myworkflow_wf')\n

If a workflow is generated by a function, the name of the function should take the form init_<basename>_wf:

def init_myworkflow_wf(name='myworkflow_wf):\n    workflow = pe.Workflow(name=name)\n    ...\n    return workflow\n\nmyworkflow_wf = init_workflow_wf(name='myworkflow_wf')\n

If multiple instances of the same workflow might be instantiated in the same namespace, the workflow names and variables should include either a numeric identifier or a one-word description, such as:

myworkflow0_wf = init_workflow_wf(name='myworkflow0_wf')\nmyworkflow1_wf = init_workflow_wf(name='myworkflow1_wf')\n\n# or\n\nmyworkflow_lh_wf = init_workflow_wf(name='myworkflow_lh_wf')\nmyworkflow_rh_wf = init_workflow_wf(name='myworkflow_rh_wf')\n
"},{"location":"community/CONTRIBUTING/#recognizing-contributions","title":"Recognizing contributions","text":"

We welcome and recognize all contributions regardless their size, content or scope: from documentation to testing and code development. You can see a list of current developers and contributors in our zenodo file. Before every release, a new zenodo file will be generated. The update script will also sort creators and contributors by the relative size of their contributions, as provided by the git-line-summary utility distributed with the git-extras package. Last positions in both the creators and contributors list will be reserved to the project leaders. These special positions can be revised to add names by punctual request and revised for removal and update of ordering in an scheduled manner every two years. All the authors enlisted as creators participate in the revision of modifications.

"},{"location":"community/CONTRIBUTING/#publications","title":"Publications","text":"

Anyone listed as a developer or a contributor can start the submission process of a manuscript as first author (please see Membership, where these concepts are described). To compose the author list, all the creators MUST be included (except for those people who opt to drop-out) and all the contributors MUST be invited to participate. First authorship(s) is (are) reserved for the authors that originated and kept the initiative of submission and wrote the manuscript. To generate the ordering of your paper, please run python .maint/paper_author_list.py from the root of the repository, on the up-to-date upstream/master branch. Then, please modify this list and place your name first. All developers and contributors are pulled together in a unique list, and last authorships assigned. NiPreps and its community adheres to open science principles, such that a pre-print should be posted on an adequate archive service (e.g., ArXiv or BioRxiv) prior publication.

"},{"location":"community/CONTRIBUTING/#licensing","title":"Licensing","text":"

NiPreps is licensed under the Apache 2.0 license. By contributing to NiPreps, you acknowledge that any contributions will be licensed under the same terms.

"},{"location":"community/CONTRIBUTING/#thank-you","title":"Thank you!","text":"

You're awesome.

\u2014 Based on contributing guidelines from the STEMMRoleModels project.

  1. The imposter syndrome disclaimer was originally written by Adrienne Lowe for a PyCon talk, and was adapted based on its use in the README file for the MetPy project.\u00a0\u21a9

"},{"location":"community/features/","title":"New features","text":"

The one bit that worries me is that fMRIPrep may become a Swiss army knife. I think instead it should just be a paring knife (small, efficient, and works for many things).

-- Satra (source)

When projects grow large, many forking paths created by newly implemented features start to open up. To account for this, the NiPreps community was created with the vision of building tools like fMRIPrep and MRIQC covering new imaging modalities, while keeping existing NiPreps tightly within scope. Defining such a scope also aids the implementation of the ease-of-use principle:

The same way the scanner does not offer an immense space of knobs to turn in the acquisition, NiPreps should not add many additional knobs to those for them to be considered a viable augmentation or extension of the scanner hw/sw.

-- Oscar (source)

"},{"location":"community/features/#the-problem-of-feature-creep","title":"The problem of feature creep","text":"

To avert feature creep and to serve each individual NiPrep, we developed the following guidelines, with the hopes of keeping these tools in a healthy state.

I'm worried fMRIPrep is catching a case of featuritis

-- Mathias (source)

These guidelines should also serve the community to transparently drive the process of including proposals into the road-map, set the ground for healthy conversation, and establish some patterns when accepting new-feature contributions. Before proposing new features, please be mindful that a road-map may not exist for a particular NiPrep. Even when a development road-map exists, please understand that it is not always possible to rigorously follow them:

I think something like this is what we tried to start sketching out with the development roadmap. The concern, as I remember it, was that we couldn't guarantee (or rule out) specific features when working with a small development team.

-- Elizabeth (source).

"},{"location":"community/features/#proposing-a-new-feature","title":"Proposing a new feature","text":""},{"location":"community/features/#why-the-new-feature-is-requested","title":"Why the new feature is requested?","text":"

Before going ahead and proposing a new feature, please take some time to learn whether the topic has been covered in the past and what decisions were made and why. This should be reasonably easy to do with the search tool of GitHub on the particular NiPrep repository.

If no previous discussion about the new idea is found, the next step is ensuring the new feature aligns with the vision and the scope of the target tool, as Elizabeth points out. Taking a look into the Development Road-map of the particular project (if it exists), may help finding an answer.

If the new feature still seems pertinent after this preliminary work or you are unsure about whether it falls within the scope, then go ahead and post an issue requesting feedback on your proposal. Please make sure to clearly state why the new feature should be considered.

"},{"location":"community/features/#some-questions-will-always-be-asked-about-a-new-feature","title":"Some questions will always be asked about a new feature","text":"

These questions by James will certainly help build up the discourse in support of the new feature, as the NiPreps maintainers will consider them:

  • Is the user interface affected? Because NiPreps generally expose a command-line interface (CLI) for the interaction with the user, new features involving changes to the CLI must be considered with caution as they may harm the ease-of-use:

    It also seems that some new features add more confusion than others. Especially when the CLI is affected, and yet another option is added, that makes the tool more complex to use.

    -- Alejandro (source).

  • Does the new feature substantially increase the internal complexity? Maintainers and developers will attempt to consolidate tools and lower the internal complexity whenever possible. This effort usually competes with the addition of new features as they typically will address particular use-cases rather than general improvements. However, that doesn't need to be the case, as some sections of the code might be objectively improvable and the integration of a new feature revising those might also lower complexity. Lowering the internal complexity will always be considered a great incentive for a new feature to be accepted.

  • Is there a standard procedure for the proposed feature in the literature?

    • if so, could we just use that procedure/value?
  • Is the feature dependent on some attribute of the input data? (e.g., TR, duration, etc.)

    • if so, can the procedure/value be determined algorithmically?
  • Does the feature interact with other settings? For instance, fmriprep#1962 interacts with the a/tCompCor implementation.

  • What is the difficulty of implementing the procedure outside of a NiPrep? In other words, does the NiPrep provide all the necessary outputs for a user to perform the non-standard analysis?

"},{"location":"community/features/#how-the-integration-of-the-new-feature-willcan-be-validated","title":"How the integration of the new feature will/can be validated?","text":"

Please propose ways to validate the new feature in the context of the workflow. Meaning, the objective here is to validate that the new feature works well within the pipeline, rather than validating a specific algorithm. To ensure the sustainability of NiPreps, the onus of this validation should be on the person/group requesting the feature.

"},{"location":"community/licensing/","title":"Licensing and Derived Works","text":"

The NiPreps community believes that software is an integral component of scientific practice, and that any scientific claim must be verifiable by following the chain of reasoning from observation to conclusion. To achieve this, software must be free to use, inspect, and critique. We also believe that you should be free to modify our software to improve it or adapt it to new use cases.

As software development is a dynamic process, code modifications can quickly become confusing as the original and modified versions depart from each other. For the sake of transparency and verification, when you modify our code, we ask that you document both the version of the software that you started with and the changes you make.

We believe these freedoms are best promoted by distributing our software under free/open source software licenses, and the license we feel best promotes these goals is the Apache License, Version 2.0.

This page outlines our commitment to transparent development and our expectations for developers who adapt NiPreps code to use in other projects.

"},{"location":"community/licensing/#licensing-of-nipreps-projects","title":"Licensing of NiPreps projects","text":"

All software packages and tools under the NiPreps umbrella must be licensed under the Apache License 2.0 by default, unless otherwise stated. The authors of new NiPreps packages may not abide by this general rule of thumb if necessary and/or sufficiently justified (e.g., the source code is actually derived from a product licensed under a copyleft license).

Containerized Images bundling NiPreps components and their dependencies can be distributed under a free and open-source license without copyleft, such as the MIT License. In such a case, the attribution notice of the MIT license must be present in the header comment of the container image bootstraping file (for instance, the so-called Dockerfile). This different licensing must be also indicated in the NOTICE file of the corresponding NiPreps components bundled within the image.

Docker-wrappers such as the fmriprep-docker package may be licensed under any free and open-source license without copyleft, such as the MIT License. This different licensing must be also indicated in the NOTICE file of the corresponding NiPreps components bundled within the image.

Data (distributed within the test data of packages or through the nipreps-data GitHub organization) will preferably be distributed under the Creative Commons Zero v1.0 Universal.

Under no circumstances any NiPreps software or data will be made publicly available unlicensed. If you find any component of NiPreps that is unlicensed, please make us aware at nipreps@gmail.com at your earliest convenience.

"},{"location":"community/licensing/#the-apache-license-20","title":"The Apache License 2.0","text":"

(This section is adapted from this blog post by D. Mar\u00edn)

The Apache License was created by the Apache Software Foundation (ASF) as the license for its Apache HTTP Server.

Just as the MIT License, it\u2019s a very permissive non-copyleft license that allows using the software for any purpose, distributing it, modifying it, and distributing derived works of it without concern for royalties. Its main differences, compared to the MIT License, are:

  • Using the Apache License, the authors of the software grant patent licenses to any user or distributor of the code. This patent licenses apply to any patent that, being licenseable by any of the software author, would be infringed by the piece of code they have created.
  • Apache License required that unmodified parts in derived works keep the License.
  • In every licensed file, any original copyright, patent, trademark or attribution notices must be preserved.
  • In every licensed file change, there must be a notification stating that changes have been made in the file.
  • If the Apache-licensed software includes a NOTICE file, this file and its contents must be preserved in all the derived works.
  • If anyone intentionally sends a contribution for an Apache-licensed software to its authors, this contribution can automatically be used under the Apache License.

This license is interesting because of the automatic patent license, and the clause about contribution submission.

It\u2019s compatible with the GPL, so you can mix Apache licensed-code into GPL software.

"},{"location":"community/licensing/#why-apache-20","title":"Why Apache-2.0?","text":"

In the case of scientific software, we believe that clearly stating that a Derived Work introduces changes into the original Work is a fundamental measure of transparency. Other than that, we wanted a permissive, non-copyleft license.

"},{"location":"community/licensing/#what-is-our-expectation-for-derived-works","title":"What is our expectation for Derived Works?","text":"

At the bare minimum, you must meet the conditions of the license (simplified version) about preserving the license text and copyright/attribution notices as well as corresponding statements of changes.

How to state that a file has been changed in a Derived Work. We suggest the following steps, heavily influenced by P. Ombredanne's recommendations at StackExchange:

  1. In each source file, add a note to the header comment stating that the file has been modified, with an approximate date, and a high-level description of the changes. The date and the description of the changes are not strictly required, but they are positive etiquette from a software engineering standpoint and substantially improve the transparency of the changes from a scientific point of view.
  2. If the source file did not have a license notice in the header comment, please add it to avoid ambiguity.
  3. Deleted files: please keep the file with just the header comment and state that the file is deleted. The change statement should follow the suggestion in 1), preferably stating whether the source has been deleted or moved over to other files. If preserving the filename as-is might become confusing to the user of the Derived Work, the filename can be modified to be marked as hidden with a dot . or underscore _ prefix, or modifying the extension.
  4. Preferably, also include a link to the original file in our GitHub repository, making sure the link is done to a particular commit state.

What changes would we like to see annotated? The high-level description of the changes will preferably contain:

  • Correction of bugs
  • Substantial performance improvement decisions
  • Replacement of relevant methods and dependencies by alternatives
  • Changes to the license
"},{"location":"community/licensing/#example-of-our-expectations","title":"Example of our expectations","text":"

Let's say a Derived Work modifies the sdcflows.viz.utils code-base. The file may or may not have the attribution notice. At the time of writing, the header comment of this file is:

Header comment in the original Work

With attribution notice
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-\n# vi: set ft=python sts=4 ts=4 sw=4 et:\n#\n# Copyright 2021 The NiPreps Developers <nipreps@gmail.com>\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\"\"\"Visualization tooling.\"\"\"\n
Without attribution notice
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-\n# vi: set ft=python sts=4 ts=4 sw=4 et:\n\"\"\"Visualization tooling.\"\"\"\n

Either way (whether the attribution notice is present or not), we suggest to update this header comment to something along the lines of the following:

Suggested header comment in the Derived Work

Required

# <shebang and editor settings can be preserved or removed freely>\n#\n# <your attribution notice, either maintaining the Apache-2.0 license or changing the license>\n#\n# STATEMENT OF CHANGES: This file is derived from sources licensed under the Apache-2.0 terms,\n# and this file has been changed.\n# The original file this work derives from is found at:\n# https://github.com/nipreps/sdcflows/blob/50393a8584dd0abf5f8e16e6ba66c43e1126f844/sdcflows/viz/utils.py\n#\n# [April 2021] CHANGES:\n#    * BUGFIX: Outdated function call from the ``svgutils`` dependency that changed API as of version 0.3.2.\n#    * ENH: Changed plotting dependency to the new `netplotbrain` package.\n#    * DOC: Added docstrings to some functions that lacked them.\n#\n# ORIGINAL WORK'S ATTRIBUTION NOTICE:\n#\n#     Copyright 2021 The NiPreps Developers <nipreps@gmail.com>\n#\n#     Licensed under the Apache License, Version 2.0 (the \"License\");\n#     you may not use this file except in compliance with the License.\n#     You may obtain a copy of the License at\n#\n#         http://www.apache.org/licenses/LICENSE-2.0\n#\n#     Unless required by applicable law or agreed to in writing, software\n#     distributed under the License is distributed on an \"AS IS\" BASIS,\n#     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n#     See the License for the specific language governing permissions and\n#     limitations under the License.\n\"\"\"Visualization tooling.\"\"\"\n
The lines highlighted with yellow color are explicitly required by the Apache-2.0 conditions.

Recommended (commit)

# <shebang and editor settings can be preserved or removed freely>\n#\n# <your attribution notice, either maintaining the Apache-2.0 license or changing the license>\n#\n# STATEMENT OF CHANGES: This file is derived from sources licensed under the Apache-2.0 terms,\n# and this file has been changed.\n# The original file this work derives from is found at:\n# https://github.com/nipreps/sdcflows/blob/50393a8584dd0abf5f8e16e6ba66c43e1126f844/sdcflows/viz/utils.py\n#\n# [April 2021] CHANGES:\n#    * BUGFIX: Outdated function call from the ``svgutils`` dependency that changed API as of version 0.3.2.\n#    * ENH: Changed plotting dependency to the new `netplotbrain` package.\n#    * DOC: Added docstrings to some functions that lacked them.\n#\n# ORIGINAL WORK'S ATTRIBUTION NOTICE:\n#\n#     Copyright 2021 The NiPreps Developers <nipreps@gmail.com>\n#\n#     Licensed under the Apache License, Version 2.0 (the \"License\");\n#     you may not use this file except in compliance with the License.\n#     You may obtain a copy of the License at\n#\n#         http://www.apache.org/licenses/LICENSE-2.0\n#\n#     Unless required by applicable law or agreed to in writing, software\n#     distributed under the License is distributed on an \"AS IS\" BASIS,\n#     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n#     See the License for the specific language governing permissions and\n#     limitations under the License.\n\"\"\"Visualization tooling.\"\"\"\n
The lines highlighted with green color are recommended by the NiPreps Developers.

Recommended (version)

# <shebang and editor settings can be preserved or removed freely>\n#\n# <your attribution notice, either maintaining the Apache-2.0 license or changing the license>\n#\n# STATEMENT OF CHANGES: This file is derived from sources licensed under the Apache-2.0 terms,\n# and this file has been changed.\n# The original file this work derives from is found within\n# the version 2.0.2 distribution of the software.\n#\n# [April 2021] CHANGES:\n#    * BUGFIX: Outdated function call from the ``svgutils`` dependency that changed API as of version 0.3.2.\n#    * ENH: Changed plotting dependency to the new `netplotbrain` package.\n#    * DOC: Added docstrings to some functions that lacked them.\n#\n# ORIGINAL WORK'S ATTRIBUTION NOTICE:\n#\n#     Copyright 2021 The NiPreps Developers <nipreps@gmail.com>\n#\n#     Licensed under the Apache License, Version 2.0 (the \"License\");\n#     you may not use this file except in compliance with the License.\n#     You may obtain a copy of the License at\n#\n#         http://www.apache.org/licenses/LICENSE-2.0\n#\n#     Unless required by applicable law or agreed to in writing, software\n#     distributed under the License is distributed on an \"AS IS\" BASIS,\n#     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n#     See the License for the specific language governing permissions and\n#     limitations under the License.\n\"\"\"Visualization tooling.\"\"\"\n
The lines highlighted with green color are recommended by the NiPreps Developers.

Although it is not mandated by the license letter, the spirit of the Apache-2.0 (and all other licenses stipulating the statement of changes, such as the CC-BY 4.0) suggests that a date of modification and an overview of outstanding changes are pertinent. We also suggest a link to the original code, including the commit-hash (that long string starting with 50393a in the URL above) for the location of the exact origin of the file. Alternatively, Derived Works may point to a exact release identifier where the original file is part of the code-base distribution. Please make sure to remove or replace with appropriate contents the comment tags <...> above.

What if a Derived Work does not modify this particular file? You should retain the original attribution notice as is (or introduce it if missing), unless you are relicensing the file. In that case, proceed with the suggestions above, and note the license change in the STATEMENT OF CHANGES block of the header comment.

"},{"location":"community/licensing/#are-papers-using-apache-20-licensed-software-considered-as-derived-works","title":"Are papers using Apache-2.0 licensed software considered as Derived Works?","text":"

No, they don't because they only reuse the software (in other words, they don't redistribute the software). The license stipulates that redistribution must retain the license and attribution notices as they are. In the scientific context, it is likely that a particular tool is modified (for example, to replace a method that you think is not appropriate for your data). Then, redistribution of the source would be desirable from the transparent reporting point of view, and therefore you should honor the License.

Generally, works using our NiPreps just need to follow the citation guidelines of the particular project and report the citation boilerplate including all software versions and literature references in the closest letter possible to that generated by the tool.

"},{"location":"community/licensing/#licensing-of-docker-and-singularity-images","title":"Licensing of Docker and Singularity images","text":"

Container images redistribute copies of NiPreps alongside their third-party dependencies, all of them bundled in the image. If the applicable license is Apache-2.0, then the text of a NOTICE file must be shown to the user. All NiPreps must insert a NOTICE file into their containerized distributions and print its contents out in the command line output, as well as in the visual reports. This NOTICE file for containers will be placed in the /.docker/NOTICE path of the repository, and this file must replace the /NOTICE file (if it exists) at image building time. Alternatively, and if the corresponding NiPreps Developers consider that the Apache-2.0 imposes too onerous requirements for the container image distribution, the source code of such images (e.g., Dockerfile) can be licensed under the MIT license.

Example NOTICE file for fMRIPrep

Python distribution /NOTICE
fMRIPrep\nCopyright 2021 The NiPreps Developers.\n\nThis product includes software developed by\nthe NiPreps Community (https://nipreps.org/).\n\nPortions of this software were developed at the Department of\nPsychology at Stanford University, Stanford, CA, US.\n\nThis software contains code ultimately derived from the epidewarp.fsl\nscript (https://www.nmr.mgh.harvard.edu/~greve/fbirn/b0/epidewarp.fsl)\nby Doug Greve, Dave Tuch, Tom Liu, and Bryon Mueller with generous\nhelp from the FSL crew (www.fmrib.ox.ac.uk/fsl) and the Biomedical\nInformatics Research Network (www.nbirn.net).\n
Container image distribution /.docker/NOTICE
fMRIPrep Container Image distribution\nCopyright 2021 The NiPreps Developers.\n\nThis product includes fMRIPrep and software developed by\nthe NiPreps Community (https://nipreps.org/).\n\nPortions of this software were developed at the Department of\nPsychology at Stanford University, Stanford, CA, US.\n\nThis product bundles AFNI <version-placeholder>, which is available under\nthe Gnu General Public License.\nMajor portions of AFNI were written at the Medical College of Wisconsin,\nwhich owns the copyright to that code. For fuller details, see\nhttp://afni.nimh.nih.gov/pub/dist/src/README.copyright.\n\nThis product bundles ANTs <version-placeholder>, which is available under\nthe BSD 3-clause license terms.\nCopyright 2009-2013 ConsortiumOfANTS.\n\nThis product bundles BIDS-Validator <version-placeholder>, which is available\nunder the MIT License.\nCopyright 2015 The Board of Trustees of the Leland Stanford Junior University.\n\nThis product bundles the Connectome Workbench <version-placeholder>, which\nis available under the GPL-v2\n(https://www.humanconnectome.org/software/connectome-workbench-license).\n\nThis product bundles FSL <version-placeholder>, which is available\nunder a custom license with commercial restrictions\n(https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/Licence).\nCopyright 2018, The University of Oxford.\n\nThis product bundles FreeSurfer <version-placeholder>, which is available\nunder a custom license and requires obtaining a license key\n(https://surfer.nmr.mgh.harvard.edu/fswiki/FreeSurferSoftwareLicense).\nCopyright 2011, The General Hospital Corporation, Boston MA, USA.\n\nThis product bundles code derived from ICA-AROMA, both (fork and original work)\nare available under the Apache-2.0 license.\n(https://github.com/oesteban/ICA-AROMA/blob/master/license.md)\nCopyright 2021, Maarten Mennes\n\nThis product bundles Miniconda <version-placeholder>, which is available\nunder a BSD 3-clause license.\n(c) 2017 Continuum Analytics, Inc. (dba Anaconda, Inc.).\nhttps://www.anaconda.com. All Rights Reserved\n\nThis product bundles NeuroDebian, which adheres to the\nDebian Free Software Guidelines (DFSG)\nhttps://www.debian.org/social_contract#guidelines\nand the terms of the Debian Social Contract version 1.1.\n\nThis product bundles tools by the NiPy community, such as NiBabel\n(MIT License, https://github.com/nipy/nibabel/blob/master/COPYING),\nand NiPype (Apache-2.0, https://github.com/nipy/nipype/blob/master/LICENSE).\n\nThis product bundles Pandoc <version-placeholder>, which is available\nunder the GPL version 2 or later.\nCopyright (C) 2006-2021 John MacFarlane <jgm at berkeley dot edu>\n\nThis product bundles SVGO <version-placeholder>, which is available\nunder the MIT License.\nCopyright (c) Kir Belevich\n\nThis product bundles tedana <version-placeholder>, which is available under\nthe GNU Lesser General Public License v2.1.\nCopyright 2018, tedana developers.\n\nTemplateFlow, a component of this bundle, contains neuroimaging template\nand atlas data under several permissive licenses.\nPlease refer to the metadata of the particular template used in your study to\ndetermine the exact terms of the license and how to acknowledge attribution\nof those works.\n\nsMRIPrep, a component of this bundle, contains code ultimately derived from\nANTs <version-placeholder>, which is available under\nthe BSD 3-clause license terms.\nCopyright 2009-2013 ConsortiumOfANTS.\n\nsMRIPrep, a component of this bundle, contains code ultimately derived from\nMindboggle <version-placeholder>, which is available under\nthe Apache License 2.0.\nCopyright 2016, Mindboggle team (http://mindboggle.info)\n\nfMRIPrep contains code ultimately derived from the epidewarp.fsl\nscript (https://www.nmr.mgh.harvard.edu/~greve/fbirn/b0/epidewarp.fsl)\nby Doug Greve, Dave Tuch, Tom Liu, and Bryon Mueller with generous\nhelp from the FSL crew (www.fmrib.ox.ac.uk/fsl) and the Biomedical\nInformatics Research Network (www.nbirn.net).\n
"},{"location":"community/members/","title":"Membership","text":"

In general, NiPreps embrace a liberal contribution model of governance structure. However, because of the scientific domain of NiPreps, the community features some structure from meritocracy models to prescribe the order in the authors list of new papers about these tools.

"},{"location":"community/members/#developers","title":"Developers","text":"

Developers are members of a wonderful team driving the project. Names and contacts of all developers are included in the .maint/developers.json file of each project. Examples of steering activities that drive the project are: actively participating in the follow-up meetings, leading documentation sprints, helping in the design of the tool and definition of the roadmap, providing resources (in the broad sense, including funding), code-review, etc.

"},{"location":"community/members/#contributors","title":"Contributors","text":"

Contributors enlisted in the .maint/contributors.json file of each project actively help or have previously helped the project in a broad sense: writing code, writing documentation, benchmarking modules of the tool, proposing new features, helping improve the scientific rigor of implementations, giving out support on the different communication channels (mattermost, NeuroStars, GitHub, etc.). If you are new to the project, don't forget to add your name and affiliation to the list of contributors there! Our Welcome Bot will send an automated message reminding this to first-time contributors. Before every release, unlisted contributors will be invited again to add their names to the file (just in case they missed the automated message from our Welcome Bot).

Contributors who have contributed at some point to the project but were required or they wished to disconnect from the project's updates and to drop-out from publications and other dissemination activities, are listed in the .maint/former.json file.

"},{"location":"devs/devenv/","title":"Developer Environment","text":"

This document explains how to prepare a new development environment and update an existing environment, as necessary, for the development of NiPreps' components. Some components may deviate from these guidelines, in such a case, please follow the guidelines provided in their documentation.

If you plan to contribute back to the community, making your code available via pull-request, please make sure to have read and understood the Community Documents and Contributor Guidelines. If you plan to distribute derived code, please follow our licensing guidelines.

Development in Docker is encouraged, for the sake of consistency and portability. By default, work should be built off of nipreps/fmriprep:unstable, which tracks the master branch, or nipreps/fmriprep:latest, which tracks the latest release version (see BIDS-Apps execution guide for the basic procedure for running).

It will be assumed the developer has a working repository in $HOME/projects/fmriprep, and examples are also given for niworkflows and NiPype.

"},{"location":"devs/devenv/#patching-a-working-copy-into-a-docker-container","title":"Patching a working copy into a Docker container","text":"

In order to test new code without rebuilding the Docker image, it is possible to mount working repositories as source directories within the container. The Docker wrapper script simplifies this for the most common repositories:

    -f PATH, --patch-fmriprep PATH\n                          working fmriprep repository (default: None)\n    -n PATH, --patch-niworkflows PATH\n                          working niworkflows repository (default: None)\n    -p PATH, --patch-nipype PATH\n                          working nipype repository (default: None)\n

For instance, if your repositories are contained in $HOME/projects:

$ fmriprep-docker -f $HOME/projects/fmriprep/fmriprep \\\n                  -n $HOME/projects/niworkflows/niworkflows \\\n                  -p $HOME/projects/nipype/nipype \\\n                  -i nipreps/fmriprep:latest \\\n                  $HOME/fullds005 $HOME/dockerout participant\n

Note the -i flag allows you to specify an image.

When invoking docker directly, the mount options must be specified with the -v flag:

-v $HOME/projects/fmriprep/fmriprep:/usr/local/miniconda/lib/python3.7/site-packages/fmriprep:ro\n-v $HOME/projects/niworkflows/niworkflows:/usr/local/miniconda/lib/python3.7/site-packages/niworkflows:ro\n-v $HOME/projects/nipype/nipype:/usr/local/miniconda/lib/python3.7/site-packages/nipype:ro\n

For example,

$ docker run --rm -v $HOME/ds005:/data:ro -v $HOME/dockerout:/out \\\n    -v $HOME/projects/fmriprep/fmriprep:/usr/local/miniconda/lib/python3.7/site-packages/fmriprep:ro \\\n    nipreps/fmriprep:latest /data /out/out participant \\\n    -w /out/work/\n

In order to work directly in the container, pass the --shell flag to fmriprep-docker

$ fmriprep-docker --shell $HOME/ds005 $HOME/dockerout participant\n

This is the equivalent of using --entrypoint=bash and omitting the fMRIPrep arguments in a docker command:

$ docker run --rm -v $HOME/ds005:/data:ro -v $HOME/dockerout:/out \\\n    -v $HOME/projects/fmriprep/fmriprep:/usr/local/miniconda/lib/python3.7/site-packages/fmriprep:ro --entrypoint=bash \\\n    nipreps/fmriprep:latest\n

Patching containers can be achieved in Singularity analogous to docker using the --bind (-B) option:

$ singularity run \\\n    -B $HOME/projects/fmriprep/fmriprep:/usr/local/miniconda/lib/python3.7/site-packages/fmriprep \\\n    fmriprep.img \\\n    /scratch/dataset /scratch/out participant -w /out/work/\n
"},{"location":"devs/devenv/#adding-dependencies","title":"Adding dependencies","text":"

New dependencies to be inserted into the Docker image will either be Python or non-Python dependencies. Python dependencies may be added in three places, depending on whether the package is large or non-release versions are required. The image must be rebuilt after any dependency changes.

Python dependencies should generally be included in the appropriate dependency metadata of the setup.cfg file found at the root of each repository. If some the dependency must be a particular version (or set thereof), it is possible to use version filters in this setup.cfg file.

For large Python dependencies where there will be a benefit to pre-compiled binaries, conda packages may also be added to the conda install line in the Dockerfile.

Non-Python dependencies must also be installed in the Dockerfile, via a RUN command. For example, installing an apt package may be done as follows:

RUN apt-get update && \\\n    apt-get install -y <PACKAGE>\n
"},{"location":"devs/devenv/#rebuilding-docker-image","title":"(Re)Building Docker image","text":"

If it is necessary to (re)build the Docker image, a local image named fmriprep may be built from within the local repository. Let's assume it is located in ~/projects/fmriprep:

~/projects/fmriprep$ VERSION=$( python get_version.py )\n~/projects/fmriprep$ docker build -t fmriprep --build-arg VERSION=$VERSION .\n

The VERSION build argument is necessary to ensure that help text can be reliably generated. The get_version.py tool constructs the version string from the current repository state.

To work in this image, replace nipreps/fmriprep:latest with just fmriprep in any of the above commands. This image may be accessed by the Docker wrapper via the -i flag, e.g.:

$ fmriprep-docker -i fmriprep --shell\n
"},{"location":"devs/devenv/#code-server-development-environment-experimental","title":"Code-Server Development Environment (Experimental)","text":"

To get the best of working with containers and having an interactive development environment, we have an experimental setup with code-server.

Important

We have a video walking through the process if you want a visual guide.

1. Build the Docker image. We will use the Dockerfile_devel file to build our development docker image:

$ cd $HOME/projects/fmriprep\n$ docker build -t fmriprep_devel -f Dockerfile_devel .\n

2. Run the Docker image We can start a docker container using the image we built (fmriprep_devel):

$ docker run -it -p 127.0.0.1:8445:8080 -v ${PWD}:/src/fmriprep fmriprep_devel:latest\n

Windows Users

If you are using windows shell, ${PWD} may not be defined, instead use the absolute path to your repository.

Docker-Toolbox

If you are using Docker-Toolbox, you will need to change your virtualbox settings using these steps as a guide. For step 6, instead of Name = rstudio; Host Port = 8787; Guest Port = 8787, have Name = code-server; Host Port = 8443; Guest Port = 8080. Then in the docker command above, change 127.0.0.1:8445:8080 to 192.168.99.100:8445:8080.

If the container started correctly, you should see the following on your console:

INFO  Server listening on http://localhost:8080\nINFO    - No authentication\nINFO    - Not serving HTTPS\n

Now you can switch to your favorite browser and go to: 127.0.0.1:8445 (or 192.168.99.100:8445 for Docker Toolbox).

3. Copy fmriprep.egg-info into your fmriprep/ project directory fmriprep.egg-info makes the package exacutable inside the docker container. Open a terminal in vscode and type the following:

$ cp -R /src/fmriprep.egg-info /src/fmriprep/\n
"},{"location":"devs/devenv/#code-server-development-environment-features","title":"Code-Server Development Environment Features","text":"
  • The editor is vscode

  • There are several preconfigured debugging tests under the debugging icon in the activity bar

  • see vscode debugging python for details.

  • The gitlens and python extensions are preinstalled to improve the development experience in vscode.

"},{"location":"devs/releases/","title":"Releases","text":"

As of January 2020, fMRIPrep has adopted a Calendar Versioning scheme, and with it we are attempting to apply more coherent semantic rules to our releases.

Note

This document is a draft for internal and external comment. Any commitments expressed here are proposals, and should not be relied upon at this time. This conversation started as a Google Doc.

"},{"location":"devs/releases/#principles","title":"Principles","text":"

The basic release form is YY.MINOR.PATCH, so the first minor release of 2020 is 20.0.0, and the first minor release of 2021 will be 21.0.0, whatever the final minor release of 2020 is. A series of releases share a YY.MINOR. prefix, which we refer to as the YY.MINOR.x series. For example, the 20.0.x series contains version 20.0.0, 20.0.1, and any other releases needed.

"},{"location":"devs/releases/#feature-releases","title":"Feature releases","text":"

Minor releases are considered feature releases. Because there is no concept of a \"major\" release (just a calendar year rollover), most changes to the code base will result in a new feature release. Changes targeting a new feature release should target the master branch. Feature releases may be released as often as is deemed appropriate.

"},{"location":"devs/releases/#bug-fix-releases","title":"Bug-fix releases","text":"

Patch releases are considered bug-fix releases. Each minor release triggers the creation of a new maint/<YY>.<MINOR>.x branch, and changes targeting a bug-fix release should target this branch. A \"minor release series\" is the initial feature release and the bug-fix releases that share the minor release prefix. Bug-fix releases may be released on minimal notice to other developers.

These releases must satisfy four conditions:

  1. Resolving one or more bugs. These mostly include failures of fMRIPrep to complete or producing invalid derivatives (e.g., a NIfTI file of all zeroes).
  2. Derivatives compatibility. If a subject may be successfully run on 20.0.n, then the imaging derivatives should be identical if rerun with 20.0.(n+1), modulo rounding errors and the effects of nondeterministic algorithms. The changes between successful runs of 20.0.n and 20.0.(n+1) should not be larger than the changes between two successful runs of 20.0.n. Cosmetic changes to reports are acceptable, while differing fields of view or data types in a NIfTI file would not be.
  3. API compatibility. Workflow-generating functions, workflow input- and outputnode fields must not change. As an end-user application, this may seem overly strict, but the odds of introducing a bug are much higher in these cases.
  4. User interface compatibility. Substantial changes to fMRIPrep command line must not happen (e.g., the addition of a new, relevant flag).

Note that not all bugs can be fixed in a way that satisfies all three of these criteria without significant effort. A developer may determine that the bug will be fixed in the next feature release.

Additional acceptable changes within a minor release series:

  1. Improved tests. These often come along with bug fixes, but they can be free-standing improvements to the code base.
  2. Improved documentation. Unless the documentation is of a feature that will not be present in a bug-fix release, this is always welcome.
  3. Updates to the Dockerfile that improve operation for Docker and/or Singularity users, but do not risk behavior change. A good example is including more templates to reduce the need for network requests. An example of an update to the Dockerfile that forces a minor release increment is a change in the pinned version of any of the dependencies or the base container image.
  4. Improvements to the lightweight wrappers. As long as a command-line invocation that worked for the previous version continues to work and produce the same Docker command, there's little chance of harm.
"},{"location":"devs/releases/#mechanics","title":"Mechanics","text":""},{"location":"devs/releases/#branch-synchronization","title":"Branch synchronization","text":"

A maintenance branch should generally follow directly from the tag of the feature release.

git checkout -b maint/20.0.x 20.0.0\ngit push upstream maint/20.0.x\n

It is expected that maint/20.0.x will diverge from master, as new features will be merged into master, and bug-fixes into maint/20.0.x. At a minimum, each new bug-fix release should be merged into master. After a 20.0.1 release:

git checkout master\ngit fetch upstream\ngit reset --hard upstream/master\ngit merge --no-commit 20.0.1\n\n# Resolve any merge conflicts\ngit add .\n\n# Manually review all changes to ensure compatibility\ngit diff --cached upstream/master\ngit commit\ngit push upstream master\n

If an unreleased bug-fix seems likely to cause merge conflicts, it may be worth doing the above more frequently.

"},{"location":"devs/releases/#dependencies","title":"Dependencies","text":"

fMRIPrep has a number of dependencies that we control at this point:

  1. sMRIPrep
  2. SDCflows
  3. NiWorkflows

These do not follow the same versioning scheme as above, but we need them to follow a compatible scheme. In particular, we need to be able to fix bugs that are situated within these dependencies in a bug-fix release without violating the criteria laid out above. At the time of an fMRIPrep feature release, all of the above tools need to also split out a maintenance branch (if they have not already) for the minor version series that fMRIPrep depends on. As an example, when 20.0.0 was released, fMRIPrep had the following dependencies in setup.cfg:

    niworkflows ~= 1.1.7\n    sdcflows ~= 1.2.0\n    smriprep ~= 0.5.2\n
~= is the compatible release specifier described in PEP 440. ~= 1.1.7 is equivalent to >= 1.1.7, == 1.1.*. This means that the current version of fMRIPrep is expected to work with niworkflows 1.1.7+ but not 1.2+. Thus, niworkflows needs to have a maint/1.1.x branch, sdcflows a maint/1.2.x and smriprep maint/0.5.x. Any changes to these tools that might violate API or derivative compatibility, must go into master, and must not be released into the current minor series of these tools. Note that fMRIPrep 20.0.0 does not depend on niworkflows ~= 1.1.0. Multiple feature releases of fMRIPrep may depend on the same minor release series of a dependency. There is no requirement to hike the dependency. However, if a dependency has started a new minor release series, a feature release of fMRIPrep is a good opportunity to bump the dependency.

We maintain a Versions Matrix to document and keep track of these dependencies.

"},{"location":"devs/releases/#support-windows","title":"Support Windows","text":""},{"location":"devs/releases/#minor-release-series","title":"Minor release series","text":"

A minor release series will continue to accept qualifying bug fixes at least until the next minor release. A minimum duration may be considered, or a fixed number of minor release series might be simultaneously supported.

An unmaintained series is a valid target for bug fixes after the support window, but the expected effort level of the contributor and maintainers will be higher and lower, respectively.

"},{"location":"devs/releases/#long-term-support-series","title":"Long-term support series","text":"

A long-term support (LTS) series is a minor release series that an LTS manager commits to maintaining for a specific duration, no less than one year. LTS series are under the same constraints as a minor release series in terms of what changes can be accepted.

The fMRIPrep developers commit to maintaining one LTS series at all times, at intervals of approximately one year. Community members may volunteer to assume maintainership after the initial period, or to maintain another minor release series as LTS.

Support windows of greater than a year have a much higher potential to run into issues with upstream dependencies going outside of their support windows. As much as possible, an fMRIPrep minor release should seek to move to the versions of upstream dependencies that will ensure the longest support before being considered for LTS.

Additional tasks required of an LTS manager:

  • Tracking possible breaking changes and broken URLs in upstream projects outside of the nipreps ecosystem.

    • Neurodebian dependencies (AFNI, FSL, Convert3D, Connectome WB)
    • FreeSurfer
    • ANTs
    • NodeJS - BIDS-validator, SVGO
    • Pandoc
    • ICA-AROMA
    • Miniconda
    • Python minor series end-of-life
    • numpy, scipy, pandas, nipype, nibabel, matplotlib
  • Backporting fixes from other maintained series.

    • If a bug is identified as existing within the LTS series and can be fixed without breaking API or derivative compatibility.

As many dependencies as possible should be pinned to specific versions relevant to the environment they are installed in. Packages (Debian .deb files, conda packages, Python wheels) should be archived in case of a loss of the external packages.

"},{"location":"devs/versions/","title":"Versions Matrix","text":"

The versions matrix is intended to allow easy reference for the dependencies within the NiPreps family of projects.

"},{"location":"devs/versions/#fmriprep","title":"fMRIPrep","text":"fMRIPrep series sMRIPrep series SDCflows series NiWorkflows series 23.1.x ~= 0.12.0 ~= 2.5.0 ~= 1.8.0 23.0.x ~= 0.11.0 ~= 2.4.0 ~= 1.7.6 22.1.x ~= 0.10.0 ~= 2.2.1 ~= 1.7.0 22.0.x ~= 0.9.2 ~= 2.1.1 ~= 1.6.3 21.0.x ~= 0.8.0 ~= 2.0.0 ~= 1.4.0 20.2.x ~= 0.7.0 ~= 1.3.1 ~= 1.3.0 20.1.x ~= 0.6.1 ~= 1.3.1 ~= 1.2.3 20.0.x ~= 0.5.2 ~= 1.2.0 ~= 1.1.7 1.5.3+ ~= 0.4.0 ~= 1.0.1 ~= 1.0.2

(Originally posted at nipreps/fmriprep#2054)

"},{"location":"devs/versions/#dmriprep","title":"dMRIPrep","text":"

(Work in progress)

"},{"location":"devs/versions/#smriprep","title":"sMRIPrep","text":"

sMRIPrep requires niworkflows and generally must depend on one minor series of niworkflows for the duration of an sMRIPrep minor series. Each sMRIPrep series may also be depended on for an fMRIPrep series and/or a dMRIPrep series. Noting these dependencies here should make it easier to track when a new minor series needs to be created.

sMRIPrep series NiWorkflows series TemplateFlow series 0.12.x ~=1.8.0 ... 0.10.x ~= 1.7.0 >= 0.6 0.9.x ~= 1.6.0 >= 0.6 0.8.x ~= 1.4.0 >= 0.6 0.7.x ~= 1.3.0 ~= 0.6 0.6.x ~= 1.2.0 ~= 0.6 0.5.x ~= 1.1.5 ~= 0.4.2

(Originally posted at nipreps/smriprep#172)

"},{"location":"devs/versions/#mriqc","title":"MRIQC","text":"

(Work in progress)

"},{"location":"intro/nipreps/","title":"Framework","text":""},{"location":"intro/nipreps/#building-on-fmripreps-success-story","title":"Building on fMRIPrep's success story","text":"

The current neuroimaging workflow has matured into a large chain of processing and analysis steps involving a large number of experts, across imaging modalities and applications. The development and fast adoption of fMRIPrep have revealed that neuroscientists need tools that simplify their research workflow, provide visual reports and checkpoints, and engender trust in the tool itself. The NiPreps framework extends fMRIPrep's approach and principles to new imaging modalities. The vision for NiPreps is to provide end-users (i.e., researchers) with applications that allow them to perform quality control smoothly and to prepare their data for modeling and statistical analysis.

"},{"location":"intro/nipreps/#leveraging-bids","title":"Leveraging BIDS","text":"

NiPreps leverage the Brain Imaging Data Structure (BIDS) to understand all the particular features and available metadata (i.e., imaging parameters) of the input dataset. BIDS allows NiPreps to automatically stage the most adequate preprocessing workflow while minimizing manual intervention.

"},{"location":"intro/nipreps/#architecture","title":"Architecture","text":"

The NiPreps framework (Figure 1) encompasses a wide array of software projects organized into three layers of scientific software:

  • Software infrastructure: including quite mature projects such as NiPype and NiBabel; the standard specifications of the Brain Imaging Data Structure (BIDS, and BIDS-Derivatives); and some other tools such as NiTransforms or TemplateFlow, under development. These tools deliver low-level interfaces (e.g., data access to images and spatial transforms) and utilities (see Figure 1).
  • Middleware: these are utilities that generalize their functionalities across the end-user tools. These utilities cover foundational processing methodologies (e.g., NiWorkflows and SDCflows), the crowdsourcing of metadata (e.g., MRIQC Web-API), and the support for deep learning models (MRIQC-nets).
  • End-user tools such as fMRIPrep: Some existing end-user tools include sMRIPrep (Structural MRI Preprocessing), which lies in between an end-user tool and middleware, as it is involved in higher-level tools such as fMRIPrep. Finally, quality control tools (e.g., MRIQC) to be executed before any preprocessing happens.

"},{"location":"intro/nipreps/#projects","title":"Projects","text":"
  • fMRIPrep (GitHub): fMRI Preprocessing
  • dMRIPrep (GitHub): dMRI Preprocessing
  • sMRIPrep (GitHub): Structural MRI Preprocessing
  • MRIQC (GitHub): MRI quality control
  • SDCflows (GitHub): Susceptibility-derived distortion correction (SDC) workflows
  • NiWorkflows (GitHub): General/miscellaneous workflow utilities
  • TemplateFlow: A registry of neuroimaging templates and spatial mappings between them.
  • NiTransforms (GitHub)
"},{"location":"intro/nipreps/#early-stage-projects","title":"Early-stage projects","text":"
  • NiRodents (GitHub): middleware adaptations for small animals imaging.
  • NiBabies (GitHub): middleware adaptations for infant imaging.
"},{"location":"intro/transparency/","title":"Transparency of workflows","text":"

NiPreps adopt fMRIPrep's foundations, and particularly resonate with the transparency principles. As discussed in (Esteban et al., 2019 -- preprint):

The rapid increase in the volume and diversity of data, as well as the evolution of available techniques for processing and analysis, presents an opportunity for considerable advancement of research in neuroscience. The drawback resides in the need for progressively more complex analysis workflows that rely on decreasingly interpretable models of the data. Such context encourages \u2018black-box\u2019 solutions that efficiently perform a valuable service but do not provide insights into how the tool has transformed the data into the expected outputs. Black boxes obscure important steps in the inductive process mediating between experimental measurements and reported findings. This way of moving forward risks producing a future generation of cognitive neuroscientists who have become experts in sophisticated computational methods but have little to no working knowledge of how their data were transformed through processing. Transparency is often identified as a remedy for these problems. fMRIPrep ascribes to \u2018glass-box\u2019 principles, which are defined in opposition to the many different facets or levels at which black-box solutions are opaque. The visual reports that fMRIPrep generates are a crucial aspect of the glass-box approach. Their quality control checkpoints represent the logical flow of preprocessing, allowing scientists to critically inspect and better understand the underlying mechanisms of the workflow. A second transparency element is the citation boilerplate that formalizes all details of the workflow and provides the versions of all involved tools along with references to the corresponding scientific literature. A third asset for transparency is thorough documentation that delivers additional details on each of the building blocks represented in the visual reports and described in the boilerplate. Further, fMRIPrep has been open-source since its inception: users have access to all of the incremental additions to the tool through the history of the version-control system. The use of GitHub grants access to the discussions held during development, allowing one to see how and why the main design decisions were made. The modular design of fMRIPrep enhances its flexibility and improves transparency, as the main features of the software are more easily accessible to potential collaborators. In combination with some coding style and contribution guidelines, this modularity has enabled multiple contributions by peers and the creation of a rapidly growing community that would be difficult to nurture behind closed doors.

"},{"location":"intro/transparency/#visual-reports-beyond-quality-control","title":"Visual reports beyond quality control","text":"

One foundational component of the NiPreps framework is the Visual Report System. End-user applications such as fMRIPrep or dMRIPrep generate individual reports after their preprocessing. Those visual reports have two fundamental purposes:

  • assessing the quality of the generated outputs, permitting the user to take quality control actions to eliminate biases originated from inadequate processing; and
  • understanding the workflow, by sequentially presenting the main steps of processing, the user can access the why the tool in particular took these steps ando more geneally why standard preprocessing involves that step.
"},{"location":"intro/transparency/#citation-boilerplates","title":"Citation boilerplates","text":"

NiPreps leverage the wealth of existing neuroimaging software that is available to researchers. To give back for standing on the shoulders of giants, NiPreps aim at the most thorough reporting possible crediting all the pieces of the prior knowledge they leverage. With the execution of some particular NiPreps, the application runs some introspection code to formalize the computational graph the particular workflow executed and iterates over all the nodes to extract the relevant articles and communications that should be cited, as well as all software tools and their versions involved. Similarly, ancillary materials such as neuroimaging templates and atlases are reported and cited.

All these references and citations are finally collated in a natural language description of the workflow. This description is therefore generated automatically, and contains all the details that are necessary to replicate the processing, as well as the abovementioned references. The text is appended to the visual report, and provided in three formats (markdown, latex and html/plain-text) with an index of citations, so that the user is only required to \"copy-and-paste\" into the Methods section of their papers.

Note for reviewers and editors

The boilerplate text generated by some NiPreps is intended to allow for clear, consistent description of the preprocessing steps used, in order to improve the reproducibility of studies. We fully intend for it to be copied verbatim, and have released it under the CC0 license, dedicating it to the public domain in jurisdictions that recognize the concept, and assert that we will take no action to enforce copyright in jurisdictions where we cannot disclaim it.

We firmly believe that requiring authors to modify this passage will serve no legitimate scientific or literary purpose and can, in fact, serve only to reduce the replicability of the analysis being described by making the preprocessing steps less clear.

We recognize that there may be automated plagiarism detection software that will flag the boilerplate text. We would be happy to discuss potential solutions for annotating boilerplate sections of documents to indicate automatic generation, and can update our software to make this annotation simpler for authors.

"},{"location":"news/","title":"News and Announcements","text":""},{"location":"news/#register-for-the-nipreps-hackathon-with-the-ohbm23-brainhack","title":"Register for the NiPreps hackathon with the OHBM'23 Brainhack!","text":"

We are thrilled to announce that the NiPreps Hackathon's second edition will be part of the upcoming OHBM'23 Brainhack (July 19-21, Maison Notman House, Montreal, Canada).

Registration To join us for this incredible event and work on NiPreps-related projects, please fill in our registration form.

Please remember to also register on the official webpage of the OHBM Brainhack. You will find all the necessary information, event schedule, and location details on Brainhack's website.

Approach and projects We will advance (online) some projects as much as possible before the BrainHack. We are putting together a list of potential projects at https://github.com/orgs/nipreps/projects/8. Please feel free to let us know your ideas and voice your questions. Projects can start at any moment (even at the venue in Montreal) to have the flexibility to accommodate all ideas.

Those projects with preliminary work will have project leaders who will organize meetings, coordinate a roadmap and help carry out the necessary tasks.

See you in Montreal!

"},{"location":"news/#nipreps-roundups-feb-22-2023","title":"NiPreps Roundups Feb 22, 2023","text":"

We resumed the bi-monthly NiPreps Roundups with a first meeting on February 22, 2023.

"},{"location":"users/educational/","title":"Educational resources","text":""},{"location":"users/educational/#fmriprep-bootcamp-geneva-2024","title":"fMRIPrep Bootcamp Geneva 2024","text":"
  • Welcome Home
  • Overview of the fMRI neuroimaging pipeline & fMRIPrep
  • The Brain Imaging Data Structure (BIDS)
  • BIDS Hands-on
  • Data and HPC
  • Containers
  • Apptainer in UNIGE's HPC
  • Links
"},{"location":"users/educational/#online-books","title":"Online books","text":"
  • QC-Book, member-initiated tutorial at ISMRM 2022
  • NiPreps Book, developing processing tools for dMRI, ISBI 2021
"},{"location":"users/educational/#qc-protocols-and-standard-operating-procedures","title":"QC protocols and Standard Operating Procedures","text":"
  • SOPs-cookiecutter, a template repository for version-controlled SOPs. The example template is rendered here
"},{"location":"users/educational/#presentation","title":"Presentation","text":"
  • Educational Talk at OHBM 2023 - Quality Control in fMRI studies with MRIQC and fMRIPrep
"},{"location":"users/talks/","title":"Talks and presentations","text":"
  • NiPreps @ BrainHack Seoul 2024
  • Standardizing neuroimaging workflows (Journal Club @ EPFL 2023)
  • Presentation about MRIQC for INCF 2022 (10 min)
  • NiPreps introduction, Educational Session at OHBM 2022
  • Building community workflows, BrainHack Donostia 2020
  • Building communities around reproducible workflows, Open Reproducible Neuroscience workshop 2020
  • Reproducible workflows, Think Open Rovereto Workshop 2020
"}]} \ No newline at end of file +{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"NeuroImaging PREProcessing toolS (NiPreps)","text":"

NiPreps augment the scanner to produce data directly consumable by analyses.

We refer to data directly consumable by analyses as analysis-grade data by analogy with the concept of \"sushi-grade (or sashimi-grade) fish\" in that both are products that have been:

  • minimally preprocessed, but are
  • safe to consume directly.

"},{"location":"#building-on-the-success-story-of-fmriprep","title":"Building on the success story of fMRIPrep","text":"

NiPreps were conceived as a generalization of fMRIPrep across new modalities, populations, cohorts, and species. fMRIPrep is widely adopted, as our telemetry with Sentry (and now, in-house with migas) shows:

fMRIPrep is executed an average of 9,500 times every week, of which, around 7,000 times it finishes successfully (72.9% success rate). The average number of executions started includes debug and dry runs where researchers do not intend actually process data. Therefore, the effective (that is, discarding test runs) success ratio of fMRIPrep is likely higher."},{"location":"apps/docker/","title":"Executing with Docker","text":"

Summary

Here, we describe how to run NiPreps with Docker containers. To illustrate the process, we will show the execution of fMRIPrep, but these guidelines extend to any other end-user NiPrep.

"},{"location":"apps/docker/#before-you-start-install-docker","title":"Before you start: install Docker","text":"

Probably, the most popular framework to execute containers is Docker. If you are to run a NiPrep on your PC/laptop, this is the RECOMMENDED way of execution. Please make sure you follow the Docker installation instructions. You can check your Docker Runtime installation running their hello-world image:

$ docker run --rm hello-world\n

If you have a functional installation, then you should obtain the following output:

Hello from Docker!\nThis message shows that your installation appears to be working correctly.\n\nTo generate this message, Docker took the following steps:\n 1. The Docker client contacted the Docker daemon.\n 2. The Docker daemon pulled the \"hello-world\" image from the Docker Hub.\n    (amd64)\n 3. The Docker daemon created a new container from that image which runs the\n    executable that produces the output you are currently reading.\n 4. The Docker daemon streamed that output to the Docker client, which sent it\n    to your terminal.\n\nTo try something more ambitious, you can run an Ubuntu container with:\n $ docker run -it ubuntu bash\n\nShare images, automate workflows, and more with a free Docker ID:\n https://hub.docker.com/\n\nFor more examples and ideas, visit:\n https://docs.docker.com/get-started/\n

After checking your Docker Engine is capable of running Docker images, you are ready to pull your first NiPreps container image.

"},{"location":"apps/docker/#docker-images","title":"Docker images","text":"

For every new version of the particular NiPrep app that is released, a corresponding Docker image is generated. The Docker image becomes a container when the execution engine loads the image and adds an extra layer that makes it runnable. In order to run NiPreps Docker images, the Docker Runtime must be installed.

Taking fMRIPrep to illustrate the usage, first you might want to make sure of the exact version of the tool to be used:

$ docker pull nipreps/fmriprep:<latest-version>\n

You can run NiPreps interacting directly with the Docker Engine via the docker run interface.

"},{"location":"apps/docker/#running-a-niprep-with-a-lightweight-wrapper","title":"Running a NiPrep with a lightweight wrapper","text":"

Some NiPreps include a lightweight wrapper script for convenience. That is the case of fMRIPrep and its fmriprep-docker wrapper. Before starting, make sure you have the wrapper installed. When you run fmriprep-docker, it will generate a Docker command line for you, print it out for reporting purposes, and then execute it without further action needed, e.g.:

$ fmriprep-docker /path/to/data/dir /path/to/output/dir participant\nRUNNING: docker run --rm -it -v /path/to/data/dir:/data:ro \\\n    -v /path/to_output/dir:/out nipreps/fmriprep:20.2.2 \\\n    /data /out participant\n...\n

fmriprep-docker implements the unified command-line interface of BIDS Apps, and automatically translates directories into Docker mount points for you.

We have published a step-by-step tutorial illustrating how to run fmriprep-docker. This tutorial also provides valuable troubleshooting insights and advice on what to do after fMRIPrep has run.

"},{"location":"apps/docker/#running-a-niprep-directly-interacting-with-the-docker-engine","title":"Running a NiPrep directly interacting with the Docker Engine","text":"

If you need a finer control over the container execution, or you feel comfortable with the Docker Engine, avoiding the extra software layer of the wrapper might be a good decision.

Accessing filesystems in the host within the container: Containers are confined in a sandbox, so they can't access the host in any ways unless you explicitly prescribe acceptable accesses to the host. The Docker Engine provides mounting filesystems into the container with the -v argument and the following syntax: -v some/path/in/host:/absolute/path/within/container:ro, where the trailing :ro specifies that the mount is read-only. The mount permissions modifiers can be omitted, which means the mount will have read-write permissions. In general, you'll want to at least provide two mount-points: one set in read-only mode for the input data and one read/write to store the outputs. Potentially, you'll want to provide one or two more mount-points: one for the working directory, in case you need to debug some issue or reuse pre-cached results; and a TemplateFlow folder to preempt the download of your favorite templates in every run.

Running containers as a user: By default, Docker will run the container as root. Some share systems my limit this feature and only allow running containers as a user. When the container is run as root, files written out to filesystems mounted from the host will have the user id 1000 by default. In other words, you'll need to be able to run as root in the host to change permissions or manage these files. Alternatively, running as a user allows preempting these permissions issues. It is possible to run as a user with the -u argument. In general, we will want to use the same user ID as the running user in the host to ensure the ownership of files written during the container execution. Therefore, you will generally run the container with -u $( id -u ).

You may also invoke docker directly:

$ docker run -ti --rm \\\n    -v path/to/data:/data:ro \\\n    -v path/to/output:/out \\\n    nipreps/fmriprep:<latest-version> \\\n    /data /out/out \\\n    participant\n

For example: :

$ docker run -ti --rm \\\n    -v $HOME/ds005:/data:ro \\\n    -v $HOME/ds005/derivatives:/out \\\n    -v $HOME/tmp/ds005-workdir:/work \\\n    nipreps/fmriprep:<latest-version> \\\n    /data /out/fmriprep-<latest-version> \\\n    participant \\\n    -w /work\n

Once the Docker Engine arguments are written, the remainder of the command line follows the usage. In other words, the first section of the command line is all equivalent to the fmriprep executable in a bare-metal installation: :

$ docker run -ti --rm \\                      # These lines\n    -v $HOME/ds005:/data:ro \\                # are equivalent to\n    -v $HOME/ds005/derivatives:/out \\        # a call to the App's\n    -v $HOME/tmp/ds005-workdir:/work \\       # entry-point.\n    nipreps/fmriprep:<latest-version> \\  #\n    \\\n    /data /out/fmriprep-<latest-version> \\   # These lines correspond\n    participant \\                            # to the particular BIDS\n    -w /work                                 # App arguments.\n
"},{"location":"apps/framework/","title":"Introduction","text":""},{"location":"apps/framework/#what-is-bids","title":"What is BIDS?","text":"

The Brain Imaging Data Structure (BIDS) is a standard for organizing and describing brain datasets, including MRI. The common naming convention and folder structure allow researchers to easily reuse BIDS datasets, re-apply analysis protocols, and run standardized automatic data preprocessing pipelines (and particularly, BIDS Apps). The BIDS starter-kit contains a wide collection of educational resources. Validity of the structure can be assessed with the online BIDS-Validator. The tree of a typical, valid (BIDS-compliant) dataset is shown below:

ds000003/\n \u251c\u2500 CHANGES\n \u251c\u2500 dataset_description.json\n \u251c\u2500 participants.tsv\n \u251c\u2500 README\n \u251c\u2500 sub-01/\n \u2502 \u251c\u2500 anat/\n \u2502 \u2502 \u251c\u2500 sub-01_inplaneT2.nii.gz\n \u2502 \u2502 \u2514\u2500 sub-01_T1w.nii.gz\n \u2502 \u2514\u2500 func/\n \u2502 \u251c\u2500 sub-01_task-rhymejudgment_bold.nii.gz\n \u2502 \u2514\u2500 sub-01_task-rhymejudgment_events.tsv\n \u251c\u2500 sub-02/\n \u251c\u2500 sub-03/\n
"},{"location":"apps/framework/#what-is-a-bids-app","title":"What is a BIDS App?","text":"

(Taken from the BIDS Apps paper)

A BIDS App is a container image capturing a neuroimaging pipeline that takes a BIDS-formatted dataset as input. Since the input is a whole dataset, apps are able to combine multiple modalities, sessions, and/or subjects, but at the same time need to implement ways to query input datasets. Each BIDS App has the same core set of command-line arguments, making them easy to run and integrate into automated platforms. BIDS Apps are constructed in a way that does not depend on any software outside of the container image other than the container engine.

BIDS Apps rely upon two technologies for container computing:

  1. Docker \u2014 for building, hosting as well as running containers on local hardware (running Windows, Mac OS X or Linux) or in the cloud.
  2. Singularity \u2014 for running containers on HPCs (high-performance computing).

BIDS Apps are deposited in the Docker Hub repository, making them openly accessible. Each app is versioned and all of the historical versions are available to download. By reporting the BIDS App name and version in a manuscript, authors can provide others with the ability to exactly replicate their analysis workflow.

Docker is used for its excellent documentation, maturity, and the Docker Hub service for storage and distribution of the images. Docker containers are easily run on personal computers and cloud services. However, the Docker Runtime was originally designed to run different components of web services (HTTP servers, databases etc.) using cloud resources. Docker thus requires root or root-like permissions, as well as modern versions of Linux kernel (to perform user mapping and management of network resources); though this is not a problem in context of renting cloud resources (which are not shared with other users), it makes it difficult or impossible to use in a multi-tenant environment such as an HPC system, which is often the most cost-effective computational resource available to researchers.

Singularity, on the other hand, is a unique container technology designed from the ground up with the encapsulation of binary dependencies and HPC use in mind. Its main advantage over Docker is that it does not require root access for container execution and thus is safe to use on multi-tenant systems. In addition, it does not require recent Linux kernel functionalities (such as namespaces, cgroups and capabilities), making it easy to install on legacy systems.

"},{"location":"apps/framework/#analysis-levels","title":"Analysis levels","text":"

BIDS Apps decouple the individual level analysis (processing of independent subjects) from group-level analyses aggregating participants. For the analysis of individual subjects, Apps need to understand the BIDS structure of the input dataset, so that the required inputs for the designated subject are found. Apps are designed to easily process derivatives generated by the participant-level or other Apps. The overall workflow has an entry-point and an end-point responsible of setting-up the map-reduce tasks and the tear-down including organizing the outputs for its archiving, respectively. Each App may implement multiple map and reduce steps.

"},{"location":"apps/framework/#a-unified-command-line-interface","title":"A unified command-line interface","text":"

To improve user experience and ability to integrate BIDS Apps into various computational platforms, each App follows a set of core command-line arguments:

$ <entrypoint> <bids_dataset> <output_path> <analysis_level>\n

For instance, to run fMRIPrep on a dataset located in /data/bids_root and write the outputs to /data/bids_root/derivatives/:

$ fmriprep /data/bids_root /data/bids_root/derivatives/ participant\n

In this case, we have selected to run the participant level (to process individual subjects). fMRIPrep does not have a group level, but other BIDS Apps may have. For instance, MRIQC generates group-level reports with the following command-line:

$ mriqc /data/bids_root /data/bids_root/derivatives/ group\n
"},{"location":"apps/framework/#what-are-bids-derivatives","title":"What are BIDS Derivatives?","text":"

NiPreps generate derivatives of the original data, and they fulfill the BIDS specification for the results of Apps that are created for subsequent consumption by other BIDS-Apps. These derivatives must follow the BIDS Derivatives specification (draft). An example of BIDS Derivatives filesystem tree, generated with fMRIPrep 1.5:

derivatives/\n\u251c\u2500\u2500 fmriprep/\n\u2502 \u251c\u2500\u2500 dataset_description.json\n\u2502 \u251c\u2500\u2500 logs\n\u2502 \u251c\u2500\u2500 sub-01.html\n\u2502 \u251c\u2500\u2500 sub-01/\n\u2502 \u2502 \u251c\u2500\u2500 anat/\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_desc-brain_mask.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_dseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_label-GM_probseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_label-WM_probseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_label-CSF_probseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_desc-preproc_T1w.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_space-MNI152_desc-brain_mask.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_space-MNI152_dseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_space-MNI152_label-GM_probseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_space-MNI152_label-WM_probseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_space-MNI152_label-CSF_probseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_space-MNI152_desc-preproc_T1w.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_from-MNI152_to-T1w_mode-image_xfm.h5\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_from-T1w_to-MNI152_mode-image_xfm.h5\n\u2502 \u2502 \u2502 \u2514\u2500\u2500 sub-01_from-orig_to-T1w_mode-image_xfm.txt\n\u2502 \u2502 \u251c\u2500\u2500 figures/\n\u2502 \u2502 \u2514\u2500\u2500 func/\n\u2502 \u2502   \u251c\u2500\u2500 sub-01_task-rhymejudgment_space-MNI152_boldref.nii.gz\n\u2502 \u2502   \u251c\u2500\u2500 sub-01_task-rhymejudgment_space-MNI152_desc-preproc_bold.nii.gz\n\u2502 \u2502   \u251c\u2500\u2500 sub-01_task-rhymejudgment_space-MNI152_desc-confounds_regressors.nii.gz\n\u2502 \u2502   \u2514\u2500\u2500 sub-01_task-rhymejudgment_space-MNI152_desc-brain_mask.nii.gz\n\u2502 \u251c\u2500\u2500 sub-02.html\n\u2502 \u251c\u2500\u2500 sub-02/\n\u2502 \u251c\u2500\u2500 sub-03.html\n\u2502 \u2514\u2500\u2500 sub-03/\n

"},{"location":"apps/singularity/","title":"Executing with Singularity","text":"

Summary

Here, we describe how to run NiPreps with Singularity containers. To illustrate the process, we will show the execution of fMRIPrep, but these guidelines extend to any other end-user NiPrep.

"},{"location":"apps/singularity/#preparing-a-singularity-image","title":"Preparing a Singularity image","text":"

Singularity version >= 2.5: If the version of Singularity installed on your HPC (High-Performance Computing) system is modern enough you can create Singularity image directly on the system. This is as simple as:

$ singularity build /my_images/fmriprep-<version>.simg \\\n                    docker://nipreps/fmriprep:<version>\n

where <version> should be replaced with the desired version of fMRIPrep that you want to download.

Singularity version < 2.5: In this case, start with a machine (e.g., your personal computer) with Docker installed. Use docker2singularity to create a singularity image. You will need an active internet connection and some time:

$ docker run --privileged -t --rm \\\n    -v /var/run/docker.sock:/var/run/docker.sock \\\n    -v D:\\host\\path\\where\\to\\output\\singularity\\image:/output \\\n    singularityware/docker2singularity \\\n    nipreps/fmriprep:<version>\n

Where <version> should be replaced with the desired version of fMRIPrep that you want to download.

Beware of the back slashes, expected for Windows systems. For *nix users the command translates as follows: :

$ docker run --privileged -t --rm \\\n    -v /var/run/docker.sock:/var/run/docker.sock \\\n    -v /absolute/path/to/output/folder:/output \\\n    singularityware/docker2singularity \\\n    nipreps/fmriprep:<version>\n

Transfer the resulting Singularity image to the HPC, for example, using scp or rsync:

$ scp nipreps_fmriprep*.img user@hcpserver.edu:/my_images\n
"},{"location":"apps/singularity/#running-a-singularity-image","title":"Running a Singularity Image","text":"

If the data to be preprocessed is also on the HPC, you are ready to run the NiPrep:

$ singularity run --cleanenv fmriprep.simg \\\n    path/to/data/dir path/to/output/dir \\\n    participant \\\n    --participant-label label\n
"},{"location":"apps/singularity/#handling-environment-variables","title":"Handling environment variables","text":"

Singularity by default exposes all environment variables from the host inside the container. Because of this, your host libraries (e.g., NiPype or a Python environment) could be accidentally used instead of the ones inside the container. To avoid such a situation, we strongly recommend using the --cleanenv argument in all scenarios. For example:

$ singularity run --cleanenv fmriprep.simg \\\n  /work/04168/asdf/lonestar/ $WORK/lonestar/output \\\n  participant \\\n  --participant-label 387 --nthreads 16 -w $WORK/lonestar/work \\\n  --omp-nthreads 16\n

Alternatively, conflicts might be preempted and some problems mitigated by unsetting potentially problematic settings, such as the PYTHONPATH variable, before running:

$ unset PYTHONPATH; singularity run fmriprep.simg \\\n  /work/04168/asdf/lonestar/ $WORK/lonestar/output \\\n  participant \\\n  --participant-label 387 --nthreads 16 -w $WORK/lonestar/work \\\n  --omp-nthreads 16\n

It is possible to define environment variables scoped within the container by using the SINGULARITYENV_* magic, in combination with --cleanenv. For example, we can set the FreeSurfer license variable (see fMRIPrep's documentation on this) as follows: :

$ export SINGULARITYENV_FS_LICENSE=$HOME/.freesurfer.txt\n$ singularity exec --cleanenv fmriprep.simg env | grep FS_LICENSE\nFS_LICENSE=/home/users/oesteban/.freesurfer.txt\n

As we can see, the export in the first line tells Singularity to set a corresponding environment variable of the same name after dropping the prefix SINGULARITYENV_.

"},{"location":"apps/singularity/#accessing-the-hosts-filesystem","title":"Accessing the host's filesystem","text":"

Depending on how Singularity is configured on your cluster it might or might not automatically bind (mount or expose) host's folders to the container (e.g., /scratch, or $HOME). This is particularly relevant because, if you can't run Singularity in privileged mode (which is almost certainly true in all the scenarios), Singularity containers are read only. This is to say that you won't be able to write anything unless Singularity can access the host's filesystem in write mode.

By default, Singularity automatically binds (mounts) the user's home directory and a scratch directory. In addition, Singularity generally allows binding the necessary folders with the -B <host_folder>:<container_folder>[:<permissions>] Singularity argument. For example:

$ singularity run --cleanenv -B /work:/work fmriprep.simg \\\n  /work/my_dataset/ /work/my_dataset/derivatives/fmriprep \\\n  participant \\\n  --participant-label 387 --nthreads 16 \\\n  --omp-nthreads 16\n

Warning

If your Singularity installation doesn't allow you to bind non-existent bind points, you'll get an error saying WARNING: Skipping user bind, non existent bind point (directory) in container. In this scenario, you can either try to bind things onto some other bind point you know it exists in the image or rebuild your singularity image with docker2singularity as follows:

$ docker run --privileged -ti --rm -v /var/run/docker.sock:/var/run/docker.sock \\\n         -v $PWD:/output singularityware/docker2singularity \\\n         -m \"/gpfs /scratch /work /share /lscratch /opt/templateflow\"\n

In the example above, the following bind points are created: /gpfs, /scratch, /work, /share, /opt/templateflow.

Important

One great feature of containers is their confinement or isolation from the host system. Binding mount points breaks this principle, as the container has now access to create changes in the host. Therefore, it is generally recommended to use binding scarcely and granting very limited access to the minimum necessary resources. In other words, it is preferred to bind just one subdirectory of $HOME than the full $HOME directory of the host (see nipreps/fmriprep#1778 (comment)).

Relevant aspects of the $HOME directory within the container: By default, Singularity will bind the user's $HOME directory in the host into the /home/$USER (or equivalent) in the container. Most of the times, it will also redefine the $HOME environment variable and update it to point to the corresponding mount point in /home/$USER. However, these defaults can be overwritten in your system. It is recommended to check your settings with your system's administrators. If your Singularity installation allows it, you can workaround the $HOME specification combining the bind mounts argument (-B) with the home overwrite argument (--home) as follows:

$ singularity run -B $HOME:/home/fmriprep --home /home/fmriprep \\\n      --cleanenv fmriprep.simg <fmriprep arguments>\n
"},{"location":"apps/singularity/#templateflow-and-singularity","title":"TemplateFlow and Singularity","text":"

TemplateFlow is a helper tool that allows neuroimaging workflows to programmatically access a repository of standard neuroimaging templates. In other words, TemplateFlow allows NiPreps to dynamically change the templates that are used, e.g., in the atlas-based brain extraction step or spatial normalization.

Default settings in the Singularity image should get along with the Singularity installation of your system. However, deviations from the default configurations of your installation may break this compatibility. A particularly problematic case arises when the home directory is mounted in the container, but the $HOME environment variable is not correspondingly updated. Typically, you will experience errors like OSError: [Errno 30] Read-only file system or FileNotFoundError: [Errno 2] No such file or directory: '/home/fmriprep/.cache'.

If it is not explicitly forbidden in your installation, the first attempt to overcome this issue is manually setting the $HOME directory as follows:

$ singularity run --home $HOME --cleanenv fmriprep.simg <fmriprep arguments>\n

If the user's home directory is not automatically bound, then the second step would include manually binding it as in the section above: :

$ singularity run -B $HOME:/home/fmriprep --home /home/fmriprep \\\n      --cleanenv fmriprep.simg <fmriprep arguments>\n

Finally, if the --home argument cannot be used, you'll need to provide the container with writable filesystems where TemplateFlow's files can be downloaded. In addition, you will need to indicate fMRIPrep to update the default paths with the new mount points setting the SINGULARITYENV_TEMPLATEFLOW_HOME variable. :

# Tell the NiPrep where TemplateFlow will place downloads\n$ export SINGULARITYENV_TEMPLATEFLOW_HOME=/opt/templateflow\n$ singularity run -B <writable-path-on-host>:/opt/templateflow \\\n      --cleanenv fmriprep.simg <fmriprep arguments>\n
"},{"location":"apps/singularity/#restricted-internet-access","title":"Restricted Internet access","text":"

We have identified several conditions in which running NiPreps might fail because of spotty or impossible access to Internet.

If your compute node cannot have access to Internet, then you'll need to pull down from TemplateFlow all the resources that will be necessary ahead of run-time.

If that is not the case (i.e., you should be able to hit HTTP/s endpoints), then you can try the following:

  • VerifiedHTTPSConnection ... Failed to establish a new connection: [Errno 110] Connection timed out. If you encounter an error like this, probably you'll need to set up an http proxy exporting SINGULARITYENV_http_proxy (see nipreps/fmriprep#1778 (comment). For example:

    $ export SINGULARITYENV_https_proxy=http://<ip or proxy name>:<port>\n
  • requests.exceptions.SSLError: HTTPSConnectionPool .... In this case, your container seems to be able to reach the Internet, but unable to use SSL encription. There are two potential solutions to the issue. The recommended one is setting REQUESTS_CA_BUNDLE to the appropriate path, and/or binding the appropriate filesystem:

    $ export SINGULARITYENV_REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt\n$ singularity run -B <path-to-certs-folder>:/etc/ssl/certs \\\n      --cleanenv fmriprep.simg <fmriprep arguments>\n
    Otherwise, some users have succeeded pre-fetching the necessary templates onto the TemplateFlow directory to then bind the folder at execution:

    $ export TEMPLATEFLOW_HOME=/path/to/keep/templateflow\n$ python -m pip install -U templateflow  # Install the client\n$ python\n>>> import templateflow.api\n>>> templateflow.api.TF_S3_ROOT = 'http://templateflow.s3.amazonaws.com'\n>>> api.get(\u2018MNI152NLin6Asym\u2019)\n

Finally, run the singularity image binding the appropriate folder:

$ export SINGULARITYENV_TEMPLATEFLOW_HOME=/templateflow\n$ singularity run -B ${TEMPLATEFLOW_HOME:-$HOME/.cache/templateflow}:/templateflow \\\n      --cleanenv fmriprep.simg <fmriprep arguments>\n
"},{"location":"apps/singularity/#troubleshooting","title":"Troubleshooting","text":"

Setting up a functional execution framework with Singularity might be tricky in some HPC (high-performance computing) systems. Please make sure you have read the relevant documentation of Singularity, and checked all the defaults and configuration in your system. The next step is checking the environment and access to fMRIPrep resources, using singularity shell.

  1. Check access to input data folder, and BIDS validity:

    $ singularity shell -B path/to/data:/data fmriprep.simg\nSingularity fmriprep.simg:~> ls /data\nCHANGES  README  dataset_description.json  participants.tsv  sub-01  sub-02  sub-03  sub-04  sub-05  sub-06  sub-07  sub-08  sub-09  sub-10  sub-11  sub-12  sub-13  sub-14  sub-15  sub-16  task-balloonanalogrisktask_bold.json\nSingularity fmriprep.simg:~> bids-validator /data\n   1: [WARN] You should define 'SliceTiming' for this file. If you don't provide this information slice time correction will not be possible. (code: 13 - SLICE_TIMING_NOT_DEFINED)\n          ./sub-01/func/sub-01_task-balloonanalogrisktask_run-01_bold.nii.gz\n          ./sub-01/func/sub-01_task-balloonanalogrisktask_run-02_bold.nii.gz\n          ./sub-01/func/sub-01_task-balloonanalogrisktask_run-03_bold.nii.gz\n          ./sub-02/func/sub-02_task-balloonanalogrisktask_run-01_bold.nii.gz\n          ./sub-02/func/sub-02_task-balloonanalogrisktask_run-02_bold.nii.gz\n          ./sub-02/func/sub-02_task-balloonanalogrisktask_run-03_bold.nii.gz\n          ./sub-03/func/sub-03_task-balloonanalogrisktask_run-01_bold.nii.gz\n          ./sub-03/func/sub-03_task-balloonanalogrisktask_run-02_bold.nii.gz\n          ./sub-03/func/sub-03_task-balloonanalogrisktask_run-03_bold.nii.gz\n          ./sub-04/func/sub-04_task-balloonanalogrisktask_run-01_bold.nii.gz\n          ... and 38 more files having this issue (Use --verbose to see them all).\n  Please visit https://neurostars.org/search?q=SLICE_TIMING_NOT_DEFINED for existing conversations about this issue.\n

  2. Check access to output data folder, and whether you have write permissions

    $ singularity shell -B path/to/data/derivatives/fmriprep-1.5.0:/out fmriprep.simg\nSingularity fmriprep.simg:~> ls /out\nSingularity fmriprep.simg:~> touch /out/test\nSingularity fmriprep.simg:~> rm /out/test\n

  3. Check access and permissions to $HOME:

    $ singularity shell fmriprep.simg\nSingularity fmriprep.simg:~> mkdir -p $HOME/.cache/testfolder\nSingularity fmriprep.simg:~> rmdir $HOME/.cache/testfolder\n

  4. Check TemplateFlow operation:

    $ singularity shell -B path/to/templateflow:/templateflow fmriprep.simg\nSingularity fmriprep.simg:~> echo ${TEMPLATEFLOW_HOME:-$HOME/.cache/templateflow}\n/home/users/oesteban/.cache/templateflow\nSingularity fmriprep.simg:~> python -c \"from templateflow.api import get; get(['MNI152NLin2009cAsym', 'MNI152NLin6Asym', 'OASIS30ANTs', 'MNIPediatricAsym', 'MNIInfant'])\"\n   Downloading https://templateflow.s3.amazonaws.com/tpl-MNI152NLin6Asym/tpl-MNI152NLin6Asym_res-01_atlas-HOCPA_desc-th0_dseg.nii.gz\n   304B [00:00, 1.28kB/s]\n   Downloading https://templateflow.s3.amazonaws.com/tpl-MNI152NLin6Asym/tpl-MNI152NLin6Asym_res-01_atlas-HOCPA_desc-th25_dseg.nii.gz\n   261B [00:00, 1.04kB/s]\n   Downloading https://templateflow.s3.amazonaws.com/tpl-MNI152NLin6Asym/tpl-MNI152NLin6Asym_res-01_atlas-HOCPA_desc-th50_dseg.nii.gz\n   219B [00:00, 867B/s]\n   ...\n

"},{"location":"apps/singularity/#running-singularity-on-a-slurm-system","title":"Running Singularity on a SLURM system","text":"

An example of sbatch script to run fMRIPrep on a SLURM system1 is given below. The submission script will generate one task per subject using a job array.

#!/bin/bash\n#\n#SBATCH -J fmriprep\n#SBATCH --time=48:00:00\n#SBATCH -n 1\n#SBATCH --cpus-per-task=16\n#SBATCH --mem-per-cpu=4G\n#SBATCH -p normal,mygroup  # Queue names you can submit to\n# Outputs ----------------------------------\n#SBATCH -o log/%x-%A-%a.out\n#SBATCH -e log/%x-%A-%a.err\n#SBATCH --mail-user=%u@domain.tld\n#SBATCH --mail-type=ALL\n# ------------------------------------------\n\nBIDS_DIR=\"$STUDY/data\"\nDERIVS_DIR=\"derivatives/fmriprep-20.2.2\"\nLOCAL_FREESURFER_DIR=\"$STUDY/data/derivatives/freesurfer-6.0.1\"\n\n# Prepare some writeable bind-mount points.\nTEMPLATEFLOW_HOST_HOME=$HOME/.cache/templateflow\nFMRIPREP_HOST_CACHE=$HOME/.cache/fmriprep\nmkdir -p ${TEMPLATEFLOW_HOST_HOME}\nmkdir -p ${FMRIPREP_HOST_CACHE}\n\n# Prepare derivatives folder\nmkdir -p ${BIDS_DIR}/${DERIVS_DIR}\n\n# Make sure FS_LICENSE is defined in the container.\nexport SINGULARITYENV_FS_LICENSE=$HOME/.freesurfer.txt\n\n# Designate a templateflow bind-mount point\nexport SINGULARITYENV_TEMPLATEFLOW_HOME=\"/templateflow\"\nSINGULARITY_CMD=\"singularity run --cleanenv -B $BIDS_DIR:/data -B ${TEMPLATEFLOW_HOST_HOME}:${SINGULARITYENV_TEMPLATEFLOW_HOME} -B $L_SCRATCH:/work -B ${LOCAL_FREESURFER_DIR}:/fsdir $STUDY/images/fmriprep_20.2.2.simg\"\n\n# Parse the participants.tsv file and extract one subject ID from the line corresponding to this SLURM task.\nsubject=$( sed -n -E \"$((${SLURM_ARRAY_TASK_ID} + 1))s/sub-(\\S*)\\>.*/\\1/gp\" ${BIDS_DIR}/participants.tsv )\n\n# Remove IsRunning files from FreeSurfer\nfind ${LOCAL_FREESURFER_DIR}/sub-$subject/ -name \"*IsRunning*\" -type f -delete\n\n# Compose the command line\ncmd=\"${SINGULARITY_CMD} /data /data/${DERIVS_DIR} participant --participant-label $subject -w /work/ -vv --omp-nthreads 8 --nthreads 12 --mem_mb 30000 --output-spaces MNI152NLin2009cAsym:res-2 anat fsnative fsaverage5 --use-aroma --fs-subjects-dir /fsdir\"\n\n# Setup done, run the command\necho Running task ${SLURM_ARRAY_TASK_ID}\necho Commandline: $cmd\neval $cmd\nexitcode=$?\n\n# Output results to a table\necho \"sub-$subject   ${SLURM_ARRAY_TASK_ID}    $exitcode\" \\\n      >> ${SLURM_JOB_NAME}.${SLURM_ARRAY_JOB_ID}.tsv\necho Finished tasks ${SLURM_ARRAY_TASK_ID} with exit code $exitcode\nexit $exitcode\n
Submission is then as easy as:

$ export STUDY=/path/to/some/folder\n$ sbatch --array=1-$(( $( wc -l $STUDY/data/participants.tsv | cut -f1 -d' ' ) - 1 )) fmriprep.slurm\n
  1. assuming that job arrays and Singularity are available\u00a0\u21a9

"},{"location":"assets/ORN-Workshop/presentation/","title":"Presentation","text":"

layout: false count: false

.middle.center[

"},{"location":"assets/ORN-Workshop/presentation/#building-communities-around-reproducible-workflows","title":"Building communities around reproducible workflows","text":""},{"location":"assets/ORN-Workshop/presentation/#o-esteban","title":"O. Esteban","text":""},{"location":"assets/ORN-Workshop/presentation/#chuv-lausanne-university-hospital","title":"CHUV | Lausanne University Hospital","text":""},{"location":"assets/ORN-Workshop/presentation/#wwwniprepsorg","title":"www.nipreps.org","text":"

]

layout: false count: false

.middle.center[

"},{"location":"assets/ORN-Workshop/presentation/#building-communities-around-reproducible-workflows_1","title":"Building communities around reproducible workflows","text":""},{"location":"assets/ORN-Workshop/presentation/#o-esteban_1","title":"O. Esteban","text":""},{"location":"assets/ORN-Workshop/presentation/#chuv-lausanne-university-hospital_1","title":"CHUV | Lausanne University Hospital","text":""},{"location":"assets/ORN-Workshop/presentation/#wwwniprepsorg_1","title":"www.nipreps.org","text":"

]

???

"},{"location":"assets/ORN-Workshop/presentation/#im-going-to-talk-about-how-we-are-building-a-framework-of-preprocessing-pipelines-for-neuroimaging-called-nipreps-based-on-the-fmriprep-experience","title":"I'm going to talk about how we are building a framework of preprocessing pipelines for neuroimaging called NiPreps, based on the fMRIPrep experience.","text":"

name: newsection layout: true class: section-separator

.perma-sidebar[

"},{"location":"assets/ORN-Workshop/presentation/#data-processing","title":"Data Processing","text":""},{"location":"assets/ORN-Workshop/presentation/#day-2-15h-cet","title":"(Day 2, 15h CET)","text":""},{"location":"assets/ORN-Workshop/presentation/#workflows","title":"Workflows","text":"

]

name: sidebar layout: true

.perma-sidebar[

"},{"location":"assets/ORN-Workshop/presentation/#data-processing_1","title":"Data Processing","text":""},{"location":"assets/ORN-Workshop/presentation/#day-2-15h-cet_1","title":"(Day 2, 15h CET)","text":""},{"location":"assets/ORN-Workshop/presentation/#workflows_1","title":"Workflows","text":"

]

template: sidebar

"},{"location":"assets/ORN-Workshop/presentation/#neuroimaging-is-now-mature","title":"Neuroimaging is now mature","text":"
  • many excellent tools available (from specialized to foundational)
  • large toolboxes (AFNI, ANTs/ITK, FreeSurfer, FSL, Nilearn, SPM, etc.)
  • workflow software (Nipype, Shellscripts, Nextflow, CWL)
  • container technology, CI/CD

  • a wealth of prior knowledge (esp. about humans)

  • LOTS of data acquired everyday

"},{"location":"assets/ORN-Workshop/presentation/#workflows-games-on","title":"Workflows - game's on!","text":"
  • although many neuroimaging areas are still in search of methodological breakthroughs,

  • challenges have moved on to the workflows:

  • workflows within traditional toolboxes - usually not flexible to adapt to new data
  • BIDS and BIDS-Apps.

???

  • researchers have a large portfolio of image processing components readily available
  • toolboxes with great support and active maintenance:
"},{"location":"assets/ORN-Workshop/presentation/#new-questions-changing-the-focus","title":"New questions changing the focus:","text":""},{"location":"assets/ORN-Workshop/presentation/#-validity-does-the-workflow-actually-work-out","title":"- validity (does the workflow actually work out?)","text":""},{"location":"assets/ORN-Workshop/presentation/#-transparency-is-it-a-black-box-how-precise-is-reporting","title":"- transparency (is it a black-box? how precise is reporting?)","text":""},{"location":"assets/ORN-Workshop/presentation/#-vibration-how-each-tool-choice-parameters-affect-overall","title":"- vibration (how each tool choice & parameters affect overall?)","text":""},{"location":"assets/ORN-Workshop/presentation/#-throughput-how-much-datatime-can-it-possible-take","title":"- throughput (how much data/time can it possible take?)","text":""},{"location":"assets/ORN-Workshop/presentation/#-robustness-can-i-use-it-on-diverse-studies","title":"- robustness (can I use it on diverse studies?)","text":""},{"location":"assets/ORN-Workshop/presentation/#-evaluation-what-is-it-unique-about-the-workflow-wrt-existing-alternatives","title":"- evaluation (what is it unique about the workflow, w.r.t. existing alternatives?)","text":""},{"location":"assets/ORN-Workshop/presentation/#the-garden-of-forking-paths","title":"The garden of forking paths","text":"

(Botvinik-Nezer et al., 2020)

Around 50% of teams used fMRIPrep'ed inputs.

"},{"location":"assets/ORN-Workshop/presentation/#the-fmriprep-story","title":"The fMRIPrep story","text":""},{"location":"assets/ORN-Workshop/presentation/#fmriprep-produces-analysis-ready-data-from-diverse-data","title":"fMRIPrep produces analysis-ready data from diverse data","text":"
  • minimal requirements (BIDS-compliant);
  • agnostic to downstream steps of the workflow
  • produces BIDS-Derivatives;
  • robust against inhomogeneity of data across studies

???

fMRIPrep takes in a task-based or resting-state functional MRI dataset in BIDS-format and returns preprocessed data ready for analysis.

Preprocessed data can be used for a broad range of analysis, and they are formatted following BIDS-Derivatives to maximize compatibility with: * major software packages (AFNI, FSL, SPM*, etc.) * further temporal filtering and denoising: fMRIDenoise * any BIDS-Derivatives compliant tool (e.g., FitLins).

--

"},{"location":"assets/ORN-Workshop/presentation/#fmriprep-is-a-bids-app-gorgolewski-et-al-2017","title":"fMRIPrep is a BIDS-App (Gorgolewski, et al. 2017)","text":"
  • adhered to modern software-engineering standards (CI/CD, containers)
  • compatible interface with other BIDS-Apps
  • optimized for automatic execution

???

fMRIPrep adopts the BIDS-App specifications. That means the software is tested with every change to the codebase, it also means that packaging, containerization, and deployment are also automated and require tests to be passing. BIDS-Apps are inter-operable (via BIDS-Derivatives), and optimized for execution in HPC, Cloud, etc.

--

"},{"location":"assets/ORN-Workshop/presentation/#minimizes-human-intervention","title":"Minimizes human intervention","text":"
  • avoid error-prone parameters settings (read them from BIDS)
  • adapts the workflow to the actual data available
  • while remaining flexible to some design choices (e.g., whether or not reconstructing surfaces or customizing target normalized standard spaces)

???

fMRIPrep minimizes human intervention because the user does not need to fiddle with any parameters - they are obtained from the BIDS structure. However, fMRIPrep does allow some flexibility to ensure the preprocessing meets the requirements of the intended analyses.

"},{"location":"assets/ORN-Workshop/presentation/#fmriprep-was-not-originally-envisioned-as-a-community-project","title":"fMRIPrep was not originally envisioned as a community project ...","text":"

(we just wanted a robust tool to automatically preprocess incoming data of OpenNeuro.org)

--

"},{"location":"assets/ORN-Workshop/presentation/#but-a-community-built-up-quickly-around-it","title":"... but a community built up quickly around it","text":"

--

.pull-left[

"},{"location":"assets/ORN-Workshop/presentation/#why","title":"Why?","text":"
  • Preprocessing of fMRI was in need for division of labor.

  • Obsession with transparency made early-adopters confident of the recipes they were applying.

  • Responsiveness to feedback. ]

.pull-right[

]

???

Preprocessing is a time-consuming effort, requires expertise converging imaging foundations & CS, typically addressed with legacy in-house pipelines.

On the right-hand side, you'll find the chart of unique visitors to fmriprep.org, which is the documentation website.

"},{"location":"assets/ORN-Workshop/presentation/#key-aspect-credit-all-direct-contributors","title":"Key aspect: credit all direct contributors","text":"

--

"},{"location":"assets/ORN-Workshop/presentation/#and-indirect-citation-boilerplate","title":".. and indirect: citation boilerplate.","text":""},{"location":"assets/ORN-Workshop/presentation/#researchers-want-to-spend-more-time-on-those-areas-most-relevant-to-them","title":"Researchers want to spend more time on those areas most relevant to them","text":"

(probably not preprocessing...)

???

With the development of fMRIPrep we understood that researchers don't want to waste their time on preprocessing (except for researchers developing new preprocessing techniques).

--

"},{"location":"assets/ORN-Workshop/presentation/#writing-fmriprep-required-a-team-of-several-experts-in-processing-methods-for-neuroimaging-with-a-solid-base-on-computer-science","title":"Writing fMRIPrep required a team of several experts in processing methods for neuroimaging, with a solid base on Computer Science.","text":"

(research programs just can't cover the neuroscience and the engineering of the whole workflow - we need to divide the labor)

???

The current neuroimaging workflow requires extensive knowledge in sometimes orthogonal fields such as neuroscience and computer science. Dividing the labor in labs, communities or individuals with the necessary expertise is the fundamental for the advance of the whole field.

--

"},{"location":"assets/ORN-Workshop/presentation/#transparency-helps-against-the-risk-of-super-easy-tools","title":"Transparency helps against the risk of super-easy tools","text":"

(easy-to-use tools are risky because they might get a researcher very far with no idea whatsoever of what they've done)

???

There is an implicit risk in making things too easy to operate:

For instance, imagine someone who runs fMRIPrep on diffusion data by tricking the BIDS naming into an apparently functional MRI dataset. If fMRIPrep reached the end at all, the garbage at the output could be fed into further tools, in a sort of a snowballing problem.

When researchers have access to the guts of the software and are given an opportunity to understand what's going on, the risk of misuse dips.

--

"},{"location":"assets/ORN-Workshop/presentation/#established-toolboxes-do-not-have-incentives-for-compatibility","title":"Established toolboxes do not have incentives for compatibility","text":"

(and to some extent this is not necessarily bad, as long as they are kept well-tested and they embrace/help-develop some minimal standards)

???

AFNI, ANTs, FSL, FreeSurfer, SPM, etc. have comprehensive software validation tests, methodological validation tests, stress tests, etc. - which pushed up their quality and made them fundamental for the field.

Therefore, it is better to keep things that way (although some minimal efforts towards convergence in compatibility are of course welcome)

"},{"location":"assets/ORN-Workshop/presentation/#the-dmriprep-story","title":"The dMRIPrep story","text":"

After the success of fMRIPrep, some neuroimagers asked \"when a diffussion MRI fMRIPrep?\"

"},{"location":"assets/ORN-Workshop/presentation/#neurostarsorg","title":"NeuroStars.org","text":"

(please note this down)

--

Same situation in the field of diffusion MRI:

"},{"location":"assets/ORN-Workshop/presentation/#image-processing-possible-guidelines-for-the-standardization-clinical-applications-j-veraart","title":"Image Processing: Possible Guidelines for the Standardization & Clinical Applications (J. Veraart)","text":"

(https://www.ismrm.org/19/program_files/MIS15.htm)

--

"},{"location":"assets/ORN-Workshop/presentation/#please-join","title":"Please join!","text":"

Joseph, M.; Pisner, D.; Richie-Halford, A.; Lerma-Usabiaga, G.; Keshavan, A.; Kent, JD.; Cieslak, M.; Poldrack, RA.; Rokem, A.; Esteban, O.

template: newsection layout: false

.middle.center[

"},{"location":"assets/ORN-Workshop/presentation/#wwwniprepsorg_2","title":"www.nipreps.org","text":""},{"location":"assets/ORN-Workshop/presentation/#nipreps-neuroimaging-preprocessing-tools","title":"(NiPreps == NeuroImaging PREProcessing toolS)","text":"

]

???

The enormous success of fMRIPrep led us to propose its generalization to other MRI and non-MRI modalities, as well as nonhuman species (for instance, rodents), and particular populations currently unsupported by fMRIPrep such as infants.

"},{"location":"assets/ORN-Workshop/presentation/#augmenting-scanners-to-produce-analysis-grade-data","title":"Augmenting scanners to produce \"analysis-grade\" data","text":""},{"location":"assets/ORN-Workshop/presentation/#data-directly-consumable-by-analyses","title":"(data directly consumable by analyses)","text":"

.pull-left[

Analysis-grade data is an analogy to the concept of \"sushi-grade (or sashimi-grade) fish\" in that both are:

.large[minimally preprocessed,]

and

.large[safe to consume directly.] ]

.pull-right[ ]

???

The goal, therefore, of NiPreps is to extend the scanner so that, in a way, they produce data ready for analysis.

We liken these analysis-grade data to sushi-grade fish, because in both cases the product is minimally preprocessed and at the same time safe to consume as is.

template: newsection layout: false

.middle.center[

"},{"location":"assets/ORN-Workshop/presentation/#deconstructing-fmriprep","title":"Deconstructing fMRIPrep","text":"

]

???

For the last two years we've been decomposing the architecture of fMRIPrep, spinning off its constituent parts that are valuable in other applications.

This process of decoupling (to use a proper CS term) has been greatly facilitated by the modular nature of the code since its inception.

???

The processing elements extracted from fMRIPrep can be mapped to three regimes of responsibility:

  • Software infrastructure composed by tools ensuring the collaboration and the most basic tooling.
  • Middleware utilities, which build more advanced tooling based on the foundational infrastructure
  • And at the top of the stack end-user applications - namely fMRIPrep, dMRIPrep, sMRIPrep and MRIQC.

As we can see, the boundaries of these three architectural layers are soft and tools such as TemplateFlow may stand in between.

Only projects enclosed in the brain shape pertain to the NiPreps community. NiPype, NiBabel and BIDS are so deeply embedded as dependencies that NiPreps can't be understood without them.

  • BIDS provides a standard, guaranteeing I/O agreements:

  • Allows workflows to self-adapt to the inputs

  • Ensures the shareability of the results

  • PyBIDS: a Python tool to query BIDS datasets (Yarkoni et al., 2019):

>>> from bids import BIDSLayout\n\n# Point PyBIDS to the dataset's path\n>>> layout = BIDSLayout(\"/data/coolproject\")\n\n# List the participant IDs of present subjects\n>>> layout.get_subjects()\n['01', '02', '03', '04', '05']\n\n# List session identifiers, if present\n>>> layout.get_sessions()\n['01', '02']\n\n# List functional MRI tasks\n>>> layout.get_tasks()\n['rest', 'nback']\n

???

BIDS is one of the keys to success for fMRIPrep and consequently, a strategic element of NiPreps.

Because the tools so far are written in Python, PyBIDS is a powerful tool to index and query inputs and outputs.

The code snippet illustrates the ease to find out the subject identifiers available in the dataset, sessions, and tasks.

"},{"location":"assets/ORN-Workshop/presentation/#bids-derivatives","title":"BIDS Derivatives","text":"

.cut-right[

derivatives/\n\u251c\u2500\u2500 fmriprep/\n\u2502 \u251c\u2500\u2500 dataset_description.json\n\u2502 \u251c\u2500\u2500 logs\n\u2502 \u251c\u2500\u2500 sub-01.html\n\u2502 \u251c\u2500\u2500 sub-01/\n\u2502 \u2502 \u251c\u2500\u2500 anat/\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_desc-brain_mask.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_dseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_label-GM_probseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_label-WM_probseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_label-CSF_probseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_desc-preproc_T1w.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_space-MNI152_desc-brain_mask.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_space-MNI152_dseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_space-MNI152_label-GM_probseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_space-MNI152_label-WM_probseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_space-MNI152_label-CSF_probseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_space-MNI152_desc-preproc_T1w.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_from-MNI152_to-T1w_mode-image_xfm.h5\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_from-T1w_to-MNI152_mode-image_xfm.h5\n\u2502 \u2502 \u2502 \u2514\u2500\u2500 sub-01_from-orig_to-T1w_mode-image_xfm.txt\n\u2502 \u2502 \u251c\u2500\u2500 figures/\n\u2502 \u2502 \u2514\u2500\u2500 func/\n\u2502 \u2502   \u251c\u2500\u2500 sub-01_task-rhymejudgment_space-MNI152_boldref.nii.gz\n\u2502 \u2502   \u251c\u2500\u2500 sub-01_task-rhymejudgment_space-MNI152_desc-preproc_bold.nii.gz\n\u2502 \u2502   \u251c\u2500\u2500 sub-01_task-rhymejudgment_space-MNI152_desc-confounds_regressors.nii.gz\n\u2502 \u2502   \u2514\u2500\u2500 sub-01_task-rhymejudgment_space-MNI152_desc-brain_mask.nii.gz\n
]

???

All NiPreps must write out BIDS-Derivatives. As illustrated in the example, the outputs of fMRIPrep are very similar to the BIDS standard for acquired data.

"},{"location":"assets/ORN-Workshop/presentation/#bids-apps","title":"BIDS-Apps","text":"
  • BIDS-Apps proposes a workflow structure model:
  • Use of containers & CI/CD

  • Uniform interface: .cut-right[

    fmriprep /data /data/derivatives/fmriprep-20.1.1 participant [+OPTIONS]\n
    ]

???

All end-user applications in NiPreps must conform to the BIDS-Apps specifications.

The BIDS-Apps paper identified a common pattern in neuroimaging studies, where individual participants (and runs) are processed first individually, and then based on the outcomes, further levels of data aggregation are executed.

For this reason, BIDS-Apps define two major levels of execution: participant and group level.

Finally, the paper also stresses the importance of containerizing applications to ensure long-term preservation of run-to-run repeatability and proposes a common command line interface as described at the bottom:

  • first the name of the BIDS-Apps (fmriprep, in this case)
  • followed by input and output directories (respectively),
  • to finally indicate the analysis level (always participant, for the case of fmriprep)

.pull-left[

from nipype.interfaces.fsl import BET\nbrain_extract = BET(\n  in_file=\"/data/coolproject/sub-01/ses-01/anat/sub-01_ses-01_T1w.nii\",\n  out_file=\"/out/sub-01/ses-01/anat/sub-01_ses-01_desc-brain_T1w.nii\"\n)\nbrain_extract.run()\n

Nipype is the gateway to mix-and-match from AFNI, ANTs, Dipy, FreeSurfer, FSL, MRTrix, SPM, etc. ]

.pull-right[

]

???

Nipype is the glue stitching together all the underlying neuroimaging toolboxes and provides the execution framework.

The snippet shows how the widely known BET tool from FSL can be executed using NiPype. This is a particular example instance of interfaces - which provide uniform access to the tooling with Python.

Finally, combining these interfaces we generate processing workflows to fulfill higher level processing tasks.

???

For instance, we may have a look into fMRIPrep's functional processing block.

Nipype helps understand (and opens windows in the black box) generating these graph representation of the workflow.

\"\"\"Fix the affine of a rodent dataset, imposing 0.2x0.2x0.2 [mm].\"\"\"\nimport numpy as np\nimport nibabel as nb\n\n# Open the file\nimg = nb.load(\"sub-25_MGE_MouseBrain_3D_MGE_150.nii.gz\")\n\n# New (correct) affine\naff = np.diag((-0.2, -0.2, 0.2, 1.0))\n\n# Use nibabel to reorient to canonical\ncard = nb.as_closest_canonical(nb.Nifti1Image(\n    img.dataobj,\n    np.diag((-0.2, -0.2, 0.2, 1.0)),\n    None\n))\n\n# Save to disk\ncard.to_filename(\"sub-25_T2star.nii.gz\")\n

???

NiBabel allows Python to easily access neuroimaging data formats such as NIfTI, GIFTI and CIFTI2.

Although this might be a trivial task, the proliferation of neuroimaging software has led to some sort of Wild West of formats, and sometimes interoperation is not ensured.

"},{"location":"assets/ORN-Workshop/presentation/#in-the-snippet-we-can-see-how-we-can-manipulate-the-orientation-headers-of-a-nifti-volume-in-particular-a-rodent-image-with-incorrect-affine-information","title":"In the snippet, we can see how we can manipulate the orientation headers of a NIfTI volume, in particular a rodent image with incorrect affine information.","text":"

.pull-left[

Transforms typically are the outcome of image registration methodologies

The proliferation of software implementations of image registration methodologies has resulted in a spread of data structures and file formats used to preserve and communicate transforms.

(Esteban et al., 2020) ]

.pull-right[

]

???

NiTransforms is a super-interesting toy project where we are exercising our finest coding skills. It completes NiBabel in the effort of making spatial transforms calculated by neuroimaging software tools interoperable.

When it goes beyond the alpha state, it is expected to be merged into NiBabel.

At the moment, NiTransforms is already integrated in fMRIPrep +20.1 to concatenate LTA (linear affine transforms) transforms obtained with FreeSurfer, ITK transforms obtained with ANTs, and motion parameters estimated with FSL.

Compatibility across formats is hard due to the many arbitrary decisions in establishing the mathematical framework of the transform and the intrinsic confusion of applying a transform.

While intuitively we understand applying a transform as \"transforming the moving image so that I can represent it overlaid or fused with the reference image and both should look aligned\", in reality, we only transform coordinates from the reference image into the moving image's space (step 1 on the right).

Once we know where the center of every voxel of the reference image falls in the moving image coordinate system, we read in the information (in other words, a value) from the moving image. Because the location will probably be off-grid, we interpolate such a value from the neighboring voxels (step 2).

Finally (step 3) we generate a new image object with the structure of the reference image and the data interpolated from the moving information. This new image object is the moving image \"moved\" on to the reference image space and thus, both look aligned.

.pull-left[

  • The Archive (right) is a repository of templates and atlases
  • The Python Client (bottom) provides easy access (with lazy-loading) to the Archive
>>> from templateflow import api as tflow\n>>> tflow.get(\n...     'MNI152NLin6Asym',\n...     desc=None,\n...     resolution=1,\n...     suffix='T1w',\n...     extension='nii.gz'\n... )\nPosixPath('/templateflow_home/tpl-MNI152NLin6Asym/tpl-MNI152NLin6Asym_res-01_T1w.nii.gz')\n

.large[www.templateflow.org] ]

.pull-right[

]

???

One of the most ancient feature requests received from fMRIPrep early adopters was improving the flexibility of spatial normalization to standard templates other than fMRIPrep's default.

For instance, infant templates.

TemplateFlow offers an Archive of templates where they are stored, maintained and re-distributed;

and a Python client that helps accessing them.

On the right hand side, an screenshot of the TemplateFlow browser shows some of the templates currently available in the repository. The browser can be reached at www.templateflow.org.

The tool is based on PyBIDS, and the snippet will surely remind you of it. In this case the example shows how to obtain the T1w template corresponding to FSL's MNI space, at the highest resolution.

If the files requested are not in TemplateFlow's cache, they will be pulled down and kept for further utilization.

"},{"location":"assets/ORN-Workshop/presentation/#templateflow-archive","title":"TemplateFlow - Archive","text":"

.small[(Ciric et al. 2020, in prep)]

???

The Archive allows a rich range of data and metadata to be stored with the template.

Datatypes in the repository cover:

  • images containing population-average templates,
  • masks (for instance brain masks),
  • atlases (including parcellations and segmentations)
  • transform files between templates

Metadata can be stored with the usual BIDS options.

Finally, templates allow having multiple cohorts, in a similar encoding to that of multi-session BIDS datasets.

Multiple cohorts are useful, for instance, in infant templates with averages at several gestational ages.

NiWorkflows is a miscellaneous mixture of tooling used by downstream NiPreps:

???

NiWorkflows is, historically, the first component detached from fMRIPrep.

For that reason, its scope and vision has very fuzzy boundaries as compared to the other tools.

The most relevant utilities incorporated within NiWorkflows are:

--

  • The reportlet aggregation and individual report generation system

???

First, the individual report system which aggregates the visual elements or the reports (which we call \"reportlets\") and generates the final HTML document.

Also, most of the engineering behind the generation of these reportlets and their integration within NiPype are part of NiWorkflows

--

  • Custom extensions to NiPype interfaces

???

Beyond the extension of NiPype to generate a reportlet from any given interface, NiWorkflows is the test bed for many utilities that are then upstreamed to nipype.

Also, special interfaces with a limited scope that should not be included in nipype are maintained here.

--

  • Workflows useful across applications

???

Finally, NiWorkflows indeed offers workflows that can be used by end-user NiPreps. For instance atlas-based brain extraction of anatomical images, based on ANTs.

???

Echo-planar imaging (EPI) are typically affected by distortions along the phase encoding axis, caused by the perturbation of the magnetic field at tissue interfaces.

Looking at the reportlet, we can see how in the \"before\" panel, the image is warped.

The distortion is most obvious in the coronal view (middle row) because this image has posterior-anterior phase encoding.

Focusing on the changes between \"before\" and \"after\" correction in this coronal view, we can see how the blue contours delineating the corpus callosum fit better the dark shade in the data after correction.

"},{"location":"assets/ORN-Workshop/presentation/#upcoming-new-utilities","title":"Upcoming new utilities","text":""},{"location":"assets/ORN-Workshop/presentation/#nibabies-fmriprep-babies","title":"NiBabies | fMRIPrep-babies","text":"
  • Mathias Goncalves
"},{"location":"assets/ORN-Workshop/presentation/#nirodents-fmriprep-rodents","title":"NiRodents | fMRIPrep-rodents","text":"
  • Eilidh MacNicol

???

So, what's coming up next?

NiBabies is some sort of NiWorkflows equivalent for the preprocessing of infant imaging. At the moment, only atlas-based brain extraction using ANTs (and adapted from NiWorkflows) is in active developments.

Next steps include brain tissue segmentation.

Similarly, NiRodents is the NiWorkflows parallel for the prepocessing of rodent preclinical imaging. Again, only atlas-based brain extraction adapted from NiWorkflows is being developed.

"},{"location":"assets/ORN-Workshop/presentation/#nipreps-is-a-framework-for-the-development-of-preprocessing-workflows","title":"NiPreps is a framework for the development of preprocessing workflows","text":"
  • Principled design, with BIDS as an strategic component
  • Leveraging existing, widely used software
  • Using NiPype as a foundation

???

To wrap-up, I've presented NiPreps, a framework for developing preprocessing workflows inspired by fMRIPrep.

The framework is heavily principle and tags along BIDS as a foundational component

NiPreps should not reinvent any wheel, trying to reuse as much as possible of the widely used and tested existing software.

Nipype serves as a glue components to orchestrate workflows.

--

"},{"location":"assets/ORN-Workshop/presentation/#why-preprocessing","title":"Why preprocessing?","text":"
  • We propose to consider preprocessing as part of the image acquisition and reconstruction
  • When setting the boundaries that way, it seems sensible to pursue some standardization in the preprocessing:
  • Less experimental degrees of freedom for the researcher
  • Researchers can focus on the analysis
  • More homogeneous data at the output (e.g., for machine learning)
  • How:
  • Transparency is key to success: individual reports and documentation (open source is implicit).
  • Best engineering practices (e.g., containers and CI/CD)

???

But why just preprocessing, with a very strict scope?

We propose to think about preprocessing as part of the image acquisition and reconstruction process (in other words, scanning), rather than part of the analysis workflow.

This decoupling from analysis comes with several upshots:

First, there are less moving parts to play with for researchers in the attempt to fit their methods to the data (instead of fitting data with their methods).

Second, such division of labor allows the researcher to use their time in the analysis.

Finally, two preprocessed datasets from two different studies and scanning sites should be more homogeneous when processed with the same instruments, in comparison to processing them with idiosyncratic, lab-managed, preprocessing workflows.

However, for NiPreps to work we need to make sure the tools are transparent.

Not just with the individual reports and thorough documentation, also because of the community driven development. For instance, the peer-review process that goes around large incremental changes is fundamental to ensure the quality of the tool.

In addition, best engineering practices suggested in the BIDS-Apps paper, along with those we have been including with fMRIPrep, are necessary to ensure the quality of the final product.

--

"},{"location":"assets/ORN-Workshop/presentation/#challenges","title":"Challenges","text":"
  • Testing / Validation!

???

As an open problem, validating the results of the tool remains extremely challenging for the lack in gold standard datasets that can tell us the best possible outcome.

"},{"location":"assets/ORN-Workshop/presentation/#the-nmind-story","title":"The NMiND story","text":"

NMiND = NeverMIND, this Neuroimaging Method Is Not Duplicated

"},{"location":"assets/ORN-Workshop/presentation/#pis-worried-about-methodological-duplicity","title":"PIs worried about methodological duplicity","text":"

M. Milham, D. Fair, T. Satterthwaite, S. Ghosh, R. Poldrack, etc.

--

"},{"location":"assets/ORN-Workshop/presentation/#nminds-workgroups","title":"NMiND's workgroups","text":"

nosology group, coding standards & patterns, sharing standards, testing standards, crediting contributors, funding strategy, benchmarking datasets.

"},{"location":"assets/ORN-Workshop/presentation/#nminds-nosology-goals","title":"NMiND's nosology goals","text":"
  • Consensus glossary of terms
  • Landscape the portfolio of methodological solutions along several experimental and computational dimensions
  • Organize and document a taxonomy along those dimensions
  • Index existing software (from unit methods to workflows) in the taxonomy

Please Join!

template: newsection layout: false

.middle.center[

"},{"location":"assets/ORN-Workshop/presentation/#thanks","title":"Thanks!","text":""},{"location":"assets/ORN-Workshop/presentation/#questions","title":"Questions?","text":"

]

"},{"location":"assets/bhd2020/presentation/","title":"Presentation","text":"

layout: false count: false

.middle.center[

"},{"location":"assets/bhd2020/presentation/#nipreps-neuroimaging-preprocessing-tools","title":"NiPreps | NeuroImaging PREProcessing toolS","text":""},{"location":"assets/bhd2020/presentation/#o-esteban","title":"O. Esteban","text":""},{"location":"assets/bhd2020/presentation/#chuv-lausanne-university-hospital","title":"CHUV | Lausanne University Hospital","text":""},{"location":"assets/bhd2020/presentation/#wwwniprepsorgassetsbhd2020","title":"www.nipreps.org/assets/bhd2020","text":"

]

layout: false count: false

.middle.center[

"},{"location":"assets/bhd2020/presentation/#nipreps-neuroimaging-preprocessing-tools_1","title":"NiPreps | NeuroImaging PREProcessing toolS","text":""},{"location":"assets/bhd2020/presentation/#o-esteban_1","title":"O. Esteban","text":""},{"location":"assets/bhd2020/presentation/#chuv-lausanne-university-hospital_1","title":"CHUV | Lausanne University Hospital","text":""},{"location":"assets/bhd2020/presentation/#wwwniprepsorgassetsbhd2020_1","title":"www.nipreps.org/assets/bhd2020","text":"

]

???

"},{"location":"assets/bhd2020/presentation/#im-going-to-talk-about-how-we-are-building-a-framework-of-preprocessing-pipelines-for-neuroimaging-called-nipreps-based-on-the-fmriprep-experience","title":"I'm going to talk about how we are building a framework of preprocessing pipelines for neuroimaging called NiPreps, based on the fMRIPrep experience.","text":"

name: newsection layout: true class: section-separator

.perma-sidebar[

"},{"location":"assets/bhd2020/presentation/#bhd2020","title":"BHD2020","text":""},{"location":"assets/bhd2020/presentation/#day-2-14h-cet","title":"(Day 2, 14h CET)","text":""},{"location":"assets/bhd2020/presentation/#nipreps","title":"NiPreps","text":"

]

name: sidebar layout: true

.perma-sidebar[

"},{"location":"assets/bhd2020/presentation/#bhd2020_1","title":"BHD2020","text":""},{"location":"assets/bhd2020/presentation/#day-2-14h-cet_1","title":"(Day 2, 14h CET)","text":""},{"location":"assets/bhd2020/presentation/#nipreps_1","title":"NiPreps","text":"

]

template: sidebar

"},{"location":"assets/bhd2020/presentation/#outlook","title":"Outlook","text":""},{"location":"assets/bhd2020/presentation/#1-understand-what-preprocessing-is-from-fmri","title":"1. Understand what preprocessing is - from fMRI","text":""},{"location":"assets/bhd2020/presentation/#2-the-fmriprep-experience","title":"2. The fMRIPrep experience","text":""},{"location":"assets/bhd2020/presentation/#3-the-dmriprep-experience","title":"3. The dMRIPrep experience","text":""},{"location":"assets/bhd2020/presentation/#4-importance-of-the-visual-reports","title":"4. Importance of the visual reports","text":""},{"location":"assets/bhd2020/presentation/#5-introducing-nipreps","title":"5. Introducing NiPreps","text":""},{"location":"assets/bhd2020/presentation/#6-open-forum-first-steps-and-contributing","title":"6. Open forum: first steps and contributing","text":""},{"location":"assets/bhd2020/presentation/#the-research-workflow-of-functional-mri-nowadays","title":"The research workflow of functional MRI (nowadays)","text":"

(source: next slide)

"},{"location":"assets/bhd2020/presentation/#the-research-workflow-of-functional-mri-2006","title":"The research workflow of functional MRI (2006)","text":"

(Strother, 2006; 10.1109/MEMB.2006.1607667)

"},{"location":"assets/bhd2020/presentation/#the-research-workflow-of-functional-mri-ab","title":"The research workflow of functional MRI (a.B.*)","text":"

Adapted (Strother, 2006)

*a.B. = after BIDS (Brain Imaging Data Structure; Gorgolewski et al. (2016))

"},{"location":"assets/bhd2020/presentation/#neuroimaging-is-now-mature","title":"Neuroimaging is now mature","text":"
  • many excellent tools available (from specialized to foundational)
  • large toolboxes (AFNI, ANTs/ITK, FreeSurfer, FSL, Nilearn, SPM, etc.)
  • workflow software (Nipype, Shellscripts, Nextflow, CWL)
  • container technology, CI/CD

  • a wealth of prior knowledge (esp. about humans)

  • LOTS of data acquired everyday

"},{"location":"assets/bhd2020/presentation/#bids-a-thrust-of-technology-driven-development","title":"BIDS - A thrust of technology-driven development","text":"
  • A uniform and complete interface to data:

  • Uniform: enables the workflow adapt to the data

  • Complete: enables validation and minimizes human-intervention

  • Extensible reproducibility:

  • BIDS-Derivatives

  • BIDS-Apps (Gorgolewski et al., 2017)

???

  • researchers have a large portfolio of image processing components readily available
  • toolboxes with great support and active maintenance:
"},{"location":"assets/bhd2020/presentation/#new-questions-changing-the-focus","title":"New questions changing the focus:","text":""},{"location":"assets/bhd2020/presentation/#-validity-does-the-workflow-actually-work-out","title":"- validity (does the workflow actually work out?)","text":""},{"location":"assets/bhd2020/presentation/#-transparency-is-it-a-black-box-how-precise-is-reporting","title":"- transparency (is it a black-box? how precise is reporting?)","text":""},{"location":"assets/bhd2020/presentation/#-vibration-how-each-tool-choice-parameters-affect-overall","title":"- vibration (how each tool choice & parameters affect overall?)","text":""},{"location":"assets/bhd2020/presentation/#-throughput-how-much-datatime-can-it-possible-take","title":"- throughput (how much data/time can it possible take?)","text":""},{"location":"assets/bhd2020/presentation/#-robustness-can-i-use-it-on-diverse-studies","title":"- robustness (can I use it on diverse studies?)","text":""},{"location":"assets/bhd2020/presentation/#-evaluation-what-is-it-unique-about-the-workflow-wrt-existing-alternatives","title":"- evaluation (what is it unique about the workflow, w.r.t. existing alternatives?)","text":""},{"location":"assets/bhd2020/presentation/#the-garden-of-forking-paths","title":"The garden of forking paths","text":"

(Botvinik-Nezer et al., 2020)

Around 50% of teams used fMRIPrep'ed inputs.

"},{"location":"assets/bhd2020/presentation/#the-fmriprep-story","title":"The fMRIPrep story","text":""},{"location":"assets/bhd2020/presentation/#fmriprep-produces-analysis-ready-data-from-diverse-data","title":"fMRIPrep produces analysis-ready data from diverse data","text":"
  • minimal requirements (BIDS-compliant);
  • agnostic to downstream steps of the workflow
  • produces BIDS-Derivatives;
  • robust against inhomogeneity of data across studies

???

fMRIPrep takes in a task-based or resting-state functional MRI dataset in BIDS-format and returns preprocessed data ready for analysis.

Preprocessed data can be used for a broad range of analysis, and they are formatted following BIDS-Derivatives to maximize compatibility with: * major software packages (AFNI, FSL, SPM*, etc.) * further temporal filtering and denoising: fMRIDenoise * any BIDS-Derivatives compliant tool (e.g., FitLins).

--

"},{"location":"assets/bhd2020/presentation/#fmriprep-is-a-bids-app-gorgolewski-et-al-2017","title":"fMRIPrep is a BIDS-App (Gorgolewski, et al. 2017)","text":"
  • adhered to modern software-engineering standards (CI/CD, containers)
  • compatible interface with other BIDS-Apps
  • optimized for automatic execution

???

fMRIPrep adopts the BIDS-App specifications. That means the software is tested with every change to the codebase, it also means that packaging, containerization, and deployment are also automated and require tests to be passing. BIDS-Apps are inter-operable (via BIDS-Derivatives), and optimized for execution in HPC, Cloud, etc.

--

"},{"location":"assets/bhd2020/presentation/#minimizes-human-intervention","title":"Minimizes human intervention","text":"
  • avoid error-prone parameters settings (read them from BIDS)
  • adapts the workflow to the actual data available
  • while remaining flexible to some design choices (e.g., whether or not reconstructing surfaces or customizing target normalized standard spaces)

???

fMRIPrep minimizes human intervention because the user does not need to fiddle with any parameters - they are obtained from the BIDS structure. However, fMRIPrep does allow some flexibility to ensure the preprocessing meets the requirements of the intended analyses.

"},{"location":"assets/bhd2020/presentation/#fmriprep-was-not-originally-envisioned-as-a-community-project","title":"fMRIPrep was not originally envisioned as a community project ...","text":"

(we just wanted a robust tool to automatically preprocess incoming data of OpenNeuro.org)

--

"},{"location":"assets/bhd2020/presentation/#but-a-community-built-up-quickly-around-it","title":"... but a community built up quickly around it","text":"

--

.pull-left[

"},{"location":"assets/bhd2020/presentation/#why","title":"Why?","text":"
  • Preprocessing of fMRI was in need for division of labor.

  • Obsession with transparency made early-adopters confident of the recipes they were applying.

  • Responsiveness to feedback. ]

.pull-right[

]

???

Preprocessing is a time-consuming effort, requires expertise converging imaging foundations & CS, typically addressed with legacy in-house pipelines.

On the right-hand side, you'll find the chart of unique visitors to fmriprep.org, which is the documentation website.

"},{"location":"assets/bhd2020/presentation/#key-aspect-credit-all-direct-contributors","title":"Key aspect: credit all direct contributors","text":"

--

"},{"location":"assets/bhd2020/presentation/#and-indirect-citation-boilerplate","title":".. and indirect: citation boilerplate.","text":""},{"location":"assets/bhd2020/presentation/#researchers-want-to-spend-more-time-on-those-areas-most-relevant-to-them","title":"Researchers want to spend more time on those areas most relevant to them","text":"

(probably not preprocessing...)

???

With the development of fMRIPrep we understood that researchers don't want to waste their time on preprocessing (except for researchers developing new preprocessing techniques).

--

"},{"location":"assets/bhd2020/presentation/#writing-fmriprep-required-a-team-of-several-experts-in-processing-methods-for-neuroimaging-with-a-solid-base-on-computer-science","title":"Writing fMRIPrep required a team of several experts in processing methods for neuroimaging, with a solid base on Computer Science.","text":"

(research programs just can't cover the neuroscience and the engineering of the whole workflow - we need to divide the labor)

???

The current neuroimaging workflow requires extensive knowledge in sometimes orthogonal fields such as neuroscience and computer science. Dividing the labor in labs, communities or individuals with the necessary expertise is the fundamental for the advance of the whole field.

--

"},{"location":"assets/bhd2020/presentation/#transparency-helps-against-the-risk-of-super-easy-tools","title":"Transparency helps against the risk of super-easy tools","text":"

(easy-to-use tools are risky because they might get a researcher very far with no idea whatsoever of what they've done)

???

There is an implicit risk in making things too easy to operate:

For instance, imagine someone who runs fMRIPrep on diffusion data by tricking the BIDS naming into an apparently functional MRI dataset. If fMRIPrep reached the end at all, the garbage at the output could be fed into further tools, in a sort of a snowballing problem.

When researchers have access to the guts of the software and are given an opportunity to understand what's going on, the risk of misuse dips.

--

"},{"location":"assets/bhd2020/presentation/#established-toolboxes-do-not-have-incentives-for-compatibility","title":"Established toolboxes do not have incentives for compatibility","text":"

(and to some extent this is not necessarily bad, as long as they are kept well-tested and they embrace/help-develop some minimal standards)

???

AFNI, ANTs, FSL, FreeSurfer, SPM, etc. have comprehensive software validation tests, methodological validation tests, stress tests, etc. - which pushed up their quality and made them fundamental for the field.

Therefore, it is better to keep things that way (although some minimal efforts towards convergence in compatibility are of course welcome)

(Esteban et al., 2019)

"},{"location":"assets/bhd2020/presentation/#the-dmriprep-story","title":"The dMRIPrep story","text":"

After the success of fMRIPrep, Dr. A. Keshavan asked \"when a dMRIPrep?\"

"},{"location":"assets/bhd2020/presentation/#neurostarsorg","title":"NeuroStars.org","text":"

(please note this down)

"},{"location":"assets/bhd2020/presentation/#the-dmriprep-story_1","title":"The dMRIPrep story","text":"

After the success of fMRIPrep, Dr. A. Keshavan asked \"when a dMRIPrep?\"

Image Processing: Possible Guidelines for the Standardization & Clinical Applications

(Veraart, 2019)

"},{"location":"assets/bhd2020/presentation/#please-join","title":"Please join!","text":"

Joseph, M.; Pisner, D.; Richie-Halford, A.; Lerma-Usabiaga, G.; Keshavan, A.; Kent, JD.; Veraart, J.; Cieslak, M.; Poldrack, RA.; Rokem, A.; Esteban, O.

template: newsection layout: false

.middle.center[

"},{"location":"assets/bhd2020/presentation/#understanding-what-preprocessing-is-with-visual-reports","title":"Understanding what preprocessing is with visual reports","text":"

]

"},{"location":"assets/bhd2020/presentation/#the-individual-report","title":"The individual report","text":"

???

Let's walk through one example of report. Reports have several sections, starting with a summary indicating the particularities of this dataset and workflow choices made based on the input data.

The anatomical section follows with several visualizations to assess the anatomical processing steps mentioned before, spatial normalization to template spaces (the flickering panel helps assess alignment) and finally surface reconstruction.

Then, all functional runs are concatenated, and all show the same structure. After an initial summary of this particular run, the alignment to the same subject's anatomical image is presented, with contours of the white and pial surfaces as cues. Next panel shows the brain mask and ROIs utilized by the CompCor denoising. For each run we then find some visualizations to assess the generated confounding signals.

After all functional runs are presented, the About section keeps information to aid reproducibility of results, such as the software's version, or the exact command line run.

The boilerplate is found next, with a text version shown by default and tabs to convert to Markdown and LaTeX.

Reports conclude with a list of encountered errors (if any).

"},{"location":"assets/bhd2020/presentation/#reports-are-a-crucial-element-to-ensure-transparency","title":"Reports are a crucial element to ensure transparency","text":"

.pull-left[

]

.pull-right[

.distribute[ fMRIPrep generates one participant-wide report after execution.

Reports describe the data as found, and the steps applied (providing .blue[visual support to look inside the box]):

  1. show researchers their data;

  2. show how fMRIPrep interpreted the data (describing the actual preprocessing steps);

  3. quality control of results, facilitating early error detection. ] ]

???

Therefore, reports have become a fundamental feature of fMRIPrep because they not only allow assessing the quality of the processing, but also provide an insight about the logic supporting such processing.

In other words, reports help respond to the what was done and the why was it done in addition to the how well it did.

template: newsection layout: false

.middle.center[

"},{"location":"assets/bhd2020/presentation/#wwwniprepsorg","title":"www.nipreps.org","text":""},{"location":"assets/bhd2020/presentation/#nipreps-neuroimaging-preprocessing-tools_2","title":"(NiPreps == NeuroImaging PREProcessing toolS)","text":"

]

???

The enormous success of fMRIPrep led us to propose its generalization to other MRI and non-MRI modalities, as well as nonhuman species (for instance, rodents), and particular populations currently unsupported by fMRIPrep such as infants.

"},{"location":"assets/bhd2020/presentation/#augmenting-scanners-to-produce-analysis-grade-data","title":"Augmenting scanners to produce \"analysis-grade\" data","text":""},{"location":"assets/bhd2020/presentation/#data-directly-consumable-by-analyses","title":"(data directly consumable by analyses)","text":"

.pull-left[

Analysis-grade data is an analogy to the concept of \"sushi-grade (or sashimi-grade) fish\" in that both are:

.large[minimally preprocessed,]

and

.large[safe to consume directly.] ]

.pull-right[ ]

???

The goal, therefore, of NiPreps is to extend the scanner so that, in a way, they produce data ready for analysis.

We liken these analysis-grade data to sushi-grade fish, because in both cases the product is minimally preprocessed and at the same time safe to consume as is.

template: newsection layout: false

.middle.center[

"},{"location":"assets/bhd2020/presentation/#deconstructing-fmriprep","title":"Deconstructing fMRIPrep","text":"

]

???

For the last two years we've been decomposing the architecture of fMRIPrep, spinning off its constituent parts that are valuable in other applications.

This process of decoupling (to use a proper CS term) has been greatly facilitated by the modular nature of the code since its inception.

???

The processing elements extracted from fMRIPrep can be mapped to three regimes of responsibility:

  • Software infrastructure composed by tools ensuring the collaboration and the most basic tooling.
  • Middleware utilities, which build more advanced tooling based on the foundational infrastructure
  • And at the top of the stack end-user applications - namely fMRIPrep, dMRIPrep, sMRIPrep and MRIQC.

As we can see, the boundaries of these three architectural layers are soft and tools such as TemplateFlow may stand in between.

Only projects enclosed in the brain shape pertain to the NiPreps community. NiPype, NiBabel and BIDS are so deeply embedded as dependencies that NiPreps can't be understood without them.

  • BIDS provides a standard, guaranteeing I/O agreements:

  • Allows workflows to self-adapt to the inputs

  • Ensures the shareability of the results

  • PyBIDS: a Python tool to query BIDS datasets (Yarkoni et al., 2019):

>>> from bids import BIDSLayout\n\n# Point PyBIDS to the dataset's path\n>>> layout = BIDSLayout(\"/data/coolproject\")\n\n# List the participant IDs of present subjects\n>>> layout.get_subjects()\n['01', '02', '03', '04', '05']\n\n# List session identifiers, if present\n>>> layout.get_sessions()\n['01', '02']\n\n# List functional MRI tasks\n>>> layout.get_tasks()\n['rest', 'nback']\n

???

BIDS is one of the keys to success for fMRIPrep and consequently, a strategic element of NiPreps.

Because the tools so far are written in Python, PyBIDS is a powerful tool to index and query inputs and outputs.

The code snippet illustrates the ease to find out the subject identifiers available in the dataset, sessions, and tasks.

"},{"location":"assets/bhd2020/presentation/#bids-derivatives","title":"BIDS Derivatives","text":"

.cut-right[

derivatives/\n\u251c\u2500\u2500 fmriprep/\n\u2502 \u251c\u2500\u2500 dataset_description.json\n\u2502 \u251c\u2500\u2500 logs\n\u2502 \u251c\u2500\u2500 sub-01.html\n\u2502 \u251c\u2500\u2500 sub-01/\n\u2502 \u2502 \u251c\u2500\u2500 anat/\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_desc-brain_mask.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_dseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_label-GM_probseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_label-WM_probseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_label-CSF_probseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_desc-preproc_T1w.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_space-MNI152_desc-brain_mask.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_space-MNI152_dseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_space-MNI152_label-GM_probseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_space-MNI152_label-WM_probseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_space-MNI152_label-CSF_probseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_space-MNI152_desc-preproc_T1w.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_from-MNI152_to-T1w_mode-image_xfm.h5\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_from-T1w_to-MNI152_mode-image_xfm.h5\n\u2502 \u2502 \u2502 \u2514\u2500\u2500 sub-01_from-orig_to-T1w_mode-image_xfm.txt\n\u2502 \u2502 \u251c\u2500\u2500 figures/\n\u2502 \u2502 \u2514\u2500\u2500 func/\n\u2502 \u2502   \u251c\u2500\u2500 sub-01_task-rhymejudgment_space-MNI152_boldref.nii.gz\n\u2502 \u2502   \u251c\u2500\u2500 sub-01_task-rhymejudgment_space-MNI152_desc-preproc_bold.nii.gz\n\u2502 \u2502   \u251c\u2500\u2500 sub-01_task-rhymejudgment_space-MNI152_desc-confounds_regressors.nii.gz\n\u2502 \u2502   \u2514\u2500\u2500 sub-01_task-rhymejudgment_space-MNI152_desc-brain_mask.nii.gz\n
]

???

All NiPreps must write out BIDS-Derivatives. As illustrated in the example, the outputs of fMRIPrep are very similar to the BIDS standard for acquired data.

"},{"location":"assets/bhd2020/presentation/#bids-apps","title":"BIDS-Apps","text":"
  • BIDS-Apps proposes a workflow structure model:
  • Use of containers & CI/CD

  • Uniform interface: .cut-right[

    fmriprep /data /data/derivatives/fmriprep-20.1.1 participant [+OPTIONS]\n
    ]

???

All end-user applications in NiPreps must conform to the BIDS-Apps specifications.

The BIDS-Apps paper identified a common pattern in neuroimaging studies, where individual participants (and runs) are processed first individually, and then based on the outcomes, further levels of data aggregation are executed.

For this reason, BIDS-Apps define two major levels of execution: participant and group level.

Finally, the paper also stresses the importance of containerizing applications to ensure long-term preservation of run-to-run repeatability and proposes a common command line interface as described at the bottom:

  • first the name of the BIDS-Apps (fmriprep, in this case)
  • followed by input and output directories (respectively),
  • to finally indicate the analysis level (always participant, for the case of fmriprep)

.pull-left[

from nipype.interfaces.fsl import BET\nbrain_extract = BET(\n  in_file=\"/data/coolproject/sub-01/ses-01/anat/sub-01_ses-01_T1w.nii\",\n  out_file=\"/out/sub-01/ses-01/anat/sub-01_ses-01_desc-brain_T1w.nii\"\n)\nbrain_extract.run()\n

Nipype is the gateway to mix-and-match from AFNI, ANTs, Dipy, FreeSurfer, FSL, MRTrix, SPM, etc. ]

.pull-right[

]

???

Nipype is the glue stitching together all the underlying neuroimaging toolboxes and provides the execution framework.

The snippet shows how the widely known BET tool from FSL can be executed using NiPype. This is a particular example instance of interfaces - which provide uniform access to the tooling with Python.

Finally, combining these interfaces we generate processing workflows to fulfill higher level processing tasks.

???

For instance, we may have a look into fMRIPrep's functional processing block.

Nipype helps understand (and opens windows in the black box) generating these graph representation of the workflow.

\"\"\"Fix the affine of a rodent dataset, imposing 0.2x0.2x0.2 [mm].\"\"\"\nimport numpy as np\nimport nibabel as nb\n\n# Open the file\nimg = nb.load(\"sub-25_MGE_MouseBrain_3D_MGE_150.nii.gz\")\n\n# New (correct) affine\naff = np.diag((-0.2, -0.2, 0.2, 1.0))\n\n# Use nibabel to reorient to canonical\ncard = nb.as_closest_canonical(nb.Nifti1Image(\n    img.dataobj,\n    np.diag((-0.2, -0.2, 0.2, 1.0)),\n    None\n))\n\n# Save to disk\ncard.to_filename(\"sub-25_T2star.nii.gz\")\n

???

NiBabel allows Python to easily access neuroimaging data formats such as NIfTI, GIFTI and CIFTI2.

Although this might be a trivial task, the proliferation of neuroimaging software has led to some sort of Wild West of formats, and sometimes interoperation is not ensured.

"},{"location":"assets/bhd2020/presentation/#in-the-snippet-we-can-see-how-we-can-manipulate-the-orientation-headers-of-a-nifti-volume-in-particular-a-rodent-image-with-incorrect-affine-information","title":"In the snippet, we can see how we can manipulate the orientation headers of a NIfTI volume, in particular a rodent image with incorrect affine information.","text":"

.pull-left[

Transforms typically are the outcome of image registration methodologies

The proliferation of software implementations of image registration methodologies has resulted in a spread of data structures and file formats used to preserve and communicate transforms.

(Esteban et al., 2020) ]

.pull-right[

]

???

NiTransforms is a super-interesting toy project where we are exercising our finest coding skills. It completes NiBabel in the effort of making spatial transforms calculated by neuroimaging software tools interoperable.

When it goes beyond the alpha state, it is expected to be merged into NiBabel.

At the moment, NiTransforms is already integrated in fMRIPrep +20.1 to concatenate LTA (linear affine transforms) transforms obtained with FreeSurfer, ITK transforms obtained with ANTs, and motion parameters estimated with FSL.

Compatibility across formats is hard due to the many arbitrary decisions in establishing the mathematical framework of the transform and the intrinsic confusion of applying a transform.

While intuitively we understand applying a transform as \"transforming the moving image so that I can represent it overlaid or fused with the reference image and both should look aligned\", in reality, we only transform coordinates from the reference image into the moving image's space (step 1 on the right).

Once we know where the center of every voxel of the reference image falls in the moving image coordinate system, we read in the information (in other words, a value) from the moving image. Because the location will probably be off-grid, we interpolate such a value from the neighboring voxels (step 2).

Finally (step 3) we generate a new image object with the structure of the reference image and the data interpolated from the moving information. This new image object is the moving image \"moved\" on to the reference image space and thus, both look aligned.

.pull-left[

  • The Archive (right) is a repository of templates and atlases
  • The Python Client (bottom) provides easy access (with lazy-loading) to the Archive
>>> from templateflow import api as tflow\n>>> tflow.get(\n...     'MNI152NLin6Asym',\n...     desc=None,\n...     resolution=1,\n...     suffix='T1w',\n...     extension='nii.gz'\n... )\nPosixPath('/templateflow_home/tpl-MNI152NLin6Asym/tpl-MNI152NLin6Asym_res-01_T1w.nii.gz')\n

.large[www.templateflow.org] ]

.pull-right[

]

???

One of the most ancient feature requests received from fMRIPrep early adopters was improving the flexibility of spatial normalization to standard templates other than fMRIPrep's default.

For instance, infant templates.

TemplateFlow offers an Archive of templates where they are stored, maintained and re-distributed;

and a Python client that helps accessing them.

On the right hand side, an screenshot of the TemplateFlow browser shows some of the templates currently available in the repository. The browser can be reached at www.templateflow.org.

The tool is based on PyBIDS, and the snippet will surely remind you of it. In this case the example shows how to obtain the T1w template corresponding to FSL's MNI space, at the highest resolution.

If the files requested are not in TemplateFlow's cache, they will be pulled down and kept for further utilization.

"},{"location":"assets/bhd2020/presentation/#templateflow-archive","title":"TemplateFlow - Archive","text":"

.small[(Ciric et al. 2020, in prep)]

???

The Archive allows a rich range of data and metadata to be stored with the template.

Datatypes in the repository cover:

  • images containing population-average templates,
  • masks (for instance brain masks),
  • atlases (including parcellations and segmentations)
  • transform files between templates

Metadata can be stored with the usual BIDS options.

Finally, templates allow having multiple cohorts, in a similar encoding to that of multi-session BIDS datasets.

Multiple cohorts are useful, for instance, in infant templates with averages at several gestational ages.

NiWorkflows is a miscellaneous mixture of tooling used by downstream NiPreps:

???

NiWorkflows is, historically, the first component detached from fMRIPrep.

For that reason, its scope and vision has very fuzzy boundaries as compared to the other tools.

The most relevant utilities incorporated within NiWorkflows are:

--

  • The reportlet aggregation and individual report generation system

???

First, the individual report system which aggregates the visual elements or the reports (which we call \"reportlets\") and generates the final HTML document.

Also, most of the engineering behind the generation of these reportlets and their integration within NiPype are part of NiWorkflows

--

  • Custom extensions to NiPype interfaces

???

Beyond the extension of NiPype to generate a reportlet from any given interface, NiWorkflows is the test bed for many utilities that are then upstreamed to nipype.

Also, special interfaces with a limited scope that should not be included in nipype are maintained here.

--

  • Workflows useful across applications

???

Finally, NiWorkflows indeed offers workflows that can be used by end-user NiPreps. For instance atlas-based brain extraction of anatomical images, based on ANTs.

???

Echo-planar imaging (EPI) are typically affected by distortions along the phase encoding axis, caused by the perturbation of the magnetic field at tissue interfaces.

Looking at the reportlet, we can see how in the \"before\" panel, the image is warped.

The distortion is most obvious in the coronal view (middle row) because this image has posterior-anterior phase encoding.

Focusing on the changes between \"before\" and \"after\" correction in this coronal view, we can see how the blue contours delineating the corpus callosum fit better the dark shade in the data after correction.

"},{"location":"assets/bhd2020/presentation/#upcoming-new-utilities","title":"Upcoming new utilities","text":""},{"location":"assets/bhd2020/presentation/#nibabies-fmriprep-babies","title":"NiBabies | fMRIPrep-babies","text":"
  • Mathias Goncalves
"},{"location":"assets/bhd2020/presentation/#nirodents-fmriprep-rodents","title":"NiRodents | fMRIPrep-rodents","text":"
  • Eilidh MacNicol

???

So, what's coming up next?

NiBabies is some sort of NiWorkflows equivalent for the preprocessing of infant imaging. At the moment, only atlas-based brain extraction using ANTs (and adapted from NiWorkflows) is in active developments.

Next steps include brain tissue segmentation.

Similarly, NiRodents is the NiWorkflows parallel for the prepocessing of rodent preclinical imaging. Again, only atlas-based brain extraction adapted from NiWorkflows is being developed.

"},{"location":"assets/bhd2020/presentation/#nipreps-is-a-framework-for-the-development-of-preprocessing-workflows","title":"NiPreps is a framework for the development of preprocessing workflows","text":"
  • Principled design, with BIDS as an strategic component
  • Leveraging existing, widely used software
  • Using NiPype as a foundation

???

To wrap-up, I've presented NiPreps, a framework for developing preprocessing workflows inspired by fMRIPrep.

The framework is heavily principle and tags along BIDS as a foundational component

NiPreps should not reinvent any wheel, trying to reuse as much as possible of the widely used and tested existing software.

Nipype serves as a glue components to orchestrate workflows.

--

"},{"location":"assets/bhd2020/presentation/#why-preprocessing","title":"Why preprocessing?","text":"
  • We propose to consider preprocessing as part of the image acquisition and reconstruction
  • When setting the boundaries that way, it seems sensible to pursue some standardization in the preprocessing:
  • Less experimental degrees of freedom for the researcher
  • Researchers can focus on the analysis
  • More homogeneous data at the output (e.g., for machine learning)
  • How:
  • Transparency is key to success: individual reports and documentation (open source is implicit).
  • Best engineering practices (e.g., containers and CI/CD)

???

But why just preprocessing, with a very strict scope?

We propose to think about preprocessing as part of the image acquisition and reconstruction process (in other words, scanning), rather than part of the analysis workflow.

This decoupling from analysis comes with several upshots:

First, there are less moving parts to play with for researchers in the attempt to fit their methods to the data (instead of fitting data with their methods).

Second, such division of labor allows the researcher to use their time in the analysis.

Finally, two preprocessed datasets from two different studies and scanning sites should be more homogeneous when processed with the same instruments, in comparison to processing them with idiosyncratic, lab-managed, preprocessing workflows.

However, for NiPreps to work we need to make sure the tools are transparent.

Not just with the individual reports and thorough documentation, also because of the community driven development. For instance, the peer-review process that goes around large incremental changes is fundamental to ensure the quality of the tool.

In addition, best engineering practices suggested in the BIDS-Apps paper, along with those we have been including with fMRIPrep, are necessary to ensure the quality of the final product.

--

"},{"location":"assets/bhd2020/presentation/#challenges","title":"Challenges","text":"
  • Testing / Validation!

???

As an open problem, validating the results of the tool remains extremely challenging for the lack in gold standard datasets that can tell us the best possible outcome.

template: newsection layout: false

.middle.center[

"},{"location":"assets/bhd2020/presentation/#where-to-start","title":"Where to start?","text":""},{"location":"assets/bhd2020/presentation/#wwwniprepsorg_1","title":"www.nipreps.org","text":""},{"location":"assets/bhd2020/presentation/#githubcomnipreps","title":"github.com/nipreps","text":"

]

template: newsection layout: false

.middle.center[

"},{"location":"assets/bhd2020/presentation/#thanks","title":"Thanks!","text":""},{"location":"assets/bhd2020/presentation/#questions","title":"Questions?","text":"

]

"},{"location":"assets/torw2020/presentation/","title":"Presentation","text":"

layout: false count: false

.middle.center[

"},{"location":"assets/torw2020/presentation/#building-next-generation-preprocessing-pipelines","title":"Building next-generation preprocessing pipelines:","text":""},{"location":"assets/torw2020/presentation/#the-fmriprep-experience","title":"the fMRIPrep experience","text":""},{"location":"assets/torw2020/presentation/#o-esteban","title":"O. Esteban","text":""},{"location":"assets/torw2020/presentation/#center-for-reproducible-neuroscience","title":"Center for Reproducible Neuroscience","text":""},{"location":"assets/torw2020/presentation/#stanford-university","title":"Stanford University","text":""},{"location":"assets/torw2020/presentation/#wwwniprepsorg","title":"www.nipreps.org","text":"

]

layout: false count: false

.middle.center[

"},{"location":"assets/torw2020/presentation/#building-next-generation-preprocessing-pipelines_1","title":"Building next-generation preprocessing pipelines:","text":""},{"location":"assets/torw2020/presentation/#the-fmriprep-experience_1","title":"the fMRIPrep experience","text":""},{"location":"assets/torw2020/presentation/#o-esteban_1","title":"O. Esteban","text":""},{"location":"assets/torw2020/presentation/#center-for-reproducible-neuroscience_1","title":"Center for Reproducible Neuroscience","text":""},{"location":"assets/torw2020/presentation/#stanford-university_1","title":"Stanford University","text":""},{"location":"assets/torw2020/presentation/#wwwniprepsorg_1","title":"www.nipreps.org","text":"

]

???

"},{"location":"assets/torw2020/presentation/#im-going-to-talk-about-how-we-are-building-a-framework-of-preprocessing-pipelines-for-neuroimaging-called-nipreps-based-on-the-fmriprep-experience","title":"I'm going to talk about how we are building a framework of preprocessing pipelines for neuroimaging called NiPreps, based on the fMRIPrep experience.","text":"

name: newsection layout: true class: section-separator

.perma-sidebar[

"},{"location":"assets/torw2020/presentation/#torw2020","title":"TORW2020","text":""},{"location":"assets/torw2020/presentation/#talk-12","title":"Talk 12","text":""},{"location":"assets/torw2020/presentation/#nipreps","title":"NiPreps","text":"

]

template: newsection layout: false

.middle.center[

"},{"location":"assets/torw2020/presentation/#introducing-fmriprep","title":"Introducing fMRIPrep","text":"

]

???

Let's begin with some of the history behind fMRIPrep.

name: sidebar layout: true

.perma-sidebar[

"},{"location":"assets/torw2020/presentation/#torw2020_1","title":"TORW2020","text":""},{"location":"assets/torw2020/presentation/#talk-12_1","title":"Talk 12","text":""},{"location":"assets/torw2020/presentation/#nipreps_1","title":"NiPreps","text":"

]

template: sidebar

"},{"location":"assets/torw2020/presentation/#fmriprep-produces-analysis-ready-data-from-acquired-fmri-data","title":"fMRIPrep produces analysis-ready data from acquired (fMRI) data","text":"
  • minimal requirements (BIDS-compliant);
  • agnostic to downstream steps of the workflow
  • produces BIDS-Derivatives;

???

fMRIPrep takes in a task-based or resting-state functional MRI dataset in BIDS-format and returns preprocessed data ready for analysis.

Preprocessed data can be used for a broad range of analysis, and they are formatted following BIDS-Derivatives to maximize compatibility with: * major software packages (AFNI, FSL, SPM*, etc.) * further temporal filtering and denoising: fMRIDenoise * any BIDS-Derivatives compliant tool (e.g., FitLins).

--

"},{"location":"assets/torw2020/presentation/#fmriprep-is-a-bids-app-gorgolewski-et-al-2017","title":"fMRIPrep is a BIDS-App (Gorgolewski, et al. 2017)","text":"
  • adhered to modern software-engineering standards (CI/CD, containers)
  • compatible interface with other BIDS-Apps
  • optimized for automatic execution

???

fMRIPrep adopts the BIDS-App specifications. That means the software is tested with every change to the codebase, it also means that packaging, containerization, and deployment are also automated and require tests to be passing. BIDS-Apps are inter-operable (via BIDS-Derivatives), and optimized for execution in HPC, Cloud, etc.

--

"},{"location":"assets/torw2020/presentation/#minimizes-human-intervention","title":"Minimizes human intervention","text":"
  • avoid error-prone parameters settings (read them from BIDS)
  • adapts the workflow to the actual data available
  • while remaining flexible to some design choices (e.g., whether or not reconstructing surfaces or customizing target normalized standard spaces)

???

fMRIPrep minimizes human intervention because the user does not need to fiddle with any parameters - they are obtained from the BIDS structure. However, fMRIPrep does allow some flexibility to ensure the preprocessing meets the requirements of the intended analyses.

--

"},{"location":"assets/torw2020/presentation/#fmriprep-bundles-many-tools-afni-fsl-freesurfer-nilearn-etc","title":"fMRIPrep bundles many tools (AFNI, FSL, FreeSurfer, Nilearn, etc.)","text":"
  • (do not reinvent the wheel)

???

Finally, fMRIPrep sits on top of giants' shoulders: AFNI, FSL, FreeSurfer, Nilearn, etc. all implement methods very well backed-up and are thoroughly tested on their own.

"},{"location":"assets/torw2020/presentation/#we-started-fmriprep-in-february-2016","title":"We started fMRIPrep in February 2016","text":""},{"location":"assets/torw2020/presentation/#objectives","title":"Objectives:","text":"
  • Develop an fMRI preprocessing tool enforcing BIDS for the inputs
  • Automatically executable within OpenNeuro
"},{"location":"assets/torw2020/presentation/#initially-inspired-by-hcp-pipelines","title":"Initially inspired by HCP Pipelines","text":"
  • Problem: robustness vs. the wide variability of inputs

???

We began working on fMRIPrep back in 2016 with much more humble expectations: - We needed to develop an fMRI preprocessing tool leveraging BIDS - smart enough to adapt the workflow for the input dataset, - and the tool should be executable in OpenNeuro without human intervention.

Please note that at the time, the BIDS-Apps specification didn't exist yet.

We started out with an eye on HCP Pipelines, and soon identified that datasets in OpenNeuro varied extremely in terms of acquisition protocols and imaging parameters, which is definitely not a problem for HCP Pipelines, which has very specific requirements for the inputs.

"},{"location":"assets/torw2020/presentation/#fmriprep-adoption-and-popularization-brought-new-challenges","title":"fMRIPrep adoption and popularization brought new challenges","text":"

.pull-right[

]

???

With the fast adoption and popularization of fMRIPrep, new challenges surfaced.

On the right-hand side, you'll find the chart of unique visitors to fmriprep.org, which is the documentation website.

--

.pull-left[

"},{"location":"assets/torw2020/presentation/#transparency-was-addressed-with","title":"Transparency was addressed with:","text":"
  • the individual reports;
  • the thorough documentation; and
  • the citation boilerplate. ]

???

We realized that transparency is indeed a very hard problem. The first leg of our solution was the creation of a solid report system. fMRIPrep generates one individual report per participant, containing information not just to quality control the results, but also to understand the processing flow.

We also strived for a comprehensive, thorough documentation.

Finally, the so-called citation boilerplate appended to the individual reports describe the actual workflow that has been run, noting all the software that was applied including their versions and references.

--

.pull-left[

"},{"location":"assets/torw2020/presentation/#run-to-run-repeatability-is-an-open-issue","title":"Run-to-run repeatability is an open issue:","text":"
  • computational precautions (e.g., unpredictable float truncation/rounding)
  • keep track of all random seeds (version +20.1) ]

???

Reproducibility in terms of run-to-run repeatability of results become as a more apparent problem, and we are always trying to minimize the vibration caused by computational factors, software versions, etc.

--

.pull-left[

"},{"location":"assets/torw2020/presentation/#overwhelming-feedback","title":"Overwhelming feedback:","text":"
  • massive amounts of bug reports, questioning the robustness
  • organic emergence of fMRIPrep enthusiasts (thanks to E. DuPre, JD. Kent) ]

???

We always maintained close attention to all the feedback channels. At some point we were washed over with bug reports that we needed to address. We also started to doubt the robustness against the variability of inputs, and set a thorough stress-test plan using data from OpenNeuro (reported in our Nat Meth paper). Among this feedback flooding, some external friends started to emerge and lent their shoulders in answering questions, fixing bugs, etc.

In particular, I want to thank Elizabeth DuPre (McGill) and James Kent (Univ. of Iowa) for being the earliest adopters and contributors.

"},{"location":"assets/torw2020/presentation/#fmriprep-is-stable-today-although-unfinished","title":"fMRIPrep is stable today, although unfinished","text":"

(Esteban et al., 2019)

???

These developments resulted in the following default processing workflow.

At the highest level, anatomical preprocessing (left-hand block) and functional preprocessing (right-hand block) can be clearly identified as the largest workflow units.

fMRIPrep combines all the anatomical images at the input in one anatomical reference, removes the intensity non-uniformity, delineates brain tissues, reconstructs surfaces, spatially normalizes the anatomical reference to one or more standard spaces.

On the functional pathway, a reference is calculated for further processes, then head-motion parameters are estimated (please note head-motion is accounted for in the last resampling step, in combination with other transforms), slice-timing correction is applied if requested.

Then, susceptibility distortion is estimated, if sufficient information (in terms of acquisition and metadata) is found in the BIDS structure.

Finally, data are mapped to the same individual's anatomical reference and outputs in the several output spaces requested are generated, along with a file gathering time-series of nuisance signals.

"},{"location":"assets/torw2020/presentation/#the-individual-report","title":"The individual report","text":"

???

Let's walk through one example of report. Reports have several sections, starting with a summary indicating the particularities of this dataset and workflow choices made based on the input data.

The anatomical section follows with several visualizations to assess the anatomical processing steps mentioned before, spatial normalization to template spaces (the flickering panel helps assess alignment) and finally surface reconstruction.

Then, all functional runs are concatenated, and all show the same structure. After an initial summary of this particular run, the alignment to the same subject's anatomical image is presented, with contours of the white and pial surfaces as cues. Next panel shows the brain mask and ROIs utilized by the CompCor denoising. For each run we then find some visualizations to assess the generated confounding signals.

After all functional runs are presented, the About section keeps information to aid reproducibility of results, such as the software's version, or the exact command line run.

The boilerplate is found next, with a text version shown by default and tabs to convert to Markdown and LaTeX.

Reports conclude with a list of encountered errors (if any).

"},{"location":"assets/torw2020/presentation/#reports-are-a-crucial-element-to-ensure-transparency","title":"Reports are a crucial element to ensure transparency","text":"

.pull-left[

]

.pull-right[

.distribute[ fMRIPrep generates one participant-wide report after execution.

Reports describe the data as found, and the steps applied (providing .blue[visual support to look inside the box]):

  1. show researchers their data;

  2. show how fMRIPrep interpreted the data (describing the actual preprocessing steps);

  3. quality control of results, facilitating early error detection. ] ]

???

Therefore, reports have become a fundamental feature of fMRIPrep because they not only allow assessing the quality of the processing, but also provide an insight about the logic supporting such processing.

In other words, reports help respond to the what was done and the why was it done in addition to the how well it did.

"},{"location":"assets/torw2020/presentation/#documentation-as-a-second-leg-of-transparency-fmripreporg","title":"Documentation as a second leg of transparency (fmriprep.org)","text":"
  • Hackathons & docu-sprints

  • the CompCor documentation example

.large[fmriprep.org]

???

We promptly identified the need for a very comprehensive documentation. The website at fmriprep.org covers a substantial area of how the tool works under the hood and how to best operate it.

The documentation turned out to be a great ice breaker for contributors, who have pushed forward fundamental sections of it.

Most of the largest increments in documentation are the result of discussions in hackathons, docusprints, neurostars, github, etc. A hallmark example was pull request 1877 by Karolina Finc, who gathered together a massive amount of knowledge from many contributors. Now this is up and open in our documentation website.

"},{"location":"assets/torw2020/presentation/#fmriprep-is-more-of-a-community-driven-project-every-day","title":"fMRIPrep is more of a community-driven project every day","text":"
  • Bug-fixes: we ensured that open feedback channels were attended (GitHub, NeuroStars, mailing list, etc.);

  • users began also proposing new features (some including code!);

  • with NiPreps we are working towards handling the project over to the community.

???

To ensure the future sustainability of the project (what some developers call Bus factor), we are transitioning the tool to NiPreps, transferring the large community nurtured over the past four years with it.

--

"},{"location":"assets/torw2020/presentation/#how-does-fmriprep-compensate-its-contributors","title":"How does fMRIPrep compensate its contributors?","text":"
  • Contributors are invited to coauthor relevant publications about fMRIPrep.
  • Anyone who helps with documentation, code or relevant discussions is a contributor.

.pull-left[

]

.pull-right[

]

???

In return, beyond the rewards of being part of an open source project, fMRIPrep gives some scientific credit back in the form of publications.

  • All contributors are invited to coauthor these publications.
  • Anything that helps the project is considered a sufficient contribution.
"},{"location":"assets/torw2020/presentation/#lessons-learned","title":"Lessons learned","text":""},{"location":"assets/torw2020/presentation/#researchers-want-to-spend-more-time-on-those-areas-most-relevant-to-them","title":"Researchers want to spend more time on those areas most relevant to them","text":"

(probably not preprocessing...)

???

With the development of fMRIPrep we understood that researchers don't want to waste their time on preprocessing (except for researchers developing new preprocessing techniques).

--

"},{"location":"assets/torw2020/presentation/#writing-fmriprep-required-a-team-of-several-experts-in-processing-methods-for-neuroimaging-with-a-solid-base-on-computer-science","title":"Writing fMRIPrep required a team of several experts in processing methods for neuroimaging, with a solid base on Computer Science.","text":"

(research programs just can't cover the neuroscience and the engineering of the whole workflow - we need to divide the labor)

???

The current neuroimaging workflow requires extensive knowledge in sometimes orthogonal fields such as neuroscience and computer science. Dividing the labor in labs, communities or individuals with the necessary expertise is the fundamental for the advance of the whole field.

--

"},{"location":"assets/torw2020/presentation/#transparency-helps-against-the-risk-of-super-easy-tools","title":"Transparency helps against the risk of super-easy tools","text":"

(easy-to-use tools are risky because they might get a researcher very far with no idea whatsoever of what they've done)

???

There is an implicit risk in making things too easy to operate:

For instance, imagine someone who runs fMRIPrep on diffusion data by tricking the BIDS naming into an apparently functional MRI dataset. If fMRIPrep reached the end at all, the garbage at the output could be fed into further tools, in a sort of a snowballing problem.

When researchers have access to the guts of the software and are given an opportunity to understand what's going on, the risk of misuse dips.

--

"},{"location":"assets/torw2020/presentation/#established-toolboxes-do-not-have-incentives-for-compatibility","title":"Established toolboxes do not have incentives for compatibility","text":"

(and to some extent this is not necessarily bad, as long as they are kept well-tested and they embrace/help-develop some minimal standards)

???

AFNI, ANTs, FSL, FreeSurfer, SPM, etc. have comprehensive software validation tests, methodological validation tests, stress tests, etc. - which pushed up their quality and made them fundamental for the field.

Therefore, it is better to keep things that way (although some minimal efforts towards convergence in compatibility are of course welcome)

template: newsection layout: false

.middle.center[

"},{"location":"assets/torw2020/presentation/#wwwniprepsorg_2","title":"www.nipreps.org","text":""},{"location":"assets/torw2020/presentation/#nipreps-neuroimaging-preprocessing-tools","title":"(NiPreps == NeuroImaging PREProcessing toolS)","text":"

]

???

The enormous success of fMRIPrep led us to propose its generalization to other MRI and non-MRI modalities, as well as nonhuman species (for instance, rodents), and particular populations currently unsupported by fMRIPrep such as infants.

"},{"location":"assets/torw2020/presentation/#augmenting-scanners-to-produce-analysis-grade-data","title":"Augmenting scanners to produce \"analysis-grade\" data","text":""},{"location":"assets/torw2020/presentation/#data-directly-consumable-by-analyses","title":"(data directly consumable by analyses)","text":"

.pull-left[

Analysis-grade data is an analogy to the concept of \"sushi-grade (or sashimi-grade) fish\" in that both are:

.large[minimally preprocessed,]

and

.large[safe to consume directly.] ]

.pull-right[ ]

???

The goal, therefore, of NiPreps is to extend the scanner so that, in a way, they produce data ready for analysis.

We liken these analysis-grade data to sushi-grade fish, because in both cases the product is minimally preprocessed and at the same time safe to consume as is.

template: newsection layout: false

.middle.center[

"},{"location":"assets/torw2020/presentation/#deconstructing-fmriprep","title":"Deconstructing fMRIPrep","text":"

]

???

For the last two years we've been decomposing the architecture of fMRIPrep, spinning off its constituent parts that are valuable in other applications.

This process of decoupling (to use a proper CS term) has been greatly facilitated by the modular nature of the code since its inception.

???

The processing elements extracted from fMRIPrep can be mapped to three regimes of responsibility:

  • Software infrastructure composed by tools ensuring the collaboration and the most basic tooling.
  • Middleware utilities, which build more advanced tooling based on the foundational infrastructure
  • And at the top of the stack end-user applications - namely fMRIPrep, dMRIPrep, sMRIPrep and MRIQC.

As we can see, the boundaries of these three architectural layers are soft and tools such as TemplateFlow may stand in between.

Only projects enclosed in the brain shape pertain to the NiPreps community. NiPype, NiBabel and BIDS are so deeply embedded as dependencies that NiPreps can't be understood without them.

  • BIDS provides a standard, guaranteeing I/O agreements:

  • Allows workflows to self-adapt to the inputs

  • Ensures the shareability of the results

  • PyBIDS: a Python tool to query BIDS datasets (Yarkoni et al., 2019):

>>> from bids import BIDSLayout\n\n# Point PyBIDS to the dataset's path\n>>> layout = BIDSLayout(\"/data/coolproject\")\n\n# List the participant IDs of present subjects\n>>> layout.get_subjects()\n['01', '02', '03', '04', '05']\n\n# List session identifiers, if present\n>>> layout.get_sessions()\n['01', '02']\n\n# List functional MRI tasks\n>>> layout.get_tasks()\n['rest', 'nback']\n

???

BIDS is one of the keys to success for fMRIPrep and consequently, a strategic element of NiPreps.

Because the tools so far are written in Python, PyBIDS is a powerful tool to index and query inputs and outputs.

The code snippet illustrates the ease to find out the subject identifiers available in the dataset, sessions, and tasks.

"},{"location":"assets/torw2020/presentation/#bids-derivatives","title":"BIDS Derivatives","text":"

.cut-right[

derivatives/\n\u251c\u2500\u2500 fmriprep/\n\u2502 \u251c\u2500\u2500 dataset_description.json\n\u2502 \u251c\u2500\u2500 logs\n\u2502 \u251c\u2500\u2500 sub-01.html\n\u2502 \u251c\u2500\u2500 sub-01/\n\u2502 \u2502 \u251c\u2500\u2500 anat/\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_desc-brain_mask.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_dseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_label-GM_probseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_label-WM_probseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_label-CSF_probseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_desc-preproc_T1w.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_space-MNI152_desc-brain_mask.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_space-MNI152_dseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_space-MNI152_label-GM_probseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_space-MNI152_label-WM_probseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_space-MNI152_label-CSF_probseg.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_space-MNI152_desc-preproc_T1w.nii.gz\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_from-MNI152_to-T1w_mode-image_xfm.h5\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 sub-01_from-T1w_to-MNI152_mode-image_xfm.h5\n\u2502 \u2502 \u2502 \u2514\u2500\u2500 sub-01_from-orig_to-T1w_mode-image_xfm.txt\n\u2502 \u2502 \u251c\u2500\u2500 figures/\n\u2502 \u2502 \u2514\u2500\u2500 func/\n\u2502 \u2502   \u251c\u2500\u2500 sub-01_task-rhymejudgment_space-MNI152_boldref.nii.gz\n\u2502 \u2502   \u251c\u2500\u2500 sub-01_task-rhymejudgment_space-MNI152_desc-preproc_bold.nii.gz\n\u2502 \u2502   \u251c\u2500\u2500 sub-01_task-rhymejudgment_space-MNI152_desc-confounds_regressors.nii.gz\n\u2502 \u2502   \u2514\u2500\u2500 sub-01_task-rhymejudgment_space-MNI152_desc-brain_mask.nii.gz\n
]

???

All NiPreps must write out BIDS-Derivatives. As illustrated in the example, the outputs of fMRIPrep are very similar to the BIDS standard for acquired data.

"},{"location":"assets/torw2020/presentation/#bids-apps","title":"BIDS-Apps","text":"
  • BIDS-Apps proposes a workflow structure model:
  • Use of containers & CI/CD

  • Uniform interface: .cut-right[

    fmriprep /data /data/derivatives/fmriprep-20.1.1 participant [+OPTIONS]\n
    ]

???

All end-user applications in NiPreps must conform to the BIDS-Apps specifications.

The BIDS-Apps paper identified a common pattern in neuroimaging studies, where individual participants (and runs) are processed first individually, and then based on the outcomes, further levels of data aggregation are executed.

For this reason, BIDS-Apps define two major levels of execution: participant and group level.

Finally, the paper also stresses the importance of containerizing applications to ensure long-term preservation of run-to-run repeatability and proposes a common command line interface as described at the bottom:

  • first the name of the BIDS-Apps (fmriprep, in this case)
  • followed by input and output directories (respectively),
  • to finally indicate the analysis level (always participant, for the case of fmriprep)

.pull-left[

from nipype.interfaces.fsl import BET\nbrain_extract = BET(\n  in_file=\"/data/coolproject/sub-01/ses-01/anat/sub-01_ses-01_T1w.nii\",\n  out_file=\"/out/sub-01/ses-01/anat/sub-01_ses-01_desc-brain_T1w.nii\"\n)\nbrain_extract.run()\n

Nipype is the gateway to mix-and-match from AFNI, ANTs, Dipy, FreeSurfer, FSL, MRTrix, SPM, etc. ]

.pull-right[

]

???

Nipype is the glue stitching together all the underlying neuroimaging toolboxes and provides the execution framework.

The snippet shows how the widely known BET tool from FSL can be executed using NiPype. This is a particular example instance of interfaces - which provide uniform access to the tooling with Python.

Finally, combining these interfaces we generate processing workflows to fulfill higher level processing tasks.

???

For instance, we may have a look into fMRIPrep's functional processing block.

Nipype helps understand (and opens windows in the black box) generating these graph representation of the workflow.

\"\"\"Fix the affine of a rodent dataset, imposing 0.2x0.2x0.2 [mm].\"\"\"\nimport numpy as np\nimport nibabel as nb\n\n# Open the file\nimg = nb.load(\"sub-25_MGE_MouseBrain_3D_MGE_150.nii.gz\")\n\n# New (correct) affine\naff = np.diag((-0.2, -0.2, 0.2, 1.0))\n\n# Use nibabel to reorient to canonical\ncard = nb.as_closest_canonical(nb.Nifti1Image(\n    img.dataobj,\n    np.diag((-0.2, -0.2, 0.2, 1.0)),\n    None\n))\n\n# Save to disk\ncard.to_filename(\"sub-25_T2star.nii.gz\")\n

???

NiBabel allows Python to easily access neuroimaging data formats such as NIfTI, GIFTI and CIFTI2.

Although this might be a trivial task, the proliferation of neuroimaging software has led to some sort of Wild West of formats, and sometimes interoperation is not ensured.

"},{"location":"assets/torw2020/presentation/#in-the-snippet-we-can-see-how-we-can-manipulate-the-orientation-headers-of-a-nifti-volume-in-particular-a-rodent-image-with-incorrect-affine-information","title":"In the snippet, we can see how we can manipulate the orientation headers of a NIfTI volume, in particular a rodent image with incorrect affine information.","text":"

.pull-left[

Transforms typically are the outcome of image registration methodologies

The proliferation of software implementations of image registration methodologies has resulted in a spread of data structures and file formats used to preserve and communicate transforms.

(Esteban et al., 2020) ]

.pull-right[

]

???

NiTransforms is a super-interesting toy project where we are exercising our finest coding skills. It completes NiBabel in the effort of making spatial transforms calculated by neuroimaging software tools interoperable.

When it goes beyond the alpha state, it is expected to be merged into NiBabel.

At the moment, NiTransforms is already integrated in fMRIPrep +20.1 to concatenate LTA (linear affine transforms) transforms obtained with FreeSurfer, ITK transforms obtained with ANTs, and motion parameters estimated with FSL.

Compatibility across formats is hard due to the many arbitrary decisions in establishing the mathematical framework of the transform and the intrinsic confusion of applying a transform.

While intuitively we understand applying a transform as \"transforming the moving image so that I can represent it overlaid or fused with the reference image and both should look aligned\", in reality, we only transform coordinates from the reference image into the moving image's space (step 1 on the right).

Once we know where the center of every voxel of the reference image falls in the moving image coordinate system, we read in the information (in other words, a value) from the moving image. Because the location will probably be off-grid, we interpolate such a value from the neighboring voxels (step 2).

Finally (step 3) we generate a new image object with the structure of the reference image and the data interpolated from the moving information. This new image object is the moving image \"moved\" on to the reference image space and thus, both look aligned.

.pull-left[

  • The Archive (right) is a repository of templates and atlases
  • The Python Client (bottom) provides easy access (with lazy-loading) to the Archive
>>> from templateflow import api as tflow\n>>> tflow.get(\n...     'MNI152NLin6Asym',\n...     desc=None,\n...     resolution=1,\n...     suffix='T1w',\n...     extension='nii.gz'\n... )\nPosixPath('/templateflow_home/tpl-MNI152NLin6Asym/tpl-MNI152NLin6Asym_res-01_T1w.nii.gz')\n

.large[www.templateflow.org] ]

.pull-right[

]

???

One of the most ancient feature requests received from fMRIPrep early adopters was improving the flexibility of spatial normalization to standard templates other than fMRIPrep's default.

For instance, infant templates.

TemplateFlow offers an Archive of templates where they are stored, maintained and re-distributed;

and a Python client that helps accessing them.

On the right hand side, an screenshot of the TemplateFlow browser shows some of the templates currently available in the repository. The browser can be reached at www.templateflow.org.

The tool is based on PyBIDS, and the snippet will surely remind you of it. In this case the example shows how to obtain the T1w template corresponding to FSL's MNI space, at the highest resolution.

If the files requested are not in TemplateFlow's cache, they will be pulled down and kept for further utilization.

"},{"location":"assets/torw2020/presentation/#templateflow-archive","title":"TemplateFlow - Archive","text":"

.small[(Ciric et al. 2020, in prep)]

???

The Archive allows a rich range of data and metadata to be stored with the template.

Datatypes in the repository cover:

  • images containing population-average templates,
  • masks (for instance brain masks),
  • atlases (including parcellations and segmentations)
  • transform files between templates

Metadata can be stored with the usual BIDS options.

Finally, templates allow having multiple cohorts, in a similar encoding to that of multi-session BIDS datasets.

Multiple cohorts are useful, for instance, in infant templates with averages at several gestational ages.

NiWorkflows is a miscellaneous mixture of tooling used by downstream NiPreps:

???

NiWorkflows is, historically, the first component detached from fMRIPrep.

For that reason, its scope and vision has very fuzzy boundaries as compared to the other tools.

The most relevant utilities incorporated within NiWorkflows are:

--

  • The reportlet aggregation and individual report generation system

???

First, the individual report system which aggregates the visual elements or the reports (which we call \"reportlets\") and generates the final HTML document.

Also, most of the engineering behind the generation of these reportlets and their integration within NiPype are part of NiWorkflows

--

  • Custom extensions to NiPype interfaces

???

Beyond the extension of NiPype to generate a reportlet from any given interface, NiWorkflows is the test bed for many utilities that are then upstreamed to nipype.

Also, special interfaces with a limited scope that should not be included in nipype are maintained here.

--

  • Workflows useful across applications

???

Finally, NiWorkflows indeed offers workflows that can be used by end-user NiPreps. For instance atlas-based brain extraction of anatomical images, based on ANTs.

???

Echo-planar imaging (EPI) are typically affected by distortions along the phase encoding axis, caused by the perturbation of the magnetic field at tissue interfaces.

Looking at the reportlet, we can see how in the \"before\" panel, the image is warped.

The distortion is most obvious in the coronal view (middle row) because this image has posterior-anterior phase encoding.

Focusing on the changes between \"before\" and \"after\" correction in this coronal view, we can see how the blue contours delineating the corpus callosum fit better the dark shade in the data after correction.

"},{"location":"assets/torw2020/presentation/#sdcflows-as-integrated-in-fmriprep","title":"SDCFlows, as integrated in fMRIPrep","text":"

.left-column3[

]

.right-column3[ * Hierarchy of SDC methods: 1. PE-Polar 2. Fieldmap 3. Fieldmap-less

  • Arguments:
  • --use-syn-sdc
  • --force-syn
  • --ignore fieldmaps

  • REQUIRES (opts. 1 or 2): setting the IntendedFor metadata field of fieldmaps. ]

???

With SDCFlows, fMRIPrep implements a rather sophisticated pipeline for the estimation of susceptibility distortions.

Depending on whether the input dataset contains EPI images with opposed phase encoding polarities (the so-called PE-Polar correction), fieldmaps (as Gradient Recalled Echo sequences) or the fieldmap-less estimation is requested,

then SDCFlows establishes a hierarchy of corrections.

After correction, we are interested in assessing that low-frequency distortions have been accounted for and that high-frequency (with extreme regions suffering severe drop-outs) are not excessively present.

.pull-left[

] .pull-right[

]

???

sMRIPrep corresponds to the split of the anatomical preprocessing workflow originally proposed with fMRIPrep.

With the support of TemplateFlow, the tool now supports spatial normalization to one or more templates found in the TemplateFlow Archive.

It also supports the use of custom templates, whenever they are correctly installed in the templateflow's cache folder.

???

dMRIPrep and fMRIPrep are, of course the tip of the iceberg.

dMRIPrep is still in an alpha state, steadily progressing through the path fMRIPrep has delineated for NiPreps.

Hopefully, at this point of the talk fMRIPrep doesn't need further description.

template: newsection layout: false

.middle.center[

"},{"location":"assets/torw2020/presentation/#other-components-of-nipreps","title":"Other components of NiPreps","text":"

]

???

Some additional components of NiPreps were never part of fMRIPrep's codebase, or they have been started recently.

???

Such is the case of the quality control tools.

MRIQC produces visual reports for the efficient screening of acquired (meaning, unprocessed) data - in particular anatomical and functional MRI of the human brain.

CrowdMRI is an internet service where anonimized quality control metrics are uploaded automatically as they are computed by MRIQC.

The endgoal is to gather enough data to describe the normative distribution of these metrics across image parameters and scanning devices and sites.

Finally, MRIQCnets encloses several machine learning projects regarding the quality of acquired images.

"},{"location":"assets/torw2020/presentation/#upcoming-new-utilities","title":"Upcoming new utilities","text":""},{"location":"assets/torw2020/presentation/#nibabies","title":"NiBabies","text":"
  • Recently started, covering infant MRI brain-extraction for now (Mathias Goncalves)
"},{"location":"assets/torw2020/presentation/#nirodents","title":"NiRodents","text":"
  • Recently started, covering rodent MRI brain-extraction for now (Eilidh MacNicol)

???

So, what's coming up next?

NiBabies is some sort of NiWorkflows equivalent for the preprocessing of infant imaging. At the moment, only atlas-based brain extraction using ANTs (and adapted from NiWorkflows) is in active developments.

Next steps include brain tissue segmentation.

Similarly, NiRodents is the NiWorkflows parallel for the prepocessing of rodent preclinical imaging. Again, only atlas-based brain extraction adapted from NiWorkflows is being developed.

--

"},{"location":"assets/torw2020/presentation/#future-lines","title":"Future lines","text":"
  • fMRIPrep-babies

  • fMRIPrep-rodents

  • MolPrep / PETPrep ?

???

In a mid-term future, both NiBabies and NiRodents should allow the extension of fMRIPrep to these new two idiosyncratic data families.

In additions, plans for a molecular imaging or PET preprocessing NiPrep are being designed.

"},{"location":"assets/torw2020/presentation/#conclusion","title":"Conclusion","text":""},{"location":"assets/torw2020/presentation/#nipreps-is-a-framework-for-the-development-of-preprocessing-workflows","title":"NiPreps is a framework for the development of preprocessing workflows","text":"
  • Principled design, with BIDS as an strategic component
  • Leveraging existing, widely used software
  • Using NiPype as a foundation

???

To wrap-up, I've presented NiPreps, a framework for developing preprocessing workflows inspired by fMRIPrep.

The framework is heavily principle and tags along BIDS as a foundational component

NiPreps should not reinvent any wheel, trying to reuse as much as possible of the widely used and tested existing software.

Nipype serves as a glue components to orchestrate workflows.

--

"},{"location":"assets/torw2020/presentation/#why-preprocessing","title":"Why preprocessing?","text":"
  • We propose to consider preprocessing as part of the image acquisition and reconstruction
  • When setting the boundaries that way, it seems sensible to pursue some standardization in the preprocessing:
  • Less experimental degrees of freedom for the researcher
  • Researchers can focus on the analysis
  • More homogeneous data at the output (e.g., for machine learning)
  • How:
  • Transparency is key to success: individual reports and documentation (open source is implicit).
  • Best engineering practices (e.g., containers and CI/CD)

???

But why just preprocessing, with a very strict scope?

We propose to think about preprocessing as part of the image acquisition and reconstruction process (in other words, scanning), rather than part of the analysis workflow.

This decoupling from analysis comes with several upshots:

First, there are less moving parts to play with for researchers in the attempt to fit their methods to the data (instead of fitting data with their methods).

Second, such division of labor allows the researcher to use their time in the analysis.

Finally, two preprocessed datasets from two different studies and scanning sites should be more homogeneous when processed with the same instruments, in comparison to processing them with idiosyncratic, lab-managed, preprocessing workflows.

However, for NiPreps to work we need to make sure the tools are transparent.

Not just with the individual reports and thorough documentation, also because of the community driven development. For instance, the peer-review process that goes around large incremental changes is fundamental to ensure the quality of the tool.

In addition, best engineering practices suggested in the BIDS-Apps paper, along with those we have been including with fMRIPrep, are necessary to ensure the quality of the final product.

--

"},{"location":"assets/torw2020/presentation/#challenges","title":"Challenges","text":"
  • Testing / Validation!

???

As an open problem, validating the results of the tool remains extremely challenging for the lack in gold standard datasets that can tell us the best possible outcome.

template: newsection layout: false

.middle.center[

"},{"location":"assets/torw2020/presentation/#open-phd-student-position","title":"Open PhD student position!","text":"

]

template: newsection layout: false

.middle.center[

"},{"location":"assets/torw2020/presentation/#thanks","title":"Thanks!","text":""},{"location":"assets/torw2020/presentation/#questions","title":"Questions?","text":"

]

"},{"location":"community/","title":"Join the NiPreps Community","text":"

One of the pillars of fMRIPrep, the seed project for NiPreps, has been nurturing an open-source community. Building Welcoming Communities is crucial for open-source software because of several reasons:

  • Engaging users and contributors (in a very liberal sense, not just with code) helps establish a development road-map:

    • In the case of fMRIPrep, many users have reported bugs via our issue tracker and Neurostars.org. Even though testing is one of the primary focuses for fMRIPrep, without these bug-report contributions the tool would have never reached the dependability level it requires to serve its purpose.
    • Users identify and propose new features, often illuminating shady areas the most involved developers did not find time or the right context to explore.
  • The community exposes the software and also increases the externality of the software. The neuroimaging discussion supported by Neurostars.org has been a key factor for the adoption of fMRIPrep.

  • Users always give back, and it is not uncommon to see elaborate responses to bug-reports and questions about fMRIPrep on Neurostars.org by users who had similar questions previously.

Because of the scientific purpose of NiPreps, there is one more fundamental reason to grow a (scientific) community around the tools: rigor/scrutiny. As one reviews a few of the most discussed pull-requests to fMRIPrep, very soon they realize that we don't just need to get the code right. We strive for integrating high-quality code, but even more importantly, that code must get the scientific method it implements right. This is particularly difficult because in most of the cases there aren't test oracles (in software engineering terms) or gold-standards (in scientific terms) to efficiently evaluate the validity of new features (even to exercise a minuscule area of the domain of inputs). The redundancy of expert eyes looking at our code has only helped make it better.

"},{"location":"community/#current-members-of-the-github-organization","title":"Current members of the GitHub organization","text":"

A total of 100 neuroimagers have already joined us. Becoming a member will give you access to additional forums for discussion, subscribing notifications for events and meetings, etc. You can request you are added to the organization by creating a new issue here.

"},{"location":"community/CODE_OF_CONDUCT/","title":"NiPreps Code of Conduct","text":""},{"location":"community/CODE_OF_CONDUCT/#our-pledge","title":"Our Pledge","text":"

In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.

"},{"location":"community/CODE_OF_CONDUCT/#our-standards","title":"Our Standards","text":"

Examples of behavior that contributes to creating a positive environment include:

  • Using welcoming and inclusive language
  • Being respectful of differing viewpoints and experiences
  • Gracefully accepting constructive criticism
  • Focusing on what is best for the community
  • Showing empathy towards other community members

Examples of unacceptable behavior by participants include:

  • The use of sexualized language or imagery and unwelcome sexual attention or advances
  • Trolling, insulting/derogatory comments, and personal or political attacks
  • Public or private harassment
  • Publishing others' private information, such as a physical or electronic address, without explicit permission
  • Other conduct which could reasonably be considered inappropriate in a professional setting
"},{"location":"community/CODE_OF_CONDUCT/#our-responsibilities","title":"Our Responsibilities","text":"

Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.

Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.

"},{"location":"community/CODE_OF_CONDUCT/#scope","title":"Scope","text":"

This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.

"},{"location":"community/CODE_OF_CONDUCT/#enforcement","title":"Enforcement","text":"

Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting Oscar Esteban at oesteban@stanford.edu or Chris Markiewicz at markiewicz@stanford.edu, two members of the project team. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.

Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.

"},{"location":"community/CODE_OF_CONDUCT/#attribution","title":"Attribution","text":"

This Code of Conduct is adapted from the Contributor Covenant, version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html

For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq

"},{"location":"community/CONTRIBUTING/","title":"Contributing Guidelines","text":"

Welcome to the NiPreps project! We're excited you're here and want to contribute.

Imposter's syndrome disclaimer

Imposter's syndrome disclaimer1: We want your help. No, really.

There may be a little voice inside your head that is telling you that you're not ready to be an open-source contributor; that your skills aren't nearly good enough to contribute. What could you possibly offer a project like this one?

We assure you - the little voice in your head is wrong. If you can write code at all, you can contribute code to open-source. Contributing to open-source projects is a fantastic way to advance one's coding skills. Writing perfect code isn't the measure of a good developer (that would disqualify all of us!); it's trying to create something, making mistakes, and learning from those mistakes. That's how we all improve, and we are happy to help others learn.

Being an open-source contributor doesn't just mean writing code, either. You can help out by writing documentation, tests, or even giving feedback about the project (and yes - that includes giving feedback about the contribution process). Some of these contributions may be the most valuable to the project as a whole, because you're coming to the project with fresh eyes, so you can see the errors and assumptions that seasoned contributors have glossed over.

"},{"location":"community/CONTRIBUTING/#driving-principles","title":"Driving principles","text":"

NiPreps are built around three overarching principles:

  1. Robustness - The pipeline adapts the preprocessing steps depending on the input dataset and should provide results as good as possible independently of scanner make, scanning parameters or presence of additional correction scans (such as fieldmaps).
  2. Ease of use - Thanks to dependence on the BIDS standard, manual parameter input is reduced to a minimum, allowing the pipeline to run in an automatic fashion.
  3. \"Glass box\" philosophy - Automation should not mean that one should not visually inspect the results or understand the methods. Thus, NiPreps provides visual reports for each subject, detailing the accuracy of the most important processing steps. This, combined with the documentation, can help researchers to understand the process and decide which subjects should be kept for the group level analysis.

These principles distill some design and organizational foundations:

  1. NiPreps only and fully support BIDS and BIDS-Derivatives for the input and output data.
  2. NiPreps are packaged as a fully-compliant BIDS-Apps, not just in its user interface, but also in the continuous integration, testing, and delivery.
  3. The scope of NiPreps is strictly limited to preprocessing tasks.
  4. NiPreps are agnostic to subsequent analysis, i.e., any software supporting BIDS-Derivatives for its inputs should be amenable to analyze data preprocessed with them.
  5. NiPreps are thoroughly and transparently documented (including the generation of individual, visual reports with a consistent format that serve as scaffolds for understanding the underpinnings and design decisions).
  6. NiPreps are community-driven, and contributors (in any sense) always get credited with authorship within relevant publications.
  7. NiPreps are modular, reliant on widely-used tools such as AFNI, ANTs, FreeSurfer, FSL, NiLearn, or DIPY [7-12] and extensible via plug-ins.
"},{"location":"community/CONTRIBUTING/#practical-guide-to-submitting-your-contribution","title":"Practical guide to submitting your contribution","text":"

These guidelines are designed to make it as easy as possible to get involved. If you have any questions that aren't discussed below, please let us know by opening an issue!

Before you start, you'll need to set up a free GitHub account and sign in. Here are some instructions.

Already know what you're looking for in this guide? Jump to the following sections:

  • Joining the conversation
  • Contributing through Github
  • Understanding issues
  • Making a change
  • Structuring contributions
  • Licensing
  • Recognizing contributors
"},{"location":"community/CONTRIBUTING/#joining-the-conversation","title":"Joining the conversation","text":"

NiPreps is maintained by a growing group of enthusiastic developers\u2014 and we're excited to have you join! Most of our discussions will take place on open issues.

We also encourage users to report any difficulties they encounter on NeuroStars, a community platform for discussing neuroimaging.

We actively monitor both spaces and look forward to hearing from you in either venue!

"},{"location":"community/CONTRIBUTING/#contributing-through-github","title":"Contributing through GitHub","text":"

git is a really useful tool for version control. GitHub sits on top of git and supports collaborative and distributed working.

If you're not yet familiar with git, there are lots of great resources to help you git started! Some of our favorites include the git Handbook and the Software Carpentry introduction to git.

On GitHub, You'll use Markdown to chat in issues and pull requests. You can think of Markdown as a few little symbols around your text that will allow GitHub to render the text with a little bit of formatting. For example, you could write words as bold (**bold**), or in italics (*italics*), or as a link ([link](https://youtu.be/dQw4w9WgXcQ)) to another webpage.

GitHub has a really helpful page for getting started with writing and formatting Markdown on GitHub.

"},{"location":"community/CONTRIBUTING/#understanding-issues","title":"Understanding issues","text":"

Every project on GitHub uses issues slightly differently.

The following outlines how the NiPreps developers think about these tools.

  • Issues are individual pieces of work that need to be completed to move the project forward. A general guideline: if you find yourself tempted to write a great big issue that is difficult to describe as one unit of work, please consider splitting it into two or more issues.

    Issues are assigned labels which explain how they relate to the overall project's goals and immediate next steps.

"},{"location":"community/CONTRIBUTING/#issue-labels","title":"Issue Labels","text":"

The current list of issue labels are here and include:

  • These issues contain a task that is amenable to new contributors because it doesn't entail a steep learning curve.

    If you feel that you can contribute to one of these issues, we especially encourage you to do so!

  • These issues point to problems in the project.

    If you find new a bug, please give as much detail as possible in your issue, including steps to recreate the error. If you experience the same bug as one already listed, please add any additional information that you have as a comment.

  • These issues are asking for new features and improvements to be considered by the project.

    Please try to make sure that your requested feature is distinct from any others that have already been requested or implemented. If you find one that's similar but there are subtle differences, please reference the other request in your issue.

In order to define priorities and directions in the development roadmap, we have two sets of special labels:

Label Description Estimation of the downstream impact the proposed feature/bugfix will have. Estimation of effort required to implement the requested feature or fix the reported bug.

One way to understand these labels is to consider how they would apply to an imaginary issue. For example, if -- after a release -- a bug is identified that re-introduces a previously solved issue (i.e., its regresses the code outputs to some undesired behavior), we might assign it the following labels: . Its development priority would then be \"high\", since it is a low-effort, high-impact change.

Long-term goals may be labelled as a combination of: and or since they will have a high-impact on the code-base, but require a medium or high amount of effort. Of note, issues with the labels: or are less likely to be addressed because they are less likely to impact the code-base, or because they will require a very high activation energy to do so.

"},{"location":"community/CONTRIBUTING/#making-a-change","title":"Making a change","text":"

We appreciate all contributions to NiPreps, but those accepted fastest will follow a workflow similar to the following:

  1. Comment on an existing issue or open a new issue referencing your addition. This allows other members of the NiPreps development team to confirm that you aren't overlapping with work that's currently underway and that everyone is on the same page with the goal of the work you're going to carry out. This blog is a nice explanation of why putting this work in up front is so useful to everyone involved.

  2. Fork the particular NiPrep repository (e.g., fMRIPrep) with your GitHub user. This is now your own unique copy of that particular NiPreps component. Changes here won't affect anyone else's work, so it's a safe space to explore edits to the code!

  3. Clone your forked NiPreps repository to your machine/computer. While you can edit files directly on github, sometimes the changes you want to make will be complex and you will want to use a text editor that you have installed on your local machine/computer. (One great text editor is vscode). In order to work on the code locally, you must clone your forked repository. To keep up with changes in the NiPreps repository, add the \"upstream\" NiPreps repository as a remote to your locally cloned repository.

    git remote add upstream https://github.com/nipreps/fmriprep.git\n
    Make sure to keep your fork up to date with the upstream repository. For example, to update your master branch on your local cloned repository:
    git fetch upstream\ngit checkout master\ngit merge upstream/master\n

  4. Create a new branch to develop and maintain the proposed code changes. For example:

    git fetch upstream  # Always start with an updated upstream\ngit checkout -b fix/bug-1222 upstream/master\n
    Please consider using appropriate branch names as those listed below, and mind that some of them are special (e.g., doc/ and docs/):

    • fix/<some-identifier>: for bugfixes
    • enh/<feature-name>: for new features
    • doc/<some-identifier>: for documentation improvements. You should name all your documentation branches with the prefix doc/ or docs/ as that will preempt triggering the full battery of continuous integration tests.
  5. Make the changes you've discussed, following the NiPreps coding style guide. Try to keep the changes focused: it is generally easy to review changes that address one feature or bug at a time. It can also be helpful to test your changes locally, using a NiPreps development environment. Once you are satisfied with your local changes, add/commit/push them to the branch on your forked repository.

  6. Submit a pull request. A member of the development team will review your changes to confirm that they can be merged into the main code base. Pull request titles should begin with a descriptive prefix (for example, ENH: Support for SB-reference in multi-band datasets):

    • ENH: enhancements or new features (example)
    • FIX: bug fixes (example)
    • TST: new or updated tests (example)
    • DOC: new or updated documentation (example)
    • STY: style changes (example)
    • REF: refactoring existing code (example)
    • CI: updates to continous integration infrastructure (example)
    • MAINT: general maintenance (example)
    • For works-in-progress, add the WIP tag in addition to the descriptive prefix. Pull-requests tagged with WIP: will not be merged until the tag is removed.
  7. Have your PR reviewed by the developers team, and update your changes accordingly in your branch. The reviewers will take special care in assisting you address their comments, as well as dealing with conflicts and other tricky situations that could emerge from distributed development.

"},{"location":"community/CONTRIBUTING/#nipreps-coding-style-guide","title":"NiPreps coding style guide","text":"

Whenever possible, instances of Nipype Nodes and Workflows should use the same names as the variables they are assigned to. This makes it easier to relate the content of the working directory to the code that generated it when debugging.

Workflow variables should end in _wf to indicate that they refer to Workflows and not Nodes. For instance, a workflow whose basename is myworkflow might be defined as follows:

from nipype.pipeline import engine as pe\n\nmyworkflow_wf = pe.Workflow(name='myworkflow_wf')\n

If a workflow is generated by a function, the name of the function should take the form init_<basename>_wf:

def init_myworkflow_wf(name='myworkflow_wf):\n    workflow = pe.Workflow(name=name)\n    ...\n    return workflow\n\nmyworkflow_wf = init_workflow_wf(name='myworkflow_wf')\n

If multiple instances of the same workflow might be instantiated in the same namespace, the workflow names and variables should include either a numeric identifier or a one-word description, such as:

myworkflow0_wf = init_workflow_wf(name='myworkflow0_wf')\nmyworkflow1_wf = init_workflow_wf(name='myworkflow1_wf')\n\n# or\n\nmyworkflow_lh_wf = init_workflow_wf(name='myworkflow_lh_wf')\nmyworkflow_rh_wf = init_workflow_wf(name='myworkflow_rh_wf')\n
"},{"location":"community/CONTRIBUTING/#recognizing-contributions","title":"Recognizing contributions","text":"

We welcome and recognize all contributions regardless their size, content or scope: from documentation to testing and code development. You can see a list of current developers and contributors in our zenodo file. Before every release, a new zenodo file will be generated. The update script will also sort creators and contributors by the relative size of their contributions, as provided by the git-line-summary utility distributed with the git-extras package. Last positions in both the creators and contributors list will be reserved to the project leaders. These special positions can be revised to add names by punctual request and revised for removal and update of ordering in an scheduled manner every two years. All the authors enlisted as creators participate in the revision of modifications.

"},{"location":"community/CONTRIBUTING/#publications","title":"Publications","text":"

Anyone listed as a developer or a contributor can start the submission process of a manuscript as first author (please see Membership, where these concepts are described). To compose the author list, all the creators MUST be included (except for those people who opt to drop-out) and all the contributors MUST be invited to participate. First authorship(s) is (are) reserved for the authors that originated and kept the initiative of submission and wrote the manuscript. To generate the ordering of your paper, please run python .maint/paper_author_list.py from the root of the repository, on the up-to-date upstream/master branch. Then, please modify this list and place your name first. All developers and contributors are pulled together in a unique list, and last authorships assigned. NiPreps and its community adheres to open science principles, such that a pre-print should be posted on an adequate archive service (e.g., ArXiv or BioRxiv) prior publication.

"},{"location":"community/CONTRIBUTING/#licensing","title":"Licensing","text":"

NiPreps is licensed under the Apache 2.0 license. By contributing to NiPreps, you acknowledge that any contributions will be licensed under the same terms.

"},{"location":"community/CONTRIBUTING/#thank-you","title":"Thank you!","text":"

You're awesome.

\u2014 Based on contributing guidelines from the STEMMRoleModels project.

  1. The imposter syndrome disclaimer was originally written by Adrienne Lowe for a PyCon talk, and was adapted based on its use in the README file for the MetPy project.\u00a0\u21a9

"},{"location":"community/features/","title":"New features","text":"

The one bit that worries me is that fMRIPrep may become a Swiss army knife. I think instead it should just be a paring knife (small, efficient, and works for many things).

-- Satra (source)

When projects grow large, many forking paths created by newly implemented features start to open up. To account for this, the NiPreps community was created with the vision of building tools like fMRIPrep and MRIQC covering new imaging modalities, while keeping existing NiPreps tightly within scope. Defining such a scope also aids the implementation of the ease-of-use principle:

The same way the scanner does not offer an immense space of knobs to turn in the acquisition, NiPreps should not add many additional knobs to those for them to be considered a viable augmentation or extension of the scanner hw/sw.

-- Oscar (source)

"},{"location":"community/features/#the-problem-of-feature-creep","title":"The problem of feature creep","text":"

To avert feature creep and to serve each individual NiPrep, we developed the following guidelines, with the hopes of keeping these tools in a healthy state.

I'm worried fMRIPrep is catching a case of featuritis

-- Mathias (source)

These guidelines should also serve the community to transparently drive the process of including proposals into the road-map, set the ground for healthy conversation, and establish some patterns when accepting new-feature contributions. Before proposing new features, please be mindful that a road-map may not exist for a particular NiPrep. Even when a development road-map exists, please understand that it is not always possible to rigorously follow them:

I think something like this is what we tried to start sketching out with the development roadmap. The concern, as I remember it, was that we couldn't guarantee (or rule out) specific features when working with a small development team.

-- Elizabeth (source).

"},{"location":"community/features/#proposing-a-new-feature","title":"Proposing a new feature","text":""},{"location":"community/features/#why-the-new-feature-is-requested","title":"Why the new feature is requested?","text":"

Before going ahead and proposing a new feature, please take some time to learn whether the topic has been covered in the past and what decisions were made and why. This should be reasonably easy to do with the search tool of GitHub on the particular NiPrep repository.

If no previous discussion about the new idea is found, the next step is ensuring the new feature aligns with the vision and the scope of the target tool, as Elizabeth points out. Taking a look into the Development Road-map of the particular project (if it exists), may help finding an answer.

If the new feature still seems pertinent after this preliminary work or you are unsure about whether it falls within the scope, then go ahead and post an issue requesting feedback on your proposal. Please make sure to clearly state why the new feature should be considered.

"},{"location":"community/features/#some-questions-will-always-be-asked-about-a-new-feature","title":"Some questions will always be asked about a new feature","text":"

These questions by James will certainly help build up the discourse in support of the new feature, as the NiPreps maintainers will consider them:

  • Is the user interface affected? Because NiPreps generally expose a command-line interface (CLI) for the interaction with the user, new features involving changes to the CLI must be considered with caution as they may harm the ease-of-use:

    It also seems that some new features add more confusion than others. Especially when the CLI is affected, and yet another option is added, that makes the tool more complex to use.

    -- Alejandro (source).

  • Does the new feature substantially increase the internal complexity? Maintainers and developers will attempt to consolidate tools and lower the internal complexity whenever possible. This effort usually competes with the addition of new features as they typically will address particular use-cases rather than general improvements. However, that doesn't need to be the case, as some sections of the code might be objectively improvable and the integration of a new feature revising those might also lower complexity. Lowering the internal complexity will always be considered a great incentive for a new feature to be accepted.

  • Is there a standard procedure for the proposed feature in the literature?

    • if so, could we just use that procedure/value?
  • Is the feature dependent on some attribute of the input data? (e.g., TR, duration, etc.)

    • if so, can the procedure/value be determined algorithmically?
  • Does the feature interact with other settings? For instance, fmriprep#1962 interacts with the a/tCompCor implementation.

  • What is the difficulty of implementing the procedure outside of a NiPrep? In other words, does the NiPrep provide all the necessary outputs for a user to perform the non-standard analysis?

"},{"location":"community/features/#how-the-integration-of-the-new-feature-willcan-be-validated","title":"How the integration of the new feature will/can be validated?","text":"

Please propose ways to validate the new feature in the context of the workflow. Meaning, the objective here is to validate that the new feature works well within the pipeline, rather than validating a specific algorithm. To ensure the sustainability of NiPreps, the onus of this validation should be on the person/group requesting the feature.

"},{"location":"community/licensing/","title":"Licensing and Derived Works","text":"

The NiPreps community believes that software is an integral component of scientific practice, and that any scientific claim must be verifiable by following the chain of reasoning from observation to conclusion. To achieve this, software must be free to use, inspect, and critique. We also believe that you should be free to modify our software to improve it or adapt it to new use cases.

As software development is a dynamic process, code modifications can quickly become confusing as the original and modified versions depart from each other. For the sake of transparency and verification, when you modify our code, we ask that you document both the version of the software that you started with and the changes you make.

We believe these freedoms are best promoted by distributing our software under free/open source software licenses, and the license we feel best promotes these goals is the Apache License, Version 2.0.

This page outlines our commitment to transparent development and our expectations for developers who adapt NiPreps code to use in other projects.

"},{"location":"community/licensing/#licensing-of-nipreps-projects","title":"Licensing of NiPreps projects","text":"

All software packages and tools under the NiPreps umbrella must be licensed under the Apache License 2.0 by default, unless otherwise stated. The authors of new NiPreps packages may not abide by this general rule of thumb if necessary and/or sufficiently justified (e.g., the source code is actually derived from a product licensed under a copyleft license).

Containerized Images bundling NiPreps components and their dependencies can be distributed under a free and open-source license without copyleft, such as the MIT License. In such a case, the attribution notice of the MIT license must be present in the header comment of the container image bootstraping file (for instance, the so-called Dockerfile). This different licensing must be also indicated in the NOTICE file of the corresponding NiPreps components bundled within the image.

Docker-wrappers such as the fmriprep-docker package may be licensed under any free and open-source license without copyleft, such as the MIT License. This different licensing must be also indicated in the NOTICE file of the corresponding NiPreps components bundled within the image.

Data (distributed within the test data of packages or through the nipreps-data GitHub organization) will preferably be distributed under the Creative Commons Zero v1.0 Universal.

Under no circumstances any NiPreps software or data will be made publicly available unlicensed. If you find any component of NiPreps that is unlicensed, please make us aware at nipreps@gmail.com at your earliest convenience.

"},{"location":"community/licensing/#the-apache-license-20","title":"The Apache License 2.0","text":"

(This section is adapted from this blog post by D. Mar\u00edn)

The Apache License was created by the Apache Software Foundation (ASF) as the license for its Apache HTTP Server.

Just as the MIT License, it\u2019s a very permissive non-copyleft license that allows using the software for any purpose, distributing it, modifying it, and distributing derived works of it without concern for royalties. Its main differences, compared to the MIT License, are:

  • Using the Apache License, the authors of the software grant patent licenses to any user or distributor of the code. This patent licenses apply to any patent that, being licenseable by any of the software author, would be infringed by the piece of code they have created.
  • Apache License required that unmodified parts in derived works keep the License.
  • In every licensed file, any original copyright, patent, trademark or attribution notices must be preserved.
  • In every licensed file change, there must be a notification stating that changes have been made in the file.
  • If the Apache-licensed software includes a NOTICE file, this file and its contents must be preserved in all the derived works.
  • If anyone intentionally sends a contribution for an Apache-licensed software to its authors, this contribution can automatically be used under the Apache License.

This license is interesting because of the automatic patent license, and the clause about contribution submission.

It\u2019s compatible with the GPL, so you can mix Apache licensed-code into GPL software.

"},{"location":"community/licensing/#why-apache-20","title":"Why Apache-2.0?","text":"

In the case of scientific software, we believe that clearly stating that a Derived Work introduces changes into the original Work is a fundamental measure of transparency. Other than that, we wanted a permissive, non-copyleft license.

"},{"location":"community/licensing/#what-is-our-expectation-for-derived-works","title":"What is our expectation for Derived Works?","text":"

At the bare minimum, you must meet the conditions of the license (simplified version) about preserving the license text and copyright/attribution notices as well as corresponding statements of changes.

How to state that a file has been changed in a Derived Work. We suggest the following steps, heavily influenced by P. Ombredanne's recommendations at StackExchange:

  1. In each source file, add a note to the header comment stating that the file has been modified, with an approximate date, and a high-level description of the changes. The date and the description of the changes are not strictly required, but they are positive etiquette from a software engineering standpoint and substantially improve the transparency of the changes from a scientific point of view.
  2. If the source file did not have a license notice in the header comment, please add it to avoid ambiguity.
  3. Deleted files: please keep the file with just the header comment and state that the file is deleted. The change statement should follow the suggestion in 1), preferably stating whether the source has been deleted or moved over to other files. If preserving the filename as-is might become confusing to the user of the Derived Work, the filename can be modified to be marked as hidden with a dot . or underscore _ prefix, or modifying the extension.
  4. Preferably, also include a link to the original file in our GitHub repository, making sure the link is done to a particular commit state.

What changes would we like to see annotated? The high-level description of the changes will preferably contain:

  • Correction of bugs
  • Substantial performance improvement decisions
  • Replacement of relevant methods and dependencies by alternatives
  • Changes to the license
"},{"location":"community/licensing/#example-of-our-expectations","title":"Example of our expectations","text":"

Let's say a Derived Work modifies the sdcflows.viz.utils code-base. The file may or may not have the attribution notice. At the time of writing, the header comment of this file is:

Header comment in the original Work

With attribution notice
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-\n# vi: set ft=python sts=4 ts=4 sw=4 et:\n#\n# Copyright 2021 The NiPreps Developers <nipreps@gmail.com>\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\"\"\"Visualization tooling.\"\"\"\n
Without attribution notice
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-\n# vi: set ft=python sts=4 ts=4 sw=4 et:\n\"\"\"Visualization tooling.\"\"\"\n

Either way (whether the attribution notice is present or not), we suggest to update this header comment to something along the lines of the following:

Suggested header comment in the Derived Work

Required

# <shebang and editor settings can be preserved or removed freely>\n#\n# <your attribution notice, either maintaining the Apache-2.0 license or changing the license>\n#\n# STATEMENT OF CHANGES: This file is derived from sources licensed under the Apache-2.0 terms,\n# and this file has been changed.\n# The original file this work derives from is found at:\n# https://github.com/nipreps/sdcflows/blob/50393a8584dd0abf5f8e16e6ba66c43e1126f844/sdcflows/viz/utils.py\n#\n# [April 2021] CHANGES:\n#    * BUGFIX: Outdated function call from the ``svgutils`` dependency that changed API as of version 0.3.2.\n#    * ENH: Changed plotting dependency to the new `netplotbrain` package.\n#    * DOC: Added docstrings to some functions that lacked them.\n#\n# ORIGINAL WORK'S ATTRIBUTION NOTICE:\n#\n#     Copyright 2021 The NiPreps Developers <nipreps@gmail.com>\n#\n#     Licensed under the Apache License, Version 2.0 (the \"License\");\n#     you may not use this file except in compliance with the License.\n#     You may obtain a copy of the License at\n#\n#         http://www.apache.org/licenses/LICENSE-2.0\n#\n#     Unless required by applicable law or agreed to in writing, software\n#     distributed under the License is distributed on an \"AS IS\" BASIS,\n#     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n#     See the License for the specific language governing permissions and\n#     limitations under the License.\n\"\"\"Visualization tooling.\"\"\"\n
The lines highlighted with yellow color are explicitly required by the Apache-2.0 conditions.

Recommended (commit)

# <shebang and editor settings can be preserved or removed freely>\n#\n# <your attribution notice, either maintaining the Apache-2.0 license or changing the license>\n#\n# STATEMENT OF CHANGES: This file is derived from sources licensed under the Apache-2.0 terms,\n# and this file has been changed.\n# The original file this work derives from is found at:\n# https://github.com/nipreps/sdcflows/blob/50393a8584dd0abf5f8e16e6ba66c43e1126f844/sdcflows/viz/utils.py\n#\n# [April 2021] CHANGES:\n#    * BUGFIX: Outdated function call from the ``svgutils`` dependency that changed API as of version 0.3.2.\n#    * ENH: Changed plotting dependency to the new `netplotbrain` package.\n#    * DOC: Added docstrings to some functions that lacked them.\n#\n# ORIGINAL WORK'S ATTRIBUTION NOTICE:\n#\n#     Copyright 2021 The NiPreps Developers <nipreps@gmail.com>\n#\n#     Licensed under the Apache License, Version 2.0 (the \"License\");\n#     you may not use this file except in compliance with the License.\n#     You may obtain a copy of the License at\n#\n#         http://www.apache.org/licenses/LICENSE-2.0\n#\n#     Unless required by applicable law or agreed to in writing, software\n#     distributed under the License is distributed on an \"AS IS\" BASIS,\n#     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n#     See the License for the specific language governing permissions and\n#     limitations under the License.\n\"\"\"Visualization tooling.\"\"\"\n
The lines highlighted with green color are recommended by the NiPreps Developers.

Recommended (version)

# <shebang and editor settings can be preserved or removed freely>\n#\n# <your attribution notice, either maintaining the Apache-2.0 license or changing the license>\n#\n# STATEMENT OF CHANGES: This file is derived from sources licensed under the Apache-2.0 terms,\n# and this file has been changed.\n# The original file this work derives from is found within\n# the version 2.0.2 distribution of the software.\n#\n# [April 2021] CHANGES:\n#    * BUGFIX: Outdated function call from the ``svgutils`` dependency that changed API as of version 0.3.2.\n#    * ENH: Changed plotting dependency to the new `netplotbrain` package.\n#    * DOC: Added docstrings to some functions that lacked them.\n#\n# ORIGINAL WORK'S ATTRIBUTION NOTICE:\n#\n#     Copyright 2021 The NiPreps Developers <nipreps@gmail.com>\n#\n#     Licensed under the Apache License, Version 2.0 (the \"License\");\n#     you may not use this file except in compliance with the License.\n#     You may obtain a copy of the License at\n#\n#         http://www.apache.org/licenses/LICENSE-2.0\n#\n#     Unless required by applicable law or agreed to in writing, software\n#     distributed under the License is distributed on an \"AS IS\" BASIS,\n#     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n#     See the License for the specific language governing permissions and\n#     limitations under the License.\n\"\"\"Visualization tooling.\"\"\"\n
The lines highlighted with green color are recommended by the NiPreps Developers.

Although it is not mandated by the license letter, the spirit of the Apache-2.0 (and all other licenses stipulating the statement of changes, such as the CC-BY 4.0) suggests that a date of modification and an overview of outstanding changes are pertinent. We also suggest a link to the original code, including the commit-hash (that long string starting with 50393a in the URL above) for the location of the exact origin of the file. Alternatively, Derived Works may point to a exact release identifier where the original file is part of the code-base distribution. Please make sure to remove or replace with appropriate contents the comment tags <...> above.

What if a Derived Work does not modify this particular file? You should retain the original attribution notice as is (or introduce it if missing), unless you are relicensing the file. In that case, proceed with the suggestions above, and note the license change in the STATEMENT OF CHANGES block of the header comment.

"},{"location":"community/licensing/#are-papers-using-apache-20-licensed-software-considered-as-derived-works","title":"Are papers using Apache-2.0 licensed software considered as Derived Works?","text":"

No, they don't because they only reuse the software (in other words, they don't redistribute the software). The license stipulates that redistribution must retain the license and attribution notices as they are. In the scientific context, it is likely that a particular tool is modified (for example, to replace a method that you think is not appropriate for your data). Then, redistribution of the source would be desirable from the transparent reporting point of view, and therefore you should honor the License.

Generally, works using our NiPreps just need to follow the citation guidelines of the particular project and report the citation boilerplate including all software versions and literature references in the closest letter possible to that generated by the tool.

"},{"location":"community/licensing/#licensing-of-docker-and-singularity-images","title":"Licensing of Docker and Singularity images","text":"

Container images redistribute copies of NiPreps alongside their third-party dependencies, all of them bundled in the image. If the applicable license is Apache-2.0, then the text of a NOTICE file must be shown to the user. All NiPreps must insert a NOTICE file into their containerized distributions and print its contents out in the command line output, as well as in the visual reports. This NOTICE file for containers will be placed in the /.docker/NOTICE path of the repository, and this file must replace the /NOTICE file (if it exists) at image building time. Alternatively, and if the corresponding NiPreps Developers consider that the Apache-2.0 imposes too onerous requirements for the container image distribution, the source code of such images (e.g., Dockerfile) can be licensed under the MIT license.

Example NOTICE file for fMRIPrep

Python distribution /NOTICE
fMRIPrep\nCopyright 2021 The NiPreps Developers.\n\nThis product includes software developed by\nthe NiPreps Community (https://nipreps.org/).\n\nPortions of this software were developed at the Department of\nPsychology at Stanford University, Stanford, CA, US.\n\nThis software contains code ultimately derived from the epidewarp.fsl\nscript (https://www.nmr.mgh.harvard.edu/~greve/fbirn/b0/epidewarp.fsl)\nby Doug Greve, Dave Tuch, Tom Liu, and Bryon Mueller with generous\nhelp from the FSL crew (www.fmrib.ox.ac.uk/fsl) and the Biomedical\nInformatics Research Network (www.nbirn.net).\n
Container image distribution /.docker/NOTICE
fMRIPrep Container Image distribution\nCopyright 2021 The NiPreps Developers.\n\nThis product includes fMRIPrep and software developed by\nthe NiPreps Community (https://nipreps.org/).\n\nPortions of this software were developed at the Department of\nPsychology at Stanford University, Stanford, CA, US.\n\nThis product bundles AFNI <version-placeholder>, which is available under\nthe Gnu General Public License.\nMajor portions of AFNI were written at the Medical College of Wisconsin,\nwhich owns the copyright to that code. For fuller details, see\nhttp://afni.nimh.nih.gov/pub/dist/src/README.copyright.\n\nThis product bundles ANTs <version-placeholder>, which is available under\nthe BSD 3-clause license terms.\nCopyright 2009-2013 ConsortiumOfANTS.\n\nThis product bundles BIDS-Validator <version-placeholder>, which is available\nunder the MIT License.\nCopyright 2015 The Board of Trustees of the Leland Stanford Junior University.\n\nThis product bundles the Connectome Workbench <version-placeholder>, which\nis available under the GPL-v2\n(https://www.humanconnectome.org/software/connectome-workbench-license).\n\nThis product bundles FSL <version-placeholder>, which is available\nunder a custom license with commercial restrictions\n(https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/Licence).\nCopyright 2018, The University of Oxford.\n\nThis product bundles FreeSurfer <version-placeholder>, which is available\nunder a custom license and requires obtaining a license key\n(https://surfer.nmr.mgh.harvard.edu/fswiki/FreeSurferSoftwareLicense).\nCopyright 2011, The General Hospital Corporation, Boston MA, USA.\n\nThis product bundles code derived from ICA-AROMA, both (fork and original work)\nare available under the Apache-2.0 license.\n(https://github.com/oesteban/ICA-AROMA/blob/master/license.md)\nCopyright 2021, Maarten Mennes\n\nThis product bundles Miniconda <version-placeholder>, which is available\nunder a BSD 3-clause license.\n(c) 2017 Continuum Analytics, Inc. (dba Anaconda, Inc.).\nhttps://www.anaconda.com. All Rights Reserved\n\nThis product bundles NeuroDebian, which adheres to the\nDebian Free Software Guidelines (DFSG)\nhttps://www.debian.org/social_contract#guidelines\nand the terms of the Debian Social Contract version 1.1.\n\nThis product bundles tools by the NiPy community, such as NiBabel\n(MIT License, https://github.com/nipy/nibabel/blob/master/COPYING),\nand NiPype (Apache-2.0, https://github.com/nipy/nipype/blob/master/LICENSE).\n\nThis product bundles Pandoc <version-placeholder>, which is available\nunder the GPL version 2 or later.\nCopyright (C) 2006-2021 John MacFarlane <jgm at berkeley dot edu>\n\nThis product bundles SVGO <version-placeholder>, which is available\nunder the MIT License.\nCopyright (c) Kir Belevich\n\nThis product bundles tedana <version-placeholder>, which is available under\nthe GNU Lesser General Public License v2.1.\nCopyright 2018, tedana developers.\n\nTemplateFlow, a component of this bundle, contains neuroimaging template\nand atlas data under several permissive licenses.\nPlease refer to the metadata of the particular template used in your study to\ndetermine the exact terms of the license and how to acknowledge attribution\nof those works.\n\nsMRIPrep, a component of this bundle, contains code ultimately derived from\nANTs <version-placeholder>, which is available under\nthe BSD 3-clause license terms.\nCopyright 2009-2013 ConsortiumOfANTS.\n\nsMRIPrep, a component of this bundle, contains code ultimately derived from\nMindboggle <version-placeholder>, which is available under\nthe Apache License 2.0.\nCopyright 2016, Mindboggle team (http://mindboggle.info)\n\nfMRIPrep contains code ultimately derived from the epidewarp.fsl\nscript (https://www.nmr.mgh.harvard.edu/~greve/fbirn/b0/epidewarp.fsl)\nby Doug Greve, Dave Tuch, Tom Liu, and Bryon Mueller with generous\nhelp from the FSL crew (www.fmrib.ox.ac.uk/fsl) and the Biomedical\nInformatics Research Network (www.nbirn.net).\n
"},{"location":"community/members/","title":"Membership","text":"

In general, NiPreps embrace a liberal contribution model of governance structure. However, because of the scientific domain of NiPreps, the community features some structure from meritocracy models to prescribe the order in the authors list of new papers about these tools.

"},{"location":"community/members/#developers","title":"Developers","text":"

Developers are members of a wonderful team driving the project. Names and contacts of all developers are included in the .maint/developers.json file of each project. Examples of steering activities that drive the project are: actively participating in the follow-up meetings, leading documentation sprints, helping in the design of the tool and definition of the roadmap, providing resources (in the broad sense, including funding), code-review, etc.

"},{"location":"community/members/#contributors","title":"Contributors","text":"

Contributors enlisted in the .maint/contributors.json file of each project actively help or have previously helped the project in a broad sense: writing code, writing documentation, benchmarking modules of the tool, proposing new features, helping improve the scientific rigor of implementations, giving out support on the different communication channels (mattermost, NeuroStars, GitHub, etc.). If you are new to the project, don't forget to add your name and affiliation to the list of contributors there! Our Welcome Bot will send an automated message reminding this to first-time contributors. Before every release, unlisted contributors will be invited again to add their names to the file (just in case they missed the automated message from our Welcome Bot).

Contributors who have contributed at some point to the project but were required or they wished to disconnect from the project's updates and to drop-out from publications and other dissemination activities, are listed in the .maint/former.json file.

"},{"location":"devs/devenv/","title":"Developer Environment","text":"

This document explains how to prepare a new development environment and update an existing environment, as necessary, for the development of NiPreps' components. Some components may deviate from these guidelines, in such a case, please follow the guidelines provided in their documentation.

If you plan to contribute back to the community, making your code available via pull-request, please make sure to have read and understood the Community Documents and Contributor Guidelines. If you plan to distribute derived code, please follow our licensing guidelines.

Development in Docker is encouraged, for the sake of consistency and portability. By default, work should be built off of nipreps/fmriprep:unstable, which tracks the master branch, or nipreps/fmriprep:latest, which tracks the latest release version (see BIDS-Apps execution guide for the basic procedure for running).

It will be assumed the developer has a working repository in $HOME/projects/fmriprep, and examples are also given for niworkflows and NiPype.

"},{"location":"devs/devenv/#patching-a-working-copy-into-a-docker-container","title":"Patching a working copy into a Docker container","text":"

In order to test new code without rebuilding the Docker image, it is possible to mount working repositories as source directories within the container. The Docker wrapper script simplifies this for the most common repositories:

    -f PATH, --patch-fmriprep PATH\n                          working fmriprep repository (default: None)\n    -n PATH, --patch-niworkflows PATH\n                          working niworkflows repository (default: None)\n    -p PATH, --patch-nipype PATH\n                          working nipype repository (default: None)\n

For instance, if your repositories are contained in $HOME/projects:

$ fmriprep-docker -f $HOME/projects/fmriprep/fmriprep \\\n                  -n $HOME/projects/niworkflows/niworkflows \\\n                  -p $HOME/projects/nipype/nipype \\\n                  -i nipreps/fmriprep:latest \\\n                  $HOME/fullds005 $HOME/dockerout participant\n

Note the -i flag allows you to specify an image.

When invoking docker directly, the mount options must be specified with the -v flag:

-v $HOME/projects/fmriprep/fmriprep:/usr/local/miniconda/lib/python3.7/site-packages/fmriprep:ro\n-v $HOME/projects/niworkflows/niworkflows:/usr/local/miniconda/lib/python3.7/site-packages/niworkflows:ro\n-v $HOME/projects/nipype/nipype:/usr/local/miniconda/lib/python3.7/site-packages/nipype:ro\n

For example,

$ docker run --rm -v $HOME/ds005:/data:ro -v $HOME/dockerout:/out \\\n    -v $HOME/projects/fmriprep/fmriprep:/usr/local/miniconda/lib/python3.7/site-packages/fmriprep:ro \\\n    nipreps/fmriprep:latest /data /out/out participant \\\n    -w /out/work/\n

In order to work directly in the container, pass the --shell flag to fmriprep-docker

$ fmriprep-docker --shell $HOME/ds005 $HOME/dockerout participant\n

This is the equivalent of using --entrypoint=bash and omitting the fMRIPrep arguments in a docker command:

$ docker run --rm -v $HOME/ds005:/data:ro -v $HOME/dockerout:/out \\\n    -v $HOME/projects/fmriprep/fmriprep:/usr/local/miniconda/lib/python3.7/site-packages/fmriprep:ro --entrypoint=bash \\\n    nipreps/fmriprep:latest\n

Patching containers can be achieved in Singularity analogous to docker using the --bind (-B) option:

$ singularity run \\\n    -B $HOME/projects/fmriprep/fmriprep:/usr/local/miniconda/lib/python3.7/site-packages/fmriprep \\\n    fmriprep.img \\\n    /scratch/dataset /scratch/out participant -w /out/work/\n
"},{"location":"devs/devenv/#adding-dependencies","title":"Adding dependencies","text":"

New dependencies to be inserted into the Docker image will either be Python or non-Python dependencies. Python dependencies may be added in three places, depending on whether the package is large or non-release versions are required. The image must be rebuilt after any dependency changes.

Python dependencies should generally be included in the appropriate dependency metadata of the setup.cfg file found at the root of each repository. If some the dependency must be a particular version (or set thereof), it is possible to use version filters in this setup.cfg file.

For large Python dependencies where there will be a benefit to pre-compiled binaries, conda packages may also be added to the conda install line in the Dockerfile.

Non-Python dependencies must also be installed in the Dockerfile, via a RUN command. For example, installing an apt package may be done as follows:

RUN apt-get update && \\\n    apt-get install -y <PACKAGE>\n
"},{"location":"devs/devenv/#rebuilding-docker-image","title":"(Re)Building Docker image","text":"

If it is necessary to (re)build the Docker image, a local image named fmriprep may be built from within the local repository. Let's assume it is located in ~/projects/fmriprep:

~/projects/fmriprep$ VERSION=$( python get_version.py )\n~/projects/fmriprep$ docker build -t fmriprep --build-arg VERSION=$VERSION .\n

The VERSION build argument is necessary to ensure that help text can be reliably generated. The get_version.py tool constructs the version string from the current repository state.

To work in this image, replace nipreps/fmriprep:latest with just fmriprep in any of the above commands. This image may be accessed by the Docker wrapper via the -i flag, e.g.:

$ fmriprep-docker -i fmriprep --shell\n
"},{"location":"devs/devenv/#code-server-development-environment-experimental","title":"Code-Server Development Environment (Experimental)","text":"

To get the best of working with containers and having an interactive development environment, we have an experimental setup with code-server.

Important

We have a video walking through the process if you want a visual guide.

1. Build the Docker image. We will use the Dockerfile_devel file to build our development docker image:

$ cd $HOME/projects/fmriprep\n$ docker build -t fmriprep_devel -f Dockerfile_devel .\n

2. Run the Docker image We can start a docker container using the image we built (fmriprep_devel):

$ docker run -it -p 127.0.0.1:8445:8080 -v ${PWD}:/src/fmriprep fmriprep_devel:latest\n

Windows Users

If you are using windows shell, ${PWD} may not be defined, instead use the absolute path to your repository.

Docker-Toolbox

If you are using Docker-Toolbox, you will need to change your virtualbox settings using these steps as a guide. For step 6, instead of Name = rstudio; Host Port = 8787; Guest Port = 8787, have Name = code-server; Host Port = 8443; Guest Port = 8080. Then in the docker command above, change 127.0.0.1:8445:8080 to 192.168.99.100:8445:8080.

If the container started correctly, you should see the following on your console:

INFO  Server listening on http://localhost:8080\nINFO    - No authentication\nINFO    - Not serving HTTPS\n

Now you can switch to your favorite browser and go to: 127.0.0.1:8445 (or 192.168.99.100:8445 for Docker Toolbox).

3. Copy fmriprep.egg-info into your fmriprep/ project directory fmriprep.egg-info makes the package exacutable inside the docker container. Open a terminal in vscode and type the following:

$ cp -R /src/fmriprep.egg-info /src/fmriprep/\n
"},{"location":"devs/devenv/#code-server-development-environment-features","title":"Code-Server Development Environment Features","text":"
  • The editor is vscode

  • There are several preconfigured debugging tests under the debugging icon in the activity bar

  • see vscode debugging python for details.

  • The gitlens and python extensions are preinstalled to improve the development experience in vscode.

"},{"location":"devs/releases/","title":"Releases","text":"

As of January 2020, fMRIPrep has adopted a Calendar Versioning scheme, and with it we are attempting to apply more coherent semantic rules to our releases.

Note

This document is a draft for internal and external comment. Any commitments expressed here are proposals, and should not be relied upon at this time. This conversation started as a Google Doc.

"},{"location":"devs/releases/#principles","title":"Principles","text":"

The basic release form is YY.MINOR.PATCH, so the first minor release of 2020 is 20.0.0, and the first minor release of 2021 will be 21.0.0, whatever the final minor release of 2020 is. A series of releases share a YY.MINOR. prefix, which we refer to as the YY.MINOR.x series. For example, the 20.0.x series contains version 20.0.0, 20.0.1, and any other releases needed.

"},{"location":"devs/releases/#feature-releases","title":"Feature releases","text":"

Minor releases are considered feature releases. Because there is no concept of a \"major\" release (just a calendar year rollover), most changes to the code base will result in a new feature release. Changes targeting a new feature release should target the master branch. Feature releases may be released as often as is deemed appropriate.

"},{"location":"devs/releases/#bug-fix-releases","title":"Bug-fix releases","text":"

Patch releases are considered bug-fix releases. Each minor release triggers the creation of a new maint/<YY>.<MINOR>.x branch, and changes targeting a bug-fix release should target this branch. A \"minor release series\" is the initial feature release and the bug-fix releases that share the minor release prefix. Bug-fix releases may be released on minimal notice to other developers.

These releases must satisfy four conditions:

  1. Resolving one or more bugs. These mostly include failures of fMRIPrep to complete or producing invalid derivatives (e.g., a NIfTI file of all zeroes).
  2. Derivatives compatibility. If a subject may be successfully run on 20.0.n, then the imaging derivatives should be identical if rerun with 20.0.(n+1), modulo rounding errors and the effects of nondeterministic algorithms. The changes between successful runs of 20.0.n and 20.0.(n+1) should not be larger than the changes between two successful runs of 20.0.n. Cosmetic changes to reports are acceptable, while differing fields of view or data types in a NIfTI file would not be.
  3. API compatibility. Workflow-generating functions, workflow input- and outputnode fields must not change. As an end-user application, this may seem overly strict, but the odds of introducing a bug are much higher in these cases.
  4. User interface compatibility. Substantial changes to fMRIPrep command line must not happen (e.g., the addition of a new, relevant flag).

Note that not all bugs can be fixed in a way that satisfies all three of these criteria without significant effort. A developer may determine that the bug will be fixed in the next feature release.

Additional acceptable changes within a minor release series:

  1. Improved tests. These often come along with bug fixes, but they can be free-standing improvements to the code base.
  2. Improved documentation. Unless the documentation is of a feature that will not be present in a bug-fix release, this is always welcome.
  3. Updates to the Dockerfile that improve operation for Docker and/or Singularity users, but do not risk behavior change. A good example is including more templates to reduce the need for network requests. An example of an update to the Dockerfile that forces a minor release increment is a change in the pinned version of any of the dependencies or the base container image.
  4. Improvements to the lightweight wrappers. As long as a command-line invocation that worked for the previous version continues to work and produce the same Docker command, there's little chance of harm.
"},{"location":"devs/releases/#mechanics","title":"Mechanics","text":""},{"location":"devs/releases/#branch-synchronization","title":"Branch synchronization","text":"

A maintenance branch should generally follow directly from the tag of the feature release.

git checkout -b maint/20.0.x 20.0.0\ngit push upstream maint/20.0.x\n

It is expected that maint/20.0.x will diverge from master, as new features will be merged into master, and bug-fixes into maint/20.0.x. At a minimum, each new bug-fix release should be merged into master. After a 20.0.1 release:

git checkout master\ngit fetch upstream\ngit reset --hard upstream/master\ngit merge --no-commit 20.0.1\n\n# Resolve any merge conflicts\ngit add .\n\n# Manually review all changes to ensure compatibility\ngit diff --cached upstream/master\ngit commit\ngit push upstream master\n

If an unreleased bug-fix seems likely to cause merge conflicts, it may be worth doing the above more frequently.

"},{"location":"devs/releases/#dependencies","title":"Dependencies","text":"

fMRIPrep has a number of dependencies that we control at this point:

  1. sMRIPrep
  2. SDCflows
  3. NiWorkflows

These do not follow the same versioning scheme as above, but we need them to follow a compatible scheme. In particular, we need to be able to fix bugs that are situated within these dependencies in a bug-fix release without violating the criteria laid out above. At the time of an fMRIPrep feature release, all of the above tools need to also split out a maintenance branch (if they have not already) for the minor version series that fMRIPrep depends on. As an example, when 20.0.0 was released, fMRIPrep had the following dependencies in setup.cfg:

    niworkflows ~= 1.1.7\n    sdcflows ~= 1.2.0\n    smriprep ~= 0.5.2\n
~= is the compatible release specifier described in PEP 440. ~= 1.1.7 is equivalent to >= 1.1.7, == 1.1.*. This means that the current version of fMRIPrep is expected to work with niworkflows 1.1.7+ but not 1.2+. Thus, niworkflows needs to have a maint/1.1.x branch, sdcflows a maint/1.2.x and smriprep maint/0.5.x. Any changes to these tools that might violate API or derivative compatibility, must go into master, and must not be released into the current minor series of these tools. Note that fMRIPrep 20.0.0 does not depend on niworkflows ~= 1.1.0. Multiple feature releases of fMRIPrep may depend on the same minor release series of a dependency. There is no requirement to hike the dependency. However, if a dependency has started a new minor release series, a feature release of fMRIPrep is a good opportunity to bump the dependency.

We maintain a Versions Matrix to document and keep track of these dependencies.

"},{"location":"devs/releases/#support-windows","title":"Support Windows","text":""},{"location":"devs/releases/#minor-release-series","title":"Minor release series","text":"

A minor release series will continue to accept qualifying bug fixes at least until the next minor release. A minimum duration may be considered, or a fixed number of minor release series might be simultaneously supported.

An unmaintained series is a valid target for bug fixes after the support window, but the expected effort level of the contributor and maintainers will be higher and lower, respectively.

"},{"location":"devs/releases/#long-term-support-series","title":"Long-term support series","text":"

A long-term support (LTS) series is a minor release series that an LTS manager commits to maintaining for a specific duration, no less than one year. LTS series are under the same constraints as a minor release series in terms of what changes can be accepted.

The fMRIPrep developers commit to maintaining one LTS series at all times, at intervals of approximately one year. Community members may volunteer to assume maintainership after the initial period, or to maintain another minor release series as LTS.

Support windows of greater than a year have a much higher potential to run into issues with upstream dependencies going outside of their support windows. As much as possible, an fMRIPrep minor release should seek to move to the versions of upstream dependencies that will ensure the longest support before being considered for LTS.

Additional tasks required of an LTS manager:

  • Tracking possible breaking changes and broken URLs in upstream projects outside of the nipreps ecosystem.

    • Neurodebian dependencies (AFNI, FSL, Convert3D, Connectome WB)
    • FreeSurfer
    • ANTs
    • NodeJS - BIDS-validator, SVGO
    • Pandoc
    • ICA-AROMA
    • Miniconda
    • Python minor series end-of-life
    • numpy, scipy, pandas, nipype, nibabel, matplotlib
  • Backporting fixes from other maintained series.

    • If a bug is identified as existing within the LTS series and can be fixed without breaking API or derivative compatibility.

As many dependencies as possible should be pinned to specific versions relevant to the environment they are installed in. Packages (Debian .deb files, conda packages, Python wheels) should be archived in case of a loss of the external packages.

"},{"location":"devs/versions/","title":"Versions Matrix","text":"

The versions matrix is intended to allow easy reference for the dependencies within the NiPreps family of projects.

"},{"location":"devs/versions/#fmriprep","title":"fMRIPrep","text":"fMRIPrep series sMRIPrep series SDCflows series NiWorkflows series 23.1.x ~= 0.12.0 ~= 2.5.0 ~= 1.8.0 23.0.x ~= 0.11.0 ~= 2.4.0 ~= 1.7.6 22.1.x ~= 0.10.0 ~= 2.2.1 ~= 1.7.0 22.0.x ~= 0.9.2 ~= 2.1.1 ~= 1.6.3 21.0.x ~= 0.8.0 ~= 2.0.0 ~= 1.4.0 20.2.x ~= 0.7.0 ~= 1.3.1 ~= 1.3.0 20.1.x ~= 0.6.1 ~= 1.3.1 ~= 1.2.3 20.0.x ~= 0.5.2 ~= 1.2.0 ~= 1.1.7 1.5.3+ ~= 0.4.0 ~= 1.0.1 ~= 1.0.2

(Originally posted at nipreps/fmriprep#2054)

"},{"location":"devs/versions/#dmriprep","title":"dMRIPrep","text":"

(Work in progress)

"},{"location":"devs/versions/#smriprep","title":"sMRIPrep","text":"

sMRIPrep requires niworkflows and generally must depend on one minor series of niworkflows for the duration of an sMRIPrep minor series. Each sMRIPrep series may also be depended on for an fMRIPrep series and/or a dMRIPrep series. Noting these dependencies here should make it easier to track when a new minor series needs to be created.

sMRIPrep series NiWorkflows series TemplateFlow series 0.12.x ~=1.8.0 ... 0.10.x ~= 1.7.0 >= 0.6 0.9.x ~= 1.6.0 >= 0.6 0.8.x ~= 1.4.0 >= 0.6 0.7.x ~= 1.3.0 ~= 0.6 0.6.x ~= 1.2.0 ~= 0.6 0.5.x ~= 1.1.5 ~= 0.4.2

(Originally posted at nipreps/smriprep#172)

"},{"location":"devs/versions/#mriqc","title":"MRIQC","text":"

(Work in progress)

"},{"location":"intro/nipreps/","title":"Framework","text":""},{"location":"intro/nipreps/#building-on-fmripreps-success-story","title":"Building on fMRIPrep's success story","text":"

The current neuroimaging workflow has matured into a large chain of processing and analysis steps involving a large number of experts, across imaging modalities and applications. The development and fast adoption of fMRIPrep have revealed that neuroscientists need tools that simplify their research workflow, provide visual reports and checkpoints, and engender trust in the tool itself. The NiPreps framework extends fMRIPrep's approach and principles to new imaging modalities. The vision for NiPreps is to provide end-users (i.e., researchers) with applications that allow them to perform quality control smoothly and to prepare their data for modeling and statistical analysis.

"},{"location":"intro/nipreps/#leveraging-bids","title":"Leveraging BIDS","text":"

NiPreps leverage the Brain Imaging Data Structure (BIDS) to understand all the particular features and available metadata (i.e., imaging parameters) of the input dataset. BIDS allows NiPreps to automatically stage the most adequate preprocessing workflow while minimizing manual intervention.

"},{"location":"intro/nipreps/#architecture","title":"Architecture","text":"

The NiPreps framework (Figure 1) encompasses a wide array of software projects organized into three layers of scientific software:

  • Software infrastructure: including quite mature projects such as NiPype and NiBabel; the standard specifications of the Brain Imaging Data Structure (BIDS, and BIDS-Derivatives); and some other tools such as NiTransforms or TemplateFlow, under development. These tools deliver low-level interfaces (e.g., data access to images and spatial transforms) and utilities (see Figure 1).
  • Middleware: these are utilities that generalize their functionalities across the end-user tools. These utilities cover foundational processing methodologies (e.g., NiWorkflows and SDCflows), the crowdsourcing of metadata (e.g., MRIQC Web-API), and the support for deep learning models (MRIQC-nets).
  • End-user tools such as fMRIPrep: Some existing end-user tools include sMRIPrep (Structural MRI Preprocessing), which lies in between an end-user tool and middleware, as it is involved in higher-level tools such as fMRIPrep. Finally, quality control tools (e.g., MRIQC) to be executed before any preprocessing happens.

"},{"location":"intro/nipreps/#projects","title":"Projects","text":"
  • fMRIPrep (GitHub): fMRI Preprocessing
  • dMRIPrep (GitHub): dMRI Preprocessing
  • sMRIPrep (GitHub): Structural MRI Preprocessing
  • MRIQC (GitHub): MRI quality control
  • SDCflows (GitHub): Susceptibility-derived distortion correction (SDC) workflows
  • NiWorkflows (GitHub): General/miscellaneous workflow utilities
  • TemplateFlow: A registry of neuroimaging templates and spatial mappings between them.
  • NiTransforms (GitHub)
"},{"location":"intro/nipreps/#early-stage-projects","title":"Early-stage projects","text":"
  • NiRodents (GitHub): middleware adaptations for small animals imaging.
  • NiBabies (GitHub): middleware adaptations for infant imaging.
"},{"location":"intro/transparency/","title":"Transparency of workflows","text":"

NiPreps adopt fMRIPrep's foundations, and particularly resonate with the transparency principles. As discussed in (Esteban et al., 2019 -- preprint):

The rapid increase in the volume and diversity of data, as well as the evolution of available techniques for processing and analysis, presents an opportunity for considerable advancement of research in neuroscience. The drawback resides in the need for progressively more complex analysis workflows that rely on decreasingly interpretable models of the data. Such context encourages \u2018black-box\u2019 solutions that efficiently perform a valuable service but do not provide insights into how the tool has transformed the data into the expected outputs. Black boxes obscure important steps in the inductive process mediating between experimental measurements and reported findings. This way of moving forward risks producing a future generation of cognitive neuroscientists who have become experts in sophisticated computational methods but have little to no working knowledge of how their data were transformed through processing. Transparency is often identified as a remedy for these problems. fMRIPrep ascribes to \u2018glass-box\u2019 principles, which are defined in opposition to the many different facets or levels at which black-box solutions are opaque. The visual reports that fMRIPrep generates are a crucial aspect of the glass-box approach. Their quality control checkpoints represent the logical flow of preprocessing, allowing scientists to critically inspect and better understand the underlying mechanisms of the workflow. A second transparency element is the citation boilerplate that formalizes all details of the workflow and provides the versions of all involved tools along with references to the corresponding scientific literature. A third asset for transparency is thorough documentation that delivers additional details on each of the building blocks represented in the visual reports and described in the boilerplate. Further, fMRIPrep has been open-source since its inception: users have access to all of the incremental additions to the tool through the history of the version-control system. The use of GitHub grants access to the discussions held during development, allowing one to see how and why the main design decisions were made. The modular design of fMRIPrep enhances its flexibility and improves transparency, as the main features of the software are more easily accessible to potential collaborators. In combination with some coding style and contribution guidelines, this modularity has enabled multiple contributions by peers and the creation of a rapidly growing community that would be difficult to nurture behind closed doors.

"},{"location":"intro/transparency/#visual-reports-beyond-quality-control","title":"Visual reports beyond quality control","text":"

One foundational component of the NiPreps framework is the Visual Report System. End-user applications such as fMRIPrep or dMRIPrep generate individual reports after their preprocessing. Those visual reports have two fundamental purposes:

  • assessing the quality of the generated outputs, permitting the user to take quality control actions to eliminate biases originated from inadequate processing; and
  • understanding the workflow, by sequentially presenting the main steps of processing, the user can access the why the tool in particular took these steps ando more geneally why standard preprocessing involves that step.
"},{"location":"intro/transparency/#citation-boilerplates","title":"Citation boilerplates","text":"

NiPreps leverage the wealth of existing neuroimaging software that is available to researchers. To give back for standing on the shoulders of giants, NiPreps aim at the most thorough reporting possible crediting all the pieces of the prior knowledge they leverage. With the execution of some particular NiPreps, the application runs some introspection code to formalize the computational graph the particular workflow executed and iterates over all the nodes to extract the relevant articles and communications that should be cited, as well as all software tools and their versions involved. Similarly, ancillary materials such as neuroimaging templates and atlases are reported and cited.

All these references and citations are finally collated in a natural language description of the workflow. This description is therefore generated automatically, and contains all the details that are necessary to replicate the processing, as well as the abovementioned references. The text is appended to the visual report, and provided in three formats (markdown, latex and html/plain-text) with an index of citations, so that the user is only required to \"copy-and-paste\" into the Methods section of their papers.

Note for reviewers and editors

The boilerplate text generated by some NiPreps is intended to allow for clear, consistent description of the preprocessing steps used, in order to improve the reproducibility of studies. We fully intend for it to be copied verbatim, and have released it under the CC0 license, dedicating it to the public domain in jurisdictions that recognize the concept, and assert that we will take no action to enforce copyright in jurisdictions where we cannot disclaim it.

We firmly believe that requiring authors to modify this passage will serve no legitimate scientific or literary purpose and can, in fact, serve only to reduce the replicability of the analysis being described by making the preprocessing steps less clear.

We recognize that there may be automated plagiarism detection software that will flag the boilerplate text. We would be happy to discuss potential solutions for annotating boilerplate sections of documents to indicate automatic generation, and can update our software to make this annotation simpler for authors.

"},{"location":"news/","title":"News and Announcements","text":""},{"location":"news/#register-for-the-nipreps-hackathon-with-the-ohbm23-brainhack","title":"Register for the NiPreps hackathon with the OHBM'23 Brainhack!","text":"

We are thrilled to announce that the NiPreps Hackathon's second edition will be part of the upcoming OHBM'23 Brainhack (July 19-21, Maison Notman House, Montreal, Canada).

Registration To join us for this incredible event and work on NiPreps-related projects, please fill in our registration form.

Please remember to also register on the official webpage of the OHBM Brainhack. You will find all the necessary information, event schedule, and location details on Brainhack's website.

Approach and projects We will advance (online) some projects as much as possible before the BrainHack. We are putting together a list of potential projects at https://github.com/orgs/nipreps/projects/8. Please feel free to let us know your ideas and voice your questions. Projects can start at any moment (even at the venue in Montreal) to have the flexibility to accommodate all ideas.

Those projects with preliminary work will have project leaders who will organize meetings, coordinate a roadmap and help carry out the necessary tasks.

See you in Montreal!

"},{"location":"news/#nipreps-roundups-feb-22-2023","title":"NiPreps Roundups Feb 22, 2023","text":"

We resumed the bi-monthly NiPreps Roundups with a first meeting on February 22, 2023.

"},{"location":"users/educational/","title":"Educational resources","text":""},{"location":"users/educational/#fmriprep-bootcamp-geneva-2024","title":"fMRIPrep Bootcamp Geneva 2024","text":"
  • Welcome Home
  • Overview of the fMRI neuroimaging pipeline & fMRIPrep
  • The Brain Imaging Data Structure (BIDS)
  • BIDS Hands-on
  • Containers
  • Data and HPC
  • Apptainer in UNIGE's HPC
  • Links
"},{"location":"users/educational/#online-books","title":"Online books","text":"
  • QC-Book, member-initiated tutorial at ISMRM 2022
  • NiPreps Book, developing processing tools for dMRI, ISBI 2021
"},{"location":"users/educational/#qc-protocols-and-standard-operating-procedures","title":"QC protocols and Standard Operating Procedures","text":"
  • SOPs-cookiecutter, a template repository for version-controlled SOPs. The example template is rendered here
"},{"location":"users/educational/#presentation","title":"Presentation","text":"
  • Educational Talk at OHBM 2023 - Quality Control in fMRI studies with MRIQC and fMRIPrep
"},{"location":"users/talks/","title":"Talks and presentations","text":"
  • NiPreps @ BrainHack Seoul 2024
  • Standardizing neuroimaging workflows (Journal Club @ EPFL 2023)
  • Presentation about MRIQC for INCF 2022 (10 min)
  • NiPreps introduction, Educational Session at OHBM 2022
  • Building community workflows, BrainHack Donostia 2020
  • Building communities around reproducible workflows, Open Reproducible Neuroscience workshop 2020
  • Reproducible workflows, Think Open Rovereto Workshop 2020
"}]} \ No newline at end of file diff --git a/sitemap.xml b/sitemap.xml index af3061c..309451d 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -2,86 +2,86 @@ https://www.nipreps.org/ - 2024-09-10 + 2024-09-18 https://www.nipreps.org/apps/docker/ - 2024-09-10 + 2024-09-18 https://www.nipreps.org/apps/framework/ - 2024-09-10 + 2024-09-18 https://www.nipreps.org/apps/singularity/ - 2024-09-10 + 2024-09-18 https://www.nipreps.org/assets/ORN-Workshop/presentation/ - 2024-09-10 + 2024-09-18 https://www.nipreps.org/assets/bhd2020/presentation/ - 2024-09-10 + 2024-09-18 https://www.nipreps.org/assets/torw2020/presentation/ - 2024-09-10 + 2024-09-18 https://www.nipreps.org/community/ - 2024-09-10 + 2024-09-18 https://www.nipreps.org/community/CODE_OF_CONDUCT/ - 2024-09-10 + 2024-09-18 https://www.nipreps.org/community/CONTRIBUTING/ - 2024-09-10 + 2024-09-18 https://www.nipreps.org/community/features/ - 2024-09-10 + 2024-09-18 https://www.nipreps.org/community/licensing/ - 2024-09-10 + 2024-09-18 https://www.nipreps.org/community/members/ - 2024-09-10 + 2024-09-18 https://www.nipreps.org/devs/devenv/ - 2024-09-10 + 2024-09-18 https://www.nipreps.org/devs/releases/ - 2024-09-10 + 2024-09-18 https://www.nipreps.org/devs/versions/ - 2024-09-10 + 2024-09-18 https://www.nipreps.org/intro/nipreps/ - 2024-09-10 + 2024-09-18 https://www.nipreps.org/intro/transparency/ - 2024-09-10 + 2024-09-18 https://www.nipreps.org/news/ - 2024-09-10 + 2024-09-18 https://www.nipreps.org/users/educational/ - 2024-09-10 + 2024-09-18 https://www.nipreps.org/users/talks/ - 2024-09-10 + 2024-09-18 \ No newline at end of file diff --git a/sitemap.xml.gz b/sitemap.xml.gz index 30fe85a..2a4598f 100644 Binary files a/sitemap.xml.gz and b/sitemap.xml.gz differ diff --git a/users/educational/index.html b/users/educational/index.html index f063d69..ce1b9c4 100644 --- a/users/educational/index.html +++ b/users/educational/index.html @@ -22,7 +22,7 @@ - + @@ -941,8 +941,8 @@

fMRIPrep Bootcamp Geneva 2024Overview of the fMRI neuroimaging pipeline & fMRIPrep
  • The Brain Imaging Data Structure (BIDS)
  • BIDS Hands-on
  • -
  • Data and HPC
  • -
  • Containers
  • +
  • Containers
  • +
  • Data and HPC
  • Apptainer in UNIGE's HPC
  • Links
  • @@ -1009,7 +1009,7 @@

    Presentation - + diff --git a/users/talks/index.html b/users/talks/index.html index dba9015..d7bed0e 100644 --- a/users/talks/index.html +++ b/users/talks/index.html @@ -22,7 +22,7 @@ - + @@ -884,7 +884,7 @@

    Talks and presentations - +