diff --git a/README.md b/README.md
index 7a021c4..5fcd844 100644
--- a/README.md
+++ b/README.md
@@ -156,8 +156,11 @@ Here is a minimal browser-based example using the JavaScript API. Adapt paths, s
> 💖 **If you find FEAScript useful, please consider supporting its development through a donation:**
+
+
+
-
+
Your support helps ensure the continued development and maintenance of this project.
diff --git a/dist/feascript.cjs.js b/dist/feascript.cjs.js
index c3b486c..28463e3 100644
--- a/dist/feascript.cjs.js
+++ b/dist/feascript.cjs.js
@@ -4,5 +4,5 @@
* Copyright 2019 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
-const N=Symbol("Comlink.proxy"),w=Symbol("Comlink.endpoint"),O=Symbol("Comlink.releaseProxy"),S=Symbol("Comlink.finalizer"),X=Symbol("Comlink.thrown"),V=e=>"object"==typeof e&&null!==e||"function"==typeof e,T=new Map([["proxy",{canHandle:e=>V(e)&&e[N],serialize(e){const{port1:t,port2:n}=new MessageChannel;return k(e,t),[n,[n]]},deserialize:e=>(e.start(),R(e))}],["throw",{canHandle:e=>V(e)&&X in e,serialize({value:e}){let t;return t=e instanceof Error?{isError:!0,value:{message:e.message,name:e.name,stack:e.stack}}:{isError:!1,value:e},[t,[]]},deserialize(e){if(e.isError)throw Object.assign(new Error(e.value.message),e.value);throw e.value}}]]);function k(e,t=globalThis,n=["*"]){t.addEventListener("message",(function o(s){if(!s||!s.data)return;if(!function(e,t){for(const n of e){if(t===n||"*"===n)return!0;if(n instanceof RegExp&&n.test(t))return!0}return!1}(n,s.origin))return void console.warn(`Invalid origin '${s.origin}' for comlink proxy`);const{id:i,type:r,path:a}=Object.assign({path:[]},s.data),l=(s.data.argumentList||[]).map(J);let d;try{const t=a.slice(0,-1).reduce(((e,t)=>e[t]),e),n=a.reduce(((e,t)=>e[t]),e);switch(r){case"GET":d=n;break;case"SET":t[a.slice(-1)[0]]=J(s.data.value),d=!0;break;case"APPLY":d=n.apply(t,l);break;case"CONSTRUCT":d=function(e){return Object.assign(e,{[N]:!0})}(new n(...l));break;case"ENDPOINT":{const{port1:t,port2:n}=new MessageChannel;k(e,n),d=function(e,t){return G.set(e,t),e}(t,[t])}break;case"RELEASE":d=void 0;break;default:return}}catch(e){d={value:e,[X]:0}}Promise.resolve(d).catch((e=>({value:e,[X]:0}))).then((n=>{const[s,a]=K(n);t.postMessage(Object.assign(Object.assign({},s),{id:i}),a),"RELEASE"===r&&(t.removeEventListener("message",o),P(t),S in e&&"function"==typeof e[S]&&e[S]())})).catch((e=>{const[n,o]=K({value:new TypeError("Unserializable return value"),[X]:0});t.postMessage(Object.assign(Object.assign({},n),{id:i}),o)}))})),t.start&&t.start()}function P(e){(function(e){return"MessagePort"===e.constructor.name})(e)&&e.close()}function R(e,t){const n=new Map;return e.addEventListener("message",(function(e){const{data:t}=e;if(!t||!t.id)return;const o=n.get(t.id);if(o)try{o(t)}finally{n.delete(t.id)}})),j(e,n,[],t)}function Y(e){if(e)throw new Error("Proxy has been released and is not useable")}function I(e){return H(e,new Map,{type:"RELEASE"}).then((()=>{P(e)}))}const B=new WeakMap,q="FinalizationRegistry"in globalThis&&new FinalizationRegistry((e=>{const t=(B.get(e)||0)-1;B.set(e,t),0===t&&I(e)}));function j(e,t,n=[],o=function(){}){let s=!1;const i=new Proxy(o,{get(o,r){if(Y(s),r===O)return()=>{!function(e){q&&q.unregister(e)}(i),I(e),t.clear(),s=!0};if("then"===r){if(0===n.length)return{then:()=>i};const o=H(e,t,{type:"GET",path:n.map((e=>e.toString()))}).then(J);return o.then.bind(o)}return j(e,t,[...n,r])},set(o,i,r){Y(s);const[a,l]=K(r);return H(e,t,{type:"SET",path:[...n,i].map((e=>e.toString())),value:a},l).then(J)},apply(o,i,r){Y(s);const a=n[n.length-1];if(a===w)return H(e,t,{type:"ENDPOINT"}).then(J);if("bind"===a)return j(e,t,n.slice(0,-1));const[l,d]=W(r);return H(e,t,{type:"APPLY",path:n.map((e=>e.toString())),argumentList:l},d).then(J)},construct(o,i){Y(s);const[r,a]=W(i);return H(e,t,{type:"CONSTRUCT",path:n.map((e=>e.toString())),argumentList:r},a).then(J)}});return function(e,t){const n=(B.get(t)||0)+1;B.set(t,n),q&&q.register(e,t,e)}(i,e),i}function W(e){const t=e.map(K);return[t.map((e=>e[0])),(n=t.map((e=>e[1])),Array.prototype.concat.apply([],n))];var n}const G=new WeakMap;function K(e){for(const[t,n]of T)if(n.canHandle(e)){const[o,s]=n.serialize(e);return[{type:"HANDLER",name:t,value:o},s]}return[{type:"RAW",value:e},G.get(e)||[]]}function J(e){switch(e.type){case"HANDLER":return T.get(e.name).deserialize(e.value);case"RAW":return e.value}}function H(e,t,n,o){return new Promise((s=>{const i=new Array(4).fill(0).map((()=>Math.floor(Math.random()*Number.MAX_SAFE_INTEGER).toString(16))).join("-");t.set(i,s),e.start&&e.start(),e.postMessage(Object.assign({id:i},n),o)}))}exports.FEAScriptModel=class{constructor(){this.solverConfig=null,this.meshConfig={},this.boundaryConditions={},this.solverMethod="lusolve",this.coefficientFunctions=null,o("FEAScriptModel instance created")}setSolverConfig(e,t={}){this.solverConfig=e,t&&t.coefficientFunctions&&(this.coefficientFunctions=t.coefficientFunctions,n("Coefficient functions set")),n(`Solver config set to: ${e}`)}setMeshConfig(e){this.meshConfig=e,n(`Mesh config set with dimensions: ${e.meshDimension}`)}addBoundaryCondition(e,t){this.boundaryConditions[e]=t,n(`Boundary condition added for boundary: ${e}, type: ${t[0]}`)}setSolverMethod(e){this.solverMethod=e,n(`Solver method set to: ${e}`)}solve(){if(!this.solverConfig||!this.meshConfig||!this.boundaryConditions){const e="Solver config, mesh config, and boundary conditions must be set before solving.";throw console.error(e),new Error(e)}let e=[],t=[],r=[],a=[];o("Preparing mesh...");const u=function(e){const{meshDimension:t,numElementsX:o,numElementsY:i,maxX:r,maxY:a,elementOrder:u,parsedMesh:c}=e;let m;"1D"===t?m=new l({numElementsX:o,maxX:r,elementOrder:u,parsedMesh:c}):"2D"===t?m=new d({numElementsX:o,maxX:r,numElementsY:i,maxY:a,elementOrder:u,parsedMesh:c}):s("Mesh dimension must be either '1D' or '2D'.");const h=m.boundaryElementsProcessed?m.parsedMesh:m.generateMesh();let f,p,b=h.nodesXCoordinates,y=h.nodesYCoordinates,g=h.totalNodesX,E=h.totalNodesY,v=h.nodalNumbering,M=h.boundaryElements;return null!=c?(f=v.length,p=b.length,n(`Using parsed mesh with ${f} elements and ${p} nodes`)):(f=o*("2D"===t?i:1),p=g*("2D"===t?E:1),n(`Using mesh generated from geometry with ${f} elements and ${p} nodes`)),{nodesXCoordinates:b,nodesYCoordinates:y,totalNodesX:g,totalNodesY:E,nop:v,boundaryElements:M,totalElements:f,totalNodes:p,meshDimension:t,elementOrder:u}}(this.meshConfig);o("Mesh preparation completed");const g={nodesXCoordinates:u.nodesXCoordinates,nodesYCoordinates:u.nodesYCoordinates};if(o("Beginning solving process..."),console.time("totalSolvingTime"),"heatConductionScript"===this.solverConfig)if(o(`Using solver: ${this.solverConfig}`),"frontal"===this.solverMethod){r=$(p,u,this.boundaryConditions).solutionVector}else{({jacobianMatrix:e,residualVector:t}=function(e,t){o("Starting solid heat transfer matrix assembly...");const{nodesXCoordinates:n,nodesYCoordinates:s,nop:i,boundaryElements:r,totalElements:a,meshDimension:l,elementOrder:d}=e,u=c(e),{residualVector:p,jacobianMatrix:b,localToGlobalMap:y,basisFunctions:g,gaussPoints:E,gaussWeights:v,numNodes:M}=u;for(let e=0;e0&&(i.initialSolution=[...r]);const o=A(y,i,100,1e-4);e=o.jacobianMatrix,t=o.residualVector,r=o.solutionVector,n+=1/s}}else if("generalFormPDEScript"===this.solverConfig)if(o(`Using solver: ${this.solverConfig}`),"frontal"===this.solverMethod)s("Frontal solver is not yet supported for generalFormPDEScript. Please use 'lusolve' or 'jacobi'.");else{({jacobianMatrix:e,residualVector:t}=function(e,t,n){o("Starting general form PDE matrix assembly...");const{nodesXCoordinates:i,nodesYCoordinates:r,nop:a,boundaryElements:l,totalElements:d,meshDimension:u,elementOrder:h}=e,{A:f,B:p,C:y,D:g}=n,E=c(e),{residualVector:v,jacobianMatrix:M,localToGlobalMap:C,basisFunctions:D,gaussPoints:$,gaussWeights:F,numNodes:x}=E;if("1D"===u)for(let e=0;e{console.error("FEAScriptWorker: Worker error:",e)};const e=R(this.worker);this.feaWorker=await new e,this.isReady=!0}catch(e){throw console.error("Failed to initialize worker",e),e}}async _ensureReady(){return this.isReady?Promise.resolve():new Promise(((e,t)=>{let n=0;const o=()=>{n++,this.isReady?e():n>=50?t(new Error("Timeout waiting for worker to be ready")):setTimeout(o,1e3)};o()}))}async setSolverConfig(e){return await this._ensureReady(),o(`FEAScriptWorker: Setting solver config to: ${e}`),this.feaWorker.setSolverConfig(e)}async setMeshConfig(e){return await this._ensureReady(),o("FEAScriptWorker: Setting mesh config"),this.feaWorker.setMeshConfig(e)}async addBoundaryCondition(e,t){return await this._ensureReady(),o(`FEAScriptWorker: Adding boundary condition for boundary: ${e}`),this.feaWorker.addBoundaryCondition(e,t)}async setSolverMethod(e){return await this._ensureReady(),o(`FEAScriptWorker: Setting solver method to: ${e}`),this.feaWorker.setSolverMethod(e)}async solve(){await this._ensureReady(),o("FEAScriptWorker: Requesting solution from worker...");const e=performance.now(),t=await this.feaWorker.solve();return o(`FEAScriptWorker: Solution completed in ${((performance.now()-e)/1e3).toFixed(2)}s`),t}async getModelInfo(){return await this._ensureReady(),this.feaWorker.getModelInfo()}async ping(){return await this._ensureReady(),this.feaWorker.ping()}terminate(){this.worker&&(this.worker.terminate(),this.worker=null,this.feaWorker=null,this.isReady=!1)}},exports.importGmshQuadTri=async e=>{let t={nodesXCoordinates:[],nodesYCoordinates:[],nodalNumbering:{quadElements:[],triangleElements:[]},boundaryElements:[],boundaryConditions:[],boundaryNodePairs:{},gmshV:0,ascii:!1,fltBytes:"8",totalNodesX:0,totalNodesY:0,physicalPropMap:[],elementTypes:{}},o=(await e.text()).split("\n").map((e=>e.trim())).filter((e=>""!==e&&" "!==e)),s="",i=0,r=0,a=0,l=0,d={numNodes:0},u=0,c=[],m=0,h=0,f=0,p={dim:0,tag:0,elementType:0,numElements:0},b=0,y={};for(;i""!==e));if("meshFormat"===s)t.gmshV=parseFloat(n[0]),t.ascii="0"===n[1],t.fltBytes=n[2];else if("physicalNames"===s){if(n.length>=3){if(!/^\d+$/.test(n[0])){i++;continue}const e=parseInt(n[0],10),o=parseInt(n[1],10);let s=n.slice(2).join(" ");s=s.replace(/^"|"$/g,""),t.physicalPropMap.push({tag:o,dimension:e,name:s})}}else if("nodes"===s){if(0===r){r=parseInt(n[0],10),a=parseInt(n[1],10),t.nodesXCoordinates=new Array(a).fill(0),t.nodesYCoordinates=new Array(a).fill(0),i++;continue}if(lparseInt(e,10)));if(1===p.elementType||8===p.elementType){const n=p.tag;y[n]||(y[n]=[]),y[n].push(e),t.boundaryNodePairs[n]||(t.boundaryNodePairs[n]=[]),t.boundaryNodePairs[n].push(e)}else 2===p.elementType?t.nodalNumbering.triangleElements.push(e):(3===p.elementType||10===p.elementType)&&t.nodalNumbering.quadElements.push(e);b++,b===p.numElements&&(f++,p={numElements:0})}}i++}return t.physicalPropMap.forEach((e=>{if(1===e.dimension){const n=y[e.tag]||[];n.length>0&&t.boundaryConditions.push({name:e.name,tag:e.tag,nodes:n})}})),n(`Parsed boundary node pairs by physical tag: ${JSON.stringify(t.boundaryNodePairs)}. These pairs will be used to identify boundary elements in the mesh.`),t},exports.logSystem=function(e){"basic"!==e&&"debug"!==e?(console.log("%c[WARN] Invalid log level: "+e+". Using basic instead.","color: #FFC107; font-weight: bold;"),t="basic"):(t=e,o(`Log level set to: ${e}`))},exports.plotSolution=function(e,t,n,o,s,i,r="structured"){const{nodesXCoordinates:a,nodesYCoordinates:l}=t;if("1D"===o&&"line"===s){let t;t=e.length>0&&Array.isArray(e[0])?e.map((e=>e[0])):e;let o=Array.from(a),s={x:o,y:t,mode:"lines",type:"scatter",line:{color:"rgb(219, 64, 82)",width:2},name:"Solution"},r=Math.min(window.innerWidth,700),l=Math.max(...o),d=r/l,u={title:`line plot - ${n}`,width:Math.max(d*l,400),height:350,xaxis:{title:"x"},yaxis:{title:"Solution"},margin:{l:70,r:40,t:50,b:50}};Plotly.newPlot(i,[s],u,{responsive:!0})}else if("2D"===o&&"contour"===s){const t="structured"===r,o=new Set(a).size,d=new Set(l).size;let u;u=Array.isArray(e[0])?e.map((e=>e[0])):e;let c=Math.min(window.innerWidth,700),m=Math.max(...a),h=Math.max(...l)/m,f=Math.min(c,600),p={title:`${s} plot - ${n}`,width:f,height:f*h*.8,xaxis:{title:"x"},yaxis:{title:"y"},margin:{l:50,r:50,t:50,b:50},hovermode:"closest"};if(t){const t=o,n=d;math.reshape(Array.from(a),[t,n]);let s=math.reshape(Array.from(l),[t,n]),r=math.reshape(Array.from(e),[t,n]),u=math.transpose(r),c=[];for(let e=0;e"object"==typeof e&&null!==e||"function"==typeof e,T=new Map([["proxy",{canHandle:e=>V(e)&&e[N],serialize(e){const{port1:t,port2:n}=new MessageChannel;return k(e,t),[n,[n]]},deserialize:e=>(e.start(),R(e))}],["throw",{canHandle:e=>V(e)&&X in e,serialize({value:e}){let t;return t=e instanceof Error?{isError:!0,value:{message:e.message,name:e.name,stack:e.stack}}:{isError:!1,value:e},[t,[]]},deserialize(e){if(e.isError)throw Object.assign(new Error(e.value.message),e.value);throw e.value}}]]);function k(e,t=globalThis,n=["*"]){t.addEventListener("message",(function o(s){if(!s||!s.data)return;if(!function(e,t){for(const n of e){if(t===n||"*"===n)return!0;if(n instanceof RegExp&&n.test(t))return!0}return!1}(n,s.origin))return void console.warn(`Invalid origin '${s.origin}' for comlink proxy`);const{id:i,type:r,path:a}=Object.assign({path:[]},s.data),l=(s.data.argumentList||[]).map(J);let d;try{const t=a.slice(0,-1).reduce(((e,t)=>e[t]),e),n=a.reduce(((e,t)=>e[t]),e);switch(r){case"GET":d=n;break;case"SET":t[a.slice(-1)[0]]=J(s.data.value),d=!0;break;case"APPLY":d=n.apply(t,l);break;case"CONSTRUCT":d=function(e){return Object.assign(e,{[N]:!0})}(new n(...l));break;case"ENDPOINT":{const{port1:t,port2:n}=new MessageChannel;k(e,n),d=function(e,t){return G.set(e,t),e}(t,[t])}break;case"RELEASE":d=void 0;break;default:return}}catch(e){d={value:e,[X]:0}}Promise.resolve(d).catch((e=>({value:e,[X]:0}))).then((n=>{const[s,a]=K(n);t.postMessage(Object.assign(Object.assign({},s),{id:i}),a),"RELEASE"===r&&(t.removeEventListener("message",o),P(t),S in e&&"function"==typeof e[S]&&e[S]())})).catch((e=>{const[n,o]=K({value:new TypeError("Unserializable return value"),[X]:0});t.postMessage(Object.assign(Object.assign({},n),{id:i}),o)}))})),t.start&&t.start()}function P(e){(function(e){return"MessagePort"===e.constructor.name})(e)&&e.close()}function R(e,t){const n=new Map;return e.addEventListener("message",(function(e){const{data:t}=e;if(!t||!t.id)return;const o=n.get(t.id);if(o)try{o(t)}finally{n.delete(t.id)}})),j(e,n,[],t)}function Y(e){if(e)throw new Error("Proxy has been released and is not useable")}function I(e){return H(e,new Map,{type:"RELEASE"}).then((()=>{P(e)}))}const B=new WeakMap,q="FinalizationRegistry"in globalThis&&new FinalizationRegistry((e=>{const t=(B.get(e)||0)-1;B.set(e,t),0===t&&I(e)}));function j(e,t,n=[],o=function(){}){let s=!1;const i=new Proxy(o,{get(o,r){if(Y(s),r===O)return()=>{!function(e){q&&q.unregister(e)}(i),I(e),t.clear(),s=!0};if("then"===r){if(0===n.length)return{then:()=>i};const o=H(e,t,{type:"GET",path:n.map((e=>e.toString()))}).then(J);return o.then.bind(o)}return j(e,t,[...n,r])},set(o,i,r){Y(s);const[a,l]=K(r);return H(e,t,{type:"SET",path:[...n,i].map((e=>e.toString())),value:a},l).then(J)},apply(o,i,r){Y(s);const a=n[n.length-1];if(a===w)return H(e,t,{type:"ENDPOINT"}).then(J);if("bind"===a)return j(e,t,n.slice(0,-1));const[l,d]=W(r);return H(e,t,{type:"APPLY",path:n.map((e=>e.toString())),argumentList:l},d).then(J)},construct(o,i){Y(s);const[r,a]=W(i);return H(e,t,{type:"CONSTRUCT",path:n.map((e=>e.toString())),argumentList:r},a).then(J)}});return function(e,t){const n=(B.get(t)||0)+1;B.set(t,n),q&&q.register(e,t,e)}(i,e),i}function W(e){const t=e.map(K);return[t.map((e=>e[0])),(n=t.map((e=>e[1])),Array.prototype.concat.apply([],n))];var n}const G=new WeakMap;function K(e){for(const[t,n]of T)if(n.canHandle(e)){const[o,s]=n.serialize(e);return[{type:"HANDLER",name:t,value:o},s]}return[{type:"RAW",value:e},G.get(e)||[]]}function J(e){switch(e.type){case"HANDLER":return T.get(e.name).deserialize(e.value);case"RAW":return e.value}}function H(e,t,n,o){return new Promise((s=>{const i=new Array(4).fill(0).map((()=>Math.floor(Math.random()*Number.MAX_SAFE_INTEGER).toString(16))).join("-");t.set(i,s),e.start&&e.start(),e.postMessage(Object.assign({id:i},n),o)}))}exports.FEAScriptModel=class{constructor(){this.solverConfig=null,this.meshConfig={},this.boundaryConditions={},this.solverMethod="lusolve",this.coefficientFunctions=null,o("FEAScriptModel instance created")}setSolverConfig(e,t={}){this.solverConfig=e,t&&t.coefficientFunctions&&(this.coefficientFunctions=t.coefficientFunctions,n("Coefficient functions set")),n(`Solver config set to: ${e}`)}setMeshConfig(e){this.meshConfig=e,n(`Mesh config set with dimensions: ${e.meshDimension}`)}addBoundaryCondition(e,t){this.boundaryConditions[e]=t,n(`Boundary condition added for boundary: ${e}, type: ${t[0]}`)}setSolverMethod(e){this.solverMethod=e,n(`Solver method set to: ${e}`)}solve(){if(!this.solverConfig||!this.meshConfig||!this.boundaryConditions){const e="Solver config, mesh config, and boundary conditions must be set before solving.";throw console.error(e),new Error(e)}let e=[],t=[],r=[],a=[];o("Preparing mesh...");const u=function(e){const{meshDimension:t,numElementsX:o,numElementsY:i,maxX:r,maxY:a,elementOrder:u,parsedMesh:c}=e;let m;"1D"===t?m=new l({numElementsX:o,maxX:r,elementOrder:u,parsedMesh:c}):"2D"===t?m=new d({numElementsX:o,maxX:r,numElementsY:i,maxY:a,elementOrder:u,parsedMesh:c}):s("Mesh dimension must be either '1D' or '2D'.");const h=m.boundaryElementsProcessed?m.parsedMesh:m.generateMesh();let f,p,b=h.nodesXCoordinates,y=h.nodesYCoordinates,g=h.totalNodesX,E=h.totalNodesY,v=h.nodalNumbering,M=h.boundaryElements;return null!=c?(f=v.length,p=b.length,n(`Using parsed mesh with ${f} elements and ${p} nodes`)):(f=o*("2D"===t?i:1),p=g*("2D"===t?E:1),n(`Using mesh generated from geometry with ${f} elements and ${p} nodes`)),{nodesXCoordinates:b,nodesYCoordinates:y,totalNodesX:g,totalNodesY:E,nop:v,boundaryElements:M,totalElements:f,totalNodes:p,meshDimension:t,elementOrder:u}}(this.meshConfig);o("Mesh preparation completed");const g={nodesXCoordinates:u.nodesXCoordinates,nodesYCoordinates:u.nodesYCoordinates};if(o("Beginning solving process..."),console.time("totalSolvingTime"),"heatConductionScript"===this.solverConfig)if(o(`Using solver: ${this.solverConfig}`),"frontal"===this.solverMethod){r=$(p,u,this.boundaryConditions).solutionVector}else{({jacobianMatrix:e,residualVector:t}=function(e,t){o("Starting solid heat transfer matrix assembly...");const{nodesXCoordinates:n,nodesYCoordinates:s,nop:i,boundaryElements:r,totalElements:a,meshDimension:l,elementOrder:d}=e,u=c(e),{residualVector:p,jacobianMatrix:b,localToGlobalMap:y,basisFunctions:g,gaussPoints:E,gaussWeights:v,numNodes:M}=u;for(let e=0;e0&&(i.initialSolution=[...r]);const o=A(y,i,100,1e-4);e=o.jacobianMatrix,t=o.residualVector,r=o.solutionVector,n+=1/s}}else if("generalFormPDEScript"===this.solverConfig)if(o(`Using solver: ${this.solverConfig}`),"frontal"===this.solverMethod)s("Frontal solver is not yet supported for generalFormPDEScript. Please use 'lusolve' or 'jacobi'.");else{({jacobianMatrix:e,residualVector:t}=function(e,t,n){o("Starting general form PDE matrix assembly...");const{nodesXCoordinates:i,nodesYCoordinates:r,nop:a,boundaryElements:l,totalElements:d,meshDimension:u,elementOrder:h}=e,{A:f,B:p,C:y,D:g}=n,E=c(e),{residualVector:v,jacobianMatrix:M,localToGlobalMap:C,basisFunctions:D,gaussPoints:$,gaussWeights:F,numNodes:x}=E;if("1D"===u)for(let e=0;e{console.error("FEAScriptWorker: Worker error:",e)};const e=R(this.worker);this.feaWorker=await new e,this.isReady=!0}catch(e){throw console.error("Failed to initialize worker",e),e}}async _ensureReady(){return this.isReady?Promise.resolve():new Promise(((e,t)=>{let n=0;const o=()=>{n++,this.isReady?e():n>=50?t(new Error("Timeout waiting for worker to be ready")):setTimeout(o,1e3)};o()}))}async setSolverConfig(e){return await this._ensureReady(),o(`FEAScriptWorker: Setting solver config to: ${e}`),this.feaWorker.setSolverConfig(e)}async setMeshConfig(e){return await this._ensureReady(),o("FEAScriptWorker: Setting mesh config"),this.feaWorker.setMeshConfig(e)}async addBoundaryCondition(e,t){return await this._ensureReady(),o(`FEAScriptWorker: Adding boundary condition for boundary: ${e}`),this.feaWorker.addBoundaryCondition(e,t)}async setSolverMethod(e){return await this._ensureReady(),o(`FEAScriptWorker: Setting solver method to: ${e}`),this.feaWorker.setSolverMethod(e)}async solve(){await this._ensureReady(),o("FEAScriptWorker: Requesting solution from worker...");const e=performance.now(),t=await this.feaWorker.solve();return o(`FEAScriptWorker: Solution completed in ${((performance.now()-e)/1e3).toFixed(2)}s`),t}async getModelInfo(){return await this._ensureReady(),this.feaWorker.getModelInfo()}async ping(){return await this._ensureReady(),this.feaWorker.ping()}terminate(){this.worker&&(this.worker.terminate(),this.worker=null,this.feaWorker=null,this.isReady=!1)}},exports.importGmshQuadTri=async e=>{let t={nodesXCoordinates:[],nodesYCoordinates:[],nodalNumbering:{quadElements:[],triangleElements:[]},boundaryElements:[],boundaryConditions:[],boundaryNodePairs:{},gmshV:0,ascii:!1,fltBytes:"8",totalNodesX:0,totalNodesY:0,physicalPropMap:[],elementTypes:{}},o=(await e.text()).split("\n").map((e=>e.trim())).filter((e=>""!==e&&" "!==e)),s="",i=0,r=0,a=0,l=0,d={numNodes:0},u=0,c=[],m=0,h=0,f=0,p={dim:0,tag:0,elementType:0,numElements:0},b=0,y={};for(;i""!==e));if("meshFormat"===s)t.gmshV=parseFloat(n[0]),t.ascii="0"===n[1],t.fltBytes=n[2];else if("physicalNames"===s){if(n.length>=3){if(!/^\d+$/.test(n[0])){i++;continue}const e=parseInt(n[0],10),o=parseInt(n[1],10);let s=n.slice(2).join(" ");s=s.replace(/^"|"$/g,""),t.physicalPropMap.push({tag:o,dimension:e,name:s})}}else if("nodes"===s){if(0===r){r=parseInt(n[0],10),a=parseInt(n[1],10),t.nodesXCoordinates=new Array(a).fill(0),t.nodesYCoordinates=new Array(a).fill(0),i++;continue}if(lparseInt(e,10)));if(1===p.elementType||8===p.elementType){const n=p.tag;y[n]||(y[n]=[]),y[n].push(e),t.boundaryNodePairs[n]||(t.boundaryNodePairs[n]=[]),t.boundaryNodePairs[n].push(e)}else 2===p.elementType?t.nodalNumbering.triangleElements.push(e):(3===p.elementType||10===p.elementType)&&t.nodalNumbering.quadElements.push(e);b++,b===p.numElements&&(f++,p={numElements:0})}}i++}return t.physicalPropMap.forEach((e=>{if(1===e.dimension){const n=y[e.tag]||[];n.length>0&&t.boundaryConditions.push({name:e.name,tag:e.tag,nodes:n})}})),n(`Parsed boundary node pairs by physical tag: ${JSON.stringify(t.boundaryNodePairs)}. These pairs will be used to identify boundary elements in the mesh.`),t},exports.logSystem=function(e){"basic"!==e&&"debug"!==e?(console.log("%c[WARN] Invalid log level: "+e+". Using basic instead.","color: #FFC107; font-weight: bold;"),t="basic"):(t=e,o(`Log level set to: ${e}`))},exports.plotSolution=function(e,t,n,o,s,i,r="structured"){const{nodesXCoordinates:a,nodesYCoordinates:l}=t;if("1D"===o&&"line"===s){let t;t=e.length>0&&Array.isArray(e[0])?e.map((e=>e[0])):e;let o=Array.from(a),s={x:o,y:t,mode:"lines",type:"scatter",line:{color:"rgb(219, 64, 82)",width:2},name:"Solution"},r=Math.min(window.innerWidth,700),l=Math.max(...o),d=r/l,u={title:`line plot - ${n}`,width:Math.max(d*l,400),height:350,xaxis:{title:"x"},yaxis:{title:"Solution"},margin:{l:70,r:40,t:50,b:50}};Plotly.newPlot(i,[s],u,{responsive:!0})}else if("2D"===o&&"contour"===s){const t="structured"===r,o=new Set(a).size,d=new Set(l).size;let u;u=Array.isArray(e[0])?e.map((e=>e[0])):e;let c=Math.min(window.innerWidth,700),m=Math.max(...a),h=Math.max(...l)/m,f=Math.min(c,600),p={title:`${s} plot - ${n}`,width:f,height:f*h*.8,xaxis:{title:"x"},yaxis:{title:"y"},margin:{l:50,r:50,t:50,b:50},hovermode:"closest"};if(t){const t=o,n=d;math.reshape(Array.from(a),[t,n]);let s=math.reshape(Array.from(l),[t,n]),r=math.reshape(Array.from(e),[t,n]),u=math.transpose(r),c=[];for(let e=0;e | |\n // 0 --- 1 0 --- 2\n\n feaScriptNodes[0] = gmshNodes[0]; // 0 -> 0\n feaScriptNodes[1] = gmshNodes[3]; // 3 -> 1\n feaScriptNodes[2] = gmshNodes[1]; // 1 -> 2\n feaScriptNodes[3] = gmshNodes[2]; // 2 -> 3\n } else if (gmshNodes.length === 9) {\n // Mapping for quadratic quad elements (9 nodes)\n // GMSH: FEAScript:\n // 3--6--2 2--5--8\n // | | | |\n // 7 8 5 --> 1 4 7\n // | | | |\n // 0--4--1 0--3--6\n\n feaScriptNodes[0] = gmshNodes[0]; // 0 -> 0\n feaScriptNodes[1] = gmshNodes[7]; // 7 -> 1\n feaScriptNodes[2] = gmshNodes[3]; // 3 -> 2\n feaScriptNodes[3] = gmshNodes[4]; // 4 -> 3\n feaScriptNodes[4] = gmshNodes[8]; // 8 -> 4\n feaScriptNodes[5] = gmshNodes[6]; // 6 -> 5\n feaScriptNodes[6] = gmshNodes[1]; // 1 -> 6\n feaScriptNodes[7] = gmshNodes[5]; // 5 -> 7\n feaScriptNodes[8] = gmshNodes[2]; // 2 -> 8\n }\n\n mappedNodalNumbering.push(feaScriptNodes);\n }\n\n this.parsedMesh.nodalNumbering = mappedNodalNumbering;\n } else if (this.parsedMesh.elementTypes[2]) {\n errorLog(\"Element type is neither triangle nor quad; mapping for this type is not implemented yet.\");\n }\n\n debugLog(\n \"Nodal numbering after mapping from GMSH to FEAScript format: \" +\n JSON.stringify(this.parsedMesh.nodalNumbering)\n );\n\n // Process boundary elements if they exist and if physical property mapping exists\n if (this.parsedMesh.physicalPropMap && this.parsedMesh.boundaryElements) {\n // Check if boundary elements need to be processed\n if (\n Array.isArray(this.parsedMesh.boundaryElements) &&\n this.parsedMesh.boundaryElements.length > 0 &&\n this.parsedMesh.boundaryElements[0] === undefined\n ) {\n // Create a new array without the empty first element\n const fixedBoundaryElements = [];\n for (let i = 1; i < this.parsedMesh.boundaryElements.length; i++) {\n if (this.parsedMesh.boundaryElements[i]) {\n fixedBoundaryElements.push(this.parsedMesh.boundaryElements[i]);\n }\n }\n this.parsedMesh.boundaryElements = fixedBoundaryElements;\n }\n\n // If boundary node pairs exist but boundary elements haven't been processed\n if (this.parsedMesh.boundaryNodePairs && !this.parsedMesh.boundaryElementsProcessed) {\n // Reset boundary elements array\n this.parsedMesh.boundaryElements = [];\n\n // Process each physical property from the Gmsh file\n this.parsedMesh.physicalPropMap.forEach((prop) => {\n // Only process 1D physical entities (boundary lines)\n if (prop.dimension === 1) {\n // Get all node pairs for this boundary\n const boundaryNodePairs = this.parsedMesh.boundaryNodePairs[prop.tag] || [];\n\n if (boundaryNodePairs.length > 0) {\n // Initialize array for this boundary tag\n if (!this.parsedMesh.boundaryElements[prop.tag]) {\n this.parsedMesh.boundaryElements[prop.tag] = [];\n }\n\n // For each boundary line segment (defined by a pair of nodes)\n boundaryNodePairs.forEach((nodesPair) => {\n const node1 = nodesPair[0]; // First node in the pair\n const node2 = nodesPair[1]; // Second node in the pair\n\n debugLog(\n `Processing boundary node pair: [${node1}, ${node2}] for boundary ${prop.tag} (${\n prop.name || \"unnamed\"\n })`\n );\n\n // Search through all elements to find which one contains both nodes\n let foundElement = false;\n\n // Loop through all elements in the mesh\n for (let elemIdx = 0; elemIdx < this.parsedMesh.nodalNumbering.length; elemIdx++) {\n const elemNodes = this.parsedMesh.nodalNumbering[elemIdx];\n\n // For linear quadrilateral linear elements (4 nodes)\n if (elemNodes.length === 4) {\n // Check if both boundary nodes are in this element\n if (elemNodes.includes(node1) && elemNodes.includes(node2)) {\n // Find which side of the element these nodes form\n let side;\n\n const node1Index = elemNodes.indexOf(node1);\n const node2Index = elemNodes.indexOf(node2);\n\n debugLog(\n ` Found element ${elemIdx} containing boundary nodes. Element nodes: [${elemNodes.join(\n \", \"\n )}]`\n );\n debugLog(\n ` Node ${node1} is at index ${node1Index}, Node ${node2} is at index ${node2Index} in the element`\n );\n\n // Based on FEAScript linear quadrilateral numbering:\n // 1 --- 3\n // | |\n // 0 --- 2\n\n if (\n (node1Index === 0 && node2Index === 2) ||\n (node1Index === 2 && node2Index === 0)\n ) {\n side = 0; // Bottom side\n debugLog(` These nodes form the BOTTOM side (${side}) of element ${elemIdx}`);\n } else if (\n (node1Index === 0 && node2Index === 1) ||\n (node1Index === 1 && node2Index === 0)\n ) {\n side = 1; // Left side\n debugLog(` These nodes form the LEFT side (${side}) of element ${elemIdx}`);\n } else if (\n (node1Index === 1 && node2Index === 3) ||\n (node1Index === 3 && node2Index === 1)\n ) {\n side = 2; // Top side\n debugLog(` These nodes form the TOP side (${side}) of element ${elemIdx}`);\n } else if (\n (node1Index === 2 && node2Index === 3) ||\n (node1Index === 3 && node2Index === 2)\n ) {\n side = 3; // Right side\n debugLog(` These nodes form the RIGHT side (${side}) of element ${elemIdx}`);\n }\n\n // Add the element and side to the boundary elements array\n this.parsedMesh.boundaryElements[prop.tag].push([elemIdx, side]);\n debugLog(\n ` Added element-side pair [${elemIdx}, ${side}] to boundary tag ${prop.tag}`\n );\n foundElement = true;\n break;\n }\n } else if (elemNodes.length === 9) {\n // For quadratic quadrilateral elements (9 nodes)\n // Check if both boundary nodes are in this element\n if (elemNodes.includes(node1) && elemNodes.includes(node2)) {\n // Find which side of the element these nodes form\n let side;\n\n const node1Index = elemNodes.indexOf(node1);\n const node2Index = elemNodes.indexOf(node2);\n\n debugLog(\n ` Found element ${elemIdx} containing boundary nodes. Element nodes: [${elemNodes.join(\n \", \"\n )}]`\n );\n debugLog(\n ` Node ${node1} is at index ${node1Index}, Node ${node2} is at index ${node2Index} in the element`\n );\n\n // Based on FEAScript quadratic quadrilateral numbering:\n // 2--5--8\n // | |\n // 1 4 7\n // | |\n // 0--3--6\n\n // TODO: Transform into dictionaries for better readability\n if (\n (node1Index === 0 && node2Index === 6) ||\n (node1Index === 6 && node2Index === 0) ||\n (node1Index === 0 && node2Index === 3) ||\n (node1Index === 3 && node2Index === 0) ||\n (node1Index === 3 && node2Index === 6) ||\n (node1Index === 6 && node2Index === 3)\n ) {\n side = 0; // Bottom side (nodes 0, 3, 6)\n debugLog(` These nodes form the BOTTOM side (${side}) of element ${elemIdx}`);\n } else if (\n (node1Index === 0 && node2Index === 2) ||\n (node1Index === 2 && node2Index === 0) ||\n (node1Index === 0 && node2Index === 1) ||\n (node1Index === 1 && node2Index === 0) ||\n (node1Index === 1 && node2Index === 2) ||\n (node1Index === 2 && node2Index === 1)\n ) {\n side = 1; // Left side (nodes 0, 1, 2)\n debugLog(` These nodes form the LEFT side (${side}) of element ${elemIdx}`);\n } else if (\n (node1Index === 2 && node2Index === 8) ||\n (node1Index === 8 && node2Index === 2) ||\n (node1Index === 2 && node2Index === 5) ||\n (node1Index === 5 && node2Index === 2) ||\n (node1Index === 5 && node2Index === 8) ||\n (node1Index === 8 && node2Index === 5)\n ) {\n side = 2; // Top side (nodes 2, 5, 8)\n debugLog(` These nodes form the TOP side (${side}) of element ${elemIdx}`);\n } else if (\n (node1Index === 6 && node2Index === 8) ||\n (node1Index === 8 && node2Index === 6) ||\n (node1Index === 6 && node2Index === 7) ||\n (node1Index === 7 && node2Index === 6) ||\n (node1Index === 7 && node2Index === 8) ||\n (node1Index === 8 && node2Index === 7)\n ) {\n side = 3; // Right side (nodes 6, 7, 8)\n debugLog(` These nodes form the RIGHT side (${side}) of element ${elemIdx}`);\n }\n\n // Add the element and side to the boundary elements array\n this.parsedMesh.boundaryElements[prop.tag].push([elemIdx, side]);\n debugLog(\n ` Added element-side pair [${elemIdx}, ${side}] to boundary tag ${prop.tag}`\n );\n foundElement = true;\n break;\n }\n }\n }\n\n if (!foundElement) {\n errorLog(\n `Could not find element containing boundary nodes ${node1} and ${node2}. Boundary may be incomplete.`\n );\n }\n });\n }\n }\n });\n\n // Mark as processed\n this.boundaryElementsProcessed = true;\n\n // Fix boundary elements array - remove undefined entries\n if (\n this.parsedMesh.boundaryElements.length > 0 &&\n this.parsedMesh.boundaryElements[0] === undefined\n ) {\n const fixedBoundaryElements = [];\n for (let i = 1; i < this.parsedMesh.boundaryElements.length; i++) {\n if (this.parsedMesh.boundaryElements[i]) {\n fixedBoundaryElements.push(this.parsedMesh.boundaryElements[i]);\n }\n }\n this.parsedMesh.boundaryElements = fixedBoundaryElements;\n }\n }\n }\n }\n\n return this.parsedMesh;\n }\n}\n\nexport class Mesh1D extends Mesh {\n /**\n * Constructor to initialize the 1D mesh\n * @param {object} config - Configuration object for the 1D mesh\n * @param {number} [config.numElementsX] - Number of elements along the x-axis (required for geometry-based mesh)\n * @param {number} [config.maxX] - Maximum x-coordinate of the mesh (required for geometry-based mesh)\n * @param {string} [config.elementOrder='linear'] - The order of elements, either 'linear' or 'quadratic'\n * @param {object} [config.parsedMesh=null] - Optional pre-parsed mesh data\n */\n constructor({ numElementsX = null, maxX = null, elementOrder = \"linear\", parsedMesh = null }) {\n super({\n numElementsX,\n maxX,\n numElementsY: 1,\n maxY: 0,\n meshDimension: \"1D\",\n elementOrder,\n parsedMesh,\n });\n\n if (this.numElementsX === null || this.maxX === null) {\n errorLog(\"numElementsX and maxX are required parameters when generating a 1D mesh from geometry\");\n }\n }\n\n generateMesh() {\n let nodesXCoordinates = [];\n const xStart = 0;\n let totalNodesX, deltaX;\n\n if (this.elementOrder === \"linear\") {\n totalNodesX = this.numElementsX + 1;\n deltaX = (this.maxX - xStart) / this.numElementsX;\n\n nodesXCoordinates[0] = xStart;\n for (let nodeIndex = 1; nodeIndex < totalNodesX; nodeIndex++) {\n nodesXCoordinates[nodeIndex] = nodesXCoordinates[nodeIndex - 1] + deltaX;\n }\n } else if (this.elementOrder === \"quadratic\") {\n totalNodesX = 2 * this.numElementsX + 1;\n deltaX = (this.maxX - xStart) / this.numElementsX;\n\n nodesXCoordinates[0] = xStart;\n for (let nodeIndex = 1; nodeIndex < totalNodesX; nodeIndex++) {\n nodesXCoordinates[nodeIndex] = nodesXCoordinates[nodeIndex - 1] + deltaX / 2;\n }\n }\n // Generate nodal numbering (NOP) array\n const nodalNumbering = this.generate1DNodalNumbering(this.numElementsX, totalNodesX, this.elementOrder);\n // Find boundary elements\n const boundaryElements = this.findBoundaryElements();\n\n debugLog(\"Generated node X coordinates: \" + JSON.stringify(nodesXCoordinates));\n\n // Return x coordinates of nodes, total nodes, NOP array, and boundary elements\n return {\n nodesXCoordinates,\n totalNodesX,\n nodalNumbering,\n boundaryElements,\n };\n }\n\n /**\n * Function to generate the nodal numbering (NOP) array for a structured mesh\n * This array represents the connectivity between elements and their corresponding nodes\n * @param {number} numElementsX - Number of elements along the x-axis\n * @param {number} totalNodesX - Total number of nodes along the x-axis\n * @param {string} elementOrder - The order of elements, either 'linear' or 'quadratic'\n * @returns {array} NOP - A two-dimensional array which represents the element-to-node connectivity for the entire mesh\n */\n generate1DNodalNumbering(numElementsX, totalNodesX, elementOrder) {\n // TODO: The totalNodesX is not used in the original function. Verify if\n // there is a multiple calculation on the totalNodes.\n\n let elementIndex = 0;\n let nop = [];\n\n if (elementOrder === \"linear\") {\n /**\n * Linear 1D elements with the following nodes representation:\n *\n * 1 --- 2\n *\n */\n for (let elementIndex = 0; elementIndex < numElementsX; elementIndex++) {\n nop[elementIndex] = [];\n for (let nodeIndex = 1; nodeIndex <= 2; nodeIndex++) {\n nop[elementIndex][nodeIndex - 1] = elementIndex + nodeIndex;\n }\n }\n } else if (elementOrder === \"quadratic\") {\n /**\n * Quadratic 1D elements with the following nodes representation:\n *\n * 1--2--3\n *\n */\n let columnCounter = 0;\n for (let elementIndex = 0; elementIndex < numElementsX; elementIndex++) {\n nop[elementIndex] = [];\n for (let nodeIndex = 1; nodeIndex <= 3; nodeIndex++) {\n nop[elementIndex][nodeIndex - 1] = elementIndex + nodeIndex + columnCounter;\n }\n columnCounter += 1;\n }\n }\n\n return nop;\n }\n\n /**\n * Function to find the elements that belong to each boundary of a domain\n * @returns {array} An array containing arrays of elements and their adjacent boundary side for each boundary\n * Each element in the array is of the form [elementIndex, side], where 'side' indicates which side\n * of the reference element is in contact with the physical boundary:\n *\n * For 1D domains (line segments):\n * 0 - Left node of reference element (maps to physical left endpoint)\n * 1 - Right node of reference element (maps to physical right endpoint)\n */\n findBoundaryElements() {\n const boundaryElements = [];\n const maxSides = 2; // For 1D, we only have two sides (left and right)\n for (let sideIndex = 0; sideIndex < maxSides; sideIndex++) {\n boundaryElements.push([]);\n }\n\n // Left boundary (element 0, side 0)\n boundaryElements[0].push([0, 0]);\n\n // Right boundary (last element, side 1)\n boundaryElements[1].push([this.numElementsX - 1, 1]);\n\n debugLog(\"Identified boundary elements by side: \" + JSON.stringify(boundaryElements));\n this.boundaryElementsProcessed = true;\n return boundaryElements;\n }\n}\n\nexport class Mesh2D extends Mesh {\n /**\n * Constructor to initialize the 2D mesh\n * @param {object} config - Configuration object for the 2D mesh\n * @param {number} [config.numElementsX] - Number of elements along the x-axis (required for geometry-based mesh)\n * @param {number} [config.maxX] - Maximum x-coordinate of the mesh (required for geometry-based mesh)\n * @param {number} [config.numElementsY] - Number of elements along the y-axis (required for geometry-based mesh)\n * @param {number} [config.maxY] - Maximum y-coordinate of the mesh (required for geometry-based mesh)\n * @param {string} [config.elementOrder='linear'] - The order of elements, either 'linear' or 'quadratic'\n * @param {object} [config.parsedMesh=null] - Optional pre-parsed mesh data\n */\n constructor({\n numElementsX = null,\n maxX = null,\n numElementsY = null,\n maxY = null,\n elementOrder = \"linear\",\n parsedMesh = null,\n }) {\n super({\n numElementsX,\n maxX,\n numElementsY,\n maxY,\n meshDimension: \"2D\",\n elementOrder,\n parsedMesh,\n });\n\n // Validate geometry parameters (when not using a parsed mesh)\n if (\n !parsedMesh &&\n (this.numElementsX === null || this.maxX === null || this.numElementsY === null || this.maxY === null)\n ) {\n errorLog(\n \"numElementsX, maxX, numElementsY, and maxY are required parameters when generating a 2D mesh from geometry\"\n );\n }\n }\n\n generateMesh() {\n let nodesXCoordinates = [];\n let nodesYCoordinates = [];\n const xStart = 0;\n const yStart = 0;\n let totalNodesX, totalNodesY, deltaX, deltaY;\n\n if (this.elementOrder === \"linear\") {\n totalNodesX = this.numElementsX + 1;\n totalNodesY = this.numElementsY + 1;\n deltaX = (this.maxX - xStart) / this.numElementsX;\n deltaY = (this.maxY - yStart) / this.numElementsY;\n\n nodesXCoordinates[0] = xStart;\n nodesYCoordinates[0] = yStart;\n for (let nodeIndexY = 1; nodeIndexY < totalNodesY; nodeIndexY++) {\n nodesXCoordinates[nodeIndexY] = nodesXCoordinates[0];\n nodesYCoordinates[nodeIndexY] = nodesYCoordinates[0] + nodeIndexY * deltaY;\n }\n for (let nodeIndexX = 1; nodeIndexX < totalNodesX; nodeIndexX++) {\n const nnode = nodeIndexX * totalNodesY;\n nodesXCoordinates[nnode] = nodesXCoordinates[0] + nodeIndexX * deltaX;\n nodesYCoordinates[nnode] = nodesYCoordinates[0];\n for (let nodeIndexY = 1; nodeIndexY < totalNodesY; nodeIndexY++) {\n nodesXCoordinates[nnode + nodeIndexY] = nodesXCoordinates[nnode];\n nodesYCoordinates[nnode + nodeIndexY] = nodesYCoordinates[nnode] + nodeIndexY * deltaY;\n }\n }\n } else if (this.elementOrder === \"quadratic\") {\n totalNodesX = 2 * this.numElementsX + 1;\n totalNodesY = 2 * this.numElementsY + 1;\n deltaX = (this.maxX - xStart) / this.numElementsX;\n deltaY = (this.maxY - yStart) / this.numElementsY;\n\n nodesXCoordinates[0] = xStart;\n nodesYCoordinates[0] = yStart;\n for (let nodeIndexY = 1; nodeIndexY < totalNodesY; nodeIndexY++) {\n nodesXCoordinates[nodeIndexY] = nodesXCoordinates[0];\n nodesYCoordinates[nodeIndexY] = nodesYCoordinates[0] + (nodeIndexY * deltaY) / 2;\n }\n for (let nodeIndexX = 1; nodeIndexX < totalNodesX; nodeIndexX++) {\n const nnode = nodeIndexX * totalNodesY;\n nodesXCoordinates[nnode] = nodesXCoordinates[0] + (nodeIndexX * deltaX) / 2;\n nodesYCoordinates[nnode] = nodesYCoordinates[0];\n for (let nodeIndexY = 1; nodeIndexY < totalNodesY; nodeIndexY++) {\n nodesXCoordinates[nnode + nodeIndexY] = nodesXCoordinates[nnode];\n nodesYCoordinates[nnode + nodeIndexY] = nodesYCoordinates[nnode] + (nodeIndexY * deltaY) / 2;\n }\n }\n }\n\n // Generate nodal numbering (NOP) array\n const nodalNumbering = this.generate2DNodalNumbering(\n this.numElementsX,\n this.numElementsY,\n totalNodesY,\n this.elementOrder\n );\n\n // Find boundary elements\n const boundaryElements = this.findBoundaryElements();\n\n debugLog(\"Generated node X coordinates: \" + JSON.stringify(nodesXCoordinates));\n debugLog(\"Generated node Y coordinates: \" + JSON.stringify(nodesYCoordinates));\n\n // Return statement\n return {\n nodesXCoordinates,\n nodesYCoordinates,\n totalNodesX,\n totalNodesY,\n nodalNumbering,\n boundaryElements,\n };\n }\n\n /**\n * Function to generate the nodal numbering (NOP) array for a structured mesh\n * This array represents the connectivity between elements and their corresponding nodes\n * @param {number} numElementsX - Number of elements along the x-axis\n * @param {number} [numElementsY] - Number of elements along the y-axis (optional for 1D)\n * @param {number} totalNodesX - Total number of nodes along the x-axis\n * @param {number} [totalNodesY] - Total number of nodes along the y-axis (optional for 1D)\n * @param {string} elementOrder - The order of elements, either 'linear' or 'quadratic'\n * @returns {array} NOP - A two-dimensional array which represents the element-to-node connectivity for the entire mesh\n */\n generate2DNodalNumbering(numElementsX, numElementsY, totalNodesY, elementOrder) {\n let elementIndex = 0;\n let nop = [];\n\n if (elementOrder === \"linear\") {\n /**\n * Linear rectangular elements with the following nodes representation:\n *\n * 1 --- 3\n * | |\n * 0 --- 2\n *\n */\n let rowCounter = 0;\n let columnCounter = 2;\n for (let elementIndex = 0; elementIndex < numElementsX * numElementsY; elementIndex++) {\n rowCounter += 1;\n nop[elementIndex] = [];\n nop[elementIndex][0] = elementIndex + columnCounter - 1;\n nop[elementIndex][1] = elementIndex + columnCounter;\n nop[elementIndex][2] = elementIndex + columnCounter + numElementsY;\n nop[elementIndex][3] = elementIndex + columnCounter + numElementsY + 1;\n if (rowCounter === numElementsY) {\n columnCounter += 1;\n rowCounter = 0;\n }\n }\n } else if (elementOrder === \"quadratic\") {\n /**\n * Quadratic rectangular elements with the following nodes representation:\n *\n * 2--5--8\n * | |\n * 1 4 7\n * | |\n * 0--3--6\n *\n */\n for (let elementIndexX = 1; elementIndexX <= numElementsX; elementIndexX++) {\n for (let elementIndexY = 1; elementIndexY <= numElementsY; elementIndexY++) {\n nop[elementIndex] = [];\n for (let nodeIndex1 = 1; nodeIndex1 <= 3; nodeIndex1++) {\n let nodeIndex2 = 3 * nodeIndex1 - 2;\n nop[elementIndex][nodeIndex2 - 1] =\n totalNodesY * (2 * elementIndexX + nodeIndex1 - 3) + 2 * elementIndexY - 1;\n nop[elementIndex][nodeIndex2] = nop[elementIndex][nodeIndex2 - 1] + 1;\n nop[elementIndex][nodeIndex2 + 1] = nop[elementIndex][nodeIndex2 - 1] + 2;\n }\n elementIndex = elementIndex + 1;\n }\n }\n }\n\n return nop;\n }\n\n /**\n * Function to find the elements that belong to each boundary of a domain\n * @returns {array} An array containing arrays of elements and their adjacent boundary side for each boundary\n * Each element in the array is of the form [elementIndex, side], where 'side' indicates which side\n * of the reference element is in contact with the physical boundary:\n *\n * For 2D domains (rectangular):\n * 0 - Bottom side of reference element (maps to physical bottom boundary)\n * 1 - Left side of reference element (maps to physical left boundary)\n * 2 - Top side of reference element (maps to physical top boundary)\n * 3 - Right side of reference element (maps to physical right boundary)\n */\n findBoundaryElements() {\n const boundaryElements = [];\n const maxSides = 4; // For 2D, we have four sides (left, right, bottom, top)\n\n for (let sideIndex = 0; sideIndex < maxSides; sideIndex++) {\n boundaryElements.push([]);\n }\n\n // TODO: Why to loop through all elements? Is it not better to loop over only the\n // elements that are on the boundary? eg: [0, this.numElementsX - 1] on x and\n // [0, this.numElementsY - 1] on y\n for (let elementIndexX = 0; elementIndexX < this.numElementsX; elementIndexX++) {\n for (let elementIndexY = 0; elementIndexY < this.numElementsY; elementIndexY++) {\n const elementIndex = elementIndexX * this.numElementsY + elementIndexY;\n\n // Bottom boundary\n if (elementIndexY === 0) {\n boundaryElements[0].push([elementIndex, 0]);\n }\n\n // Left boundary\n if (elementIndexX === 0) {\n boundaryElements[1].push([elementIndex, 1]);\n }\n\n // Top boundary\n if (elementIndexY === this.numElementsY - 1) {\n boundaryElements[2].push([elementIndex, 2]);\n }\n\n // Right boundary\n if (elementIndexX === this.numElementsX - 1) {\n boundaryElements[3].push([elementIndex, 3]);\n }\n }\n }\n\n debugLog(\"Identified boundary elements by side: \" + JSON.stringify(boundaryElements));\n this.boundaryElementsProcessed = true;\n return boundaryElements;\n }\n}\n","// ______ ______ _____ _ _ //\n// | ____| ____| /\\ / ____| (_) | | //\n// | |__ | |__ / \\ | (___ ___ ____ _ ____ | |_ //\n// | __| | __| / /\\ \\ \\___ \\ / __| __| | _ \\| __| //\n// | | | |____ / ____ \\ ____) | (__| | | | |_) | | //\n// |_| |______/_/ \\_\\_____/ \\___|_| |_| __/| | //\n// | | | | //\n// |_| | |_ //\n// Website: https://feascript.com/ \\__| //\n\n/**\n * Class to handle numerical integration using Gauss quadrature\n */\nexport class NumericalIntegration {\n /**\n * Constructor to initialize the NumericalIntegration class\n * @param {string} meshDimension - The dimension of the mesh\n * @param {string} elementOrder - The order of elements\n */\n constructor({ meshDimension, elementOrder }) {\n this.meshDimension = meshDimension;\n this.elementOrder = elementOrder;\n }\n\n /**\n * Function to return Gauss points and weights based on element configuration\n * @returns {object} An object containing:\n * - gaussPoints: Array of Gauss points\n * - gaussWeights: Array of Gauss weights\n */\n getGaussPointsAndWeights() {\n let gaussPoints = []; // Gauss points\n let gaussWeights = []; // Gauss weights\n\n if (this.elementOrder === \"linear\") {\n // For linear elements, use 1-point Gauss quadrature\n gaussPoints[0] = 0.5;\n gaussWeights[0] = 1;\n } else if (this.elementOrder === \"quadratic\") {\n // For quadratic elements, use 3-point Gauss quadrature\n gaussPoints[0] = (1 - Math.sqrt(3 / 5)) / 2;\n gaussPoints[1] = 0.5;\n gaussPoints[2] = (1 + Math.sqrt(3 / 5)) / 2;\n gaussWeights[0] = 5 / 18;\n gaussWeights[1] = 8 / 18;\n gaussWeights[2] = 5 / 18;\n }\n\n return { gaussPoints, gaussWeights };\n }\n}\n","// ______ ______ _____ _ _ //\n// | ____| ____| /\\ / ____| (_) | | //\n// | |__ | |__ / \\ | (___ ___ ____ _ ____ | |_ //\n// | __| | __| / /\\ \\ \\___ \\ / __| __| | _ \\| __| //\n// | | | |____ / ____ \\ ____) | (__| | | | |_) | | //\n// |_| |______/_/ \\_\\_____/ \\___|_| |_| __/| | //\n// | | | | //\n// |_| | |_ //\n// Website: https://feascript.com/ \\__| //\n\nimport { BasisFunctions } from \"./basisFunctionsScript.js\";\nimport { Mesh1D, Mesh2D } from \"./meshGenerationScript.js\";\nimport { NumericalIntegration } from \"../methods/numericalIntegrationScript.js\";\nimport { basicLog, debugLog, errorLog } from \"../utilities/loggingScript.js\";\n\n/**\n * Function to prepare the mesh for finite element analysis\n * @param {object} meshConfig - Object containing computational mesh details\n * @returns {object} An object containing all mesh-related data\n */\nexport function prepareMesh(meshConfig) {\n const { meshDimension, numElementsX, numElementsY, maxX, maxY, elementOrder, parsedMesh } = meshConfig;\n\n // Create a new instance of the Mesh class\n let mesh;\n if (meshDimension === \"1D\") {\n mesh = new Mesh1D({ numElementsX, maxX, elementOrder, parsedMesh });\n } else if (meshDimension === \"2D\") {\n mesh = new Mesh2D({ numElementsX, maxX, numElementsY, maxY, elementOrder, parsedMesh });\n } else {\n errorLog(\"Mesh dimension must be either '1D' or '2D'.\");\n }\n\n // Use the parsed mesh in case it was already passed with Gmsh format\n const nodesCoordinatesAndNumbering = mesh.boundaryElementsProcessed ? mesh.parsedMesh : mesh.generateMesh();\n\n // Extract nodes coordinates and nodal numbering (NOP) from the mesh data\n let nodesXCoordinates = nodesCoordinatesAndNumbering.nodesXCoordinates;\n let nodesYCoordinates = nodesCoordinatesAndNumbering.nodesYCoordinates;\n let totalNodesX = nodesCoordinatesAndNumbering.totalNodesX;\n let totalNodesY = nodesCoordinatesAndNumbering.totalNodesY;\n let nop = nodesCoordinatesAndNumbering.nodalNumbering;\n let boundaryElements = nodesCoordinatesAndNumbering.boundaryElements;\n\n // Check the mesh type\n const isParsedMesh = parsedMesh !== undefined && parsedMesh !== null;\n\n // Calculate totalElements and totalNodes based on mesh type\n let totalElements, totalNodes;\n\n if (isParsedMesh) {\n totalElements = nop.length; // Number of elements is the length of the nodal numbering array\n totalNodes = nodesXCoordinates.length; // Number of nodes is the length of the coordinates array\n debugLog(`Using parsed mesh with ${totalElements} elements and ${totalNodes} nodes`);\n } else {\n // For structured mesh, calculate based on dimensions\n totalElements = numElementsX * (meshDimension === \"2D\" ? numElementsY : 1);\n totalNodes = totalNodesX * (meshDimension === \"2D\" ? totalNodesY : 1);\n debugLog(`Using mesh generated from geometry with ${totalElements} elements and ${totalNodes} nodes`);\n }\n\n return {\n nodesXCoordinates,\n nodesYCoordinates,\n totalNodesX,\n totalNodesY,\n nop,\n boundaryElements,\n totalElements,\n totalNodes,\n meshDimension,\n elementOrder,\n };\n}\n\n/**\n * Function to initialize the FEA matrices and numerical tools\n * @param {object} meshData - Object containing mesh data from prepareMesh()\n * @returns {object} An object containing initialized matrices and numerical tools\n */\nexport function initializeFEA(meshData) {\n const { totalNodes, nop, meshDimension, elementOrder } = meshData;\n\n // Initialize variables for matrix assembly\n let residualVector = [];\n let jacobianMatrix = [];\n let localToGlobalMap = [];\n\n // Initialize jacobianMatrix and residualVector arrays\n for (let nodeIndex = 0; nodeIndex < totalNodes; nodeIndex++) {\n residualVector[nodeIndex] = 0;\n jacobianMatrix.push([]);\n for (let colIndex = 0; colIndex < totalNodes; colIndex++) {\n jacobianMatrix[nodeIndex][colIndex] = 0;\n }\n }\n\n // Initialize the BasisFunctions class\n const basisFunctions = new BasisFunctions({\n meshDimension,\n elementOrder,\n });\n\n // Initialize the NumericalIntegration class\n const numericalIntegration = new NumericalIntegration({\n meshDimension,\n elementOrder,\n });\n\n // Calculate Gauss points and weights\n let gaussPointsAndWeights = numericalIntegration.getGaussPointsAndWeights();\n let gaussPoints = gaussPointsAndWeights.gaussPoints;\n let gaussWeights = gaussPointsAndWeights.gaussWeights;\n\n // Determine the number of nodes in the reference element based on the first element in the nop array\n const numNodes = nop[0].length;\n\n return {\n residualVector,\n jacobianMatrix,\n localToGlobalMap,\n basisFunctions,\n gaussPoints,\n gaussWeights,\n numNodes,\n };\n}\n\n/**\n * Function to perform isoparametric mapping for 1D elements\n * @param {object} params - Parameters for the mapping\n * @returns {object} An object containing the mapped data\n */\nexport function performIsoparametricMapping1D(params) {\n const { basisFunction, basisFunctionDerivKsi, nodesXCoordinates, localToGlobalMap, numNodes } = params;\n\n let xCoordinates = 0;\n let ksiDerivX = 0;\n\n // Isoparametric mapping\n for (let localNodeIndex = 0; localNodeIndex < numNodes; localNodeIndex++) {\n xCoordinates += nodesXCoordinates[localToGlobalMap[localNodeIndex]] * basisFunction[localNodeIndex];\n ksiDerivX += nodesXCoordinates[localToGlobalMap[localNodeIndex]] * basisFunctionDerivKsi[localNodeIndex];\n }\n let detJacobian = ksiDerivX;\n\n // Compute x-derivative of basis functions\n let basisFunctionDerivX = [];\n for (let localNodeIndex = 0; localNodeIndex < numNodes; localNodeIndex++) {\n basisFunctionDerivX[localNodeIndex] = basisFunctionDerivKsi[localNodeIndex] / detJacobian;\n }\n\n return {\n xCoordinates,\n detJacobian,\n basisFunctionDerivX,\n };\n}\n\n/**\n * Function to perform isoparametric mapping for 2D elements\n * @param {object} params - Parameters for the mapping\n * @returns {object} An object containing the mapped data\n */\nexport function performIsoparametricMapping2D(params) {\n const {\n basisFunction,\n basisFunctionDerivKsi,\n basisFunctionDerivEta,\n nodesXCoordinates,\n nodesYCoordinates,\n localToGlobalMap,\n numNodes,\n } = params;\n\n let xCoordinates = 0;\n let yCoordinates = 0;\n let ksiDerivX = 0;\n let etaDerivX = 0;\n let ksiDerivY = 0;\n let etaDerivY = 0;\n\n // Isoparametric mapping\n for (let localNodeIndex = 0; localNodeIndex < numNodes; localNodeIndex++) {\n xCoordinates += nodesXCoordinates[localToGlobalMap[localNodeIndex]] * basisFunction[localNodeIndex];\n yCoordinates += nodesYCoordinates[localToGlobalMap[localNodeIndex]] * basisFunction[localNodeIndex];\n ksiDerivX += nodesXCoordinates[localToGlobalMap[localNodeIndex]] * basisFunctionDerivKsi[localNodeIndex];\n etaDerivX += nodesXCoordinates[localToGlobalMap[localNodeIndex]] * basisFunctionDerivEta[localNodeIndex];\n ksiDerivY += nodesYCoordinates[localToGlobalMap[localNodeIndex]] * basisFunctionDerivKsi[localNodeIndex];\n etaDerivY += nodesYCoordinates[localToGlobalMap[localNodeIndex]] * basisFunctionDerivEta[localNodeIndex];\n }\n let detJacobian = ksiDerivX * etaDerivY - etaDerivX * ksiDerivY;\n\n // Compute x-derivative and y-derivative of basis functions\n let basisFunctionDerivX = [];\n let basisFunctionDerivY = [];\n for (let localNodeIndex = 0; localNodeIndex < numNodes; localNodeIndex++) {\n // The x-derivative of the n basis function\n basisFunctionDerivX[localNodeIndex] =\n (etaDerivY * basisFunctionDerivKsi[localNodeIndex] -\n ksiDerivY * basisFunctionDerivEta[localNodeIndex]) /\n detJacobian;\n // The y-derivative of the n basis function\n basisFunctionDerivY[localNodeIndex] =\n (ksiDerivX * basisFunctionDerivEta[localNodeIndex] -\n etaDerivX * basisFunctionDerivKsi[localNodeIndex]) /\n detJacobian;\n }\n\n return {\n xCoordinates,\n yCoordinates,\n detJacobian,\n basisFunctionDerivX,\n basisFunctionDerivY,\n };\n}\n","// ______ ______ _____ _ _ //\n// | ____| ____| /\\ / ____| (_) | | //\n// | |__ | |__ / \\ | (___ ___ ____ _ ____ | |_ //\n// | __| | __| / /\\ \\ \\___ \\ / __| __| | _ \\| __| //\n// | | | |____ / ____ \\ ____) | (__| | | | |_) | | //\n// |_| |______/_/ \\_\\_____/ \\___|_| |_| __/| | //\n// | | | | //\n// |_| | |_ //\n// Website: https://feascript.com/ \\__| //\n\n// Internal imports\nimport { basicLog, debugLog, errorLog } from \"../utilities/loggingScript.js\";\n\n/**\n * Class to handle thermal boundary conditions application\n */\nexport class ThermalBoundaryConditions {\n /**\n * Constructor to initialize the ThermalBoundaryConditions class\n * @param {object} boundaryConditions - Object containing boundary conditions for the finite element analysis\n * @param {array} boundaryElements - Array containing elements that belong to each boundary\n * @param {array} nop - Nodal numbering (NOP) array representing the connectivity between elements and nodes\n * @param {string} meshDimension - The dimension of the mesh (e.g., \"2D\")\n * @param {string} elementOrder - The order of elements (e.g., \"linear\", \"quadratic\")\n */\n constructor(boundaryConditions, boundaryElements, nop, meshDimension, elementOrder) {\n this.boundaryConditions = boundaryConditions;\n this.boundaryElements = boundaryElements;\n this.nop = nop;\n this.meshDimension = meshDimension;\n this.elementOrder = elementOrder;\n }\n\n /**\n * Function to impose constant temperature boundary conditions (Dirichlet type)\n * @param {array} residualVector - The residual vector to be modified\n * @param {array} jacobianMatrix - The Jacobian matrix to be modified\n *\n * For consistency across both linear and nonlinear formulations,\n * this project always refers to the assembled right-hand side vector\n * as `residualVector` and the assembled system matrix as `jacobianMatrix`.\n *\n * In linear problems `jacobianMatrix` is equivalent to the\n * classic stiffness/conductivity matrix and `residualVector`\n * corresponds to the traditional load (RHS) vector.\n */\n imposeConstantTempBoundaryConditions(residualVector, jacobianMatrix) {\n if (this.meshDimension === \"1D\") {\n Object.keys(this.boundaryConditions).forEach((boundaryKey) => {\n if (this.boundaryConditions[boundaryKey][0] === \"constantTemp\") {\n const tempValue = this.boundaryConditions[boundaryKey][1];\n debugLog(\n `Boundary ${boundaryKey}: Applying constant temperature of ${tempValue} K (Dirichlet condition)`\n );\n this.boundaryElements[boundaryKey].forEach(([elementIndex, side]) => {\n if (this.elementOrder === \"linear\") {\n const boundarySides = {\n 0: [0], // Node at the left side of the reference element\n 1: [1], // Node at the right side of the reference element\n };\n boundarySides[side].forEach((nodeIndex) => {\n const globalNodeIndex = this.nop[elementIndex][nodeIndex] - 1;\n debugLog(\n ` - Applied constant temperature to node ${globalNodeIndex + 1} (element ${\n elementIndex + 1\n }, local node ${nodeIndex + 1})`\n );\n // Set the residual vector to the ConstantTemp value\n residualVector[globalNodeIndex] = tempValue;\n // Set the Jacobian matrix row to zero\n for (let colIndex = 0; colIndex < residualVector.length; colIndex++) {\n jacobianMatrix[globalNodeIndex][colIndex] = 0;\n }\n // Set the diagonal entry of the Jacobian matrix to one\n jacobianMatrix[globalNodeIndex][globalNodeIndex] = 1;\n });\n } else if (this.elementOrder === \"quadratic\") {\n const boundarySides = {\n 0: [0], // Node at the left side of the reference element\n 2: [2], // Node at the right side of the reference element\n };\n boundarySides[side].forEach((nodeIndex) => {\n const globalNodeIndex = this.nop[elementIndex][nodeIndex] - 1;\n debugLog(\n ` - Applied constant temperature to node ${globalNodeIndex + 1} (element ${\n elementIndex + 1\n }, local node ${nodeIndex + 1})`\n );\n // Set the residual vector to the ConstantTemp value\n residualVector[globalNodeIndex] = tempValue;\n // Set the Jacobian matrix row to zero\n for (let colIndex = 0; colIndex < residualVector.length; colIndex++) {\n jacobianMatrix[globalNodeIndex][colIndex] = 0;\n }\n // Set the diagonal entry of the Jacobian matrix to one\n jacobianMatrix[globalNodeIndex][globalNodeIndex] = 1;\n });\n }\n });\n }\n });\n } else if (this.meshDimension === \"2D\") {\n Object.keys(this.boundaryConditions).forEach((boundaryKey) => {\n if (this.boundaryConditions[boundaryKey][0] === \"constantTemp\") {\n const tempValue = this.boundaryConditions[boundaryKey][1];\n debugLog(\n `Boundary ${boundaryKey}: Applying constant temperature of ${tempValue} K (Dirichlet condition)`\n );\n this.boundaryElements[boundaryKey].forEach(([elementIndex, side]) => {\n if (this.elementOrder === \"linear\") {\n const boundarySides = {\n 0: [0, 2], // Nodes at the bottom side of the reference element\n 1: [0, 1], // Nodes at the left side of the reference element\n 2: [1, 3], // Nodes at the top side of the reference element\n 3: [2, 3], // Nodes at the right side of the reference element\n };\n boundarySides[side].forEach((nodeIndex) => {\n const globalNodeIndex = this.nop[elementIndex][nodeIndex] - 1;\n debugLog(\n ` - Applied constant temperature to node ${globalNodeIndex + 1} (element ${\n elementIndex + 1\n }, local node ${nodeIndex + 1})`\n );\n // Set the residual vector to the ConstantTemp value\n residualVector[globalNodeIndex] = tempValue;\n // Set the Jacobian matrix row to zero\n for (let colIndex = 0; colIndex < residualVector.length; colIndex++) {\n jacobianMatrix[globalNodeIndex][colIndex] = 0;\n }\n // Set the diagonal entry of the Jacobian matrix to one\n jacobianMatrix[globalNodeIndex][globalNodeIndex] = 1;\n });\n } else if (this.elementOrder === \"quadratic\") {\n const boundarySides = {\n 0: [0, 3, 6], // Nodes at the bottom side of the reference element\n 1: [0, 1, 2], // Nodes at the left side of the reference element\n 2: [2, 5, 8], // Nodes at the top side of the reference element\n 3: [6, 7, 8], // Nodes at the right side of the reference element\n };\n boundarySides[side].forEach((nodeIndex) => {\n const globalNodeIndex = this.nop[elementIndex][nodeIndex] - 1;\n debugLog(\n ` - Applied constant temperature to node ${globalNodeIndex + 1} (element ${\n elementIndex + 1\n }, local node ${nodeIndex + 1})`\n );\n // Set the residual vector to the ConstantTemp value\n residualVector[globalNodeIndex] = tempValue;\n // Set the Jacobian matrix row to zero\n for (let colIndex = 0; colIndex < residualVector.length; colIndex++) {\n jacobianMatrix[globalNodeIndex][colIndex] = 0;\n }\n // Set the diagonal entry of the Jacobian matrix to one\n jacobianMatrix[globalNodeIndex][globalNodeIndex] = 1;\n });\n }\n });\n }\n });\n }\n }\n\n /**\n * Function to impose constant temperature boundary conditions for the frontal solver\n * @param {array} nodeConstraintCode - Array indicating boundary condition code for each node\n * @param {array} boundaryValues - Array containing boundary condition values\n */\n imposeConstantTempBoundaryConditionsFront(nodeConstraintCode, boundaryValues) {\n if (this.meshDimension === \"1D\") {\n Object.keys(this.boundaryConditions).forEach((boundaryKey) => {\n if (this.boundaryConditions[boundaryKey][0] === \"constantTemp\") {\n const tempValue = this.boundaryConditions[boundaryKey][1];\n debugLog(\n `Boundary ${boundaryKey}: Applying constant temperature of ${tempValue} K (Dirichlet condition)`\n );\n\n this.boundaryElements[boundaryKey].forEach(([elementIndex, side]) => {\n if (this.elementOrder === \"linear\") {\n const boundarySides = {\n 0: [0], // Node at the left side of the reference element\n 1: [1], // Node at the right side of the reference element\n };\n\n boundarySides[side].forEach((nodeIndex) => {\n const globalNodeIndex = this.nop[elementIndex][nodeIndex] - 1;\n debugLog(\n ` - Applied constant temperature to node ${globalNodeIndex + 1} (element ${\n elementIndex + 1\n }, local node ${nodeIndex + 1})`\n );\n\n // Set boundary condition code and value\n nodeConstraintCode[globalNodeIndex] = 1;\n boundaryValues[globalNodeIndex] = tempValue;\n });\n } else if (this.elementOrder === \"quadratic\") {\n const boundarySides = {\n 0: [0], // Node at the left side of the reference element\n 2: [2], // Node at the right side of the reference element\n };\n\n boundarySides[side].forEach((nodeIndex) => {\n const globalNodeIndex = this.nop[elementIndex][nodeIndex] - 1;\n debugLog(\n ` - Applied constant temperature to node ${globalNodeIndex + 1} (element ${\n elementIndex + 1\n }, local node ${nodeIndex + 1})`\n );\n\n // Set boundary condition code and value\n nodeConstraintCode[globalNodeIndex] = 1;\n boundaryValues[globalNodeIndex] = tempValue;\n });\n }\n });\n }\n });\n } else if (this.meshDimension === \"2D\") {\n Object.keys(this.boundaryConditions).forEach((boundaryKey) => {\n if (this.boundaryConditions[boundaryKey][0] === \"constantTemp\") {\n const tempValue = this.boundaryConditions[boundaryKey][1];\n debugLog(\n `Boundary ${boundaryKey}: Applying constant temperature of ${tempValue} K (Dirichlet condition)`\n );\n\n this.boundaryElements[boundaryKey].forEach(([elementIndex, side]) => {\n if (this.elementOrder === \"linear\") {\n const boundarySides = {\n 0: [0, 2], // Nodes at the bottom side of the reference element\n 1: [0, 1], // Nodes at the left side of the reference element\n 2: [1, 3], // Nodes at the top side of the reference element\n 3: [2, 3], // Nodes at the right side of the reference element\n };\n\n boundarySides[side].forEach((nodeIndex) => {\n const globalNodeIndex = this.nop[elementIndex][nodeIndex] - 1;\n debugLog(\n ` - Applied constant temperature to node ${globalNodeIndex + 1} (element ${\n elementIndex + 1\n }, local node ${nodeIndex + 1})`\n );\n\n // Set boundary condition code and value\n nodeConstraintCode[globalNodeIndex] = 1;\n boundaryValues[globalNodeIndex] = tempValue;\n });\n } else if (this.elementOrder === \"quadratic\") {\n const boundarySides = {\n 0: [0, 3, 6], // Nodes at the bottom side of the reference element\n 1: [0, 1, 2], // Nodes at the left side of the reference element\n 2: [2, 5, 8], // Nodes at the top side of the reference element\n 3: [6, 7, 8], // Nodes at the right side of the reference element\n };\n\n boundarySides[side].forEach((nodeIndex) => {\n const globalNodeIndex = this.nop[elementIndex][nodeIndex] - 1;\n debugLog(\n ` - Applied constant temperature to node ${globalNodeIndex + 1} (element ${\n elementIndex + 1\n }, local node ${nodeIndex + 1})`\n );\n\n // Set boundary condition code and value\n nodeConstraintCode[globalNodeIndex] = 1;\n boundaryValues[globalNodeIndex] = tempValue;\n });\n }\n });\n }\n });\n }\n }\n\n /**\n * Function to impose convection boundary conditions (Robin type)\n * @param {array} residualVector - The residual vector to be modified\n * @param {array} jacobianMatrix - The Jacobian matrix to be modified\n * @param {array} gaussPoints - Array of Gauss points for numerical integration\n * @param {array} gaussWeights - Array of Gauss weights for numerical integration\n * @param {array} nodesXCoordinates - Array of x-coordinates of nodes\n * @param {array} nodesYCoordinates - Array of y-coordinates of nodes\n * @param {object} basisFunctions - Object containing basis functions and their derivatives\n */\n imposeConvectionBoundaryConditions(\n residualVector,\n jacobianMatrix,\n gaussPoints,\n gaussWeights,\n nodesXCoordinates,\n nodesYCoordinates,\n basisFunctions\n ) {\n // Extract convection parameters from boundary conditions\n let convectionHeatTranfCoeff = [];\n let convectionExtTemp = [];\n Object.keys(this.boundaryConditions).forEach((key) => {\n const boundaryCondition = this.boundaryConditions[key];\n if (boundaryCondition[0] === \"convection\") {\n convectionHeatTranfCoeff[key] = boundaryCondition[1];\n convectionExtTemp[key] = boundaryCondition[2];\n }\n });\n\n if (this.meshDimension === \"1D\") {\n Object.keys(this.boundaryConditions).forEach((boundaryKey) => {\n if (this.boundaryConditions[boundaryKey][0] === \"convection\") {\n const convectionCoeff = convectionHeatTranfCoeff[boundaryKey];\n const extTemp = convectionExtTemp[boundaryKey];\n debugLog(\n `Boundary ${boundaryKey}: Applying convection with heat transfer coefficient h=${convectionCoeff} W/(m²·K) and external temperature T∞=${extTemp} K`\n );\n this.boundaryElements[boundaryKey].forEach(([elementIndex, side]) => {\n let nodeIndex;\n if (this.elementOrder === \"linear\") {\n if (side === 0) {\n // Node at the left side of the reference element\n nodeIndex = 0;\n } else {\n // Node at the right side of the reference element\n nodeIndex = 1;\n }\n } else if (this.elementOrder === \"quadratic\") {\n if (side === 0) {\n // Node at the left side of the reference element\n nodeIndex = 0;\n } else {\n // Node at the right side of the reference element\n nodeIndex = 2;\n }\n }\n\n const globalNodeIndex = this.nop[elementIndex][nodeIndex] - 1;\n debugLog(\n ` - Applied convection boundary condition to node ${globalNodeIndex + 1} (element ${\n elementIndex + 1\n }, local node ${nodeIndex + 1})`\n );\n residualVector[globalNodeIndex] += -convectionCoeff * extTemp;\n jacobianMatrix[globalNodeIndex][globalNodeIndex] += convectionCoeff;\n });\n }\n });\n } else if (this.meshDimension === \"2D\") {\n Object.keys(this.boundaryConditions).forEach((boundaryKey) => {\n if (this.boundaryConditions[boundaryKey][0] === \"convection\") {\n const convectionCoeff = convectionHeatTranfCoeff[boundaryKey];\n const extTemp = convectionExtTemp[boundaryKey];\n debugLog(\n `Boundary ${boundaryKey}: Applying convection with heat transfer coefficient h=${convectionCoeff} W/(m²·K) and external temperature T∞=${extTemp} K`\n );\n this.boundaryElements[boundaryKey].forEach(([elementIndex, side]) => {\n if (this.elementOrder === \"linear\") {\n let gaussPoint1, gaussPoint2, firstNodeIndex, lastNodeIndex, nodeIncrement;\n if (side === 0) {\n // Nodes at the bottom side of the reference element\n gaussPoint1 = gaussPoints[0];\n gaussPoint2 = 0;\n firstNodeIndex = 0;\n lastNodeIndex = 3;\n nodeIncrement = 2;\n } else if (side === 1) {\n // Nodes at the left side of the reference element\n gaussPoint1 = 0;\n gaussPoint2 = gaussPoints[0];\n firstNodeIndex = 0;\n lastNodeIndex = 2;\n nodeIncrement = 1;\n } else if (side === 2) {\n // Nodes at the top side of the reference element\n gaussPoint1 = gaussPoints[0];\n gaussPoint2 = 1;\n firstNodeIndex = 1;\n lastNodeIndex = 4;\n nodeIncrement = 2;\n } else if (side === 3) {\n // Nodes at the right side of the reference element\n gaussPoint1 = 1;\n gaussPoint2 = gaussPoints[0];\n firstNodeIndex = 2;\n lastNodeIndex = 4;\n nodeIncrement = 1;\n }\n\n let basisFunctionsAndDerivatives = basisFunctions.getBasisFunctions(gaussPoint1, gaussPoint2);\n let basisFunction = basisFunctionsAndDerivatives.basisFunction;\n let basisFunctionDerivKsi = basisFunctionsAndDerivatives.basisFunctionDerivKsi;\n let basisFunctionDerivEta = basisFunctionsAndDerivatives.basisFunctionDerivEta;\n\n let ksiDerivX = 0;\n let ksiDerivY = 0;\n let etaDerivX = 0;\n let etaDerivY = 0;\n const numNodes = this.nop[elementIndex].length;\n for (let nodeIndex = 0; nodeIndex < numNodes; nodeIndex++) {\n const globalNodeIndex = this.nop[elementIndex][nodeIndex] - 1;\n\n // For boundaries along Ksi (horizontal), use Ksi derivatives\n if (side === 0 || side === 2) {\n ksiDerivX += nodesXCoordinates[globalNodeIndex] * basisFunctionDerivKsi[nodeIndex];\n ksiDerivY += nodesYCoordinates[globalNodeIndex] * basisFunctionDerivKsi[nodeIndex];\n }\n // For boundaries along Eta (vertical), use Eta derivatives\n else if (side === 1 || side === 3) {\n etaDerivX += nodesXCoordinates[globalNodeIndex] * basisFunctionDerivEta[nodeIndex];\n etaDerivY += nodesYCoordinates[globalNodeIndex] * basisFunctionDerivEta[nodeIndex];\n }\n }\n\n // Compute the length of tangent vector\n let tangentVectorLength;\n if (side === 0 || side === 2) {\n tangentVectorLength = Math.sqrt(ksiDerivX ** 2 + ksiDerivY ** 2);\n } else {\n tangentVectorLength = Math.sqrt(etaDerivX ** 2 + etaDerivY ** 2);\n }\n\n for (\n let localNodeIndex = firstNodeIndex;\n localNodeIndex < lastNodeIndex;\n localNodeIndex += nodeIncrement\n ) {\n let globalNodeIndex = this.nop[elementIndex][localNodeIndex] - 1;\n debugLog(\n ` - Applied convection boundary condition to node ${globalNodeIndex + 1} (element ${\n elementIndex + 1\n }, local node ${localNodeIndex + 1})`\n );\n\n // Apply boundary condition with proper Jacobian for all sides\n residualVector[globalNodeIndex] +=\n -gaussWeights[0] *\n tangentVectorLength *\n basisFunction[localNodeIndex] *\n convectionCoeff *\n extTemp;\n\n for (\n let localNodeIndex2 = firstNodeIndex;\n localNodeIndex2 < lastNodeIndex;\n localNodeIndex2 += nodeIncrement\n ) {\n let globalNodeIndex2 = this.nop[elementIndex][localNodeIndex2] - 1;\n jacobianMatrix[globalNodeIndex][globalNodeIndex2] +=\n -gaussWeights[0] *\n tangentVectorLength *\n basisFunction[localNodeIndex] *\n basisFunction[localNodeIndex2] *\n convectionCoeff;\n }\n }\n } else if (this.elementOrder === \"quadratic\") {\n for (let gaussPointIndex = 0; gaussPointIndex < 3; gaussPointIndex++) {\n let gaussPoint1, gaussPoint2, firstNodeIndex, lastNodeIndex, nodeIncrement;\n if (side === 0) {\n // Nodes at the bottom side of the reference element\n gaussPoint1 = gaussPoints[gaussPointIndex];\n gaussPoint2 = 0;\n firstNodeIndex = 0;\n lastNodeIndex = 7;\n nodeIncrement = 3;\n } else if (side === 1) {\n // Nodes at the left side of the reference element\n gaussPoint1 = 0;\n gaussPoint2 = gaussPoints[gaussPointIndex];\n firstNodeIndex = 0;\n lastNodeIndex = 3;\n nodeIncrement = 1;\n } else if (side === 2) {\n // Nodes at the top side of the reference element\n gaussPoint1 = gaussPoints[gaussPointIndex];\n gaussPoint2 = 1;\n firstNodeIndex = 2;\n lastNodeIndex = 9;\n nodeIncrement = 3;\n } else if (side === 3) {\n // Nodes at the right side of the reference element\n gaussPoint1 = 1;\n gaussPoint2 = gaussPoints[gaussPointIndex];\n firstNodeIndex = 6;\n lastNodeIndex = 9;\n nodeIncrement = 1;\n }\n let basisFunctionsAndDerivatives = basisFunctions.getBasisFunctions(gaussPoint1, gaussPoint2);\n let basisFunction = basisFunctionsAndDerivatives.basisFunction;\n let basisFunctionDerivKsi = basisFunctionsAndDerivatives.basisFunctionDerivKsi;\n let basisFunctionDerivEta = basisFunctionsAndDerivatives.basisFunctionDerivEta;\n\n let ksiDerivX = 0;\n let ksiDerivY = 0;\n let etaDerivX = 0;\n let etaDerivY = 0;\n const numNodes = this.nop[elementIndex].length;\n for (let nodeIndex = 0; nodeIndex < numNodes; nodeIndex++) {\n const globalNodeIndex = this.nop[elementIndex][nodeIndex] - 1;\n\n // For boundaries along Ksi (horizontal), use Ksi derivatives\n if (side === 0 || side === 2) {\n ksiDerivX += nodesXCoordinates[globalNodeIndex] * basisFunctionDerivKsi[nodeIndex];\n ksiDerivY += nodesYCoordinates[globalNodeIndex] * basisFunctionDerivKsi[nodeIndex];\n }\n // For boundaries along Eta (vertical), use Eta derivatives\n else if (side === 1 || side === 3) {\n etaDerivX += nodesXCoordinates[globalNodeIndex] * basisFunctionDerivEta[nodeIndex];\n etaDerivY += nodesYCoordinates[globalNodeIndex] * basisFunctionDerivEta[nodeIndex];\n }\n }\n\n // Compute the length of tangent vector\n let tangentVectorLength;\n if (side === 0 || side === 2) {\n tangentVectorLength = Math.sqrt(ksiDerivX ** 2 + ksiDerivY ** 2);\n } else {\n tangentVectorLength = Math.sqrt(etaDerivX ** 2 + etaDerivY ** 2);\n }\n\n for (\n let localNodeIndex = firstNodeIndex;\n localNodeIndex < lastNodeIndex;\n localNodeIndex += nodeIncrement\n ) {\n let globalNodeIndex = this.nop[elementIndex][localNodeIndex] - 1;\n debugLog(\n ` - Applied convection boundary condition to node ${globalNodeIndex + 1} (element ${\n elementIndex + 1\n }, local node ${localNodeIndex + 1})`\n );\n\n // Apply boundary condition with proper Jacobian for all sides\n residualVector[globalNodeIndex] +=\n -gaussWeights[gaussPointIndex] *\n tangentVectorLength *\n basisFunction[localNodeIndex] *\n convectionCoeff *\n extTemp;\n\n for (\n let localNodeIndex2 = firstNodeIndex;\n localNodeIndex2 < lastNodeIndex;\n localNodeIndex2 += nodeIncrement\n ) {\n let globalNodeIndex2 = this.nop[elementIndex][localNodeIndex2] - 1;\n jacobianMatrix[globalNodeIndex][globalNodeIndex2] +=\n -gaussWeights[gaussPointIndex] *\n tangentVectorLength *\n basisFunction[localNodeIndex] *\n basisFunction[localNodeIndex2] *\n convectionCoeff;\n }\n }\n }\n }\n });\n }\n });\n }\n }\n\n /**\n * Function to impose convection boundary conditions for the frontal solver\n * @param {number} elementIndex - Index of the element being processed\n * @param {array} nodesXCoordinates - Array of x-coordinates of nodes\n * @param {array} nodesYCoordinates - Array of y-coordinates of nodes\n * @param {array} gaussPoints - Array of Gauss points for numerical integration\n * @param {array} gaussWeights - Array of Gauss weights for numerical integration\n * @param {object} basisFunctions - Object containing basis functions and their derivatives\n * @returns {object} An object containing:\n * - localJacobianMatrix: Local Jacobian matrix with convection contributions\n * - localResidualVector: Residual vector with convection contributions\n */\n imposeConvectionBoundaryConditionsFront(\n elementIndex,\n nodesXCoordinates,\n nodesYCoordinates,\n gaussPoints,\n gaussWeights,\n basisFunctions\n ) {\n // Extract convection parameters from boundary conditions\n let convectionHeatTranfCoeff = [];\n let convectionExtTemp = [];\n Object.keys(this.boundaryConditions).forEach((key) => {\n const boundaryCondition = this.boundaryConditions[key];\n if (boundaryCondition[0] === \"convection\") {\n convectionHeatTranfCoeff[key] = boundaryCondition[1];\n convectionExtTemp[key] = boundaryCondition[2];\n }\n });\n\n // Initialize local Jacobian matrix and local residual vector\n const numNodes = this.nop[elementIndex].length;\n const localJacobianMatrix = Array(numNodes)\n .fill()\n .map(() => Array(numNodes).fill(0));\n const localResidualVector = Array(numNodes).fill(0);\n\n // Check if this element is on a convection boundary\n for (const boundaryKey in this.boundaryElements) {\n if (this.boundaryConditions[boundaryKey]?.[0] === \"convection\") {\n const convectionCoeff = convectionHeatTranfCoeff[boundaryKey];\n const extTemp = convectionExtTemp[boundaryKey];\n debugLog(\n `Boundary ${boundaryKey}: Applying convection with heat transfer coefficient h=${convectionCoeff} W/(m²·K) and external temperature T∞=${extTemp} K`\n );\n\n // Find if this element is on this boundary and which side\n const boundaryElement = this.boundaryElements[boundaryKey].find(\n ([elemIdx, _]) => elemIdx === elementIndex\n );\n\n if (boundaryElement) {\n const side = boundaryElement[1];\n\n if (this.meshDimension === \"1D\") {\n // Handle 1D case\n let nodeIndex;\n if (this.elementOrder === \"linear\") {\n nodeIndex = side === 0 ? 0 : 1;\n } else if (this.elementOrder === \"quadratic\") {\n nodeIndex = side === 0 ? 0 : 2;\n }\n\n // Add contribution to local Jacobian matrix and local residual vector\n debugLog(\n ` - Applied convection boundary condition to node ${nodeIndex + 1} (element ${\n elementIndex + 1\n }, local node ${nodeIndex + 1})`\n );\n localResidualVector[nodeIndex] += -convectionCoeff * extTemp;\n localJacobianMatrix[nodeIndex][nodeIndex] += convectionCoeff;\n } else if (this.meshDimension === \"2D\") {\n // Handle 2D case\n if (this.elementOrder === \"linear\") {\n let gaussPoint1, gaussPoint2, firstNodeIndex, lastNodeIndex, nodeIncrement;\n\n if (side === 0) {\n // Nodes at the bottom side of the reference element\n gaussPoint1 = gaussPoints[0];\n gaussPoint2 = 0;\n firstNodeIndex = 0;\n lastNodeIndex = 3;\n nodeIncrement = 2;\n } else if (side === 1) {\n // Nodes at the left side of the reference element\n gaussPoint1 = 0;\n gaussPoint2 = gaussPoints[0];\n firstNodeIndex = 0;\n lastNodeIndex = 2;\n nodeIncrement = 1;\n } else if (side === 2) {\n // Nodes at the top side of the reference element\n gaussPoint1 = gaussPoints[0];\n gaussPoint2 = 1;\n firstNodeIndex = 1;\n lastNodeIndex = 4;\n nodeIncrement = 2;\n } else if (side === 3) {\n // Nodes at the right side of the reference element\n gaussPoint1 = 1;\n gaussPoint2 = gaussPoints[0];\n firstNodeIndex = 2;\n lastNodeIndex = 4;\n nodeIncrement = 1;\n }\n\n // Get basis functions\n const basisFunctionsAndDerivatives = basisFunctions.getBasisFunctions(gaussPoint1, gaussPoint2);\n const basisFunction = basisFunctionsAndDerivatives.basisFunction;\n const basisFunctionDerivKsi = basisFunctionsAndDerivatives.basisFunctionDerivKsi;\n const basisFunctionDerivEta = basisFunctionsAndDerivatives.basisFunctionDerivEta;\n\n // Calculate tangent vector components\n let ksiDerivX = 0,\n ksiDerivY = 0,\n etaDerivX = 0,\n etaDerivY = 0;\n for (let nodeIndex = 0; nodeIndex < numNodes; nodeIndex++) {\n const globalNodeIndex = this.nop[elementIndex][nodeIndex] - 1;\n\n if (side === 0 || side === 2) {\n ksiDerivX += nodesXCoordinates[globalNodeIndex] * basisFunctionDerivKsi[nodeIndex];\n ksiDerivY += nodesYCoordinates[globalNodeIndex] * basisFunctionDerivKsi[nodeIndex];\n } else if (side === 1 || side === 3) {\n etaDerivX += nodesXCoordinates[globalNodeIndex] * basisFunctionDerivEta[nodeIndex];\n etaDerivY += nodesYCoordinates[globalNodeIndex] * basisFunctionDerivEta[nodeIndex];\n }\n }\n\n // Compute tangent vector length\n let tangentVectorLength;\n if (side === 0 || side === 2) {\n tangentVectorLength = Math.sqrt(ksiDerivX ** 2 + ksiDerivY ** 2);\n } else {\n tangentVectorLength = Math.sqrt(etaDerivX ** 2 + etaDerivY ** 2);\n }\n\n // Apply boundary conditions to local matrices\n for (\n let localNodeIndex = firstNodeIndex;\n localNodeIndex < lastNodeIndex;\n localNodeIndex += nodeIncrement\n ) {\n localResidualVector[localNodeIndex] +=\n -gaussWeights[0] *\n tangentVectorLength *\n basisFunction[localNodeIndex] *\n convectionCoeff *\n extTemp;\n\n for (\n let localNodeIndex2 = firstNodeIndex;\n localNodeIndex2 < lastNodeIndex;\n localNodeIndex2 += nodeIncrement\n ) {\n localJacobianMatrix[localNodeIndex][localNodeIndex2] +=\n -gaussWeights[0] *\n tangentVectorLength *\n basisFunction[localNodeIndex] *\n basisFunction[localNodeIndex2] *\n convectionCoeff;\n }\n }\n } else if (this.elementOrder === \"quadratic\") {\n // Handle quadratic elements (similar pattern but with more Gauss points)\n for (let gaussPointIndex = 0; gaussPointIndex < 3; gaussPointIndex++) {\n let gaussPoint1, gaussPoint2, firstNodeIndex, lastNodeIndex, nodeIncrement;\n\n if (side === 0) {\n // Nodes at the bottom side of the reference element\n gaussPoint1 = gaussPoints[gaussPointIndex];\n gaussPoint2 = 0;\n firstNodeIndex = 0;\n lastNodeIndex = 7;\n nodeIncrement = 3;\n } else if (side === 1) {\n // Nodes at the left side of the reference element\n gaussPoint1 = 0;\n gaussPoint2 = gaussPoints[gaussPointIndex];\n firstNodeIndex = 0;\n lastNodeIndex = 3;\n nodeIncrement = 1;\n } else if (side === 2) {\n // Nodes at the top side of the reference element\n gaussPoint1 = gaussPoints[gaussPointIndex];\n gaussPoint2 = 1;\n firstNodeIndex = 2;\n lastNodeIndex = 9;\n nodeIncrement = 3;\n } else if (side === 3) {\n // Nodes at the right side of the reference element\n gaussPoint1 = 1;\n gaussPoint2 = gaussPoints[gaussPointIndex];\n firstNodeIndex = 6;\n lastNodeIndex = 9;\n nodeIncrement = 1;\n }\n let basisFunctionsAndDerivatives = basisFunctions.getBasisFunctions(gaussPoint1, gaussPoint2);\n let basisFunction = basisFunctionsAndDerivatives.basisFunction;\n let basisFunctionDerivKsi = basisFunctionsAndDerivatives.basisFunctionDerivKsi;\n let basisFunctionDerivEta = basisFunctionsAndDerivatives.basisFunctionDerivEta;\n\n let ksiDerivX = 0;\n let ksiDerivY = 0;\n let etaDerivX = 0;\n let etaDerivY = 0;\n const numNodes = this.nop[elementIndex].length;\n for (let nodeIndex = 0; nodeIndex < numNodes; nodeIndex++) {\n const globalNodeIndex = this.nop[elementIndex][nodeIndex] - 1;\n\n // For boundaries along Ksi (horizontal), use Ksi derivatives\n if (side === 0 || side === 2) {\n ksiDerivX += nodesXCoordinates[globalNodeIndex] * basisFunctionDerivKsi[nodeIndex];\n ksiDerivY += nodesYCoordinates[globalNodeIndex] * basisFunctionDerivKsi[nodeIndex];\n }\n // For boundaries along Eta (vertical), use Eta derivatives\n else if (side === 1 || side === 3) {\n etaDerivX += nodesXCoordinates[globalNodeIndex] * basisFunctionDerivEta[nodeIndex];\n etaDerivY += nodesYCoordinates[globalNodeIndex] * basisFunctionDerivEta[nodeIndex];\n }\n }\n\n // Compute the length of tangent vector\n let tangentVectorLength;\n if (side === 0 || side === 2) {\n tangentVectorLength = Math.sqrt(ksiDerivX ** 2 + ksiDerivY ** 2);\n } else {\n tangentVectorLength = Math.sqrt(etaDerivX ** 2 + etaDerivY ** 2);\n }\n\n // Apply boundary conditions to local matrices\n for (\n let localNodeIndex = firstNodeIndex;\n localNodeIndex < lastNodeIndex;\n localNodeIndex += nodeIncrement\n ) {\n localResidualVector[localNodeIndex] +=\n -gaussWeights[gaussPointIndex] *\n tangentVectorLength *\n basisFunction[localNodeIndex] *\n convectionCoeff *\n extTemp;\n\n for (\n let localNodeIndex2 = firstNodeIndex;\n localNodeIndex2 < lastNodeIndex;\n localNodeIndex2 += nodeIncrement\n ) {\n localJacobianMatrix[localNodeIndex][localNodeIndex2] +=\n -gaussWeights[gaussPointIndex] *\n tangentVectorLength *\n basisFunction[localNodeIndex] *\n basisFunction[localNodeIndex2] *\n convectionCoeff;\n }\n }\n }\n }\n }\n }\n }\n }\n\n return { localJacobianMatrix, localResidualVector };\n }\n}\n","// ______ ______ _____ _ _ //\n// | ____| ____| /\\ / ____| (_) | | //\n// | |__ | |__ / \\ | (___ ___ ____ _ ____ | |_ //\n// | __| | __| / /\\ \\ \\___ \\ / __| __| | _ \\| __| //\n// | | | |____ / ____ \\ ____) | (__| | | | |_) | | //\n// |_| |______/_/ \\_\\_____/ \\___|_| |_| __/| | //\n// | | | | //\n// |_| | |_ //\n// Website: https://feascript.com/ \\__| //\n\n// Internal imports\nimport {\n initializeFEA,\n performIsoparametricMapping1D,\n performIsoparametricMapping2D,\n} from \"../mesh/meshUtilsScript.js\";\nimport { ThermalBoundaryConditions } from \"./thermalBoundaryConditionsScript.js\";\nimport { basicLog, debugLog } from \"../utilities/loggingScript.js\";\n\n/**\n * Function to assemble the Jacobian matrix and residuals vector for the solid heat transfer model\n * @param {object} meshData - Object containing prepared mesh data\n * @param {object} boundaryConditions - Object containing boundary conditions for the finite element analysis\n * @returns {object} An object containing:\n * - jacobianMatrix: The assembled Jacobian matrix\n * - residualVector: The assembled residual vector\n *\n * For consistency across both linear and nonlinear formulations,\n * this project always refers to the assembled right-hand side vector\n * as `residualVector` and the assembled system matrix as `jacobianMatrix`.\n *\n * In linear problems `jacobianMatrix` is equivalent to the\n * classic stiffness/conductivity matrix and `residualVector`\n * corresponds to the traditional load (RHS) vector.\n */\nexport function assembleHeatConductionMat(meshData, boundaryConditions) {\n basicLog(\"Starting solid heat transfer matrix assembly...\");\n\n // Extract mesh data\n const {\n nodesXCoordinates,\n nodesYCoordinates,\n nop,\n boundaryElements,\n totalElements,\n meshDimension,\n elementOrder,\n } = meshData;\n\n // Initialize FEA components\n const FEAData = initializeFEA(meshData);\n const {\n residualVector,\n jacobianMatrix,\n localToGlobalMap,\n basisFunctions,\n gaussPoints,\n gaussWeights,\n numNodes,\n } = FEAData;\n\n // Matrix assembly\n for (let elementIndex = 0; elementIndex < totalElements; elementIndex++) {\n // Map local element nodes to global mesh nodes\n for (let localNodeIndex = 0; localNodeIndex < numNodes; localNodeIndex++) {\n // Subtract 1 from nop in order to start numbering from 0\n localToGlobalMap[localNodeIndex] = nop[elementIndex][localNodeIndex] - 1;\n }\n\n // Loop over Gauss points\n for (let gaussPointIndex1 = 0; gaussPointIndex1 < gaussPoints.length; gaussPointIndex1++) {\n // 1D solid heat transfer\n if (meshDimension === \"1D\") {\n // Get basis functions for the current Gauss point\n const basisFunctionsAndDerivatives = basisFunctions.getBasisFunctions(gaussPoints[gaussPointIndex1]);\n\n // Perform isoparametric mapping\n const mappingResult = performIsoparametricMapping1D({\n basisFunction: basisFunctionsAndDerivatives.basisFunction,\n basisFunctionDerivKsi: basisFunctionsAndDerivatives.basisFunctionDerivKsi,\n nodesXCoordinates,\n localToGlobalMap,\n numNodes,\n });\n\n // Extract mapping results\n const { detJacobian, basisFunctionDerivX } = mappingResult;\n\n // Computation of Galerkin's residuals and Jacobian matrix\n for (let localNodeIndex1 = 0; localNodeIndex1 < numNodes; localNodeIndex1++) {\n let localToGlobalMap1 = localToGlobalMap[localNodeIndex1];\n // residualVector is zero for this case\n\n for (let localNodeIndex2 = 0; localNodeIndex2 < numNodes; localNodeIndex2++) {\n let localToGlobalMap2 = localToGlobalMap[localNodeIndex2];\n jacobianMatrix[localToGlobalMap1][localToGlobalMap2] +=\n -gaussWeights[gaussPointIndex1] *\n detJacobian *\n (basisFunctionDerivX[localNodeIndex1] * basisFunctionDerivX[localNodeIndex2]);\n }\n }\n }\n // 2D solid heat transfer\n else if (meshDimension === \"2D\") {\n for (let gaussPointIndex2 = 0; gaussPointIndex2 < gaussPoints.length; gaussPointIndex2++) {\n // Get basis functions for the current Gauss point\n const basisFunctionsAndDerivatives = basisFunctions.getBasisFunctions(\n gaussPoints[gaussPointIndex1],\n gaussPoints[gaussPointIndex2]\n );\n\n // Perform isoparametric mapping\n const mappingResult = performIsoparametricMapping2D({\n basisFunction: basisFunctionsAndDerivatives.basisFunction,\n basisFunctionDerivKsi: basisFunctionsAndDerivatives.basisFunctionDerivKsi,\n basisFunctionDerivEta: basisFunctionsAndDerivatives.basisFunctionDerivEta,\n nodesXCoordinates,\n nodesYCoordinates,\n localToGlobalMap,\n numNodes,\n });\n\n // Extract mapping results\n const { detJacobian, basisFunctionDerivX, basisFunctionDerivY } = mappingResult;\n\n // Computation of Galerkin's residuals and Jacobian matrix\n for (let localNodeIndex1 = 0; localNodeIndex1 < numNodes; localNodeIndex1++) {\n let localToGlobalMap1 = localToGlobalMap[localNodeIndex1];\n // residualVector is zero for this case\n\n for (let localNodeIndex2 = 0; localNodeIndex2 < numNodes; localNodeIndex2++) {\n let localToGlobalMap2 = localToGlobalMap[localNodeIndex2];\n jacobianMatrix[localToGlobalMap1][localToGlobalMap2] +=\n -gaussWeights[gaussPointIndex1] *\n gaussWeights[gaussPointIndex2] *\n detJacobian *\n (basisFunctionDerivX[localNodeIndex1] * basisFunctionDerivX[localNodeIndex2] +\n basisFunctionDerivY[localNodeIndex1] * basisFunctionDerivY[localNodeIndex2]);\n }\n }\n }\n }\n }\n }\n\n // Apply boundary conditions\n const thermalBoundaryConditions = new ThermalBoundaryConditions(\n boundaryConditions,\n boundaryElements,\n nop,\n meshDimension,\n elementOrder\n );\n\n // Impose Convection boundary conditions\n thermalBoundaryConditions.imposeConvectionBoundaryConditions(\n residualVector,\n jacobianMatrix,\n gaussPoints,\n gaussWeights,\n nodesXCoordinates,\n nodesYCoordinates,\n basisFunctions\n );\n\n // Impose ConstantTemp boundary conditions\n thermalBoundaryConditions.imposeConstantTempBoundaryConditions(residualVector, jacobianMatrix);\n basicLog(\"Solid heat transfer matrix assembly completed\");\n\n return {\n jacobianMatrix,\n residualVector,\n };\n}\n\n/**\n * Function to assemble the local Jacobian matrix and residual vector for the solid heat transfer model when using the frontal system solver\n * @param {number} elementIndex - Index of the element being processed\n * @param {array} nop - Nodal connectivity array (element-to-node mapping)\n * @param {object} meshData - Object containing prepared mesh data\n * @param {object} basisFunctions - Object containing basis functions and their derivatives\n * @param {object} FEAData - Object containing FEA-related data\n * @returns {object} An object containing:\n * - localJacobianMatrix: Local Jacobian matrix\n * - localResidualVector: Residual vector contributions\n * - ngl: Array mapping local node indices to global node indices\n */\nexport function assembleHeatConductionFront({ elementIndex, nop, meshData, basisFunctions, FEAData }) {\n // Extract numerical integration parameters and mesh coordinates\n const { gaussPoints, gaussWeights, numNodes } = FEAData;\n const { nodesXCoordinates, nodesYCoordinates, meshDimension } = meshData;\n\n // Initialize local Jacobian matrix and local residual vector\n const localJacobianMatrix = Array(numNodes)\n .fill()\n .map(() => Array(numNodes).fill(0));\n const localResidualVector = Array(numNodes).fill(0);\n\n // Build the mapping from local node indices to global node indices\n const ngl = Array(numNodes);\n const localToGlobalMap = Array(numNodes);\n for (let localNodeIndex = 0; localNodeIndex < numNodes; localNodeIndex++) {\n ngl[localNodeIndex] = Math.abs(nop[elementIndex][localNodeIndex]);\n localToGlobalMap[localNodeIndex] = Math.abs(nop[elementIndex][localNodeIndex]) - 1;\n }\n\n // Loop over Gauss points\n if (meshDimension === \"1D\") {\n // 1D solid heat transfer\n for (let gaussPointIndex1 = 0; gaussPointIndex1 < gaussPoints.length; gaussPointIndex1++) {\n // Get basis functions for the current Gauss point\n const { basisFunction, basisFunctionDerivKsi } = basisFunctions.getBasisFunctions(\n gaussPoints[gaussPointIndex1]\n );\n\n // Perform isoparametric mapping\n const { detJacobian, basisFunctionDerivX } = performIsoparametricMapping1D({\n basisFunction,\n basisFunctionDerivKsi,\n nodesXCoordinates,\n localToGlobalMap,\n numNodes,\n });\n\n // Computation of Galerkin's residuals and local Jacobian matrix\n for (let localNodeIndex1 = 0; localNodeIndex1 < numNodes; localNodeIndex1++) {\n for (let localNodeIndex2 = 0; localNodeIndex2 < numNodes; localNodeIndex2++) {\n localJacobianMatrix[localNodeIndex1][localNodeIndex2] -=\n gaussWeights[gaussPointIndex1] *\n detJacobian *\n (basisFunctionDerivX[localNodeIndex1] * basisFunctionDerivX[localNodeIndex2]);\n }\n }\n }\n } else if (meshDimension === \"2D\") {\n // 2D solid heat transfer\n for (let gaussPointIndex1 = 0; gaussPointIndex1 < gaussPoints.length; gaussPointIndex1++) {\n for (let gaussPointIndex2 = 0; gaussPointIndex2 < gaussPoints.length; gaussPointIndex2++) {\n // Get basis functions for the current Gauss point\n const { basisFunction, basisFunctionDerivKsi, basisFunctionDerivEta } =\n basisFunctions.getBasisFunctions(gaussPoints[gaussPointIndex1], gaussPoints[gaussPointIndex2]);\n\n // Create mapping from local element space to global mesh (convert to 0-based indexing)\n const localToGlobalMap = ngl.map((globalIndex) => globalIndex - 1);\n\n // Perform isoparametric mapping\n const { detJacobian, basisFunctionDerivX, basisFunctionDerivY } = performIsoparametricMapping2D({\n basisFunction,\n basisFunctionDerivKsi,\n basisFunctionDerivEta,\n nodesXCoordinates,\n nodesYCoordinates,\n localToGlobalMap,\n numNodes,\n });\n\n // Computation of Galerkin's residuals and local Jacobian matrix\n for (let localNodeIndex1 = 0; localNodeIndex1 < numNodes; localNodeIndex1++) {\n for (let localNodeIndex2 = 0; localNodeIndex2 < numNodes; localNodeIndex2++) {\n localJacobianMatrix[localNodeIndex1][localNodeIndex2] -=\n gaussWeights[gaussPointIndex1] *\n gaussWeights[gaussPointIndex2] *\n detJacobian *\n (basisFunctionDerivX[localNodeIndex1] * basisFunctionDerivX[localNodeIndex2] +\n basisFunctionDerivY[localNodeIndex1] * basisFunctionDerivY[localNodeIndex2]);\n }\n }\n }\n }\n }\n\n return { localJacobianMatrix, localResidualVector, ngl };\n}\n","// ______ ______ _____ _ _ //\n// | ____| ____| /\\ / ____| (_) | | //\n// | |__ | |__ / \\ | (___ ___ ____ _ ____ | |_ //\n// | __| | __| / /\\ \\ \\___ \\ / __| __| | _ \\| __| //\n// | | | |____ / ____ \\ ____) | (__| | | | |_) | | //\n// |_| |______/_/ \\_\\_____/ \\___|_| |_| __/| | //\n// | | | | //\n// |_| | |_ //\n// Website: https://feascript.com/ \\__| //\n\n// Internal imports\nimport { basicLog, debugLog, errorLog } from \"../utilities/loggingScript.js\";\n\n/**\n * Class to handle generic boundary conditions application\n */\nexport class GenericBoundaryConditions {\n /**\n * Constructor to initialize the GenericBoundaryConditions class\n * @param {object} boundaryConditions - Object containing boundary conditions for the finite element analysis\n * @param {array} boundaryElements - Array containing elements that belong to each boundary\n * @param {array} nop - Nodal numbering (NOP) array representing the connectivity between elements and nodes\n * @param {string} meshDimension - The dimension of the mesh (e.g., \"2D\")\n * @param {string} elementOrder - The order of elements (e.g., \"linear\", \"quadratic\")\n */\n constructor(boundaryConditions, boundaryElements, nop, meshDimension, elementOrder) {\n this.boundaryConditions = boundaryConditions;\n this.boundaryElements = boundaryElements;\n this.nop = nop;\n this.meshDimension = meshDimension;\n this.elementOrder = elementOrder;\n }\n\n /**\n * Function to impose Dirichlet boundary conditions\n * @param {array} residualVector - The residual vector to be modified\n * @param {array} jacobianMatrix - The Jacobian matrix to be modified\n *\n * For consistency across both linear and nonlinear formulations,\n * this project always refers to the assembled right-hand side vector\n * as `residualVector` and the assembled system matrix as `jacobianMatrix`.\n *\n * In linear problems `jacobianMatrix` is equivalent to the\n * classic stiffness/conductivity matrix and `residualVector`\n * corresponds to the traditional load (RHS) vector.\n */\n imposeDirichletBoundaryConditions(residualVector, jacobianMatrix) {\n if (this.meshDimension === \"1D\") {\n Object.keys(this.boundaryConditions).forEach((boundaryKey) => {\n if (this.boundaryConditions[boundaryKey][0] === \"constantValue\") {\n const value = this.boundaryConditions[boundaryKey][1];\n debugLog(`Boundary ${boundaryKey}: Applying constant value of ${value} (Dirichlet condition)`);\n this.boundaryElements[boundaryKey].forEach(([elementIndex, side]) => {\n if (this.elementOrder === \"linear\") {\n const boundarySides = {\n 0: [0], // Node at the left side of the reference element\n 1: [1], // Node at the right side of the reference element\n };\n boundarySides[side].forEach((nodeIndex) => {\n const globalNodeIndex = this.nop[elementIndex][nodeIndex] - 1;\n debugLog(\n ` - Applied constant value to node ${globalNodeIndex + 1} (element ${\n elementIndex + 1\n }, local node ${nodeIndex + 1})`\n );\n // Set the residual vector to the value\n residualVector[globalNodeIndex] = value;\n // Set the Jacobian matrix row to zero\n for (let colIndex = 0; colIndex < residualVector.length; colIndex++) {\n jacobianMatrix[globalNodeIndex][colIndex] = 0;\n }\n // Set the diagonal entry of the Jacobian matrix to one\n jacobianMatrix[globalNodeIndex][globalNodeIndex] = 1;\n });\n } else if (this.elementOrder === \"quadratic\") {\n const boundarySides = {\n 0: [0], // Node at the left side of the reference element\n 2: [2], // Node at the right side of the reference element\n };\n boundarySides[side].forEach((nodeIndex) => {\n const globalNodeIndex = this.nop[elementIndex][nodeIndex] - 1;\n debugLog(\n ` - Applied constant value to node ${globalNodeIndex + 1} (element ${\n elementIndex + 1\n }, local node ${nodeIndex + 1})`\n );\n // Set the residual vector to the value\n residualVector[globalNodeIndex] = value;\n // Set the Jacobian matrix row to zero\n for (let colIndex = 0; colIndex < residualVector.length; colIndex++) {\n jacobianMatrix[globalNodeIndex][colIndex] = 0;\n }\n // Set the diagonal entry of the Jacobian matrix to one\n jacobianMatrix[globalNodeIndex][globalNodeIndex] = 1;\n });\n }\n });\n }\n });\n } else if (this.meshDimension === \"2D\") {\n Object.keys(this.boundaryConditions).forEach((boundaryKey) => {\n if (this.boundaryConditions[boundaryKey][0] === \"constantValue\") {\n const value = this.boundaryConditions[boundaryKey][1];\n debugLog(`Boundary ${boundaryKey}: Applying constant value of ${value} (Dirichlet condition)`);\n this.boundaryElements[boundaryKey].forEach(([elementIndex, side]) => {\n if (this.elementOrder === \"linear\") {\n const boundarySides = {\n 0: [0, 2], // Nodes at the bottom side of the reference element\n 1: [0, 1], // Nodes at the left side of the reference element\n 2: [1, 3], // Nodes at the top side of the reference element\n 3: [2, 3], // Nodes at the right side of the reference element\n };\n boundarySides[side].forEach((nodeIndex) => {\n const globalNodeIndex = this.nop[elementIndex][nodeIndex] - 1;\n debugLog(\n ` - Applied constant value to node ${globalNodeIndex + 1} (element ${\n elementIndex + 1\n }, local node ${nodeIndex + 1})`\n );\n // Set the residual vector to the value\n residualVector[globalNodeIndex] = value;\n // Set the Jacobian matrix row to zero\n for (let colIndex = 0; colIndex < residualVector.length; colIndex++) {\n jacobianMatrix[globalNodeIndex][colIndex] = 0;\n }\n // Set the diagonal entry of the Jacobian matrix to one\n jacobianMatrix[globalNodeIndex][globalNodeIndex] = 1;\n });\n } else if (this.elementOrder === \"quadratic\") {\n const boundarySides = {\n 0: [0, 3, 6], // Nodes at the bottom side of the reference element\n 1: [0, 1, 2], // Nodes at the left side of the reference element\n 2: [2, 5, 8], // Nodes at the top side of the reference element\n 3: [6, 7, 8], // Nodes at the right side of the reference element\n };\n boundarySides[side].forEach((nodeIndex) => {\n const globalNodeIndex = this.nop[elementIndex][nodeIndex] - 1;\n debugLog(\n ` - Applied constant value to node ${globalNodeIndex + 1} (element ${\n elementIndex + 1\n }, local node ${nodeIndex + 1})`\n );\n // Set the residual vector to the value\n residualVector[globalNodeIndex] = value;\n // Set the Jacobian matrix row to zero\n for (let colIndex = 0; colIndex < residualVector.length; colIndex++) {\n jacobianMatrix[globalNodeIndex][colIndex] = 0;\n }\n // Set the diagonal entry of the Jacobian matrix to one\n jacobianMatrix[globalNodeIndex][globalNodeIndex] = 1;\n });\n }\n });\n }\n });\n }\n }\n\n /**\n * Function to impose constant value (Dirichlet) boundary conditions for the frontal solver\n * @param {array} nodeConstraintCode - Array indicating boundary condition code for each node\n * @param {array} boundaryValues - Array containing boundary condition values\n */\n imposeConstantValueBoundaryConditionsFront(nodeConstraintCode, boundaryValues) {\n if (this.meshDimension === \"1D\") {\n Object.keys(this.boundaryConditions).forEach((boundaryKey) => {\n if (this.boundaryConditions[boundaryKey][0] === \"constantValue\") {\n const value = this.boundaryConditions[boundaryKey][1];\n debugLog(`Boundary ${boundaryKey}: Applying constant value of ${value} (Dirichlet condition)`);\n this.boundaryElements[boundaryKey].forEach(([elementIndex, side]) => {\n if (this.elementOrder === \"linear\") {\n const boundarySides = {\n 0: [0], // Node at the left side of the reference element\n 1: [1], // Node at the right side of the reference element\n };\n boundarySides[side].forEach((nodeIndex) => {\n const globalNodeIndex = this.nop[elementIndex][nodeIndex] - 1;\n debugLog(\n ` - Applied constant value to node ${globalNodeIndex + 1} (element ${\n elementIndex + 1\n }, local node ${nodeIndex + 1})`\n );\n nodeConstraintCode[globalNodeIndex] = 1;\n boundaryValues[globalNodeIndex] = value;\n });\n } else if (this.elementOrder === \"quadratic\") {\n const boundarySides = {\n 0: [0], // Node at the left side of the reference element\n 2: [2], // Node at the right side of the reference element\n };\n boundarySides[side].forEach((nodeIndex) => {\n const globalNodeIndex = this.nop[elementIndex][nodeIndex] - 1;\n debugLog(\n ` - Applied constant value to node ${globalNodeIndex + 1} (element ${\n elementIndex + 1\n }, local node ${nodeIndex + 1})`\n );\n nodeConstraintCode[globalNodeIndex] = 1;\n boundaryValues[globalNodeIndex] = value;\n });\n }\n });\n }\n });\n } else if (this.meshDimension === \"2D\") {\n Object.keys(this.boundaryConditions).forEach((boundaryKey) => {\n if (this.boundaryConditions[boundaryKey][0] === \"constantValue\") {\n const value = this.boundaryConditions[boundaryKey][1];\n debugLog(`Boundary ${boundaryKey}: Applying constant value of ${value} (Dirichlet condition)`);\n this.boundaryElements[boundaryKey].forEach(([elementIndex, side]) => {\n if (this.elementOrder === \"linear\") {\n const boundarySides = {\n 0: [0, 2], // Nodes at the bottom side of the reference element\n 1: [0, 1], // Nodes at the left side of the reference element\n 2: [1, 3], // Nodes at the top side of the reference element\n 3: [2, 3], // Nodes at the right side of the reference element\n };\n boundarySides[side].forEach((nodeIndex) => {\n const globalNodeIndex = this.nop[elementIndex][nodeIndex] - 1;\n debugLog(\n ` - Applied constant value to node ${globalNodeIndex + 1} (element ${\n elementIndex + 1\n }, local node ${nodeIndex + 1})`\n );\n nodeConstraintCode[globalNodeIndex] = 1;\n boundaryValues[globalNodeIndex] = value;\n });\n } else if (this.elementOrder === \"quadratic\") {\n const boundarySides = {\n 0: [0, 3, 6], // Nodes at the bottom side of the reference element\n 1: [0, 1, 2], // Nodes at the left side of the reference element\n 2: [2, 5, 8], // Nodes at the top side of the reference element\n 3: [6, 7, 8], // Nodes at the right side of the reference element\n };\n boundarySides[side].forEach((nodeIndex) => {\n const globalNodeIndex = this.nop[elementIndex][nodeIndex] - 1;\n debugLog(\n ` - Applied constant value to node ${globalNodeIndex + 1} (element ${\n elementIndex + 1\n }, local node ${nodeIndex + 1})`\n );\n nodeConstraintCode[globalNodeIndex] = 1;\n boundaryValues[globalNodeIndex] = value;\n });\n }\n });\n }\n });\n }\n }\n}\n","// ______ ______ _____ _ _ //\n// | ____| ____| /\\ / ____| (_) | | //\n// | |__ | |__ / \\ | (___ ___ ____ _ ____ | |_ //\n// | __| | __| / /\\ \\ \\___ \\ / __| __| | _ \\| __| //\n// | | | |____ / ____ \\ ____) | (__| | | | |_) | | //\n// |_| |______/_/ \\_\\_____/ \\___|_| |_| __/| | //\n// | | | | //\n// |_| | |_ //\n// Website: https://feascript.com/ \\__| //\n\n// Internal imports\nimport { GenericBoundaryConditions } from \"./genericBoundaryConditionsScript.js\";\nimport {\n initializeFEA,\n performIsoparametricMapping1D,\n performIsoparametricMapping2D,\n} from \"../mesh/meshUtilsScript.js\";\nimport { basicLog, debugLog } from \"../utilities/loggingScript.js\";\n\n// Base viscous term that remains when eikonal equation is fully activated\nconst baseEikonalViscousTerm = 1e-2;\n\n/**\n * Function to assemble the Jacobian matrix and residuals vector for the front propagation model\n * @param {object} meshData - Object containing prepared mesh data\n * @param {object} boundaryConditions - Object containing boundary conditions for the finite element analysis\n * @param {array} solutionVector - The solution vector for non-linear equations\n * @param {number} eikonalActivationFlag - Activation parameter for the eikonal equation\n * @returns {object} An object containing:\n * - jacobianMatrix: The assembled Jacobian matrix\n * - residualVector: The assembled residual vector\n */\nexport function assembleFrontPropagationMat(\n meshData,\n boundaryConditions,\n solutionVector,\n eikonalActivationFlag\n) {\n basicLog(\"Starting front propagation matrix assembly...\");\n\n // Calculate eikonal viscous term\n let eikonalViscousTerm = 1 - eikonalActivationFlag + baseEikonalViscousTerm; // Viscous term for the front propagation (eikonal) equation\n debugLog(`eikonalViscousTerm: ${eikonalViscousTerm}`);\n debugLog(`eikonalActivationFlag: ${eikonalActivationFlag}`);\n\n // Extract mesh data\n const {\n nodesXCoordinates,\n nodesYCoordinates,\n nop,\n boundaryElements,\n totalElements,\n meshDimension,\n elementOrder,\n } = meshData;\n\n // Initialize FEA components\n const FEAData = initializeFEA(meshData);\n const {\n residualVector,\n jacobianMatrix,\n localToGlobalMap,\n basisFunctions,\n gaussPoints,\n gaussWeights,\n numNodes,\n } = FEAData;\n\n // Matrix assembly\n for (let elementIndex = 0; elementIndex < totalElements; elementIndex++) {\n // Map local element nodes to global mesh nodes\n for (let localNodeIndex = 0; localNodeIndex < numNodes; localNodeIndex++) {\n // Subtract 1 from nop in order to start numbering from 0\n localToGlobalMap[localNodeIndex] = nop[elementIndex][localNodeIndex] - 1;\n }\n\n // Loop over Gauss points\n for (let gaussPointIndex1 = 0; gaussPointIndex1 < gaussPoints.length; gaussPointIndex1++) {\n // 1D front propagation (eikonal) equation\n if (meshDimension === \"1D\") {\n // Unsupported 1D front propagation\n errorLog(\"1D front propagation is not yet supported\");\n\n // Get basis functions for the current Gauss point\n let basisFunctionsAndDerivatives = basisFunctions.getBasisFunctions(gaussPoints[gaussPointIndex1]);\n\n // Perform isoparametric mapping\n const mappingResult = performIsoparametricMapping1D({\n basisFunction: basisFunctionsAndDerivatives.basisFunction,\n basisFunctionDerivKsi: basisFunctionsAndDerivatives.basisFunctionDerivKsi,\n nodesXCoordinates,\n localToGlobalMap,\n numNodes,\n });\n\n // Extract mapping results\n const { detJacobian, basisFunctionDerivX } = mappingResult;\n const basisFunction = basisFunctionsAndDerivatives.basisFunction;\n\n // Calculate solution derivative\n let solutionDerivX = 0;\n for (let localNodeIndex = 0; localNodeIndex < numNodes; localNodeIndex++) {\n solutionDerivX +=\n solutionVector[localToGlobalMap[localNodeIndex]] * basisFunctionDerivX[localNodeIndex];\n }\n\n // Computation of Galerkin's residuals and Jacobian matrix\n for (let localNodeIndex1 = 0; localNodeIndex1 < numNodes; localNodeIndex1++) {\n let localToGlobalMap1 = localToGlobalMap[localNodeIndex1];\n // residualVector\n // TODO residualVector calculation here\n\n for (let localNodeIndex2 = 0; localNodeIndex2 < numNodes; localNodeIndex2++) {\n let localToGlobalMap2 = localToGlobalMap[localNodeIndex2];\n // jacobianMatrix\n // TODO jacobianMatrix calculation here\n }\n }\n }\n // 2D front propagation (eikonal) equation\n else if (meshDimension === \"2D\") {\n for (let gaussPointIndex2 = 0; gaussPointIndex2 < gaussPoints.length; gaussPointIndex2++) {\n // Get basis functions for the current Gauss point\n let basisFunctionsAndDerivatives = basisFunctions.getBasisFunctions(\n gaussPoints[gaussPointIndex1],\n gaussPoints[gaussPointIndex2]\n );\n\n // Perform isoparametric mapping\n const mappingResult = performIsoparametricMapping2D({\n basisFunction: basisFunctionsAndDerivatives.basisFunction,\n basisFunctionDerivKsi: basisFunctionsAndDerivatives.basisFunctionDerivKsi,\n basisFunctionDerivEta: basisFunctionsAndDerivatives.basisFunctionDerivEta,\n nodesXCoordinates,\n nodesYCoordinates,\n localToGlobalMap,\n numNodes,\n });\n\n // Extract mapping results\n const { detJacobian, basisFunctionDerivX, basisFunctionDerivY } = mappingResult;\n const basisFunction = basisFunctionsAndDerivatives.basisFunction;\n\n // Calculate solution derivatives\n let solutionDerivX = 0;\n let solutionDerivY = 0;\n for (let localNodeIndex = 0; localNodeIndex < numNodes; localNodeIndex++) {\n solutionDerivX +=\n solutionVector[localToGlobalMap[localNodeIndex]] * basisFunctionDerivX[localNodeIndex];\n solutionDerivY +=\n solutionVector[localToGlobalMap[localNodeIndex]] * basisFunctionDerivY[localNodeIndex];\n }\n\n // Computation of Galerkin's residuals and Jacobian matrix\n for (let localNodeIndex1 = 0; localNodeIndex1 < numNodes; localNodeIndex1++) {\n let localToGlobalMap1 = localToGlobalMap[localNodeIndex1];\n\n // residualVector: Viscous term contribution (to stabilize the solution)\n residualVector[localToGlobalMap1] +=\n eikonalViscousTerm *\n gaussWeights[gaussPointIndex1] *\n gaussWeights[gaussPointIndex2] *\n detJacobian *\n basisFunctionDerivX[localNodeIndex1] *\n solutionDerivX +\n eikonalViscousTerm *\n gaussWeights[gaussPointIndex1] *\n gaussWeights[gaussPointIndex2] *\n detJacobian *\n basisFunctionDerivY[localNodeIndex1] *\n solutionDerivY;\n\n // residualVector: Eikonal equation contribution\n if (eikonalActivationFlag !== 0) {\n residualVector[localToGlobalMap1] +=\n eikonalActivationFlag *\n (gaussWeights[gaussPointIndex1] *\n gaussWeights[gaussPointIndex2] *\n detJacobian *\n basisFunction[localNodeIndex1] *\n Math.sqrt(solutionDerivX ** 2 + solutionDerivY ** 2) -\n gaussWeights[gaussPointIndex1] *\n gaussWeights[gaussPointIndex2] *\n detJacobian *\n basisFunction[localNodeIndex1]);\n }\n\n for (let localNodeIndex2 = 0; localNodeIndex2 < numNodes; localNodeIndex2++) {\n let localToGlobalMap2 = localToGlobalMap[localNodeIndex2];\n\n // jacobianMatrix: Viscous term contribution\n jacobianMatrix[localToGlobalMap1][localToGlobalMap2] +=\n -eikonalViscousTerm *\n gaussWeights[gaussPointIndex1] *\n gaussWeights[gaussPointIndex2] *\n detJacobian *\n (basisFunctionDerivX[localNodeIndex1] * basisFunctionDerivX[localNodeIndex2] +\n basisFunctionDerivY[localNodeIndex1] * basisFunctionDerivY[localNodeIndex2]);\n\n // jacobianMatrix: Eikonal equation contribution\n if (eikonalActivationFlag !== 0) {\n jacobianMatrix[localToGlobalMap1][localToGlobalMap2] +=\n eikonalActivationFlag *\n (-(\n detJacobian *\n solutionDerivX *\n basisFunction[localNodeIndex1] *\n gaussWeights[gaussPointIndex1] *\n gaussWeights[gaussPointIndex2]\n ) /\n Math.sqrt(solutionDerivX ** 2 + solutionDerivY ** 2 + 1e-8)) *\n basisFunctionDerivX[localNodeIndex2] -\n eikonalActivationFlag *\n ((detJacobian *\n solutionDerivY *\n basisFunction[localNodeIndex1] *\n gaussWeights[gaussPointIndex1] *\n gaussWeights[gaussPointIndex2]) /\n Math.sqrt(solutionDerivX ** 2 + solutionDerivY ** 2 + 1e-8)) *\n basisFunctionDerivY[localNodeIndex2];\n }\n }\n }\n }\n }\n }\n }\n\n // Apply boundary conditions\n const genericBoundaryConditions = new GenericBoundaryConditions(\n boundaryConditions,\n boundaryElements,\n nop,\n meshDimension,\n elementOrder\n );\n\n // Impose Dirichlet boundary conditions\n genericBoundaryConditions.imposeDirichletBoundaryConditions(residualVector, jacobianMatrix);\n basicLog(\"Front propagation matrix assembly completed\");\n\n return {\n jacobianMatrix,\n residualVector,\n };\n}\n\n/**\n * Function to assemble the local Jacobian matrix and residual vector for the front propagation model when using the frontal system solver\n * @param {number} elementIndex - Index of the element being processed\n * @param {array} nop - Nodal connectivity array (element-to-node mapping)\n * @param {object} meshData - Object containing prepared mesh data\n * @param {object} basisFunctions - Object containing basis functions and their derivatives\n * @param {object} FEAData - Object containing FEA-related data\n * @param {array} solutionVector - The solution vector for non-linear equations\n * @param {number} eikonalActivationFlag - Activation parameter for the eikonal equation\n * @returns {object} An object containing:\n * - localJacobianMatrix: Local Jacobian matrix\n * - residualVector: Residual vector contributions\n * - ngl: Array mapping local node indices to global node indices\n */\nexport function assembleFrontPropagationFront({\n elementIndex,\n nop,\n meshData,\n basisFunctions,\n FEAData,\n solutionVector,\n eikonalActivationFlag,\n}) {\n // Extract numerical integration parameters and mesh coordinates\n const { gaussPoints, gaussWeights, numNodes } = FEAData;\n const { nodesXCoordinates, nodesYCoordinates, meshDimension } = meshData;\n\n // Calculate eikonal viscous term\n let eikonalViscousTerm = 1 - eikonalActivationFlag + baseEikonalViscousTerm; // Viscous term for the front propagation (eikonal) equation\n\n // Initialize local Jacobian matrix and local residual vector\n const localJacobianMatrix = Array(numNodes)\n .fill()\n .map(() => Array(numNodes).fill(0));\n const localResidualVector = Array(numNodes).fill(0);\n\n // Build the mapping from local node indices to global node indices\n const ngl = Array(numNodes);\n const localToGlobalMap = Array(numNodes);\n for (let localNodeIndex = 0; localNodeIndex < numNodes; localNodeIndex++) {\n ngl[localNodeIndex] = Math.abs(nop[elementIndex][localNodeIndex]);\n localToGlobalMap[localNodeIndex] = Math.abs(nop[elementIndex][localNodeIndex]) - 1;\n }\n\n // Loop over Gauss points\n for (let gaussPointIndex1 = 0; gaussPointIndex1 < gaussPoints.length; gaussPointIndex1++) {\n // 1D front propagation (eikonal) equation\n if (meshDimension === \"1D\") {\n // Unsupported 1D front propagation\n errorLog(\"1D front propagation is not yet supported\");\n\n // Get basis functions for the current Gauss point\n let basisFunctionsAndDerivatives = basisFunctions.getBasisFunctions(gaussPoints[gaussPointIndex1]);\n\n // Perform isoparametric mapping\n const mappingResult = performIsoparametricMapping1D({\n basisFunction: basisFunctionsAndDerivatives.basisFunction,\n basisFunctionDerivKsi: basisFunctionsAndDerivatives.basisFunctionDerivKsi,\n nodesXCoordinates,\n localToGlobalMap,\n numNodes,\n });\n\n // Extract mapping results\n const { detJacobian, basisFunctionDerivX } = mappingResult;\n const basisFunction = basisFunctionsAndDerivatives.basisFunction;\n\n // Calculate solution derivative\n let solutionDerivX = 0;\n for (let localNodeIndex = 0; localNodeIndex < numNodes; localNodeIndex++) {\n solutionDerivX +=\n solutionVector[localToGlobalMap[localNodeIndex]] * basisFunctionDerivX[localNodeIndex];\n }\n\n // Computation of Galerkin's residuals and Jacobian matrix\n for (let localNodeIndex1 = 0; localNodeIndex1 < numNodes; localNodeIndex1++) {\n let localToGlobalMap1 = localToGlobalMap[localNodeIndex1];\n // residualVector\n // TODO residualVector calculation here\n\n for (let localNodeIndex2 = 0; localNodeIndex2 < numNodes; localNodeIndex2++) {\n let localToGlobalMap2 = localToGlobalMap[localNodeIndex2];\n // localJacobianMatrix\n // TODO localJacobianMatrix calculation here\n }\n }\n // 2D front propagation (eikonal) equation\n } else if (meshDimension === \"2D\") {\n for (let gaussPointIndex2 = 0; gaussPointIndex2 < gaussPoints.length; gaussPointIndex2++) {\n // Get basis functions for the current Gauss point\n const { basisFunction, basisFunctionDerivKsi, basisFunctionDerivEta } =\n basisFunctions.getBasisFunctions(gaussPoints[gaussPointIndex1], gaussPoints[gaussPointIndex2]);\n\n // Perform isoparametric mapping\n const { detJacobian, basisFunctionDerivX, basisFunctionDerivY } = performIsoparametricMapping2D({\n basisFunction,\n basisFunctionDerivKsi,\n basisFunctionDerivEta,\n nodesXCoordinates,\n nodesYCoordinates,\n localToGlobalMap,\n numNodes,\n });\n\n // Calculate solution derivatives\n let solutionDerivX = 0;\n let solutionDerivY = 0;\n for (let localNodeIndex = 0; localNodeIndex < numNodes; localNodeIndex++) {\n solutionDerivX +=\n solutionVector[localToGlobalMap[localNodeIndex]] * basisFunctionDerivX[localNodeIndex];\n solutionDerivY +=\n solutionVector[localToGlobalMap[localNodeIndex]] * basisFunctionDerivY[localNodeIndex];\n }\n\n // Computation of Galerkin's residuals and Jacobian matrix\n for (let localNodeIndex1 = 0; localNodeIndex1 < numNodes; localNodeIndex1++) {\n let localToGlobalMap1 = localToGlobalMap[localNodeIndex1];\n // Viscous term contribution\n localResidualVector[localNodeIndex1] +=\n eikonalViscousTerm *\n gaussWeights[gaussPointIndex1] *\n gaussWeights[gaussPointIndex2] *\n detJacobian *\n basisFunctionDerivX[localNodeIndex1] *\n solutionDerivX +\n eikonalViscousTerm *\n gaussWeights[gaussPointIndex1] *\n gaussWeights[gaussPointIndex2] *\n detJacobian *\n basisFunctionDerivY[localNodeIndex1] *\n solutionDerivY;\n\n // Eikonal equation contribution\n if (eikonalActivationFlag !== 0) {\n localResidualVector[localNodeIndex1] +=\n eikonalActivationFlag *\n (gaussWeights[gaussPointIndex1] *\n gaussWeights[gaussPointIndex2] *\n detJacobian *\n basisFunction[localNodeIndex1] *\n Math.sqrt(solutionDerivX ** 2 + solutionDerivY ** 2) -\n gaussWeights[gaussPointIndex1] *\n gaussWeights[gaussPointIndex2] *\n detJacobian *\n basisFunction[localNodeIndex1]);\n }\n\n for (let localNodeIndex2 = 0; localNodeIndex2 < numNodes; localNodeIndex2++) {\n // Viscous term contribution\n localJacobianMatrix[localNodeIndex1][localNodeIndex2] -=\n eikonalViscousTerm *\n gaussWeights[gaussPointIndex1] *\n gaussWeights[gaussPointIndex2] *\n detJacobian *\n (basisFunctionDerivX[localNodeIndex1] * basisFunctionDerivX[localNodeIndex2] +\n basisFunctionDerivY[localNodeIndex1] * basisFunctionDerivY[localNodeIndex2]);\n\n // Eikonal equation contribution\n if (eikonalActivationFlag !== 0) {\n localJacobianMatrix[localNodeIndex1][localNodeIndex2] +=\n eikonalActivationFlag *\n (-(\n detJacobian *\n solutionDerivX *\n basisFunction[localNodeIndex1] *\n gaussWeights[gaussPointIndex1] *\n gaussWeights[gaussPointIndex2]\n ) /\n Math.sqrt(solutionDerivX ** 2 + solutionDerivY ** 2 + 1e-8)) *\n basisFunctionDerivX[localNodeIndex2] -\n eikonalActivationFlag *\n ((detJacobian *\n solutionDerivY *\n basisFunction[localNodeIndex1] *\n gaussWeights[gaussPointIndex1] *\n gaussWeights[gaussPointIndex2]) /\n Math.sqrt(solutionDerivX ** 2 + solutionDerivY ** 2 + 1e-8)) *\n basisFunctionDerivY[localNodeIndex2];\n }\n }\n }\n }\n }\n }\n\n return { localJacobianMatrix, localResidualVector, ngl };\n}\n","// ______ ______ _____ _ _ //\n// | ____| ____| /\\ / ____| (_) | | //\n// | |__ | |__ / \\ | (___ ___ ____ _ ____ | |_ //\n// | __| | __| / /\\ \\ \\___ \\ / __| __| | _ \\| __| //\n// | | | |____ / ____ \\ ____) | (__| | | | |_) | | //\n// |_| |______/_/ \\_\\_____/ \\___|_| |_| __/| | //\n// | | | | //\n// |_| | |_ //\n// Website: https://feascript.com/ \\__| //\n\n// Internal imports\nimport { BasisFunctions } from \"../mesh/basisFunctionsScript.js\";\nimport { initializeFEA } from \"../mesh/meshUtilsScript.js\";\nimport { assembleHeatConductionFront } from \"../solvers/heatConductionScript.js\";\nimport { ThermalBoundaryConditions } from \"../solvers/thermalBoundaryConditionsScript.js\";\nimport { assembleFrontPropagationFront } from \"../solvers/frontPropagationScript.js\";\nimport { GenericBoundaryConditions } from \"../solvers/genericBoundaryConditionsScript.js\";\nimport { basicLog, debugLog, errorLog } from \"../utilities/loggingScript.js\";\n\n// Create object templates\nconst frontalData = {};\nconst frontalState = {};\nconst elementData = { currentElementIndex: 0 };\nconst frontStorage = {};\nlet basisFunctions;\n\n/**\n * Function to run the frontal solver and obtain results for plotting\n * @param {function} assembleFront - Matrix assembler based on the physical model\n * @param {object} meshData - Object containing mesh data\n * @param {object} boundaryConditions - Object containing boundary conditions\n * @param {object} [options] - Additional options for the solver\n * @returns {object} An object containing the solution vector and node coordinates\n */\nexport function runFrontalSolver(assembleFront, meshData, boundaryConditions, options = {}) {\n // Initialize FEA components\n const FEAData = initializeFEA(meshData);\n const totalNodes = meshData.nodesXCoordinates.length;\n const numElements = meshData.totalElements;\n const numNodes = FEAData.numNodes;\n\n // Calculate required array sizes\n initializeFrontalArrays(numNodes, numElements);\n\n // Start timing for system solving (frontal algorithm)\n basicLog(\"Solving system using frontal...\");\n console.time(\"systemSolving\");\n\n // Initialize basis functions\n basisFunctions = new BasisFunctions({\n meshDimension: meshData.meshDimension,\n elementOrder: meshData.elementOrder,\n });\n\n // Copy node connectivity array into frontalData storage\n for (let elementIndex = 0; elementIndex < meshData.totalElements; elementIndex++) {\n for (let nodeIndex = 0; nodeIndex < FEAData.numNodes; nodeIndex++) {\n frontalData.nodalNumbering[elementIndex][nodeIndex] = meshData.nop[elementIndex][nodeIndex];\n }\n }\n\n // Apply Dirichlet-type boundary conditions\n // Initialize all nodes with no boundary condition\n for (let nodeIndex = 0; nodeIndex < meshData.nodesXCoordinates.length; nodeIndex++) {\n frontalData.nodeConstraintCode[nodeIndex] = 0;\n frontalData.boundaryValues[nodeIndex] = 0;\n }\n\n // Handle Dirichlet-type boundary conditions differently based on which solver is being used\n let dirichletBoundaryConditionsHandler;\n // Solid heat transfer model (heatConductionScript solver)\n if (assembleFront === assembleHeatConductionFront) {\n dirichletBoundaryConditionsHandler = new ThermalBoundaryConditions(\n boundaryConditions,\n meshData.boundaryElements,\n meshData.nop,\n meshData.meshDimension,\n meshData.elementOrder\n );\n\n dirichletBoundaryConditionsHandler.imposeConstantTempBoundaryConditionsFront(\n frontalData.nodeConstraintCode,\n frontalData.boundaryValues\n );\n // Front propagation model (frontPropagationScript solver)\n } else if (assembleFront === assembleFrontPropagationFront) {\n dirichletBoundaryConditionsHandler = new GenericBoundaryConditions(\n boundaryConditions,\n meshData.boundaryElements,\n meshData.nop,\n meshData.meshDimension,\n meshData.elementOrder\n );\n\n dirichletBoundaryConditionsHandler.imposeConstantValueBoundaryConditionsFront(\n frontalData.nodeConstraintCode,\n frontalData.boundaryValues\n );\n }\n // Initialize global residual vector\n for (let nodeIndex = 0; nodeIndex < meshData.nodesXCoordinates.length; nodeIndex++) {\n frontalData.globalResidualVector[nodeIndex] = 0;\n }\n\n frontalState.totalNodes = meshData.nodesXCoordinates.length;\n frontalState.writeFlag = 0;\n frontalState.transformationFlag = 1;\n frontalState.determinant = 1;\n\n for (let elementIndex = 0; elementIndex < meshData.totalElements; elementIndex++) {\n frontalState.nodesPerElement[elementIndex] = FEAData.numNodes;\n }\n\n // Parameters for non-linear assemblers\n frontalState.currentSolutionVector = options.solutionVector;\n frontalState.eikonalActivationFlag = options.eikonalActivationFlag;\n\n // Pass assembleFront and dirichletBoundaryConditionsHandler to runFrontalAlgorithm\n runFrontalAlgorithm(meshData, FEAData, dirichletBoundaryConditionsHandler, assembleFront);\n\n // Copy solution\n for (let nodeIndex = 0; nodeIndex < meshData.nodesXCoordinates.length; nodeIndex++) {\n frontalData.solutionVector[nodeIndex] = frontalState.globalSolutionVector[nodeIndex];\n }\n\n // Output results to console for debugging\n const { nodesXCoordinates, nodesYCoordinates } = meshData;\n for (let nodeIndex = 0; nodeIndex < meshData.nodesXCoordinates.length; nodeIndex++) {\n if (meshData.meshDimension === \"1D\") {\n // 1D case - only output X coordinates and temperature\n debugLog(\n `${nodesXCoordinates[nodeIndex].toExponential(5)} ${frontalData.solutionVector[\n nodeIndex\n ].toExponential(5)}`\n );\n } else {\n // 2D case - output X, Y coordinates and temperature\n debugLog(\n `${nodesXCoordinates[nodeIndex].toExponential(5)} ${nodesYCoordinates[nodeIndex].toExponential(\n 5\n )} ${frontalData.solutionVector[nodeIndex].toExponential(5)}`\n );\n }\n }\n\n console.timeEnd(\"systemSolving\");\n basicLog(\"System solved successfully\");\n\n const { nodesXCoordinates: finalNodesX, nodesYCoordinates: finalNodesY } = meshData;\n return {\n solutionVector: frontalData.solutionVector.slice(0, totalNodes),\n nodesCoordinates: {\n nodesXCoordinates: finalNodesX,\n nodesYCoordinates: finalNodesY,\n },\n };\n}\n\n/**\n * Function to initialize arrays dynamically based on problem size\n * @param {number} numNodes - Number of nodes per element\n * @param {number} numElements - Number of elements in the mesh\n */\nfunction initializeFrontalArrays(numNodes, numElements) {\n // Use the actual number of elements from the mesh\n frontalData.nodalNumbering = Array(numElements)\n .fill()\n .map(() => Array(numNodes).fill(0));\n frontalData.nodeConstraintCode = Array(numNodes).fill(0);\n frontalData.boundaryValues = Array(numNodes).fill(0);\n frontalData.globalResidualVector = Array(numNodes).fill(0);\n frontalData.solutionVector = Array(numNodes).fill(0);\n frontalData.topologyData = Array(numElements).fill(0);\n frontalData.lateralData = Array(numElements).fill(0);\n\n // Initialize frontalState arrays\n frontalState.writeFlag = 0;\n frontalState.totalNodes = numNodes;\n frontalState.transformationFlag = 0;\n frontalState.nodesPerElement = Array(numElements).fill(0);\n frontalState.determinant = 1;\n\n // For matrix operations, estimate required size based on problem complexity\n const systemSize = Math.max(numNodes, 2000);\n frontalState.globalSolutionVector = Array(systemSize).fill(0);\n frontalState.frontDataIndex = 0;\n\n // Initialize elementData arrays\n elementData.localJacobianMatrix = Array(numNodes)\n .fill()\n .map(() => Array(numNodes).fill(0));\n elementData.currentElementIndex = 0;\n\n // Initialize frontStorage arrays\n const frontSize = estimateFrontSize(numNodes, numElements);\n frontStorage.frontValues = Array(frontSize).fill(0);\n frontStorage.columnHeaders = Array(systemSize).fill(0);\n frontStorage.pivotRow = Array(systemSize).fill(0);\n frontStorage.pivotData = Array(frontSize).fill(0);\n}\n\n/**\n * Function to estimate the required front size\n * @param {number} numNodes - Number of of nodes per element\n * @param {number} numElements - Number of elements in the mesh\n * @returns {number} Estimated front size\n */\nfunction estimateFrontSize(numNodes, numElements) {\n const frontWidthEstimate = Math.max(Math.ceil(Math.sqrt(numElements)) * numNodes, numNodes * 2);\n return frontWidthEstimate * numElements;\n}\n// Old function to estimate the required front size\n// function estimateFrontSize(numNodes, numElements, numNodes) {\n// const frontWidthEstimate = Math.ceil(Math.sqrt(numElements) * numNodes * 2);\n// const frontSize = frontWidthEstimate * numNodes * 4;\n// return Math.max(frontSize, 10000);\n// }\n\n/**\n * Function to compute local Jacobian matrix and local residual vector\n * @param {object} meshData - Object containing mesh data\n * @param {object} FEAData - Object containing FEA-related data\n * @param {object} thermalBoundaryConditions - Object containing thermal boundary conditions\n * @param {function} assembleFront - Matrix assembler based on the physical model\n */\nfunction assembleElementContribution(meshData, FEAData, thermalBoundaryConditions, assembleFront) {\n const elementIndex = elementData.currentElementIndex - 1;\n\n // Guard against out-of-range indices\n if (elementIndex < 0 || elementIndex >= meshData.totalElements) {\n errorLog(`Skipping out-of-range elementIndex=${elementIndex} (totalElements=${meshData.totalElements})`);\n return false;\n }\n\n // Domain terms\n const { localJacobianMatrix, localResidualVector, ngl } = assembleFront({\n elementIndex,\n nop: frontalData.nodalNumbering,\n meshData,\n basisFunctions: basisFunctions,\n FEAData,\n // These are ignored by linear assemblers\n solutionVector: frontalState.currentSolutionVector,\n eikonalActivationFlag: frontalState.eikonalActivationFlag,\n });\n\n // Handle Robin-type boundary conditions differently based on which solver is being used\n let boundaryLocalJacobianMatrix = Array(FEAData.numNodes)\n .fill()\n .map(() => Array(FEAData.numNodes).fill(0));\n let boundaryResidualVector = Array(FEAData.numNodes).fill(0);\n\n // heatConductionScript solver\n if (assembleFront === assembleHeatConductionFront) {\n // Check if this element is on a Robin-type boundary\n let isOnRobinTypeBoundary = false;\n for (const boundaryKey in meshData.boundaryElements) {\n if (\n thermalBoundaryConditions.boundaryConditions[boundaryKey]?.[0] === \"convection\" &&\n meshData.boundaryElements[boundaryKey].some(([elemIdx, _]) => elemIdx === elementIndex)\n ) {\n isOnRobinTypeBoundary = true;\n break;\n }\n }\n\n // Only calculate Robin-type for elements when required\n if (isOnRobinTypeBoundary) {\n const { gaussPoints, gaussWeights } = FEAData;\n const result = thermalBoundaryConditions.imposeConvectionBoundaryConditionsFront(\n elementIndex,\n meshData.nodesXCoordinates,\n meshData.nodesYCoordinates,\n gaussPoints,\n gaussWeights,\n basisFunctions\n );\n boundaryLocalJacobianMatrix = result.localJacobianMatrix;\n boundaryResidualVector = result.localResidualVector;\n }\n } else if (assembleFront === assembleFrontPropagationFront) {\n // For now, no Robin-type boundary conditions exist for any other solver\n }\n\n // Combine domain and boundary contributions\n for (let localNodeI = 0; localNodeI < FEAData.numNodes; localNodeI++) {\n for (let localNodeJ = 0; localNodeJ < FEAData.numNodes; localNodeJ++) {\n elementData.localJacobianMatrix[localNodeI][localNodeJ] =\n localJacobianMatrix[localNodeI][localNodeJ] + boundaryLocalJacobianMatrix[localNodeI][localNodeJ];\n }\n }\n\n // Assemble local element residual\n for (let localNodeIndex = 0; localNodeIndex < FEAData.numNodes; localNodeIndex++) {\n const globalNodeIndex = ngl[localNodeIndex] - 1;\n frontalData.globalResidualVector[globalNodeIndex] +=\n localResidualVector[localNodeIndex] + boundaryResidualVector[localNodeIndex];\n }\n\n return true;\n}\n\n/**\n * Function to implement the frontal solver algorithm\n * @param {object} meshData - Object containing mesh data\n * @param {object} FEAData - Object containing FEA-related data\n * @param {object} thermalBoundaryConditions - Object containing thermal boundary conditions\n * @param {function} assembleFront - Matrix assembler based on the physical model\n */\nfunction runFrontalAlgorithm(meshData, FEAData, thermalBoundaryConditions, assembleFront) {\n // Allocate local arrays dynamically\n const totalElements = meshData.totalElements;\n const numNodes = meshData.nodesXCoordinates.length;\n const systemSize = Math.max(numNodes, frontalState.globalSolutionVector.length);\n let localDestination = Array(FEAData.numNodes).fill(0);\n let rowDestination = Array(FEAData.numNodes).fill(0);\n let rowHeaders = Array(systemSize).fill(0);\n let pivotRowIndices = Array(systemSize).fill(0);\n let pivotColumnIndices = Array(systemSize).fill(0);\n let modifiedRows = Array(systemSize).fill(0);\n let pivotColumn = Array(systemSize).fill(0);\n let frontMatrix = Array(systemSize)\n .fill()\n .map(() => Array(systemSize).fill(0));\n let rowSwapCount = Array(numNodes).fill(0);\n let columnSwapCount = Array(numNodes).fill(0);\n let lastAppearanceCheck = Array(numNodes).fill(0);\n let pivotColumnGlobalIndex; // Pivot column global index\n\n let frontDataCounter = 1;\n frontalState.writeFlag++;\n let pivotDataIndex = 1;\n let summedRows = 1;\n elementData.currentElementIndex = 0;\n\n for (let nodeIndex = 0; nodeIndex < frontalState.totalNodes; nodeIndex++) {\n rowSwapCount[nodeIndex] = 0;\n columnSwapCount[nodeIndex] = 0;\n }\n\n if (frontalState.transformationFlag !== 0) {\n // Prefront: find last appearance of each node\n for (let nodeIndex = 0; nodeIndex < frontalState.totalNodes; nodeIndex++) {\n lastAppearanceCheck[nodeIndex] = 0;\n }\n\n for (let elementIndex = 0; elementIndex < totalElements; elementIndex++) {\n let reverseElementIndex = totalElements - elementIndex - 1;\n for (\n let localNodeIndex = 0;\n localNodeIndex < frontalState.nodesPerElement[reverseElementIndex];\n localNodeIndex++\n ) {\n let globalNodeIndex = frontalData.nodalNumbering[reverseElementIndex][localNodeIndex];\n if (lastAppearanceCheck[globalNodeIndex - 1] === 0) {\n lastAppearanceCheck[globalNodeIndex - 1] = 1;\n frontalData.nodalNumbering[reverseElementIndex][localNodeIndex] =\n -frontalData.nodalNumbering[reverseElementIndex][localNodeIndex];\n }\n }\n }\n }\n\n frontalState.transformationFlag = 0;\n let columnCount = 0;\n let rowCount = 0;\n\n for (let i = 0; i < systemSize; i++) {\n for (let j = 0; j < systemSize; j++) {\n frontMatrix[j][i] = 0;\n }\n }\n\n while (true) {\n // Assemble a new element only while we still have elements\n let assembled = false;\n let numElementNodes = 0;\n let numElementColumns = 0;\n\n if (elementData.currentElementIndex < totalElements) {\n elementData.currentElementIndex++;\n assembled = assembleElementContribution(meshData, FEAData, thermalBoundaryConditions, assembleFront);\n }\n\n if (assembled) {\n const currentElement = elementData.currentElementIndex;\n numElementNodes = frontalState.nodesPerElement[currentElement - 1];\n numElementColumns = frontalState.nodesPerElement[currentElement - 1];\n\n for (let localNodeIndex = 0; localNodeIndex < numElementColumns; localNodeIndex++) {\n let globalNodeIndex = frontalData.nodalNumbering[currentElement - 1][localNodeIndex];\n let columnIndex;\n\n if (columnCount === 0) {\n columnCount++;\n localDestination[localNodeIndex] = columnCount;\n frontStorage.columnHeaders[columnCount - 1] = globalNodeIndex;\n } else {\n for (columnIndex = 0; columnIndex < columnCount; columnIndex++) {\n if (Math.abs(globalNodeIndex) === Math.abs(frontStorage.columnHeaders[columnIndex])) break;\n }\n\n if (columnIndex === columnCount) {\n columnCount++;\n localDestination[localNodeIndex] = columnCount;\n frontStorage.columnHeaders[columnCount - 1] = globalNodeIndex;\n } else {\n localDestination[localNodeIndex] = columnIndex + 1;\n frontStorage.columnHeaders[columnIndex] = globalNodeIndex;\n }\n }\n\n let rowIndex;\n if (rowCount === 0) {\n rowCount++;\n rowDestination[localNodeIndex] = rowCount;\n rowHeaders[rowCount - 1] = globalNodeIndex;\n } else {\n for (rowIndex = 0; rowIndex < rowCount; rowIndex++) {\n if (Math.abs(globalNodeIndex) === Math.abs(rowHeaders[rowIndex])) break;\n }\n\n if (rowIndex === rowCount) {\n rowCount++;\n rowDestination[localNodeIndex] = rowCount;\n rowHeaders[rowCount - 1] = globalNodeIndex;\n } else {\n rowDestination[localNodeIndex] = rowIndex + 1;\n rowHeaders[rowIndex] = globalNodeIndex;\n }\n }\n }\n\n if (rowCount > systemSize || columnCount > systemSize) {\n errorLog(\"Error: systemSize not large enough\");\n return;\n }\n\n for (let localColumnIndex = 0; localColumnIndex < numElementColumns; localColumnIndex++) {\n let frontColumnIndex = localDestination[localColumnIndex];\n for (let localRowIndex = 0; localRowIndex < numElementNodes; localRowIndex++) {\n let frontRowIndex = rowDestination[localRowIndex];\n frontMatrix[frontRowIndex - 1][frontColumnIndex - 1] +=\n elementData.localJacobianMatrix[localRowIndex][localColumnIndex];\n }\n }\n }\n\n // Pivoting/elimination continues whether or not a new element was assembled\n let availableColumnCount = 0;\n for (let columnIndex = 0; columnIndex < columnCount; columnIndex++) {\n if (frontStorage.columnHeaders[columnIndex] < 0) {\n pivotColumnIndices[availableColumnCount] = columnIndex + 1;\n availableColumnCount++;\n }\n }\n\n let constrainedRowCount = 0;\n let availableRowCount = 0;\n for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {\n let globalNodeIndex = rowHeaders[rowIndex];\n if (globalNodeIndex < 0) {\n pivotRowIndices[availableRowCount] = rowIndex + 1;\n availableRowCount++;\n let absoluteNodeIndex = Math.abs(globalNodeIndex);\n if (frontalData.nodeConstraintCode[absoluteNodeIndex - 1] === 1) {\n modifiedRows[constrainedRowCount] = rowIndex + 1;\n constrainedRowCount++;\n frontalData.nodeConstraintCode[absoluteNodeIndex - 1] = 2;\n frontalData.globalResidualVector[absoluteNodeIndex - 1] =\n frontalData.boundaryValues[absoluteNodeIndex - 1];\n }\n }\n }\n\n if (constrainedRowCount > 0) {\n for (let constrainedIndex = 0; constrainedIndex < constrainedRowCount; constrainedIndex++) {\n let rowIndex = modifiedRows[constrainedIndex] - 1;\n let globalNodeIndex = Math.abs(rowHeaders[rowIndex]);\n for (let columnIndex = 0; columnIndex < columnCount; columnIndex++) {\n frontMatrix[rowIndex][columnIndex] = 0;\n let columnGlobalIndex = Math.abs(frontStorage.columnHeaders[columnIndex]);\n if (columnGlobalIndex === globalNodeIndex) frontMatrix[rowIndex][columnIndex] = 1;\n }\n }\n }\n\n if (availableColumnCount > summedRows || elementData.currentElementIndex < totalElements) {\n if (availableColumnCount === 0) {\n errorLog(\"Error: no more rows fully summed\");\n return;\n }\n\n let pivotRowIndex = pivotRowIndices[0];\n let pivotColumnIndex = pivotColumnIndices[0];\n let pivotValue = frontMatrix[pivotRowIndex - 1][pivotColumnIndex - 1];\n\n if (Math.abs(pivotValue) < 1e-4) {\n pivotValue = 0;\n for (let columnIndex = 0; columnIndex < availableColumnCount; columnIndex++) {\n let testColumnIndex = pivotColumnIndices[columnIndex];\n for (let rowIndex = 0; rowIndex < availableRowCount; rowIndex++) {\n let testRowIndex = pivotRowIndices[rowIndex];\n let testValue = frontMatrix[testRowIndex - 1][testColumnIndex - 1];\n if (Math.abs(testValue) > Math.abs(pivotValue)) {\n pivotValue = testValue;\n pivotColumnIndex = testColumnIndex;\n pivotRowIndex = testRowIndex;\n }\n }\n }\n }\n\n let pivotGlobalRowIndex = Math.abs(rowHeaders[pivotRowIndex - 1]);\n pivotColumnGlobalIndex = Math.abs(frontStorage.columnHeaders[pivotColumnIndex - 1]); // Assign, don't declare\n let permutationHelper =\n pivotGlobalRowIndex +\n pivotColumnGlobalIndex +\n rowSwapCount[pivotGlobalRowIndex - 1] +\n columnSwapCount[pivotColumnGlobalIndex - 1];\n frontalState.determinant =\n (frontalState.determinant * pivotValue * (-1) ** permutationHelper) / Math.abs(pivotValue);\n\n for (let nodeIndex = 0; nodeIndex < frontalState.totalNodes; nodeIndex++) {\n if (nodeIndex >= pivotGlobalRowIndex) rowSwapCount[nodeIndex]--;\n if (nodeIndex >= pivotColumnGlobalIndex) columnSwapCount[nodeIndex]--;\n }\n\n if (Math.abs(pivotValue) < 1e-10) {\n errorLog(\n `Matrix singular or ill-conditioned, currentElementIndex=${elementData.currentElementIndex}, pivotGlobalRowIndex=${pivotGlobalRowIndex}, pivotColumnGlobalIndex=${pivotColumnGlobalIndex}, pivotValue=${pivotValue}`\n );\n }\n\n if (pivotValue === 0) return;\n\n for (let columnIndex = 0; columnIndex < columnCount; columnIndex++) {\n frontStorage.pivotRow[columnIndex] = frontMatrix[pivotRowIndex - 1][columnIndex] / pivotValue;\n }\n\n let rightHandSide = frontalData.globalResidualVector[pivotGlobalRowIndex - 1] / pivotValue;\n frontalData.globalResidualVector[pivotGlobalRowIndex - 1] = rightHandSide;\n pivotColumn[pivotRowIndex - 1] = pivotValue;\n\n if (pivotRowIndex > 1) {\n for (let rowIndex = 0; rowIndex < pivotRowIndex - 1; rowIndex++) {\n let globalRowIndex = Math.abs(rowHeaders[rowIndex]);\n let eliminationFactor = frontMatrix[rowIndex][pivotColumnIndex - 1];\n pivotColumn[rowIndex] = eliminationFactor;\n if (pivotColumnIndex > 1 && eliminationFactor !== 0) {\n for (let columnIndex = 0; columnIndex < pivotColumnIndex - 1; columnIndex++) {\n frontMatrix[rowIndex][columnIndex] -= eliminationFactor * frontStorage.pivotRow[columnIndex];\n }\n }\n if (pivotColumnIndex < columnCount) {\n for (let columnIndex = pivotColumnIndex; columnIndex < columnCount; columnIndex++) {\n frontMatrix[rowIndex][columnIndex - 1] =\n frontMatrix[rowIndex][columnIndex] - eliminationFactor * frontStorage.pivotRow[columnIndex];\n }\n }\n frontalData.globalResidualVector[globalRowIndex - 1] -= eliminationFactor * rightHandSide;\n }\n }\n\n if (pivotRowIndex < rowCount) {\n for (let rowIndex = pivotRowIndex; rowIndex < rowCount; rowIndex++) {\n let globalRowIndex = Math.abs(rowHeaders[rowIndex]);\n let eliminationFactor = frontMatrix[rowIndex][pivotColumnIndex - 1];\n pivotColumn[rowIndex] = eliminationFactor;\n if (pivotColumnIndex > 1) {\n for (let columnIndex = 0; columnIndex < pivotColumnIndex - 1; columnIndex++) {\n frontMatrix[rowIndex - 1][columnIndex] =\n frontMatrix[rowIndex][columnIndex] - eliminationFactor * frontStorage.pivotRow[columnIndex];\n }\n }\n if (pivotColumnIndex < columnCount) {\n for (let columnIndex = pivotColumnIndex; columnIndex < columnCount; columnIndex++) {\n frontMatrix[rowIndex - 1][columnIndex - 1] =\n frontMatrix[rowIndex][columnIndex] - eliminationFactor * frontStorage.pivotRow[columnIndex];\n }\n }\n frontalData.globalResidualVector[globalRowIndex - 1] -= eliminationFactor * rightHandSide;\n }\n }\n\n for (let i = 0; i < rowCount; i++) {\n frontStorage.pivotData[pivotDataIndex + i - 1] = pivotColumn[i];\n }\n pivotDataIndex += rowCount;\n\n for (let i = 0; i < rowCount; i++) {\n frontStorage.pivotData[pivotDataIndex + i - 1] = rowHeaders[i];\n }\n pivotDataIndex += rowCount;\n\n frontStorage.pivotData[pivotDataIndex - 1] = pivotRowIndex;\n pivotDataIndex++;\n\n for (let i = 0; i < columnCount; i++) {\n frontStorage.frontValues[frontDataCounter - 1 + i] = frontStorage.pivotRow[i];\n }\n frontDataCounter += columnCount;\n\n for (let i = 0; i < columnCount; i++) {\n frontStorage.frontValues[frontDataCounter - 1 + i] = frontStorage.columnHeaders[i];\n }\n frontDataCounter += columnCount;\n\n frontStorage.frontValues[frontDataCounter - 1] = pivotGlobalRowIndex;\n frontStorage.frontValues[frontDataCounter] = columnCount;\n frontStorage.frontValues[frontDataCounter + 1] = pivotColumnIndex;\n frontStorage.frontValues[frontDataCounter + 2] = pivotValue;\n frontDataCounter += 4;\n\n for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {\n frontMatrix[rowIndex][columnCount - 1] = 0;\n }\n\n for (let columnIndex = 0; columnIndex < columnCount; columnIndex++) {\n frontMatrix[rowCount - 1][columnIndex] = 0;\n }\n\n columnCount--;\n if (pivotColumnIndex < columnCount + 1) {\n for (let columnIndex = pivotColumnIndex - 1; columnIndex < columnCount; columnIndex++) {\n frontStorage.columnHeaders[columnIndex] = frontStorage.columnHeaders[columnIndex + 1];\n }\n }\n\n rowCount--;\n if (pivotRowIndex < rowCount + 1) {\n for (let rowIndex = pivotRowIndex - 1; rowIndex < rowCount; rowIndex++) {\n rowHeaders[rowIndex] = rowHeaders[rowIndex + 1];\n }\n }\n\n if (rowCount > 1 || elementData.currentElementIndex < totalElements) continue;\n\n pivotColumnGlobalIndex = Math.abs(frontStorage.columnHeaders[0]); // Assign, don't declare\n pivotRowIndex = 1;\n pivotValue = frontMatrix[0][0];\n pivotGlobalRowIndex = Math.abs(rowHeaders[0]);\n pivotColumnIndex = 1;\n permutationHelper =\n pivotGlobalRowIndex +\n pivotColumnGlobalIndex +\n rowSwapCount[pivotGlobalRowIndex - 1] +\n columnSwapCount[pivotColumnGlobalIndex - 1];\n frontalState.determinant =\n (frontalState.determinant * pivotValue * (-1) ** permutationHelper) / Math.abs(pivotValue);\n\n frontStorage.pivotRow[0] = 1;\n if (Math.abs(pivotValue) < 1e-10) {\n errorLog(\n `Matrix singular or ill-conditioned, currentElementIndex=${elementData.currentElementIndex}, pivotGlobalRowIndex=${pivotGlobalRowIndex}, pivotColumnGlobalIndex=${pivotColumnGlobalIndex}, pivotValue=${pivotValue}`\n );\n }\n\n if (pivotValue === 0) return;\n\n frontalData.globalResidualVector[pivotGlobalRowIndex - 1] =\n frontalData.globalResidualVector[pivotGlobalRowIndex - 1] / pivotValue;\n frontStorage.frontValues[frontDataCounter - 1] = frontStorage.pivotRow[0];\n frontDataCounter++;\n frontStorage.frontValues[frontDataCounter - 1] = frontStorage.columnHeaders[0];\n frontDataCounter++;\n frontStorage.frontValues[frontDataCounter - 1] = pivotGlobalRowIndex;\n frontStorage.frontValues[frontDataCounter] = columnCount;\n frontStorage.frontValues[frontDataCounter + 1] = pivotColumnIndex;\n frontStorage.frontValues[frontDataCounter + 2] = pivotValue;\n frontDataCounter += 4;\n\n frontStorage.pivotData[pivotDataIndex - 1] = pivotColumn[0];\n pivotDataIndex++;\n frontStorage.pivotData[pivotDataIndex - 1] = rowHeaders[0];\n pivotDataIndex++;\n frontStorage.pivotData[pivotDataIndex - 1] = pivotRowIndex;\n pivotDataIndex++;\n\n frontalState.frontDataIndex = frontDataCounter;\n if (frontalState.writeFlag === 1)\n debugLog(`total ecs transfer in matrix reduction=${frontDataCounter}`);\n\n // Back substitution\n performBackSubstitution(frontDataCounter);\n break;\n }\n }\n}\n\n/**\n * Function to perform back substitution for the frontal solver\n * @param {number} frontDataCounter - Index counter for the element contributions\n */\nfunction performBackSubstitution(frontDataCounter) {\n for (let nodeIndex = 0; nodeIndex < frontalState.totalNodes; nodeIndex++) {\n frontalState.globalSolutionVector[nodeIndex] = frontalData.boundaryValues[nodeIndex];\n }\n\n for (let iterationIndex = 1; iterationIndex <= frontalState.totalNodes; iterationIndex++) {\n frontDataCounter -= 4;\n let pivotGlobalRowIndex = frontStorage.frontValues[frontDataCounter - 1];\n let columnCount = frontStorage.frontValues[frontDataCounter];\n let pivotColumnIndex = frontStorage.frontValues[frontDataCounter + 1];\n let pivotValue = frontStorage.frontValues[frontDataCounter + 2];\n\n if (iterationIndex === 1) {\n frontDataCounter--;\n frontStorage.columnHeaders[0] = frontStorage.frontValues[frontDataCounter - 1];\n frontDataCounter--;\n frontStorage.pivotRow[0] = frontStorage.frontValues[frontDataCounter - 1];\n } else {\n frontDataCounter -= columnCount;\n for (let columnIndex = 0; columnIndex < columnCount; columnIndex++) {\n frontStorage.columnHeaders[columnIndex] =\n frontStorage.frontValues[frontDataCounter - 1 + columnIndex];\n }\n frontDataCounter -= columnCount;\n for (let columnIndex = 0; columnIndex < columnCount; columnIndex++) {\n frontStorage.pivotRow[columnIndex] = frontStorage.frontValues[frontDataCounter - 1 + columnIndex];\n }\n }\n\n let pivotColumnGlobalIndex = Math.abs(frontStorage.columnHeaders[pivotColumnIndex - 1]);\n if (frontalData.nodeConstraintCode[pivotColumnGlobalIndex - 1] > 0) continue;\n\n let accumulatedValue = 0;\n frontStorage.pivotRow[pivotColumnIndex - 1] = 0;\n for (let columnIndex = 0; columnIndex < columnCount; columnIndex++) {\n accumulatedValue -=\n frontStorage.pivotRow[columnIndex] *\n frontalState.globalSolutionVector[Math.abs(frontStorage.columnHeaders[columnIndex]) - 1];\n }\n\n frontalState.globalSolutionVector[pivotColumnGlobalIndex - 1] =\n accumulatedValue + frontalData.globalResidualVector[pivotGlobalRowIndex - 1];\n\n frontalData.nodeConstraintCode[pivotColumnGlobalIndex - 1] = 1;\n }\n\n if (frontalState.writeFlag === 1)\n debugLog(`value of frontDataCounter after backsubstitution=${frontDataCounter}`);\n}\n","// ______ ______ _____ _ _ //\n// | ____| ____| /\\ / ____| (_) | | //\n// | |__ | |__ / \\ | (___ ___ ____ _ ____ | |_ //\n// | __| | __| / /\\ \\ \\___ \\ / __| __| | _ \\| __| //\n// | | | |____ / ____ \\ ____) | (__| | | | |_) | | //\n// |_| |______/_/ \\_\\_____/ \\___|_| |_| __/| | //\n// | | | | //\n// |_| | |_ //\n// Website: https://feascript.com/ \\__| //\n\n// Internal imports\nimport { euclideanNorm } from \"../methods/euclideanNormScript.js\";\nimport { solveLinearSystem } from \"./linearSystemSolverScript.js\";\nimport { basicLog, debugLog, errorLog } from \"../utilities/loggingScript.js\";\nimport { runFrontalSolver } from \"./frontalSolverScript.js\";\nimport { assembleFrontPropagationFront } from \"../solvers/frontPropagationScript.js\";\n\n/**\n * Function to solve a system of non-linear equations using the Newton-Raphson method\n * @param {function} assembleMat - Matrix assembler based on the physical model\n * @param {object} context - Context object containing simulation data and options\n * @param {number} [maxIterations=100] - Maximum number of iterations\n * @param {number} [tolerance=1e-4] - Convergence tolerance\n * @returns {object} An object containing:\n * - solutionVector: The solution vector\n * - iterations: The number of iterations performed\n * - converged: Boolean indicating whether the method converged\n */\n\nexport function newtonRaphson(assembleMat, context, maxIterations = 100, tolerance = 1e-4) {\n let errorNorm = 0;\n let converged = false;\n let iterations = 0;\n let deltaX = [];\n let solutionVector = [];\n let jacobianMatrix = [];\n let residualVector = [];\n\n // Calculate system size\n let totalNodes = context.meshData.nodesXCoordinates.length;\n\n // Initialize arrays with proper size\n for (let i = 0; i < totalNodes; i++) {\n deltaX[i] = 0;\n solutionVector[i] = 0;\n }\n\n // Initialize solution from context if available\n if (context.initialSolution && context.initialSolution.length === totalNodes) {\n solutionVector = [...context.initialSolution];\n }\n\n while (iterations < maxIterations && !converged) {\n // Update solution\n for (let i = 0; i < solutionVector.length; i++) {\n solutionVector[i] = Number(solutionVector[i]) + Number(deltaX[i]);\n }\n\n // Check if using frontal solver\n if (context.solverMethod === \"frontal\") {\n const frontalResult = runFrontalSolver(\n assembleFrontPropagationFront,\n context.meshData,\n context.boundaryConditions,\n { solutionVector, eikonalActivationFlag: context.eikonalActivationFlag }\n );\n deltaX = frontalResult.solutionVector;\n } else {\n // Compute Jacobian and residual matrices\n ({ jacobianMatrix, residualVector } = assembleMat(\n context.meshData,\n context.boundaryConditions,\n solutionVector, // The solution vector is required in the case of a non-linear equation\n context.eikonalActivationFlag // Currently used only in the front propagation solver (TODO refactor in case of a solver not needing it)\n ));\n\n // Solve the linear system based on the specified solver method\n const linearSystemResult = solveLinearSystem(context.solverMethod, jacobianMatrix, residualVector);\n deltaX = linearSystemResult.solutionVector;\n }\n\n // Check convergence\n errorNorm = euclideanNorm(deltaX);\n\n // Norm for each iteration\n basicLog(`Newton-Raphson iteration ${iterations + 1}: Error norm = ${errorNorm.toExponential(4)}`);\n\n if (errorNorm <= tolerance) {\n converged = true;\n } else if (errorNorm > 1e2) {\n errorLog(`Solution not converged. Error norm: ${errorNorm}`);\n break;\n }\n\n iterations++;\n }\n\n return {\n solutionVector,\n converged,\n iterations,\n jacobianMatrix,\n residualVector,\n };\n}\n","/**\n * @license\n * Copyright 2019 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\nconst proxyMarker = Symbol(\"Comlink.proxy\");\nconst createEndpoint = Symbol(\"Comlink.endpoint\");\nconst releaseProxy = Symbol(\"Comlink.releaseProxy\");\nconst finalizer = Symbol(\"Comlink.finalizer\");\nconst throwMarker = Symbol(\"Comlink.thrown\");\nconst isObject = (val) => (typeof val === \"object\" && val !== null) || typeof val === \"function\";\n/**\n * Internal transfer handle to handle objects marked to proxy.\n */\nconst proxyTransferHandler = {\n canHandle: (val) => isObject(val) && val[proxyMarker],\n serialize(obj) {\n const { port1, port2 } = new MessageChannel();\n expose(obj, port1);\n return [port2, [port2]];\n },\n deserialize(port) {\n port.start();\n return wrap(port);\n },\n};\n/**\n * Internal transfer handler to handle thrown exceptions.\n */\nconst throwTransferHandler = {\n canHandle: (value) => isObject(value) && throwMarker in value,\n serialize({ value }) {\n let serialized;\n if (value instanceof Error) {\n serialized = {\n isError: true,\n value: {\n message: value.message,\n name: value.name,\n stack: value.stack,\n },\n };\n }\n else {\n serialized = { isError: false, value };\n }\n return [serialized, []];\n },\n deserialize(serialized) {\n if (serialized.isError) {\n throw Object.assign(new Error(serialized.value.message), serialized.value);\n }\n throw serialized.value;\n },\n};\n/**\n * Allows customizing the serialization of certain values.\n */\nconst transferHandlers = new Map([\n [\"proxy\", proxyTransferHandler],\n [\"throw\", throwTransferHandler],\n]);\nfunction isAllowedOrigin(allowedOrigins, origin) {\n for (const allowedOrigin of allowedOrigins) {\n if (origin === allowedOrigin || allowedOrigin === \"*\") {\n return true;\n }\n if (allowedOrigin instanceof RegExp && allowedOrigin.test(origin)) {\n return true;\n }\n }\n return false;\n}\nfunction expose(obj, ep = globalThis, allowedOrigins = [\"*\"]) {\n ep.addEventListener(\"message\", function callback(ev) {\n if (!ev || !ev.data) {\n return;\n }\n if (!isAllowedOrigin(allowedOrigins, ev.origin)) {\n console.warn(`Invalid origin '${ev.origin}' for comlink proxy`);\n return;\n }\n const { id, type, path } = Object.assign({ path: [] }, ev.data);\n const argumentList = (ev.data.argumentList || []).map(fromWireValue);\n let returnValue;\n try {\n const parent = path.slice(0, -1).reduce((obj, prop) => obj[prop], obj);\n const rawValue = path.reduce((obj, prop) => obj[prop], obj);\n switch (type) {\n case \"GET\" /* MessageType.GET */:\n {\n returnValue = rawValue;\n }\n break;\n case \"SET\" /* MessageType.SET */:\n {\n parent[path.slice(-1)[0]] = fromWireValue(ev.data.value);\n returnValue = true;\n }\n break;\n case \"APPLY\" /* MessageType.APPLY */:\n {\n returnValue = rawValue.apply(parent, argumentList);\n }\n break;\n case \"CONSTRUCT\" /* MessageType.CONSTRUCT */:\n {\n const value = new rawValue(...argumentList);\n returnValue = proxy(value);\n }\n break;\n case \"ENDPOINT\" /* MessageType.ENDPOINT */:\n {\n const { port1, port2 } = new MessageChannel();\n expose(obj, port2);\n returnValue = transfer(port1, [port1]);\n }\n break;\n case \"RELEASE\" /* MessageType.RELEASE */:\n {\n returnValue = undefined;\n }\n break;\n default:\n return;\n }\n }\n catch (value) {\n returnValue = { value, [throwMarker]: 0 };\n }\n Promise.resolve(returnValue)\n .catch((value) => {\n return { value, [throwMarker]: 0 };\n })\n .then((returnValue) => {\n const [wireValue, transferables] = toWireValue(returnValue);\n ep.postMessage(Object.assign(Object.assign({}, wireValue), { id }), transferables);\n if (type === \"RELEASE\" /* MessageType.RELEASE */) {\n // detach and deactive after sending release response above.\n ep.removeEventListener(\"message\", callback);\n closeEndPoint(ep);\n if (finalizer in obj && typeof obj[finalizer] === \"function\") {\n obj[finalizer]();\n }\n }\n })\n .catch((error) => {\n // Send Serialization Error To Caller\n const [wireValue, transferables] = toWireValue({\n value: new TypeError(\"Unserializable return value\"),\n [throwMarker]: 0,\n });\n ep.postMessage(Object.assign(Object.assign({}, wireValue), { id }), transferables);\n });\n });\n if (ep.start) {\n ep.start();\n }\n}\nfunction isMessagePort(endpoint) {\n return endpoint.constructor.name === \"MessagePort\";\n}\nfunction closeEndPoint(endpoint) {\n if (isMessagePort(endpoint))\n endpoint.close();\n}\nfunction wrap(ep, target) {\n const pendingListeners = new Map();\n ep.addEventListener(\"message\", function handleMessage(ev) {\n const { data } = ev;\n if (!data || !data.id) {\n return;\n }\n const resolver = pendingListeners.get(data.id);\n if (!resolver) {\n return;\n }\n try {\n resolver(data);\n }\n finally {\n pendingListeners.delete(data.id);\n }\n });\n return createProxy(ep, pendingListeners, [], target);\n}\nfunction throwIfProxyReleased(isReleased) {\n if (isReleased) {\n throw new Error(\"Proxy has been released and is not useable\");\n }\n}\nfunction releaseEndpoint(ep) {\n return requestResponseMessage(ep, new Map(), {\n type: \"RELEASE\" /* MessageType.RELEASE */,\n }).then(() => {\n closeEndPoint(ep);\n });\n}\nconst proxyCounter = new WeakMap();\nconst proxyFinalizers = \"FinalizationRegistry\" in globalThis &&\n new FinalizationRegistry((ep) => {\n const newCount = (proxyCounter.get(ep) || 0) - 1;\n proxyCounter.set(ep, newCount);\n if (newCount === 0) {\n releaseEndpoint(ep);\n }\n });\nfunction registerProxy(proxy, ep) {\n const newCount = (proxyCounter.get(ep) || 0) + 1;\n proxyCounter.set(ep, newCount);\n if (proxyFinalizers) {\n proxyFinalizers.register(proxy, ep, proxy);\n }\n}\nfunction unregisterProxy(proxy) {\n if (proxyFinalizers) {\n proxyFinalizers.unregister(proxy);\n }\n}\nfunction createProxy(ep, pendingListeners, path = [], target = function () { }) {\n let isProxyReleased = false;\n const proxy = new Proxy(target, {\n get(_target, prop) {\n throwIfProxyReleased(isProxyReleased);\n if (prop === releaseProxy) {\n return () => {\n unregisterProxy(proxy);\n releaseEndpoint(ep);\n pendingListeners.clear();\n isProxyReleased = true;\n };\n }\n if (prop === \"then\") {\n if (path.length === 0) {\n return { then: () => proxy };\n }\n const r = requestResponseMessage(ep, pendingListeners, {\n type: \"GET\" /* MessageType.GET */,\n path: path.map((p) => p.toString()),\n }).then(fromWireValue);\n return r.then.bind(r);\n }\n return createProxy(ep, pendingListeners, [...path, prop]);\n },\n set(_target, prop, rawValue) {\n throwIfProxyReleased(isProxyReleased);\n // FIXME: ES6 Proxy Handler `set` methods are supposed to return a\n // boolean. To show good will, we return true asynchronously ¯\\_(ツ)_/¯\n const [value, transferables] = toWireValue(rawValue);\n return requestResponseMessage(ep, pendingListeners, {\n type: \"SET\" /* MessageType.SET */,\n path: [...path, prop].map((p) => p.toString()),\n value,\n }, transferables).then(fromWireValue);\n },\n apply(_target, _thisArg, rawArgumentList) {\n throwIfProxyReleased(isProxyReleased);\n const last = path[path.length - 1];\n if (last === createEndpoint) {\n return requestResponseMessage(ep, pendingListeners, {\n type: \"ENDPOINT\" /* MessageType.ENDPOINT */,\n }).then(fromWireValue);\n }\n // We just pretend that `bind()` didn’t happen.\n if (last === \"bind\") {\n return createProxy(ep, pendingListeners, path.slice(0, -1));\n }\n const [argumentList, transferables] = processArguments(rawArgumentList);\n return requestResponseMessage(ep, pendingListeners, {\n type: \"APPLY\" /* MessageType.APPLY */,\n path: path.map((p) => p.toString()),\n argumentList,\n }, transferables).then(fromWireValue);\n },\n construct(_target, rawArgumentList) {\n throwIfProxyReleased(isProxyReleased);\n const [argumentList, transferables] = processArguments(rawArgumentList);\n return requestResponseMessage(ep, pendingListeners, {\n type: \"CONSTRUCT\" /* MessageType.CONSTRUCT */,\n path: path.map((p) => p.toString()),\n argumentList,\n }, transferables).then(fromWireValue);\n },\n });\n registerProxy(proxy, ep);\n return proxy;\n}\nfunction myFlat(arr) {\n return Array.prototype.concat.apply([], arr);\n}\nfunction processArguments(argumentList) {\n const processed = argumentList.map(toWireValue);\n return [processed.map((v) => v[0]), myFlat(processed.map((v) => v[1]))];\n}\nconst transferCache = new WeakMap();\nfunction transfer(obj, transfers) {\n transferCache.set(obj, transfers);\n return obj;\n}\nfunction proxy(obj) {\n return Object.assign(obj, { [proxyMarker]: true });\n}\nfunction windowEndpoint(w, context = globalThis, targetOrigin = \"*\") {\n return {\n postMessage: (msg, transferables) => w.postMessage(msg, targetOrigin, transferables),\n addEventListener: context.addEventListener.bind(context),\n removeEventListener: context.removeEventListener.bind(context),\n };\n}\nfunction toWireValue(value) {\n for (const [name, handler] of transferHandlers) {\n if (handler.canHandle(value)) {\n const [serializedValue, transferables] = handler.serialize(value);\n return [\n {\n type: \"HANDLER\" /* WireValueType.HANDLER */,\n name,\n value: serializedValue,\n },\n transferables,\n ];\n }\n }\n return [\n {\n type: \"RAW\" /* WireValueType.RAW */,\n value,\n },\n transferCache.get(value) || [],\n ];\n}\nfunction fromWireValue(value) {\n switch (value.type) {\n case \"HANDLER\" /* WireValueType.HANDLER */:\n return transferHandlers.get(value.name).deserialize(value.value);\n case \"RAW\" /* WireValueType.RAW */:\n return value.value;\n }\n}\nfunction requestResponseMessage(ep, pendingListeners, msg, transfers) {\n return new Promise((resolve) => {\n const id = generateUUID();\n pendingListeners.set(id, resolve);\n if (ep.start) {\n ep.start();\n }\n ep.postMessage(Object.assign({ id }, msg), transfers);\n });\n}\nfunction generateUUID() {\n return new Array(4)\n .fill(0)\n .map(() => Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString(16))\n .join(\"-\");\n}\n\nexport { createEndpoint, expose, finalizer, proxy, proxyMarker, releaseProxy, transfer, transferHandlers, windowEndpoint, wrap };\n//# sourceMappingURL=comlink.mjs.map\n","// ______ ______ _____ _ _ //\n// | ____| ____| /\\ / ____| (_) | | //\n// | |__ | |__ / \\ | (___ ___ ____ _ ____ | |_ //\n// | __| | __| / /\\ \\ \\___ \\ / __| __| | _ \\| __| //\n// | | | |____ / ____ \\ ____) | (__| | | | |_) | | //\n// |_| |______/_/ \\_\\_____/ \\___|_| |_| __/| | //\n// | | | | //\n// |_| | |_ //\n// Website: https://feascript.com/ \\__| //\n\n// Internal imports\nimport { newtonRaphson } from \"./methods/newtonRaphsonScript.js\";\nimport { solveLinearSystem } from \"./methods/linearSystemSolverScript.js\";\nimport { prepareMesh } from \"./mesh/meshUtilsScript.js\";\nimport { assembleFrontPropagationMat } from \"./solvers/frontPropagationScript.js\";\nimport { assembleGeneralFormPDEMat, assembleGeneralFormPDEFront } from \"./solvers/generalFormPDEScript.js\";\nimport { assembleHeatConductionMat, assembleHeatConductionFront } from \"./solvers/heatConductionScript.js\";\nimport { runFrontalSolver } from \"./methods/frontalSolverScript.js\";\nimport { basicLog, debugLog, errorLog } from \"./utilities/loggingScript.js\";\n\n/**\n * Class to implement finite element analysis in JavaScript\n * @param {string} solverConfig - Parameter specifying the type of solver\n * @param {object} meshConfig - Object containing computational mesh details\n * @param {object} boundaryConditions - Object containing boundary conditions for the finite element analysis\n * @returns {object} An object containing the solution vector and additional mesh information\n */\nexport class FEAScriptModel {\n constructor() {\n this.solverConfig = null;\n this.meshConfig = {};\n this.boundaryConditions = {};\n this.solverMethod = \"lusolve\"; // Default solver method\n this.coefficientFunctions = null; // Add storage for coefficient functions\n basicLog(\"FEAScriptModel instance created\");\n }\n\n /**\n * Sets the solver configuration\n * @param {string} solverConfig - Parameter specifying the type of solver\n * @param {object} [options] - Optional additional configuration\n */\n setSolverConfig(solverConfig, options = {}) {\n this.solverConfig = solverConfig;\n\n // Store coefficient functions if provided\n if (options && options.coefficientFunctions) {\n this.coefficientFunctions = options.coefficientFunctions;\n debugLog(\"Coefficient functions set\");\n }\n\n debugLog(`Solver config set to: ${solverConfig}`);\n }\n\n setMeshConfig(meshConfig) {\n this.meshConfig = meshConfig;\n debugLog(`Mesh config set with dimensions: ${meshConfig.meshDimension}`);\n }\n\n addBoundaryCondition(boundaryKey, condition) {\n this.boundaryConditions[boundaryKey] = condition;\n debugLog(`Boundary condition added for boundary: ${boundaryKey}, type: ${condition[0]}`);\n }\n\n setSolverMethod(solverMethod) {\n this.solverMethod = solverMethod;\n debugLog(`Solver method set to: ${solverMethod}`);\n }\n\n solve() {\n if (!this.solverConfig || !this.meshConfig || !this.boundaryConditions) {\n const error = \"Solver config, mesh config, and boundary conditions must be set before solving.\";\n console.error(error);\n throw new Error(error);\n }\n\n /**\n * For consistency across both linear and nonlinear formulations,\n * this project always refers to the assembled right-hand side vector\n * as `residualVector` and the assembled system matrix as `jacobianMatrix`.\n *\n * In linear problems `jacobianMatrix` is equivalent to the\n * classic stiffness/conductivity matrix and `residualVector`\n * corresponds to the traditional load (RHS) vector.\n */\n\n let jacobianMatrix = [];\n let residualVector = [];\n let solutionVector = [];\n let initialSolution = [];\n\n // Prepare the mesh\n basicLog(\"Preparing mesh...\");\n const meshData = prepareMesh(this.meshConfig);\n basicLog(\"Mesh preparation completed\");\n\n // Extract node coordinates from meshData\n const nodesCoordinates = {\n nodesXCoordinates: meshData.nodesXCoordinates,\n nodesYCoordinates: meshData.nodesYCoordinates,\n };\n\n // Select and execute the appropriate solver based on solverConfig\n basicLog(\"Beginning solving process...\");\n console.time(\"totalSolvingTime\");\n if (this.solverConfig === \"heatConductionScript\") {\n basicLog(`Using solver: ${this.solverConfig}`);\n\n // Check if using frontal solver\n if (this.solverMethod === \"frontal\") {\n const frontalResult = runFrontalSolver(\n assembleHeatConductionFront,\n meshData,\n this.boundaryConditions\n );\n solutionVector = frontalResult.solutionVector;\n } else {\n // Use regular linear solver methods\n ({ jacobianMatrix, residualVector } = assembleHeatConductionMat(meshData, this.boundaryConditions));\n const linearSystemResult = solveLinearSystem(this.solverMethod, jacobianMatrix, residualVector);\n solutionVector = linearSystemResult.solutionVector;\n }\n } else if (this.solverConfig === \"frontPropagationScript\") {\n basicLog(`Using solver: ${this.solverConfig}`);\n\n // Initialize eikonalActivationFlag\n let eikonalActivationFlag = 0;\n const eikonalExteralIterations = 5; // Number of incremental steps for the eikonal equation\n\n // Create context object with all necessary properties\n const context = {\n meshData: meshData,\n boundaryConditions: this.boundaryConditions,\n eikonalActivationFlag: eikonalActivationFlag,\n solverMethod: this.solverMethod,\n initialSolution,\n };\n\n while (eikonalActivationFlag <= 1) {\n // Update the context object with current eikonalActivationFlag\n context.eikonalActivationFlag = eikonalActivationFlag;\n\n // Pass the previous solution as initial guess\n if (solutionVector.length > 0) {\n context.initialSolution = [...solutionVector];\n }\n\n // Solve the assembled non-linear system\n const newtonRaphsonResult = newtonRaphson(assembleFrontPropagationMat, context, 100, 1e-4);\n\n // Extract results\n jacobianMatrix = newtonRaphsonResult.jacobianMatrix;\n residualVector = newtonRaphsonResult.residualVector;\n solutionVector = newtonRaphsonResult.solutionVector;\n\n // Increment for next iteration\n eikonalActivationFlag += 1 / eikonalExteralIterations;\n }\n } else if (this.solverConfig === \"generalFormPDEScript\") {\n basicLog(`Using solver: ${this.solverConfig}`);\n // Check if using frontal solver\n if (this.solverMethod === \"frontal\") {\n errorLog(\n \"Frontal solver is not yet supported for generalFormPDEScript. Please use 'lusolve' or 'jacobi'.\"\n );\n } else {\n // Use regular linear solver methods\n ({ jacobianMatrix, residualVector } = assembleGeneralFormPDEMat(\n meshData,\n this.boundaryConditions,\n this.coefficientFunctions\n ));\n\n const linearSystemResult = solveLinearSystem(this.solverMethod, jacobianMatrix, residualVector);\n solutionVector = linearSystemResult.solutionVector;\n }\n }\n console.timeEnd(\"totalSolvingTime\");\n basicLog(\"Solving process completed\");\n\n return { solutionVector, nodesCoordinates };\n }\n}\n","// ______ ______ _____ _ _ //\n// | ____| ____| /\\ / ____| (_) | | //\n// | |__ | |__ / \\ | (___ ___ ____ _ ____ | |_ //\n// | __| | __| / /\\ \\ \\___ \\ / __| __| | _ \\| __| //\n// | | | |____ / ____ \\ ____) | (__| | | | |_) | | //\n// |_| |______/_/ \\_\\_____/ \\___|_| |_| __/| | //\n// | | | | //\n// |_| | |_ //\n// Website: https://feascript.com/ \\__| //\n\n// Internal imports\nimport { initializeFEA, performIsoparametricMapping1D } from \"../mesh/meshUtilsScript.js\";\nimport { GenericBoundaryConditions } from \"./genericBoundaryConditionsScript.js\";\nimport { basicLog, debugLog, errorLog } from \"../utilities/loggingScript.js\";\n\n/**\n * Function to assemble the Jacobian matrix and residuals vector for the general form PDE model\n * @param {object} meshData - Object containing prepared mesh data\n * @param {object} boundaryConditions - Object containing boundary conditions\n * @param {object} coefficientFunctions - Functions A(x), B(x), C(x), D(x) for the PDE\n * @returns {object} An object containing:\n * - jacobianMatrix: The assembled Jacobian matrix\n * - residualVector: The assembled residual vector\n */\nexport function assembleGeneralFormPDEMat(meshData, boundaryConditions, coefficientFunctions) {\n basicLog(\"Starting general form PDE matrix assembly...\");\n\n // Extract mesh data\n const {\n nodesXCoordinates,\n nodesYCoordinates,\n nop,\n boundaryElements,\n totalElements,\n meshDimension,\n elementOrder,\n } = meshData;\n\n // Extract coefficient functions\n const { A, B, C, D } = coefficientFunctions;\n\n // Initialize FEA components\n const FEAData = initializeFEA(meshData);\n const {\n residualVector,\n jacobianMatrix,\n localToGlobalMap,\n basisFunctions,\n gaussPoints,\n gaussWeights,\n numNodes,\n } = FEAData;\n\n if (meshDimension === \"1D\") {\n // 1D general form PDE\n\n // Matrix assembly\n for (let elementIndex = 0; elementIndex < totalElements; elementIndex++) {\n // Map local element nodes to global mesh nodes\n for (let localNodeIndex = 0; localNodeIndex < numNodes; localNodeIndex++) {\n // Convert to 0-based indexing\n localToGlobalMap[localNodeIndex] = Math.abs(nop[elementIndex][localNodeIndex]) - 1;\n }\n\n // Loop over Gauss points\n for (let gaussPointIndex = 0; gaussPointIndex < gaussPoints.length; gaussPointIndex++) {\n // Get basis functions for the current Gauss point\n const { basisFunction, basisFunctionDerivKsi } = basisFunctions.getBasisFunctions(\n gaussPoints[gaussPointIndex]\n );\n\n // Perform isoparametric mapping\n const { detJacobian, basisFunctionDerivX } = performIsoparametricMapping1D({\n basisFunction,\n basisFunctionDerivKsi,\n nodesXCoordinates,\n localToGlobalMap,\n numNodes,\n });\n\n // Calculate the physical coordinate for this Gauss point\n let xCoord = 0;\n for (let i = 0; i < numNodes; i++) {\n xCoord += nodesXCoordinates[localToGlobalMap[i]] * basisFunction[i];\n }\n\n // Evaluate coefficient functions at this physical coordinate\n const a = A(xCoord);\n const b = B(xCoord);\n const c = C(xCoord);\n const d = D(xCoord);\n\n // Computation of Galerkin's residuals and local Jacobian matrix\n for (let localNodeIndex1 = 0; localNodeIndex1 < numNodes; localNodeIndex1++) {\n const globalNodeIndex1 = localToGlobalMap[localNodeIndex1];\n\n // Source term contribution to residual vector\n residualVector[globalNodeIndex1] -=\n gaussWeights[gaussPointIndex] * detJacobian * d * basisFunction[localNodeIndex1];\n\n for (let localNodeIndex2 = 0; localNodeIndex2 < numNodes; localNodeIndex2++) {\n const globalNodeIndex2 = localToGlobalMap[localNodeIndex2];\n\n // Diffusion term\n jacobianMatrix[globalNodeIndex1][globalNodeIndex2] +=\n gaussWeights[gaussPointIndex] *\n detJacobian *\n a *\n basisFunctionDerivX[localNodeIndex1] *\n basisFunctionDerivX[localNodeIndex2];\n\n // Advection term\n jacobianMatrix[globalNodeIndex1][globalNodeIndex2] -=\n gaussWeights[gaussPointIndex] *\n detJacobian *\n b *\n basisFunctionDerivX[localNodeIndex2] *\n basisFunction[localNodeIndex1];\n\n // Reaction term\n jacobianMatrix[globalNodeIndex1][globalNodeIndex2] +=\n gaussWeights[gaussPointIndex] *\n detJacobian *\n c *\n basisFunction[localNodeIndex1] *\n basisFunction[localNodeIndex2];\n }\n }\n }\n }\n } else if (meshDimension === \"2D\") {\n errorLog(\"2D general form PDE is not yet supported in assembleGeneralFormPDEMat.\");\n // 2D general form PDE - empty for now\n }\n\n // Apply boundary conditions\n const genericBoundaryConditions = new GenericBoundaryConditions(\n boundaryConditions,\n boundaryElements,\n nop,\n meshDimension,\n elementOrder\n );\n\n // Apply Dirichlet boundary conditions only\n genericBoundaryConditions.imposeDirichletBoundaryConditions(residualVector, jacobianMatrix);\n\n basicLog(\"General form PDE matrix assembly completed\");\n\n return {\n jacobianMatrix,\n residualVector,\n };\n}\n\n/**\n * Function to assemble the frontal solver matrix for the general form PDE model\n * @param {object} data - Object containing element data for the frontal solver\n * @returns {object} An object containing local Jacobian matrix and residual vector\n */\nexport function assembleGeneralFormPDEFront({\n elementIndex,\n nop,\n meshData,\n basisFunctions,\n FEAData,\n coefficientFunctions,\n}) {\n // Extract numerical integration parameters and mesh coordinates\n const { gaussPoints, gaussWeights, numNodes } = FEAData;\n const { nodesXCoordinates, nodesYCoordinates, meshDimension } = meshData;\n const { A, B, C, D } = coefficientFunctions;\n\n // Initialize local Jacobian matrix and local residual vector\n const localJacobianMatrix = Array(numNodes)\n .fill()\n .map(() => Array(numNodes).fill(0));\n const localResidualVector = Array(numNodes).fill(0);\n\n // Build the mapping from local node indices to global node indices\n const ngl = Array(numNodes);\n const localToGlobalMap = Array(numNodes);\n for (let localNodeIndex = 0; localNodeIndex < numNodes; localNodeIndex++) {\n ngl[localNodeIndex] = Math.abs(nop[elementIndex][localNodeIndex]);\n localToGlobalMap[localNodeIndex] = Math.abs(nop[elementIndex][localNodeIndex]) - 1;\n }\n\n if (meshDimension === \"1D\") {\n // 1D general form PDE\n\n // Loop over Gauss points\n for (let gaussPointIndex = 0; gaussPointIndex < gaussPoints.length; gaussPointIndex++) {\n // Get basis functions for the current Gauss point\n const { basisFunction, basisFunctionDerivKsi } = basisFunctions.getBasisFunctions(\n gaussPoints[gaussPointIndex]\n );\n\n // Perform isoparametric mapping\n const { detJacobian, basisFunctionDerivX } = performIsoparametricMapping1D({\n basisFunction,\n basisFunctionDerivKsi,\n nodesXCoordinates,\n localToGlobalMap,\n numNodes,\n });\n\n // Calculate the physical coordinate for this Gauss point\n let xCoord = 0;\n for (let i = 0; i < numNodes; i++) {\n xCoord += nodesXCoordinates[localToGlobalMap[i]] * basisFunction[i];\n }\n\n // Evaluate coefficient functions at this physical coordinate\n const a = A(xCoord);\n const b = B(xCoord);\n const c = C(xCoord);\n const d = D(xCoord);\n\n // Computation of local Jacobian matrix and residual vector\n for (let localNodeIndex1 = 0; localNodeIndex1 < numNodes; localNodeIndex1++) {\n // Source term contribution to local residual vector\n localResidualVector[localNodeIndex1] -=\n gaussWeights[gaussPointIndex] * detJacobian * d * basisFunction[localNodeIndex1];\n\n for (let localNodeIndex2 = 0; localNodeIndex2 < numNodes; localNodeIndex2++) {\n // Diffusion term\n localJacobianMatrix[localNodeIndex1][localNodeIndex2] +=\n gaussWeights[gaussPointIndex] *\n detJacobian *\n a *\n basisFunctionDerivX[localNodeIndex1] *\n basisFunctionDerivX[localNodeIndex2];\n\n // Advection term\n localJacobianMatrix[localNodeIndex1][localNodeIndex2] -=\n gaussWeights[gaussPointIndex] *\n detJacobian *\n b *\n basisFunctionDerivX[localNodeIndex2] *\n basisFunction[localNodeIndex1];\n\n // Reaction term\n localJacobianMatrix[localNodeIndex1][localNodeIndex2] +=\n gaussWeights[gaussPointIndex] *\n detJacobian *\n c *\n basisFunction[localNodeIndex1] *\n basisFunction[localNodeIndex2];\n }\n }\n }\n } else if (meshDimension === \"2D\") {\n errorLog(\"2D general form PDE is not yet supported in assembleGeneralFormPDEFront.\");\n // 2D general form PDE - empty for now\n }\n\n return {\n localJacobianMatrix,\n localResidualVector,\n ngl,\n };\n}\n","// ______ ______ _____ _ _ //\n// | ____| ____| /\\ / ____| (_) | | //\n// | |__ | |__ / \\ | (___ ___ ____ _ ____ | |_ //\n// | __| | __| / /\\ \\ \\___ \\ / __| __| | _ \\| __| //\n// | | | |____ / ____ \\ ____) | (__| | | | |_) | | //\n// |_| |______/_/ \\_\\_____/ \\___|_| |_| __/| | //\n// | | | | //\n// |_| | |_ //\n// Website: https://feascript.com/ \\__| //\n\n// External imports\nimport * as Comlink from \"../vendor/comlink.mjs\";\n\n// Internal imports\nimport { basicLog } from \"../utilities/loggingScript.js\";\n\n/**\n * Class to facilitate communication with web workers for FEAScript operations\n */\nexport class FEAScriptWorker {\n /**\n * Constructor to initialize the FEAScriptWorker class\n * Sets up the worker and initializes the workerWrapper.\n */\n constructor() {\n this.worker = null;\n this.feaWorker = null;\n this.isReady = false;\n\n this._initWorker();\n }\n\n /**\n * Function to initialize the web worker and wrap it using Comlink.\n * @private\n * @throws Will throw an error if the worker fails to initialize.\n */\n async _initWorker() {\n try {\n this.worker = new Worker(new URL(\"./wrapperScript.js\", import.meta.url), {\n type: \"module\",\n });\n\n this.worker.onerror = (event) => {\n console.error(\"FEAScriptWorker: Worker error:\", event);\n };\n const workerWrapper = Comlink.wrap(this.worker);\n\n this.feaWorker = await new workerWrapper();\n\n this.isReady = true;\n } catch (error) {\n console.error(\"Failed to initialize worker\", error);\n throw error;\n }\n }\n\n /**\n * Function to ensure that the worker is ready before performing any operations.\n * @private\n * @returns {Promise} Resolves when the worker is ready.\n * @throws Will throw an error if the worker is not ready within the timeout period.\n */\n async _ensureReady() {\n if (this.isReady) return Promise.resolve();\n\n return new Promise((resolve, reject) => {\n let attempts = 0;\n const maxAttempts = 50; // 5 seconds max\n\n const checkReady = () => {\n attempts++;\n if (this.isReady) {\n resolve();\n } else if (attempts >= maxAttempts) {\n reject(new Error(\"Timeout waiting for worker to be ready\"));\n } else {\n setTimeout(checkReady, 1000);\n }\n };\n checkReady();\n });\n }\n\n /**\n * Function to set the solver configuration in the worker.\n * @param {string} solverConfig - The solver configuration to set.\n * @returns {Promise} Resolves when the configuration is set.\n */\n async setSolverConfig(solverConfig) {\n await this._ensureReady();\n basicLog(`FEAScriptWorker: Setting solver config to: ${solverConfig}`);\n return this.feaWorker.setSolverConfig(solverConfig);\n }\n\n /**\n * Sets the mesh configuration in the worker.\n * @param {object} meshConfig - The mesh configuration to set.\n * @returns {Promise} Resolves when the configuration is set.\n */\n async setMeshConfig(meshConfig) {\n await this._ensureReady();\n basicLog(`FEAScriptWorker: Setting mesh config`);\n return this.feaWorker.setMeshConfig(meshConfig);\n }\n\n /**\n * Adds a boundary condition to the worker.\n * @param {string} boundaryKey - The key identifying the boundary.\n * @param {array} condition - The boundary condition to add.\n * @returns {Promise} Resolves when the boundary condition is added.\n */\n async addBoundaryCondition(boundaryKey, condition) {\n await this._ensureReady();\n basicLog(`FEAScriptWorker: Adding boundary condition for boundary: ${boundaryKey}`);\n return this.feaWorker.addBoundaryCondition(boundaryKey, condition);\n }\n\n /**\n * Sets the solver method in the worker.\n * @param {string} solverMethod - The solver method to set.\n * @returns {Promise} Resolves when the solver method is set.\n */\n async setSolverMethod(solverMethod) {\n await this._ensureReady();\n basicLog(`FEAScriptWorker: Setting solver method to: ${solverMethod}`);\n return this.feaWorker.setSolverMethod(solverMethod);\n }\n\n /**\n * Requests the worker to solve the problem.\n * @returns {Promise