From 7b2e396f2e12d365c74ce2b70a5c0f8bb67d382f Mon Sep 17 00:00:00 2001 From: Evgeny Metelkin Date: Sat, 16 Dec 2023 21:03:30 +0200 Subject: [PATCH] move Expor.get*Image to Namespace.get*Image --- cases/0-hello-world/master/mm_julia/model.jl | 1 - src/dbsolve-export/expression.js | 2 +- src/dbsolve-export/index.js | 188 +------------------ src/dbsolve-export/namespace.js | 173 +++++++++++++++++ src/dot-export/index.js | 40 +--- src/dot-export/namespace.js | 35 ++++ src/heta-code-export/index.js | 3 +- src/heta-code-export/namespace.js | 7 + src/julia-export/index.js | 130 +------------ src/julia-export/namespace.js | 127 +++++++++++++ src/matlab-export/index.js | 99 +--------- src/matlab-export/namespace.js | 94 ++++++++++ src/mrgsolve-export/index.js | 133 +------------ src/mrgsolve-export/namespace.js | 128 +++++++++++++ src/sbml-export/index.js | 48 +---- src/sbml-export/namespace.js | 45 +++++ src/simbio-export/index.js | 87 ++------- src/simbio-export/namespace.js | 53 ++++++ src/slv-export/index.js | 151 +-------------- src/slv-export/namespace.js | 148 +++++++++++++++ src/templates/julia-model.jl.njk | 1 - 21 files changed, 860 insertions(+), 833 deletions(-) create mode 100644 src/dbsolve-export/namespace.js create mode 100644 src/dot-export/namespace.js create mode 100644 src/heta-code-export/namespace.js create mode 100644 src/julia-export/namespace.js create mode 100644 src/matlab-export/namespace.js create mode 100644 src/mrgsolve-export/namespace.js create mode 100644 src/sbml-export/namespace.js create mode 100644 src/simbio-export/namespace.js create mode 100644 src/slv-export/namespace.js diff --git a/cases/0-hello-world/master/mm_julia/model.jl b/cases/0-hello-world/master/mm_julia/model.jl index e150535f..b472c941 100644 --- a/cases/0-hello-world/master/mm_julia/model.jl +++ b/cases/0-hello-world/master/mm_julia/model.jl @@ -1,6 +1,5 @@ #= This code was generated by heta-compiler * - =# __platform__ = (function() diff --git a/src/dbsolve-export/expression.js b/src/dbsolve-export/expression.js index cc05b726..dd8cacbc 100644 --- a/src/dbsolve-export/expression.js +++ b/src/dbsolve-export/expression.js @@ -1,6 +1,6 @@ const { Expression } = require('../core/expression'); -Expression.prototype.toSLVString = function(powTransform = 'keep'){ +Expression.prototype.toSLVString = function(powTransform = 'keep') { if (['keep', 'operator', 'function'].indexOf(powTransform) === -1) { throw new TypeError('powTransform must be one of values: "keep", "operator", "function".'); } diff --git a/src/dbsolve-export/index.js b/src/dbsolve-export/index.js index c69cf4ef..abfa9323 100644 --- a/src/dbsolve-export/index.js +++ b/src/dbsolve-export/index.js @@ -1,7 +1,7 @@ -const { AbstractExport } = require('../abstract-export'); /* global compiledTemplates */ -const _get = require('lodash/get'); +const { AbstractExport } = require('../abstract-export'); require('./expression'); +require('./namespace'); const { ajv } = require('../utils'); const schema = { @@ -14,7 +14,7 @@ const schema = { }; class DBSolveExport extends AbstractExport{ - constructor(q = {}, isCore = false){ + constructor(q = {}, isCore = false) { super(q, isCore); // check arguments here @@ -44,7 +44,7 @@ class DBSolveExport extends AbstractExport{ * * @return {string} Text code of exported format. */ - makeText(){ + makeText() { let logger = this._container.logger; // display that function definition is not supported @@ -56,7 +56,7 @@ class DBSolveExport extends AbstractExport{ let selectedNamespaces = this.selectedNamespaces(); let results = selectedNamespaces.map(([spaceName, ns]) => { - let image = this.getSLVImage(ns); + let image = ns.getDBSolveImage(this.powTransform, this.groupConstBy, this.version); let content = this.getSLVCode(image); return { @@ -68,186 +68,16 @@ class DBSolveExport extends AbstractExport{ return results; } - /** - * Creates single model image by nesessary components based on space. - * @param {string} targetSpace - Model image to update. - * - * @return {undefined} - */ - getSLVImage(ns){ - let logger = this._container.logger; - - // push active processes - let processes = ns - .selectByInstanceOf('Process') - .filter((x) => { - return x.actors.length > 0 // process with actors - && x.actors.some((actor) => { // true if there is at least non boundary target - return !actor.targetObj.boundary && !actor.targetObj.isRule; - }); - }); - // push non boundary ode variables which are mentioned in processes - let dynamicRecords = ns - .selectByInstanceOf('Record') - .filter((x) => x.isDynamic); - /* - let staticRecords = ns - .selectByInstanceOf('Record') - .filter((x) => !x.isDynamic && !x.isRule); - */ - let initRecords = ns - .sortExpressionsByContext('start_', true) - .filter((x) => { - return x.instanceOf('Record') - && (x.assignments?.start_ !== undefined || x.isRule); - }); - // create matrix - let matrix = []; - processes.forEach((process, processNum) => { - process.actors.filter((actor) => { - return !actor.targetObj.boundary - && !actor.targetObj.isRule; - }).forEach((actor) => { - let variableNum = dynamicRecords.indexOf(actor.targetObj); - matrix.push([processNum, variableNum, actor.stoichiometry]); - }); - }); - - // create and sort expressions for RHS (rules) - let ruleRecords = ns - .sortExpressionsByContext('ode_', true) - .filter((x) => x.isDynamic || x.isRule ); - - // create TimeEvents - let timeEvents = []; - ns - .selectByInstanceOf('TimeSwitcher') - .forEach((switcher) => { // scan for switch - // if period===undefined or period===0 or repeatCount===0 => single dose - // if period > 0 and (repeatCount > 0 or repeatCount===undefined) => multiple dose - let period = switcher.periodObj === undefined || switcher.repeatCountObj?.num === 0 - ? 0 - : switcher.getPeriod(); - ns - .selectRecordsByContext(switcher.id) - .forEach((record) => { // scan for records in switch - let expr = record.isDynamic && record.instanceOf('Species') && !record.isAmount - ? record.getAssignment(switcher.id).multiply(record.compartment) - : record.getAssignment(switcher.id); - - let evt = { - start: switcher.getStart(), - period: period, - on: switcher.id + '_', - target: record.id + (record.isDynamic ? '_' : ''), - multiply: 0, - add: record.id + '_' + switcher.id + '_', - expr: expr.toSLVString(this.powTransform) - }; - timeEvents.push(evt); - }); - - // transform `stop` to `event` - if (switcher.stopObj !== undefined) { - let evt = { - start: switcher.getStop(), - period: 0, - on: 1, - target: switcher.id + '_', - multiply: 0, - add: 0, - isStop: true // if false then do not put in RHS - }; - timeEvents.push(evt); - } - }); - - // Discrete Events - let discreteEvents = ns - .selectByClassName('DSwitcher') - .map((switcher) => { - // check boolean expression in trigger - if (!switcher.trigger.isComparison) { - let msg = `DBSolve supports only simple comparison operators in DSwitcher trigger, got: "${switcher.trigger.toString()}"`; - logger.error(msg, {type: 'ExportError'}); - } - - let assignments = ns - .selectRecordsByContext(switcher.id) - .map((record) => { - let expr = record.isDynamic && record.instanceOf('Species') && !record.isAmount - ? record.getAssignment(switcher.id).multiply(record.compartment) - : record.getAssignment(switcher.id); - - return { - targetObj: record, - expr: expr - }; - }); - - return { - switcher, - assignments - }; - }); - - // Continuous Events - let continuousEvents = ns - .selectByClassName('CSwitcher') - .map((switcher) => { - let assignments = ns - .selectRecordsByContext(switcher.id) - .map((record) => { - let expr = record.isDynamic && record.instanceOf('Species') && !record.isAmount - ? record.getAssignment(switcher.id).multiply(record.compartment) - : record.getAssignment(switcher.id); - - return { - targetObj: record, - expr: expr - }; - }); - - return { - switcher, - assignments - }; - }); - // group Const, instead of groupBy - let groupedConst = {}; // {group1: [const1, const2], group2: [const3, const4]} - ns.selectByClassName('Const').forEach((constant) => { - let key = _get(constant, this.groupConstBy) + ''; - if (!groupedConst.hasOwnProperty(key)) { - groupedConst[key] = []; - } - groupedConst[key].push(constant); - }); - - return { - population: ns, - dynamicRecords, - initRecords, - ruleRecords, - processes, - matrix, - powTransform: this.powTransform, - version: this.version, - timeEvents, - discreteEvents, - continuousEvents, - groupedConst, - }; - } - getSLVCode(image = {}){ + getSLVCode(image = {}) { return compiledTemplates['dbsolve-model.slv.njk'].render(image); } - get className(){ + get className() { return 'DBSolveExport'; } - get format(){ + get format() { return 'DBSolve'; } - static get validate(){ + static get validate() { return ajv.compile(schema); } } diff --git a/src/dbsolve-export/namespace.js b/src/dbsolve-export/namespace.js new file mode 100644 index 00000000..623cb9fc --- /dev/null +++ b/src/dbsolve-export/namespace.js @@ -0,0 +1,173 @@ +const { Namespace } = require('../namespace'); +const _get = require('lodash/get'); + +/** + * Creates single model image by nesessary components based on space. + * @param {string} targetSpace - Model image to update. + * + * @return {undefined} + */ +Namespace.prototype.getDBSolveImage = function(powTransform, groupConstBy, version) { + let { logger } = this.container; + + // push active processes + let processes = this + .selectByInstanceOf('Process') + .filter((x) => { + return x.actors.length > 0 // process with actors + && x.actors.some((actor) => { // true if there is at least non boundary target + return !actor.targetObj.boundary && !actor.targetObj.isRule; + }); + }); + // push non boundary ode variables which are mentioned in processes + let dynamicRecords = this + .selectByInstanceOf('Record') + .filter((x) => x.isDynamic); + /* + let staticRecords = this + .selectByInstanceOf('Record') + .filter((x) => !x.isDynamic && !x.isRule); + */ + let initRecords = this + .sortExpressionsByContext('start_', true) + .filter((x) => { + return x.instanceOf('Record') + && (x.assignments?.start_ !== undefined || x.isRule); + }); + // create matrix + let matrix = []; + processes.forEach((process, processNum) => { + process.actors.filter((actor) => { + return !actor.targetObj.boundary + && !actor.targetObj.isRule; + }).forEach((actor) => { + let variableNum = dynamicRecords.indexOf(actor.targetObj); + matrix.push([processNum, variableNum, actor.stoichiometry]); + }); + }); + + // create and sort expressions for RHS (rules) + let ruleRecords = this + .sortExpressionsByContext('ode_', true) + .filter((x) => x.isDynamic || x.isRule ); + + // create TimeEvents + let timeEvents = []; + this + .selectByInstanceOf('TimeSwitcher') + .forEach((switcher) => { // scan for switch + // if period===undefined or period===0 or repeatCount===0 => single dose + // if period > 0 and (repeatCount > 0 or repeatCount===undefined) => multiple dose + let period = switcher.periodObj === undefined || switcher.repeatCountObj?.num === 0 + ? 0 + : switcher.getPeriod(); + this + .selectRecordsByContext(switcher.id) + .forEach((record) => { // scan for records in switch + let expr = record.isDynamic && record.instanceOf('Species') && !record.isAmount + ? record.getAssignment(switcher.id).multiply(record.compartment) + : record.getAssignment(switcher.id); + + let evt = { + start: switcher.getStart(), + period: period, + on: switcher.id + '_', + target: record.id + (record.isDynamic ? '_' : ''), + multiply: 0, + add: record.id + '_' + switcher.id + '_', + expr: expr.toSLVString(powTransform) + }; + timeEvents.push(evt); + }); + + // transform `stop` to `event` + if (switcher.stopObj !== undefined) { + let evt = { + start: switcher.getStop(), + period: 0, + on: 1, + target: switcher.id + '_', + multiply: 0, + add: 0, + isStop: true // if false then do not put in RHS + }; + timeEvents.push(evt); + } + }); + + // Discrete Events + let discreteEvents = this + .selectByClassName('DSwitcher') + .map((switcher) => { + // check boolean expression in trigger + if (!switcher.trigger.isComparison) { + let msg = `DBSolve supports only simple comparison operators in DSwitcher trigger, got: "${switcher.trigger.toString()}"`; + logger.error(msg, {type: 'ExportError'}); + } + + let assignments = this + .selectRecordsByContext(switcher.id) + .map((record) => { + let expr = record.isDynamic && record.instanceOf('Species') && !record.isAmount + ? record.getAssignment(switcher.id).multiply(record.compartment) + : record.getAssignment(switcher.id); + + return { + targetObj: record, + expr: expr + }; + }); + + return { + switcher, + assignments + }; + }); + + // Continuous Events + let continuousEvents = this + .selectByClassName('CSwitcher') + .map((switcher) => { + let assignments = this + .selectRecordsByContext(switcher.id) + .map((record) => { + let expr = record.isDynamic && record.instanceOf('Species') && !record.isAmount + ? record.getAssignment(switcher.id).multiply(record.compartment) + : record.getAssignment(switcher.id); + + return { + targetObj: record, + expr: expr + }; + }); + + return { + switcher, + assignments + }; + }); + // group Const, instead of groupBy + let groupedConst = {}; // {group1: [const1, const2], group2: [const3, const4]} + this.selectByClassName('Const').forEach((constant) => { + let key = _get(constant, groupConstBy) + ''; + if (!groupedConst.hasOwnProperty(key)) { + groupedConst[key] = []; + } + groupedConst[key].push(constant); + }); + + return { + population: this, + dynamicRecords, + initRecords, + ruleRecords, + processes, + matrix, + powTransform: powTransform, + version: version, + timeEvents, + discreteEvents, + continuousEvents, + groupedConst, + }; +}; \ No newline at end of file diff --git a/src/dot-export/index.js b/src/dot-export/index.js index aceb2f92..604f10be 100644 --- a/src/dot-export/index.js +++ b/src/dot-export/index.js @@ -1,6 +1,7 @@ -const { AbstractExport } = require('../abstract-export'); /* global compiledTemplates */ +const { AbstractExport } = require('../abstract-export'); const { ajv } = require('../utils'); +require('./namespace'); const schema = { type: 'object', @@ -8,7 +9,7 @@ const schema = { } }; -class DotExport extends AbstractExport{ +class DotExport extends AbstractExport { constructor(q = {}, isCore = false){ super(q, isCore); @@ -32,7 +33,7 @@ class DotExport extends AbstractExport{ let selectedNamespaces = this.selectedNamespaces(); let results = selectedNamespaces.map(([spaceName, ns]) => { - let image = this.getDotImage(ns); + let image = ns.getDotImage(); let content = this.getDotCode(image); return { @@ -44,39 +45,6 @@ class DotExport extends AbstractExport{ return results; } - getDotImage(ns){ - // group by clusters - let clustersDict = {_: []}; - ns.selectByInstanceOf('Compartment') - .forEach((comp) => clustersDict[comp.id] = []); - ns.selectByInstanceOf('Process') - .forEach((proc) => { - let substrates = proc.actors.filter((x) => x.stoichiometry < 0); - // push records - proc.actors.forEach((actor) => { - let record = ns.get(actor.target) || { id: actor.target }; // use fake record - let compartmentId = record.compartment || '_'; - clustersDict[compartmentId]?.push(record) || (clustersDict[compartmentId] = [record]); - }); - // push process - let compartmentOfFirstSubstrate = ns.get(substrates[0]?.target)?.compartment || '_'; - clustersDict[compartmentOfFirstSubstrate]?.push(proc); - }); - /* display all records - ns.selectByInstanceOf('Record') - .forEach((rec) => { - if (rec.compartment !== undefined) { - clustersDict[rec.compartment].push(rec); - } else { - clustersDict['_'].push(rec); - } - }); - */ - return { - ns, - clustersDict - }; - } getDotCode(image = {}){ return compiledTemplates['dot.dot.njk'].render(image); } diff --git a/src/dot-export/namespace.js b/src/dot-export/namespace.js new file mode 100644 index 00000000..69737125 --- /dev/null +++ b/src/dot-export/namespace.js @@ -0,0 +1,35 @@ +const { Namespace } = require('../namespace'); + +Namespace.prototype.getDotImage = function() { + // group by clusters + let clustersDict = {_: []}; + this.selectByInstanceOf('Compartment') + .forEach((comp) => clustersDict[comp.id] = []); + this.selectByInstanceOf('Process') + .forEach((proc) => { + let substrates = proc.actors.filter((x) => x.stoichiometry < 0); + // push records + proc.actors.forEach((actor) => { + let record = this.get(actor.target) || { id: actor.target }; // use fake record + let compartmentId = record.compartment || '_'; + clustersDict[compartmentId]?.push(record) || (clustersDict[compartmentId] = [record]); + }); + // push process + let compartmentOfFirstSubstrate = this.get(substrates[0]?.target)?.compartment || '_'; + clustersDict[compartmentOfFirstSubstrate]?.push(proc); + }); + /* display all records + this.selectByInstanceOf('Record') + .forEach((rec) => { + if (rec.compartment !== undefined) { + clustersDict[rec.compartment].push(rec); + } else { + clustersDict['_'].push(rec); + } + }); + */ + return { + ns: this, + clustersDict + }; +}; \ No newline at end of file diff --git a/src/heta-code-export/index.js b/src/heta-code-export/index.js index b401d2ed..657dea03 100644 --- a/src/heta-code-export/index.js +++ b/src/heta-code-export/index.js @@ -1,6 +1,7 @@ -const { AbstractExport } = require('../abstract-export'); /* global compiledTemplates */ +const { AbstractExport } = require('../abstract-export'); const { ajv } = require('../utils'); +require('./namespace'); const schema = { type: 'object', diff --git a/src/heta-code-export/namespace.js b/src/heta-code-export/namespace.js new file mode 100644 index 00000000..40b5a526 --- /dev/null +++ b/src/heta-code-export/namespace.js @@ -0,0 +1,7 @@ +const { Namespace } = require('../namespace'); + +/* +Namespace.prototype.getHetaCodeImage = function() { + +}; +*/ \ No newline at end of file diff --git a/src/julia-export/index.js b/src/julia-export/index.js index f84b014f..302da120 100644 --- a/src/julia-export/index.js +++ b/src/julia-export/index.js @@ -1,8 +1,8 @@ -const { AbstractExport } = require('../abstract-export'); /* global compiledTemplates */ +const { AbstractExport } = require('../abstract-export'); const pkg = require('../../package'); -require('./expression'); // to use method toJuliaString() -const { ajv, uniqBy } = require('../utils'); +const { ajv } = require('../utils'); +require('./namespace'); const schema = { type: 'object', @@ -40,7 +40,7 @@ class JuliaExport extends AbstractExport { //let logger = this._container.logger; // create image for multiple namespaces let nsImages = this.selectedNamespaces() - .map(([spaceName, ns]) => this.getJuliaImage(ns)); + .map(([spaceName, ns]) => ns.getJuliaImage()); // create Content let image = { @@ -64,118 +64,6 @@ class JuliaExport extends AbstractExport { } ]; } - getJuliaImage(ns){ - // constants - let constants = ns - .selectByInstanceOf('Const'); - // ODE variables - let dynamicRecords = ns - .selectByInstanceOf('Record') - .filter((x) => x.isDynamic); - // currently we output all records - let extendedRuleRecords = ns - .sortExpressionsByContext('ode_', true) - .filter((x) => x.isExtendedRule); - let staticRecords = ns - .selectByInstanceOf('Record') - .filter((x) => !x.isDynamic && !x.isRule); - // RHS of ODE - let rhs = dynamicRecords.map((record) => { - return record.backReferences.map((ref, i) => { - if (ref.stoichiometry === -1) { - var st = '-'; - } else if (ref.stoichiometry < 0) { - st = ref.stoichiometry + '*'; - } else if (ref.stoichiometry === 1) { - st = i === 0 ? '' : '+'; - } else { // ref.stoichiometry >= 0 - st = i === 0 ? ref.stoichiometry + '*' : '+' + ref.stoichiometry + '*'; - } - - // XXX this is wrong solution because it results in problem d(comp1*S1)/dt = r1*comp1 - let isCompartmentRequired = ref._process_.className === 'Process' - && record.instanceOf('Species') - && !record.isAmount; - if (isCompartmentRequired) { - return st + ref.process + '*' + record.compartment; - } else { - return st + ref.process; - } - }).join(''); - }); - - // initialize at start records - let initRecordsRaw = ns - .sortExpressionsByContext('start_') - .filter((x) => x.instanceOf('Record') && (x.assignments['start_'] !== undefined || x.isRule)); - /* shorter version of rules, but not sure it's effective - let initDeps = [].concat( - dynamicRecords.map(x => x.id), - staticRecords.map(x => x.id) - ); - let initRecords = _minimalRuleList(initRecordsRaw, initDeps); - */ - let initRecords = initRecordsRaw; - - // select only rules to calculate ode - // TODO: maybe it is betted to calculate only active Processes - let odeDeps = ns - .selectByInstanceOf('Process') - .map((x) => x.id); - let odeRules = _minimalRuleList(extendedRuleRecords, odeDeps); - - // other switchers - let events = ns - .selectByInstanceOf('_Switcher') - .map((switcher) => { - let affect = ns.toArray() - .filter((x) => { - return x.instanceOf('Record') - && x.assignments !== undefined - && x.assignments[switcher.id] !== undefined; - }); - - // find all unique dependencies inside assignments - let affectDeps = []; - affect.forEach((x) => { - let dep = x.dependOn(switcher.id, true); - affectDeps.push(...dep); - }); - - // select rules required for affect - let affectRules = _minimalRuleList(extendedRuleRecords, uniqBy(affectDeps)); - - // find all unique dependencies inside trigger - let triggerDeps = switcher.trigger ? switcher.trigger.dependOn() : []; - // select rules required for switcher - let triggerRules = _minimalRuleList(extendedRuleRecords, uniqBy(triggerDeps)); - - return { - switcher, - triggerRules, - affect, - affectRules - }; - }); - - let pTranslatorObject = {}; - constants.forEach((constant, i) => { - pTranslatorObject[constant.id] = `__constants__[${i+1}]`; - }); - - return { - namespace: ns, - constants, - dynamicRecords, - staticRecords, - rhs, - initRecords, - extendedRuleRecords, - odeRules, - events, - pTranslator: pTranslatorObject, - }; - } getModelCode(image = []){ return compiledTemplates['julia-model.jl.njk'].render(image); } @@ -186,13 +74,3 @@ class JuliaExport extends AbstractExport { module.exports = JuliaExport; -// select sub-array from rulesList which describes deps -function _minimalRuleList(rulesList, deps = []){ - // calculate number of rules to include - let rulesListIds = rulesList.map((x) => x.id); - let rulesListNum = deps.map((x) => rulesListIds.indexOf(x)); - let rulesMaxIndex = Math.max(...rulesListNum); - - // select rules required - return rulesList.slice(0, rulesMaxIndex + 1); -} diff --git a/src/julia-export/namespace.js b/src/julia-export/namespace.js new file mode 100644 index 00000000..68aa2c66 --- /dev/null +++ b/src/julia-export/namespace.js @@ -0,0 +1,127 @@ +const { Namespace } = require('../namespace'); +require('./expression'); // to use method toJuliaString() +const { uniqBy } = require('../utils'); + +Namespace.prototype.getJuliaImage = function() { + // constants + let constants = this + .selectByInstanceOf('Const'); + // ODE variables + let dynamicRecords = this + .selectByInstanceOf('Record') + .filter((x) => x.isDynamic); + // currently we output all records + let extendedRuleRecords = this + .sortExpressionsByContext('ode_', true) + .filter((x) => x.isExtendedRule); + let staticRecords = this + .selectByInstanceOf('Record') + .filter((x) => !x.isDynamic && !x.isRule); + // RHS of ODE + let rhs = dynamicRecords.map((record) => { + return record.backReferences.map((ref, i) => { + if (ref.stoichiometry === -1) { + var st = '-'; + } else if (ref.stoichiometry < 0) { + st = ref.stoichiometry + '*'; + } else if (ref.stoichiometry === 1) { + st = i === 0 ? '' : '+'; + } else { // ref.stoichiometry >= 0 + st = i === 0 ? ref.stoichiometry + '*' : '+' + ref.stoichiometry + '*'; + } + + // XXX this is wrong solution because it results in problem d(comp1*S1)/dt = r1*comp1 + let isCompartmentRequired = ref._process_.className === 'Process' + && record.instanceOf('Species') + && !record.isAmount; + if (isCompartmentRequired) { + return st + ref.process + '*' + record.compartment; + } else { + return st + ref.process; + } + }).join(''); + }); + + // initialize at start records + let initRecordsRaw = this + .sortExpressionsByContext('start_') + .filter((x) => x.instanceOf('Record') && (x.assignments['start_'] !== undefined || x.isRule)); + /* shorter version of rules, but not sure it's effective + let initDeps = [].concat( + dynamicRecords.map(x => x.id), + staticRecords.map(x => x.id) + ); + let initRecords = _minimalRuleList(initRecordsRaw, initDeps); + */ + let initRecords = initRecordsRaw; + + // select only rules to calculate ode + // TODO: maybe it is betted to calculate only active Processes + let odeDeps = this + .selectByInstanceOf('Process') + .map((x) => x.id); + let odeRules = _minimalRuleList(extendedRuleRecords, odeDeps); + + // other switchers + let events = this + .selectByInstanceOf('_Switcher') + .map((switcher) => { + let affect = this.toArray() + .filter((x) => { + return x.instanceOf('Record') + && x.assignments !== undefined + && x.assignments[switcher.id] !== undefined; + }); + + // find all unique dependencies inside assignments + let affectDeps = []; + affect.forEach((x) => { + let dep = x.dependOn(switcher.id, true); + affectDeps.push(...dep); + }); + + // select rules required for affect + let affectRules = _minimalRuleList(extendedRuleRecords, uniqBy(affectDeps)); + + // find all unique dependencies inside trigger + let triggerDeps = switcher.trigger ? switcher.trigger.dependOn() : []; + // select rules required for switcher + let triggerRules = _minimalRuleList(extendedRuleRecords, uniqBy(triggerDeps)); + + return { + switcher, + triggerRules, + affect, + affectRules + }; + }); + + let pTranslatorObject = {}; + constants.forEach((constant, i) => { + pTranslatorObject[constant.id] = `__constants__[${i+1}]`; + }); + + return { + namespace: this, + constants, + dynamicRecords, + staticRecords, + rhs, + initRecords, + extendedRuleRecords, + odeRules, + events, + pTranslator: pTranslatorObject, + }; +}; + +// select sub-array from rulesList which describes deps +function _minimalRuleList(rulesList, deps = []){ + // calculate number of rules to include + let rulesListIds = rulesList.map((x) => x.id); + let rulesListNum = deps.map((x) => rulesListIds.indexOf(x)); + let rulesMaxIndex = Math.max(...rulesListNum); + + // select rules required + return rulesList.slice(0, rulesMaxIndex + 1); +} \ No newline at end of file diff --git a/src/matlab-export/index.js b/src/matlab-export/index.js index c826b0b6..6078706c 100644 --- a/src/matlab-export/index.js +++ b/src/matlab-export/index.js @@ -1,7 +1,7 @@ -const { AbstractExport } = require('../abstract-export'); /* global compiledTemplates */ -const pkg = require('../../package'); +const { AbstractExport } = require('../abstract-export'); require('./expression'); // to use method toMatlabString() +require('./namespace'); const { ajv } = require('../utils'); const schema = { @@ -46,13 +46,12 @@ class MatlabExport extends AbstractExport { let results = []; selectedNamespaces.forEach(([spaceName, ns]) => { - let image = this.getMatlabImage(ns); + let image = ns.getMatlabImage(); let modelContent = this.getModelCode(image); let paramContent = this.getParamCode(image); let runContent = this.getRunCode(image); - results.push({ content: modelContent, pathSuffix: `/${spaceName}_model.m`, @@ -74,98 +73,6 @@ class MatlabExport extends AbstractExport { return results; } - getMatlabImage(ns){ - let builderName = pkg.name + ' of v' + pkg.version; - - // constants - let constants = ns - .selectByInstanceOf('Const'); - // ODE variables - let dynamicRecords = ns.toArray() - .filter((x) => x.instanceOf('Record') && !x.isRule); - // initialize at start records - let initRecords = ns - .sortExpressionsByContext('start_') - .filter((x) => { - return x.instanceOf('Record') - && (x.assignments?.start_ !== undefined || x.isRule); - }); - // currently we output all records - let sharedRecords = ns - .sortExpressionsByContext('ode_', true) - .filter((x) => x.instanceOf('Record')); - // RHS of ODE - let rhs = dynamicRecords - .map((record) => { - if (!record.isDynamic) { - return 0; - } else { - return record.backReferences.map((ref, i) => { - if (ref.stoichiometry === -1) { - var st = '-'; - } else if (ref.stoichiometry < 0) { - st = ref.stoichiometry + '*'; - } else if (ref.stoichiometry === 1){ - st = i === 0 ? '' : '+'; - } else { // ref.stoichiometry >= 0 - st = i === 0 ? ref.stoichiometry + '*' : '+' + ref.stoichiometry + '*'; - } - - return st + ref.process; - }).join(' '); - } - }); - - // create events from switchers - let events = ns - .selectByInstanceOf('_Switcher') - .map((switcher) => { - let affect = switcher.namespace.toArray() - .filter((x) => { - return x.instanceOf('Record') - && x.assignments !== undefined - && x.assignments[switcher.id] !== undefined; - }); - - return { - switcher, - affect - }; - }); - - let yTranslator = {}; - dynamicRecords.forEach((x, i) => { - yTranslator[x.id] = `y(${i+1})`; - }); - - let pTranslator = {}; - constants.forEach((x, i) => { - pTranslator[x.id] = `p(${i+1})`; - }); - // add from events - let const_len = constants.length; - events.forEach((x, i) => { - pTranslator[x.switcher.id + '_'] = `p(${const_len + i + 1})`; - }); - - let functionDefArray = [...ns.container.functionDefStorage.values()]; - - return { - builderName, - options: this, - namespace: ns, // set externally in Container - constants, - dynamicRecords, - rhs, - initRecords, - sharedRecords, - yTranslator, - pTranslator, - translator: Object.assign({}, yTranslator, pTranslator), - events, - functionDefArray - }; - } getModelCode(image = {}){ return compiledTemplates['matlab-model.m.njk'].render(image); } diff --git a/src/matlab-export/namespace.js b/src/matlab-export/namespace.js new file mode 100644 index 00000000..9084d2b8 --- /dev/null +++ b/src/matlab-export/namespace.js @@ -0,0 +1,94 @@ +const { Namespace } = require('../namespace'); +const pkg = require('../../package'); + +Namespace.prototype.getMatlabImage = function() { + let builderName = pkg.name + ' of v' + pkg.version; + + // constants + let constants = this + .selectByInstanceOf('Const'); + // ODE variables + let dynamicRecords = this.toArray() + .filter((x) => x.instanceOf('Record') && !x.isRule); + // initialize at start records + let initRecords = this + .sortExpressionsByContext('start_') + .filter((x) => { + return x.instanceOf('Record') + && (x.assignments?.start_ !== undefined || x.isRule); + }); + // currently we output all records + let sharedRecords = this + .sortExpressionsByContext('ode_', true) + .filter((x) => x.instanceOf('Record')); + // RHS of ODE + let rhs = dynamicRecords + .map((record) => { + if (!record.isDynamic) { + return 0; + } else { + return record.backReferences.map((ref, i) => { + if (ref.stoichiometry === -1) { + var st = '-'; + } else if (ref.stoichiometry < 0) { + st = ref.stoichiometry + '*'; + } else if (ref.stoichiometry === 1){ + st = i === 0 ? '' : '+'; + } else { // ref.stoichiometry >= 0 + st = i === 0 ? ref.stoichiometry + '*' : '+' + ref.stoichiometry + '*'; + } + + return st + ref.process; + }).join(' '); + } + }); + + // create events from switchers + let events = this + .selectByInstanceOf('_Switcher') + .map((switcher) => { + let affect = switcher.namespace.toArray() + .filter((x) => { + return x.instanceOf('Record') + && x.assignments !== undefined + && x.assignments[switcher.id] !== undefined; + }); + + return { + switcher, + affect + }; + }); + + let yTranslator = {}; + dynamicRecords.forEach((x, i) => { + yTranslator[x.id] = `y(${i+1})`; + }); + + let pTranslator = {}; + constants.forEach((x, i) => { + pTranslator[x.id] = `p(${i+1})`; + }); + // add from events + let const_len = constants.length; + events.forEach((x, i) => { + pTranslator[x.switcher.id + '_'] = `p(${const_len + i + 1})`; + }); + + let functionDefArray = [...this.container.functionDefStorage.values()]; + + return { + builderName, + namespace: this, // set externally in Container + constants, + dynamicRecords, + rhs, + initRecords, + sharedRecords, + yTranslator, + pTranslator, + translator: Object.assign({}, yTranslator, pTranslator), + events, + functionDefArray + }; +}; \ No newline at end of file diff --git a/src/mrgsolve-export/index.js b/src/mrgsolve-export/index.js index c360cf62..a4f62f54 100644 --- a/src/mrgsolve-export/index.js +++ b/src/mrgsolve-export/index.js @@ -1,7 +1,7 @@ -const { AbstractExport } = require('../abstract-export'); /* global compiledTemplates */ -require('./expression'); -const { ajv, intersection } = require('../utils'); +const { AbstractExport } = require('../abstract-export'); +const { ajv } = require('../utils'); +require('./namespace'); const schema = { type: 'object', @@ -46,7 +46,7 @@ class MrgsolveExport extends AbstractExport { } let results = selectedNamespaces.map(([spaceName, ns]) => { - let image = this.getMrgsolveImage(ns); + let image = ns.getMrgsolveImage(); var codeContent = this.getMrgsolveCode(image); return { @@ -65,131 +65,6 @@ class MrgsolveExport extends AbstractExport { return results; } - getMrgsolveImage(ns){ - // set dynamic variables - let dynamicRecords = ns - .selectByInstanceOf('Record') - .filter((x) => x.isDynamic); - let dynamicIds = dynamicRecords - .map((component) => component.id); - - // check if initials depends on dynamic initials, than stop - ns.toArray() - .filter((component) => { - return component.instanceOf('Record') - && !component.isRule; - }).forEach((record) => { - let deps = record.dependOn('start_', true); - let diff = intersection(dynamicIds, deps); - if (diff.length > 0) { - let logger = ns.container.logger; - let errorMsg = `Mrgsolve does not support when initial assignments depends on dynamic values: ${diff}\n` - + `${record.index} .= ${record.assignments.start_.toString()}`; - - logger.error(errorMsg, {type: 'ExportError'}); - } - }); - - // set array of output records - let output = ns - .selectByInstanceOf('Record') - .filter((rec) => rec.output) // only output: true - .filter((rec) => { - // remove all dynamic records written directly - return !rec.isDynamic - || (rec.instanceOf('Species') && !rec.isAmount); - }); - - // set sorted array of initials - let initRecords = ns - .sortExpressionsByContext('start_') - .filter((component) => { - return component.instanceOf('Record') - && component.assignments - && component.assignments.start_; - }); - - // set sorted array of rules - let ruleRecords = ns - .sortExpressionsByContext('ode_', true) - .filter((component) => { - return component.instanceOf('Record') - && component.assignments - && component.assignments.ode_; - }); - - // Time Events - let timeEvents = ns - .selectByInstanceOf('_Switcher') - .filter((switcher) => switcher.className === 'TimeSwitcher') - .map((switcher) => { - let assignments = ns - .selectRecordsByContext(switcher.id) - .map((record) => { - let expr = record.isDynamic && record.instanceOf('Species') && !record.isAmount - ? record.getAssignment(switcher.id).multiply(record.compartment) - : record.getAssignment(switcher.id); - - // find number of dynamic record (compartment) - // -1 means non-dynamic - let num = dynamicIds.indexOf(record.id); - - return { - target: record.id, - expr, - num - }; - }); - - return { - switcher, - assignments - }; - }); - - // Continuous Events - let continuousEvents = ns - .selectByInstanceOf('_Switcher') - .filter((switcher) => { - return switcher.className === 'CSwitcher' - || switcher.className === 'DSwitcher'; - }) - .map((switcher) => { - let assignments = ns - .selectRecordsByContext(switcher.id) - .map((record) => { - let expr = record.isDynamic && record.instanceOf('Species') && !record.isAmount - ? record.getAssignment(switcher.id).multiply(record.compartment) - : record.getAssignment(switcher.id); - - // find number of dynamic record (compartment) - // -1 means non-dynamic - let num = dynamicIds.indexOf(record.id); - - return { - target: record.id, - expr, - num - }; - }); - - return { - switcher, - assignments - }; - }); - - return { - population: ns, - dynamicRecords, - initRecords, - ruleRecords, - timeEvents, - continuousEvents, - output, - options: this - }; - } getMrgsolveCode(image = {}){ return compiledTemplates['mrgsolve-model.cpp.njk'].render(image); } diff --git a/src/mrgsolve-export/namespace.js b/src/mrgsolve-export/namespace.js new file mode 100644 index 00000000..ffa83fe4 --- /dev/null +++ b/src/mrgsolve-export/namespace.js @@ -0,0 +1,128 @@ +const { Namespace } = require('../namespace'); +require('./expression'); +const { intersection } = require('../utils'); + +Namespace.prototype.getMrgsolveImage = function() { + // set dynamic variables + let dynamicRecords = this + .selectByInstanceOf('Record') + .filter((x) => x.isDynamic); + let dynamicIds = dynamicRecords + .map((component) => component.id); + + // check if initials depends on dynamic initials, than stop + this.toArray() + .filter((component) => { + return component.instanceOf('Record') + && !component.isRule; + }).forEach((record) => { + let deps = record.dependOn('start_', true); + let diff = intersection(dynamicIds, deps); + if (diff.length > 0) { + let logger = this.container.logger; + let errorMsg = `Mrgsolve does not support when initial assignments depends on dynamic values: ${diff}\n` + + `${record.index} .= ${record.assignments.start_.toString()}`; + + logger.error(errorMsg, {type: 'ExportError'}); + } + }); + + // set array of output records + let output = this + .selectByInstanceOf('Record') + .filter((rec) => rec.output) // only output: true + .filter((rec) => { + // remove all dynamic records written directly + return !rec.isDynamic + || (rec.instanceOf('Species') && !rec.isAmount); + }); + + // set sorted array of initials + let initRecords = this + .sortExpressionsByContext('start_') + .filter((component) => { + return component.instanceOf('Record') + && component.assignments + && component.assignments.start_; + }); + + // set sorted array of rules + let ruleRecords = this + .sortExpressionsByContext('ode_', true) + .filter((component) => { + return component.instanceOf('Record') + && component.assignments + && component.assignments.ode_; + }); + + // Time Events + let timeEvents = this + .selectByInstanceOf('_Switcher') + .filter((switcher) => switcher.className === 'TimeSwitcher') + .map((switcher) => { + let assignments = this + .selectRecordsByContext(switcher.id) + .map((record) => { + let expr = record.isDynamic && record.instanceOf('Species') && !record.isAmount + ? record.getAssignment(switcher.id).multiply(record.compartment) + : record.getAssignment(switcher.id); + + // find number of dynamic record (compartment) + // -1 means non-dynamic + let num = dynamicIds.indexOf(record.id); + + return { + target: record.id, + expr, + num + }; + }); + + return { + switcher, + assignments + }; + }); + + // Continuous Events + let continuousEvents = this + .selectByInstanceOf('_Switcher') + .filter((switcher) => { + return switcher.className === 'CSwitcher' + || switcher.className === 'DSwitcher'; + }) + .map((switcher) => { + let assignments = this + .selectRecordsByContext(switcher.id) + .map((record) => { + let expr = record.isDynamic && record.instanceOf('Species') && !record.isAmount + ? record.getAssignment(switcher.id).multiply(record.compartment) + : record.getAssignment(switcher.id); + + // find number of dynamic record (compartment) + // -1 means non-dynamic + let num = dynamicIds.indexOf(record.id); + + return { + target: record.id, + expr, + num + }; + }); + + return { + switcher, + assignments + }; + }); + + return { + population: this, + dynamicRecords, + initRecords, + ruleRecords, + timeEvents, + continuousEvents, + output + }; +}; \ No newline at end of file diff --git a/src/sbml-export/index.js b/src/sbml-export/index.js index a7f6332d..8fb4afc2 100644 --- a/src/sbml-export/index.js +++ b/src/sbml-export/index.js @@ -1,8 +1,7 @@ -const { AbstractExport } = require('../abstract-export'); /* global compiledTemplates */ -require('./expression'); -const legalUnits = require('../legal-sbml-units'); +const { AbstractExport } = require('../abstract-export'); const { ajv } = require('../utils'); +require('./namespace'); const schema = { type: 'object', @@ -45,13 +44,8 @@ class SBMLExport extends AbstractExport { let selectedNamespaces = this.selectedNamespaces(); let results = selectedNamespaces.map(([spaceName, ns]) => { - let image = this.getSBMLImage(ns); + let image = ns.getSBMLImage(); var content = this.getSBMLCode(image); - - if (ns.isAbstract) { - let msg = `UnitDefinitions in SBML will be skipped for the abstract namespace "${spaceName}".`; - logger.info(msg); - } return { content: content, @@ -60,45 +54,9 @@ class SBMLExport extends AbstractExport { }; }); - return results; } - getSBMLImage(ns){ - let logger = ns.container.logger; - // set unitDefinitions for concrete namespace - if (ns.isAbstract) { - var listOfUnitDefinitions = []; - } else { - try { - listOfUnitDefinitions = ns.getUniqueUnits() - /* - .filter((units) => { - return units.length !== 1 - || legalUnits.indexOf(units[0].kind) < 0 - || units[0].exponent !== 1 - || units[0].multiplier !== 1; - }) - */ - .map((units) => { - return units - .toXmlUnitDefinition(legalUnits, { nameStyle: 'string', simplify: true }); - }); - } catch(err){ - logger.warn(err.message); - listOfUnitDefinitions = []; - } - } - - // set functionDefinition - let listOfFunctionDefinitions = [...ns.container.functionDefStorage.values()]; - - return { - population: ns, - listOfUnitDefinitions, - listOfFunctionDefinitions - }; - } getSBMLCode(image = {}){ switch (this.version) { case 'L2V3': diff --git a/src/sbml-export/namespace.js b/src/sbml-export/namespace.js new file mode 100644 index 00000000..c6cbd8a7 --- /dev/null +++ b/src/sbml-export/namespace.js @@ -0,0 +1,45 @@ +const { Namespace } = require('../namespace'); +require('./expression'); +const legalUnits = require('../legal-sbml-units'); + +Namespace.prototype.getSBMLImage = function() { + let { logger, functionDefStorage } = this.container; + + // set unitDefinitions for concrete namespace + if (this.isAbstract) { + var listOfUnitDefinitions = []; + } else { + try { + listOfUnitDefinitions = this.getUniqueUnits() + /* + .filter((units) => { + return units.length !== 1 + || legalUnits.indexOf(units[0].kind) < 0 + || units[0].exponent !== 1 + || units[0].multiplier !== 1; + }) + */ + .map((units) => { + return units + .toXmlUnitDefinition(legalUnits, { nameStyle: 'string', simplify: true }); + }); + } catch(err){ + logger.warn(err.message); + listOfUnitDefinitions = []; + } + } + + if (this.isAbstract) { + let msg = `UnitDefinitions in SBML will be skipped for the abstract namespace "${this.spaceName}".`; + logger.info(msg); + } + + // set functionDefinition + let listOfFunctionDefinitions = [...functionDefStorage.values()]; + + return { + population: this, + listOfUnitDefinitions, + listOfFunctionDefinitions + }; +}; \ No newline at end of file diff --git a/src/simbio-export/index.js b/src/simbio-export/index.js index 95b5673b..8912e19e 100644 --- a/src/simbio-export/index.js +++ b/src/simbio-export/index.js @@ -1,7 +1,7 @@ -const { AbstractExport } = require('../abstract-export'); /* global compiledTemplates */ -const legalUnits = require('./legal-units'); +const { AbstractExport } = require('../abstract-export'); const { ajv } = require('../utils'); +require('./namespace'); const schema = { type: 'object', @@ -9,7 +9,7 @@ const schema = { } }; -class SimbioExport extends AbstractExport{ +class SimbioExport extends AbstractExport { constructor(q = {}, isCore = false){ super(q, isCore); @@ -30,92 +30,39 @@ class SimbioExport extends AbstractExport{ get requireConcrete() { return true; } + // return text for all namespaces makeText(){ let logger = this._container.logger; - let selectedNamespaces = this.selectedNamespaces(); - - let results = selectedNamespaces.map(([spaceName, ns]) => { - // checking unitTerm for Species - ns.selectByInstanceOf('Species') - .filter((species) => species.isAmount) - .forEach((species) => { - if (typeof species.unitsParsed === 'undefined') { - logger.error(`Units for "${species.index}" is not found which is not allowed for Simbio format when {isAmount: true}.`); - return true; // BRAKE - } - let term = species.unitsParsed.toTerm(); - if (term === undefined) { - let msg = `Unit term cannot be calculated for species "${species.index}" that is not allowed for Simbio format when {isAmount: true}.`; - logger.error(msg, {type: 'UnitError'}); - return true; // BRAKE - } - let isLegal = species.legalTerms.some((x) => term.equal(x)); - if (!isLegal) { - let msg = `Species {isAmount: true} "${species.index}" has wrong unit term. It must be "amount" or "mass", got "${term}".`; - logger.error(msg, {type: 'UnitError'}); - return true; // BRAKE - } - }); + // display that function definition is not supported + let functionsNames = [...this._container.functionDefStorage.keys()]; + if (functionsNames.length > 0) { + logger.warn(`"FunctionDef" object: ${functionsNames.join(', ')} are presented in platform but not supported by Simbio export.`); + } - // checking unitTerm for Reaction - ns.selectByInstanceOf('Reaction') - .forEach((reaction) => { - let units = reaction.assignments['ode_'].calcUnit(reaction); - if (typeof units === 'undefined') { - //let msg = `Cannot calculate units for Reaction "${reaction.index}" which is not allowed for Simbio.`; // OK if cannot calculate - //logger.error(msg, {type: 'UnitError'}); - return true; // BRAKE - } - let term = units.toTerm(); - let isLegal = reaction.legalTerms.some((x) => term.equal(x)); - if (!isLegal) { - let msg = `Reaction "${reaction.index}" has wrong CALCULATED unit term. It must be "amount/time" or "mass/time", got ${term}`; - logger.error(msg, {type: 'UnitError'}); - return true; // BRAKE - } - }); + let results = this.selectedNamespaces().map(([spaceName, ns]) => { - // display that function definition is not supported - let functionsNames = [...this._container.functionDefStorage.keys()]; - if (functionsNames.length > 0) { - logger.warn(`"FunctionDef" object: ${functionsNames.join(', ')} are presented in platform but not supported by Simbio export.`); - } - - let image = this.getSimbioImage(ns); - let content = this.getSimbioCode(image); + let image = ns.getSimbioImage(); + let modelCode = compiledTemplates['simbio.m.njk'].render(image); return { - content: content, + content: modelCode, pathSuffix: `/${spaceName}.m`, type: 'text' }; }); + // add function definitions code + let functionsCode = compiledTemplates['simbio-tern__.m.njk'].render(this); + results.push({ - content: this.getFunCode(), + content: functionsCode, pathSuffix: '/tern__.m', type: 'text' }); return results; } - getSimbioImage(ns){ - // set functionDefinition - let listOfFunctionDefinitions = [...ns.container.functionDefStorage.values()]; - - return { - population: ns, - legalUnits, - listOfFunctionDefinitions // currently not used - }; - } - getSimbioCode(image = {}){ - return compiledTemplates['simbio.m.njk'].render(image); - } - getFunCode(){ - return compiledTemplates['simbio-tern__.m.njk'].render(this); - } static get validate(){ return ajv.compile(schema); } diff --git a/src/simbio-export/namespace.js b/src/simbio-export/namespace.js new file mode 100644 index 00000000..7b41b14a --- /dev/null +++ b/src/simbio-export/namespace.js @@ -0,0 +1,53 @@ +const { Namespace } = require('../namespace'); +const legalUnits = require('./legal-units'); + +Namespace.prototype.getSimbioImage = function() { + let { logger, functionDefStorage } = this.container; + + // checking unitTerm for Species + this.selectByInstanceOf('Species') + .filter((species) => species.isAmount) + .forEach((species) => { + if (typeof species.unitsParsed === 'undefined') { + logger.error(`Units for "${species.index}" is not found which is not allowed for Simbio format when {isAmount: true}.`); + return; // BRAKE + } + let term = species.unitsParsed.toTerm(); + if (term === undefined) { + let msg = `Unit term cannot be calculated for species "${species.index}" that is not allowed for Simbio format when {isAmount: true}.`; + logger.error(msg, {type: 'UnitError'}); + return; // BRAKE + } + let isLegal = species.legalTerms.some((x) => term.equal(x)); + if (!isLegal) { + let msg = `Species {isAmount: true} "${species.index}" has wrong unit term. It must be "amount" or "mass", got "${term}".`; + logger.error(msg, {type: 'UnitError'}); + return; // BRAKE + } + }); + + // checking unitTerm for Reaction + this.selectByInstanceOf('Reaction') + .forEach((reaction) => { + let units = reaction.assignments['ode_'].calcUnit(reaction); + if (typeof units === 'undefined') { + //let msg = `Cannot calculate units for Reaction "${reaction.index}" which is not allowed for Simbio.`; // OK if cannot calculate + //logger.error(msg, {type: 'UnitError'}); + return; // BRAKE + } + let term = units.toTerm(); + let isLegal = reaction.legalTerms.some((x) => term.equal(x)); + if (!isLegal) { + let msg = `Reaction "${reaction.index}" has wrong CALCULATED unit term. It must be "amount/time" or "mass/time", got ${term}`; + logger.error(msg, {type: 'UnitError'}); + return; // BRAKE + } + }); + + let listOfFunctionDefinitions = [...functionDefStorage.values()]; + return { + population: this, + legalUnits, + listOfFunctionDefinitions // currently not used + }; +}; diff --git a/src/slv-export/index.js b/src/slv-export/index.js index 304ec48b..a34b0be7 100644 --- a/src/slv-export/index.js +++ b/src/slv-export/index.js @@ -1,7 +1,7 @@ -const { AbstractExport } = require('../abstract-export'); /* global compiledTemplates */ -const _get = require('lodash/get'); +const { AbstractExport } = require('../abstract-export'); const { ajv } = require('../utils'); +require('./namespace'); const schema = { type: 'object', @@ -62,7 +62,7 @@ class SLVExport extends AbstractExport{ let selectedNamespaces = this.selectedNamespaces(); let results = selectedNamespaces.map(([spaceName, ns]) => { - let image = this.getSLVImage(ns); + let image = ns.getSLVImage(this.groupConstBy, this.powTransform, this.version); let content = this.getSLVCode(image); return { @@ -74,151 +74,6 @@ class SLVExport extends AbstractExport{ return results; } - /** - * Creates model image by necessary components based on space. - * @param {string} ns - Model image to update. - * - * @return {undefined} - */ - getSLVImage(ns){ - let logger = this._container.logger; - - // push active processes - let processes = []; - ns - .toArray() - .filter((x) => { - return x.instanceOf('Process') - && x.actors.length>0 // process with actors - && x.actors.some((actor) => { // true if there is at least non boundary target - return !actor.targetObj.boundary && !actor.targetObj.isRule; - }); - }) - .forEach((process) => processes.push(process)); - // push non boundary ode variables which are mentioned in processes - let variables = []; - ns - .toArray() - .filter((x) => x.instanceOf('Record') && x.isDynamic) - .forEach((record) => variables.push(record)); - // create matrix - let matrix = []; - processes.forEach((process, processNum) => { - process.actors.filter((actor) => { - return !actor.targetObj.boundary - && !actor.targetObj.isRule; - }).forEach((actor) => { - let variableNum = variables.indexOf(actor.targetObj); - matrix.push([processNum, variableNum, actor.stoichiometry]); - }); - }); - - // create and sort expressions for RHS - let rhs = ns - .sortExpressionsByContext('ode_', false) - .filter((record) => record.instanceOf('Record') && record.assignments?.ode_ !== undefined); - // check that all record in start are not Expression - let startExpressions = ns - .selectRecordsByContext('start_') - .filter((record) => record.assignments.start_.num === undefined); // check if it is not Number - if (startExpressions.length > 0) { - let errorMsg = 'SLV does not support expressions string in InitialValues.\n' - + startExpressions - .map((x) => `${x.index} []= ${x.assignments.start_.toString()}`) - .join('\n'); - logger.error(errorMsg, {type: 'ExportError'}); - } - - // create TimeEvents - let timeEvents = []; - ns - .selectByClassName('TimeSwitcher') - .forEach((switcher) => { // scan for switch - // if period===undefined or period===0 or repeatCount===0 => single dose - // if period > 0 and (repeatCount > 0 or repeatCount===undefined) => multiple dose - let period = switcher.periodObj === undefined || switcher.repeatCountObj?.num === 0 - ? 0 - : switcher.getPeriod(); - ns - .selectRecordsByContext(switcher.id) - .forEach((record) => { // scan for records in switch - let expression = record.assignments[switcher.id]; - let [multiply, add] = expression - .linearizeFor(record.id) - .map((tree) => { - if (tree.type === 'SymbolNode') { // a is symbol case, i.e. 'p1' - return tree.toString(); - } else { - try { // a can be evaluated, i.e. '3/4' - return tree.evaluate(); - } catch (e) { // other cases, i.e. 'p1*2' - logger.error(`SLV format cannot export expression "${record.id} [${switcher.id}]= ${expression.toString()}". Use only expressions of type: 'a * ${record.id} + b'`, {type: 'ExportError'}); - } - } - }); - - timeEvents.push({ - start: switcher.getStart(), - period: period, - on: switcher.id + '_', - target: record.id, - multiply: multiply, - add: add - }); - }); - // transform `stop` to `event` - if (switcher.stopObj !== undefined) { - timeEvents.push({ - start: switcher.getStop(), - period: 0, - on: 1, - target: switcher.id + '_', - multiply: 0, - add: 0 - }); - } - }); - - // DEvents - let dSwitchers = ns - .selectByClassName('DSwitcher') - .map((x) => x.id); - if (dSwitchers.length > 0) { - let msg = `SLV doesn't support @DSwitchers: ${dSwitchers.join(', ')}.`; - logger.error(msg, {type: 'ExportError'}); - } - - // CEvents - let cSwitchers = ns - .selectByClassName('CSwitcher') - .map((x) => x.id); - if (cSwitchers.length > 0) { - let msg = `SLV doesn't support @CSwitchers: ${cSwitchers.join(', ')}.`; - logger.error(msg, {type: 'ExportError'}); - } - - // group Const, instead of groupBy - let groupedConst = {}; // {group1: [const1, const2], group2: [const3, const4]} - ns.selectByClassName('Const').forEach((constant) => { - let key = _get(constant, this.groupConstBy) + ''; - if (!groupedConst.hasOwnProperty(key)) { - groupedConst[key] = []; - } - groupedConst[key].push(constant); - }); - - return { - population: ns, - processes, - variables, - matrix, - rhs, - events: timeEvents, - groupedConst: groupedConst, - powTransform: this.powTransform, - version: this.version, - }; - } getSLVCode(image = {}){ return compiledTemplates['slv-blocks-template.slv.njk'].render(image); } diff --git a/src/slv-export/namespace.js b/src/slv-export/namespace.js new file mode 100644 index 00000000..71c6b81a --- /dev/null +++ b/src/slv-export/namespace.js @@ -0,0 +1,148 @@ +const { Namespace } = require('../namespace'); +const _get = require('lodash/get'); + +/** + * Creates model image by necessary components based on space. + * @param {string} ns - Model image to update. + * + * @return {undefined} + */ +Namespace.prototype.getSLVImage = function(groupConstBy, powTransform, version) { + let { logger } = this.container; + + // push active processes + let processes = []; + this + .toArray() + .filter((x) => { + return x.instanceOf('Process') + && x.actors.length>0 // process with actors + && x.actors.some((actor) => { // true if there is at least non boundary target + return !actor.targetObj.boundary && !actor.targetObj.isRule; + }); + }) + .forEach((process) => processes.push(process)); + // push non boundary ode variables which are mentioned in processes + let variables = []; + this + .toArray() + .filter((x) => x.instanceOf('Record') && x.isDynamic) + .forEach((record) => variables.push(record)); + // create matrix + let matrix = []; + processes.forEach((process, processNum) => { + process.actors.filter((actor) => { + return !actor.targetObj.boundary + && !actor.targetObj.isRule; + }).forEach((actor) => { + let variableNum = variables.indexOf(actor.targetObj); + matrix.push([processNum, variableNum, actor.stoichiometry]); + }); + }); + + // create and sort expressions for RHS + let rhs = this + .sortExpressionsByContext('ode_', false) + .filter((record) => record.instanceOf('Record') && record.assignments?.ode_ !== undefined); + // check that all record in start are not Expression + let startExpressions = this + .selectRecordsByContext('start_') + .filter((record) => record.assignments.start_.num === undefined); // check if it is not Number + if (startExpressions.length > 0) { + let errorMsg = 'SLV does not support expressions string in InitialValues.\n' + + startExpressions + .map((x) => `${x.index} []= ${x.assignments.start_.toString()}`) + .join('\n'); + logger.error(errorMsg, {type: 'ExportError'}); + } + + // create TimeEvents + let timeEvents = []; + this + .selectByClassName('TimeSwitcher') + .forEach((switcher) => { // scan for switch + // if period===undefined or period===0 or repeatCount===0 => single dose + // if period > 0 and (repeatCount > 0 or repeatCount===undefined) => multiple dose + let period = switcher.periodObj === undefined || switcher.repeatCountObj?.num === 0 + ? 0 + : switcher.getPeriod(); + this + .selectRecordsByContext(switcher.id) + .forEach((record) => { // scan for records in switch + let expression = record.assignments[switcher.id]; + let [multiply, add] = expression + .linearizeFor(record.id) + .map((tree) => { + if (tree.type === 'SymbolNode') { // a is symbol case, i.e. 'p1' + return tree.toString(); + } else { + try { // a can be evaluated, i.e. '3/4' + return tree.evaluate(); + } catch (e) { // other cases, i.e. 'p1*2' + logger.error(`SLV format cannot export expression "${record.id} [${switcher.id}]= ${expression.toString()}". Use only expressions of type: 'a * ${record.id} + b'`, {type: 'ExportError'}); + } + } + }); + + timeEvents.push({ + start: switcher.getStart(), + period: period, + on: switcher.id + '_', + target: record.id, + multiply: multiply, + add: add + }); + }); + // transform `stop` to `event` + if (switcher.stopObj !== undefined) { + timeEvents.push({ + start: switcher.getStop(), + period: 0, + on: 1, + target: switcher.id + '_', + multiply: 0, + add: 0 + }); + } + }); + + // DEvents + let dSwitchers = this + .selectByClassName('DSwitcher') + .map((x) => x.id); + if (dSwitchers.length > 0) { + let msg = `SLV doesn't support @DSwitchers: ${dSwitchers.join(', ')}.`; + logger.error(msg, {type: 'ExportError'}); + } + + // CEvents + let cSwitchers = this + .selectByClassName('CSwitcher') + .map((x) => x.id); + if (cSwitchers.length > 0) { + let msg = `SLV doesn't support @CSwitchers: ${cSwitchers.join(', ')}.`; + logger.error(msg, {type: 'ExportError'}); + } + + // group Const, instead of groupBy + let groupedConst = {}; // {group1: [const1, const2], group2: [const3, const4]} + this.selectByClassName('Const').forEach((constant) => { + let key = _get(constant, groupConstBy) + ''; + if (!groupedConst.hasOwnProperty(key)) { + groupedConst[key] = []; + } + groupedConst[key].push(constant); + }); + + return { + population: this, + processes, + variables, + matrix, + rhs, + events: timeEvents, + groupedConst: groupedConst, + powTransform: powTransform, + version: version, + }; +}; \ No newline at end of file diff --git a/src/templates/julia-model.jl.njk b/src/templates/julia-model.jl.njk index b378931f..ecabb119 100644 --- a/src/templates/julia-model.jl.njk +++ b/src/templates/julia-model.jl.njk @@ -14,7 +14,6 @@ #= This code was generated by heta-compiler {{ builderVersion }} - {{ options.title + '.' if options.title}}{{ options.notes }} =# __platform__ = (function()