diff --git a/.gitignore b/.gitignore index 07081b1b5b..b6ec4a4606 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ stochss/dist/stochss-loading-page.html stochss/dist/stochss-quick-start.html stochss/dist/stochss-user-home.html stochss/dist/stochss-example-library.html +stochss/dist/stochss-user-settings.html jupyterhub/templates/page.html jupyterhub/templates/stochss-home.html jupyterhub/templates/stochss-job-presentation.html diff --git a/.pylintrc b/.pylintrc index 668c9fe2bd..6749525c85 100644 --- a/.pylintrc +++ b/.pylintrc @@ -329,7 +329,7 @@ indent-after-paren=4 indent-string=' ' # Maximum number of characters on a single line. -max-line-length=100 +max-line-length=120 # Maximum number of lines in a module. max-module-lines=1000 @@ -555,31 +555,31 @@ valid-metaclass-classmethod-first-arg=cls [DESIGN] # Maximum number of arguments for function / method. -max-args=5 +max-args=20 # Maximum number of attributes for a class (see R0902). -max-attributes=7 +max-attributes=25 # Maximum number of boolean expressions in an if statement (see R0916). max-bool-expr=5 # Maximum number of branch for function / method body. -max-branches=12 +max-branches=20 # Maximum number of locals for function / method body. -max-locals=15 +max-locals=25 # Maximum number of parents for a class (see R0901). max-parents=7 # Maximum number of public methods for a class (see R0904). -max-public-methods=20 +max-public-methods=30 # Maximum number of return / yield for function / method body. max-returns=6 # Maximum number of statements in function / method body. -max-statements=50 +max-statements=60 # Minimum number of public methods for a class (see R0903). min-public-methods=2 diff --git a/COPYRIGHT b/COPYRIGHT index 1472719f26..9d0ffc3188 100644 --- a/COPYRIGHT +++ b/COPYRIGHT @@ -1,5 +1,5 @@ StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/Dockerfile b/Dockerfile index 27d57c6e4d..c511ae5629 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,6 +3,7 @@ FROM jupyter/minimal-notebook:612aa5710bf9 ARG STOCHSS_PIP_EDITABLE ARG JUPYTER_CONFIG_DIR + USER root WORKDIR /stochss @@ -17,6 +18,10 @@ COPY --chown=jovyan:users requirements.txt . RUN python -m pip install --no-cache-dir -r requirements.txt +COPY --chown=jovyan:users package.json /stochss + +RUN npm install + COPY --chown=jovyan:users public_models/ /home/jovyan/Examples COPY --chown=jovyan:users . /stochss @@ -31,30 +36,10 @@ COPY --chown=jovyan:users custom.css $JUPYTER_CONFIG_DIR/custom/custom.css COPY --chown=jovyan:users jupyter_notebook_config.py $JUPYTER_CONFIG_DIR/jupyter_notebook_config.py -USER root - -#RUN wget -q https://julialang-s3.julialang.org/bin/linux/x64/1.4/julia-1.4.2-linux-x86_64.tar.gz -#RUN tar -xvzf julia-1.4.2-linux-x86_64.tar.gz -#RUN mv julia-1.4.2 /usr/local/ -#RUN chown -R jovyan:users /usr/local/julia-1.4.2/ - -USER jovyan - -#ENV PATH="/usr/local/julia-1.4.2/bin:${PATH}" - -#RUN julia -e 'using Pkg; Pkg.add("IJulia")' -#RUN julia -e 'using Pkg; Pkg.add("Plots")' -#RUN julia -e 'using Pkg; Pkg.add(PackageSpec(url="https://github.com/stochss/gillespy2lia", rev="main"))' - -RUN npm install - -RUN npm run webpack - -#RUN julia -e 'using Pkg; Pkg.add("IJulia")' -#RUN julia -e 'using Pkg; Pkg.add(PackageSpec(url="https://github.com/stochss/gillespy2lia", rev="main"))' - RUN pip install --no-cache-dir -e . RUN rm -r /home/jovyan/work +RUN npm run webpack + WORKDIR /home/jovyan diff --git a/__version__.py b/__version__.py index afa635caba..d3476703d8 100644 --- a/__version__.py +++ b/__version__.py @@ -5,7 +5,7 @@ # @website https://github.com/stochss/stochss # ============================================================================= -__version__ = '2.4.15' +__version__ = '2.5.0' __title__ = 'StochSS' __description__ = 'StochSS is an integrated development environment (IDE) \ for simulation of biochemical networks.' @@ -14,4 +14,4 @@ __author__ = 'StochSS Team' __email__ = 'bdrawert@unca.edu' __license__ = 'GPL License' -__copyright__ = 'Copyright (c) 2008-2022' +__copyright__ = 'Copyright (c) 2008-2023' diff --git a/client/app.js b/client/app.js index 65275f7076..f734e9b2d7 100644 --- a/client/app.js +++ b/client/app.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -25,61 +25,49 @@ let modals = require('./modals'); let routePrefix = 'stochss'; let apiPrefix = path.join(routePrefix, 'api'); -let getBasePath = () => { - try { - let base = document.querySelector('base') - let href = base.getAttribute('href') - return href - } catch (error) { - return '/' - } -}; -let getApiPath = () => path.join(getBasePath(), apiPrefix); -var BrowserDetect = { +let BrowserDetect = { + dataBrowser: [ + {subString: "Edge", identity: "MS Edge"}, + {subString: "MSIE", identity: "Explorer"}, + {subString: "Trident", identity: "Explorer"}, + {subString: "Firefox", identity: "Firefox"}, + {subString: "Opera", identity: "Opera"}, + {subString: "OPR", identity: "Opera"}, + {subString: "Chrome", identity: "Chrome"}, + {subString: "Safari", identity: "Safari"} + ], init: function () { this.browser = this.searchString(this.dataBrowser) || "Other"; this.version = this.searchVersion(navigator.userAgent) || this.searchVersion(navigator.appVersion) || "Unknown"; }, searchString: function (data) { - var dataString = navigator.userAgent; + let dataString = navigator.userAgent; for (var i = 0; i < data.length; i++) { this.versionSearchString = data[i].subString; - if (dataString.indexOf(data[i].subString) !== -1) { return data[i].identity; } } }, searchVersion: function (dataString) { - var index = dataString.indexOf(this.versionSearchString); - if (index === -1) { - return; - } + let index = dataString.indexOf(this.versionSearchString); + if (index === -1) { return; } - var rv = dataString.indexOf("rv:"); + let rv = dataString.indexOf("rv:"); if (this.versionSearchString === "Trident" && rv !== -1) { return parseFloat(dataString.substring(rv + 3)); - }else if(this.versionSearchString === "Safari"){ + } + + if(this.versionSearchString === "Safari"){ let versionSearchString = "Version"; - let versionIndex = dataString.indexOf("Version") + let versionIndex = dataString.indexOf("Version"); return parseFloat(dataString.substring(versionIndex + versionSearchString.length + 1, index - 1)); - }else{ - return parseFloat(dataString.substring(index + this.versionSearchString.length + 1)); } - }, - dataBrowser: [ - {subString: "Edge", identity: "MS Edge"}, - {subString: "MSIE", identity: "Explorer"}, - {subString: "Trident", identity: "Explorer"}, - {subString: "Firefox", identity: "Firefox"}, - {subString: "Opera", identity: "Opera"}, - {subString: "OPR", identity: "Opera"}, - {subString: "Chrome", identity: "Chrome"}, - {subString: "Safari", identity: "Safari"} - ] -}; + return parseFloat(dataString.substring(index + this.versionSearchString.length + 1)); + } +} let changeCollapseButtonText = (view, e) => { let source = e.target.dataset.hook; @@ -90,13 +78,40 @@ let changeCollapseButtonText = (view, e) => { let text = collapseBtn.text(); text === '+' ? collapseBtn.text('-') : collapseBtn.text('+'); } -}; - -let registerRenderSubview = (parent, view, hook) => { - parent.registerSubview(view); - parent.renderSubview(view, parent.queryByHook(hook)); -}; - +} +let copyToClipboard = (text, success, error) => { + fullURL = window.location.protocol + '//' + window.location.hostname + text; + if (window.clipboardData && window.clipboardData.setData) { + // Internet Explorer-specific code path to prevent textarea being shown while dialog is visible. + return window.clipboardData.setData("Text", fullURL); + } + else { + navigator.clipboard.writeText(fullURL).then(success, error); + } +} +let documentSetup = () => { + tooltipSetup(); + $(document).on('shown.bs.modal', (e) => { + $('[autofocus]', e.target).focus(); + }); + $(document).on('hide.bs.modal', '.modal', (e) => { + e.target.remove(); + }); +} +let getApiPath = () => path.join(getBasePath(), apiPrefix); +let getBasePath = () => { + try { + let base = document.querySelector('base') + let href = base.getAttribute('href') + return href + } catch (error) { + return '/' + } +} +let getBrowser = () => { + BrowserDetect.init(); + return {"name":BrowserDetect.browser,"version":BrowserDetect.version}; +} let getXHR = (endpoint, { always = function (err, response, body) {}, success = function (err, response, body) {}, error = function (err, response, body) {}}={}) => { @@ -117,140 +132,7 @@ let getXHR = (endpoint, { let body = {response: response, err: exception} error(exception, response, body); } -}; - -let postXHR = (endpoint, data, { - always = function (err, response, body) {}, success = function (err, response, body) {}, - error = function (err, response, body) {}}={}, isJSON) => { - try { - xhr({uri: endpoint, json: isJSON !== undefined ? isJSON : true, method: "post", body: data}, function (err, response, body) { - if(response.statusCode < 400) { - success(err, response, body); - }else if(response.statusCode < 500) { - error(err, response, body); - }else{ - console.log("Critical Error Detected"); - } - always(err, response, body); - }); - }catch(exception){ - console.log(exception); - let response = {Reason: "Network Error", Message: exception}; - let body = {response: response, err: exception} - error(exception, response, body); - } -}; - -let getBrowser = () => { - BrowserDetect.init(); - return {"name":BrowserDetect.browser,"version":BrowserDetect.version}; -} - -let validateName = (input, {rename=false, saveAs=true}={}) => { - var error = ""; - if(input.endsWith('/')) { - error = 'forward'; - } - var invalidChars = "`~!@#$%^&*=+[{]}\"|:;'<,>?\\"; - if(rename || !saveAs) { - invalidChars += "/"; - } - for(var i = 0; i < input.length; i++) { - if(invalidChars.includes(input.charAt(i))) { - error = error === "" || error === "special" ? "special" : "both"; - } - } - return error; -} - -let newWorkflow = (parent, mdlPath, isSpatial, type) => { - if(document.querySelector('#newWorkflowModal')) { - document.querySelector('#newWorkflowModal').remove() - } - let typeCodes = { - "Ensemble Simulation": "_ES", - "Spatial Ensemble Simulation": "_SES", - "Parameter Sweep": "_PS" - } - let self = parent; - let ext = isSpatial ? /.smdl/g : /.mdl/g - let typeCode = typeCodes[type]; - let name = mdlPath.split('/').pop().replace(ext, typeCode) - let modal = $(modals.createWorkflowHtml(name, type)).modal(); - let okBtn = document.querySelector('#newWorkflowModal .ok-model-btn'); - let input = document.querySelector('#newWorkflowModal #workflowNameInput'); - okBtn.disabled = false; - input.addEventListener("keyup", (event) => { - if(event.keyCode === 13){ - event.preventDefault(); - okBtn.click(); - } - }); - input.addEventListener("input", (e) => { - let endErrMsg = document.querySelector('#newWorkflowModal #workflowNameInputEndCharError') - let charErrMsg = document.querySelector('#newWorkflowModal #workflowNameInputSpecCharError') - let error = validateName(input.value) - okBtn.disabled = error !== "" || input.value.trim() === "" - charErrMsg.style.display = error === "both" || error === "special" ? "block" : "none" - endErrMsg.style.display = error === "both" || error === "forward" ? "block" : "none" - }); - okBtn.addEventListener('click', (e) => { - modal.modal("hide"); - let wkflFile = `${input.value.trim()}.wkfl`; - if(mdlPath.includes(".proj") && !mdlPath.includes(".wkgp")){ - var wkflPath = path.join(path.dirname(mdlPath), "WorkflowGroup1.wkgp", wkflFile); - }else{ - var wkflPath = path.join(path.dirname(mdlPath), wkflFile); - } - let queryString = `?path=${wkflPath}&model=${mdlPath}&type=${type}`; - let endpoint = path.join(getApiPath(), "workflow/new") + queryString; - getXHR(endpoint, { - success: (err, response, body) => { - window.location.href = `${path.join(getBasePath(), "stochss/workflow/edit")}?path=${body.path}`; - } - }); - }); -} - -tooltipSetup = () => { - $(function () { - $('[data-toggle="tooltip"]').tooltip(); - $('[data-toggle="tooltip"]').on('click ', function () { - $('[data-toggle="tooltip"]').tooltip("hide"); - }); - }); -} - -documentSetup = () => { - tooltipSetup(); - $(document).on('shown.bs.modal', function (e) { - $('[autofocus]', e.target).focus(); - }); - $(document).on('hide.bs.modal', '.modal', function (e) { - e.target.remove(); - }); -} - -copyToClipboard = (text, success, error) => { - fullURL = window.location.protocol + '//' + window.location.hostname + text; - if (window.clipboardData && window.clipboardData.setData) { - // Internet Explorer-specific code path to prevent textarea being shown while dialog is visible. - return window.clipboardData.setData("Text", fullURL); - } - else { - navigator.clipboard.writeText(fullURL).then(success, error) - } -} - -let switchToEditTab = (view, section) => { - let elementID = Boolean(view.model && view.model.elementID) ? view.model.elementID + "-" : ""; - if($(view.queryByHook(elementID + 'view-' + section)).hasClass('active')) { - $(view.queryByHook(elementID + section + '-edit-tab')).tab('show'); - $(view.queryByHook(elementID + 'edit-' + section)).addClass('active'); - $(view.queryByHook(elementID + 'view-' + section)).removeClass('active'); - } } - let maintenance = (view) => { getXHR("stochss/api/message", { always: (err, response, body) => { @@ -304,23 +186,125 @@ let maintenance = (view) => { } }); } +let newWorkflow = (parent, mdlPath, isSpatial, type) => { + if(document.querySelector('#newWorkflowModal')) { + document.querySelector('#newWorkflowModal').remove(); + } + let typeCodes = { + "Ensemble Simulation": "_ES", + "Spatial Ensemble Simulation": "_SES", + "Parameter Sweep": "_PS" + } + let ext = isSpatial ? /.smdl/g : /.mdl/g; + let typeCode = typeCodes[type]; + let name = mdlPath.split('/').pop().replace(ext, typeCode); + let modal = $(modals.createWorkflowHtml(name, type)).modal(); + let okBtn = document.querySelector('#newWorkflowModal .ok-model-btn'); + let input = document.querySelector('#newWorkflowModal #workflowNameInput'); + okBtn.disabled = false; + input.addEventListener("keyup", (event) => { + if(event.keyCode === 13){ + event.preventDefault(); + okBtn.click(); + } + }); + input.addEventListener("input", (e) => { + let endErrMsg = document.querySelector('#newWorkflowModal #workflowNameInputEndCharError'); + let charErrMsg = document.querySelector('#newWorkflowModal #workflowNameInputSpecCharError'); + let error = validateName(input.value); + okBtn.disabled = error !== "" || input.value.trim() === ""; + charErrMsg.style.display = error === "both" || error === "special" ? "block" : "none"; + endErrMsg.style.display = error === "both" || error === "forward" ? "block" : "none"; + }); + okBtn.addEventListener('click', (e) => { + modal.modal("hide"); + let wkflFile = `${input.value.trim()}.wkfl`; + if(mdlPath.includes(".proj") && !mdlPath.includes(".wkgp")){ + var wkflPath = path.join(path.dirname(mdlPath), "WorkflowGroup1.wkgp", wkflFile); + }else{ + var wkflPath = path.join(path.dirname(mdlPath), wkflFile); + } + let queryString = `?path=${wkflPath}&model=${mdlPath}&type=${type}`; + let endpoint = path.join(getApiPath(), "workflow/new") + queryString; + getXHR(endpoint, { + success: (err, response, body) => { + window.location.href = `${path.join(getBasePath(), "stochss/workflow/edit")}?path=${body.path}`; + } + }); + }); +} +let postXHR = (endpoint, data, { + always = function (err, response, body) {}, success = function (err, response, body) {}, + error = function (err, response, body) {}}={}, isJSON) => { + try { + xhr({uri: endpoint, json: isJSON !== undefined ? isJSON : true, method: "post", body: data}, function (err, response, body) { + if(response.statusCode < 400) { + success(err, response, body); + }else if(response.statusCode < 500) { + error(err, response, body); + }else{ + console.log("Critical Error Detected"); + } + always(err, response, body); + }); + }catch(exception){ + console.log(exception); + let response = {Reason: "Network Error", Message: exception}; + let body = {response: response, err: exception} + error(exception, response, body); + } +} +let registerRenderSubview = (parent, view, hook) => { + parent.registerSubview(view); + parent.renderSubview(view, parent.queryByHook(hook)); +} +let switchToEditTab = (view, section) => { + let elementID = Boolean(view.model && view.model.elementID) ? view.model.elementID + "-" : ""; + if($(view.queryByHook(elementID + 'view-' + section)).hasClass('active')) { + $(view.queryByHook(elementID + section + '-edit-tab')).tab('show'); + $(view.queryByHook(elementID + 'edit-' + section)).addClass('active'); + $(view.queryByHook(elementID + 'view-' + section)).removeClass('active'); + } +} +let tooltipSetup = () => { + $(() => { + $('[data-toggle="tooltip"]').tooltip(); + $('[data-toggle="tooltip"]').on('click ', () => { + $('[data-toggle="tooltip"]').tooltip("hide"); + }); + }); +} +let validateName = (input, {rename=false, saveAs=true}={}) => { + var error = ""; + if(input.endsWith('/')) { + error = 'forward'; + } + var invalidChars = "`~!@#$%^&*=+[{]}\"|:;'<,>?\\"; + if(rename || !saveAs) { + invalidChars += "/"; + } + for(var i = 0; i < input.length; i++) { + if(invalidChars.includes(input.charAt(i))) { + error = error === "" || error === "special" ? "special" : "both"; + } + } + return error; +} module.exports = { - routePrefix: routePrefix, - getApiPath: getApiPath, - getBasePath: getBasePath, - getBrowser: getBrowser, - registerRenderSubview: registerRenderSubview, - changeCollapseButtonText: changeCollapseButtonText, - newWorkflow: newWorkflow, - getXHR: getXHR, - postXHR: postXHR, - tooltipSetup: tooltipSetup, - documentSetup: documentSetup, - copyToClipboard: copyToClipboard, - switchToEditTab: switchToEditTab, - validateName: validateName, - maintenance: maintenance + changeCollapseButtonText: changeCollapseButtonText, + copyToClipboard: copyToClipboard, + documentSetup: documentSetup, + getApiPath: getApiPath, + getBasePath: getBasePath, + getBrowser: getBrowser, + getXHR: getXHR, + maintenance: maintenance, + newWorkflow: newWorkflow, + postXHR: postXHR, + registerRenderSubview: registerRenderSubview, + routePrefix: routePrefix, + switchToEditTab: switchToEditTab, + tooltipSetup: tooltipSetup, + validateName: validateName }; - - diff --git a/client/config.js b/client/config.js index 0d696a21c8..f6d4c42f05 100644 --- a/client/config.js +++ b/client/config.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/domain-view/domain-view.js b/client/domain-view/domain-view.js index c90b8fbefb..2274055cfe 100644 --- a/client/domain-view/domain-view.js +++ b/client/domain-view/domain-view.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -22,20 +22,21 @@ let _ = require('underscore'); //support files let app = require('../app'); let Plotly = require('plotly.js-dist'); +//collections +let Particles = require('../models/particles'); //models -let Particle = require('../models/particle'); +let Action = require('../models/action'); //views let View = require('ampersand-view'); let TypesView = require('./views/types-view'); let LimitsView = require('./views/limits-view'); +let ShapesView = require('./views/shapes-view'); +let ActionsView = require('./views/actions-view'); let QuickviewType = require('./views/quickview-type'); let PropertiesView = require('./views/properties-view'); let EditParticleView = require('./views/particle-view'); let ViewParticleView = require('./views/view-particle'); -let FillGeometryView = require('./views/fill-geometry-view'); -let ImportMeshView = require('./views/import-mesh-view'); -let Edit3DDomainView = require('./views/edit-3D-domain-view'); -let TypesDescriptionView = require('./views/types-description-view'); +let TransformationsView = require('./views/transformations-view'); //templates let template = require('./domainView.pug'); @@ -43,10 +44,11 @@ module.exports = View.extend({ template: template, events: { 'click [data-hook=collapse-domain-particles]' : 'changeCollapseButtonText', - 'click [data-hook=add-new-particle]' : 'handleAddParticle', 'click [data-hook=save-selected-particle]' : 'handleSaveParticle', 'click [data-hook=remove-selected-particle]' : 'handleRemoveParticle', - 'click [data-hook=collapse-domain-figure]' : 'changeCollapseButtonText' + 'click [data-hook=collapse-domain-figure]' : 'changeCollapseButtonText', + 'click [data-hook=collapse-dv-advanced-section]' : 'changeCollapseButtonText', + 'click [data-hook=update-preview-plot]' : 'updatePlotPreview' }, initialize: function (attrs, options) { View.prototype.initialize.apply(this, arguments); @@ -54,233 +56,98 @@ module.exports = View.extend({ this.plot = attrs.plot ? attrs.plot : null; this.elements = attrs.elements ? attrs.elements : null; this.queryStr = attrs.queryStr; - this.newPart = this.createNewParticle(); - this.actPart = {"part":null, "tn":0, "pn":0}; + this.actPart = {"action": null, "part": null, "tn": 0, "pn": 0}; this.model.updateValid(); }, render: function (attrs, options) { View.prototype.render.apply(this, arguments); - this.renderPropertiesView(); - this.renderLimitsView(); - this.renderTypesView(); if(this.readOnly) { $(this.queryByHook('domain-particles-editor')).css('display', 'none'); $(this.queryByHook('domain-figure-preview')).css('display', 'none'); this.renderTypesQuickview(); }else{ - this.updateParticleViews({includeGeometry: true}); - this.renderTypesDescriptionView(); + this.model.on('update-type-deps', this.updateTypeDeps, this); + this.model.on('update-shape-deps', this.updateShapeDeps, this); + this.model.on('update-transformation-deps', this.updateTransformationDeps, this); + this.model.on('update-particle-type-options', this.updateParticleTypeOptions, this); + this.renderEditParticleView(); } + this.renderPropertiesView(); + this.renderLimitsView(); + this.renderTypesView(); + this.renderShapesView(); + this.renderTransformationsView(); + this.renderActionsView(); if(!this.elements) { this.elements = { figure: this.queryByHook('domain-figure'), figureEmpty: this.queryByHook('domain-figure-empty') } } - let endpoint = path.join(app.getApiPath(), "spatial-model/domain-plot") + this.queryStr; + this.model.on('update-plot-preview', this.updatePlotPreview, this); if(this.plot) { this.displayFigure(); }else{ - app.getXHR(endpoint, {success: (err, response, body) => { - this.plot = body.fig; - this.traceTemp = body.trace_temp; - this.displayFigure(); - }}); + this.model.trigger('update-plot-preview', {resetFigure: false}); } }, - add3DDomain: function (limits, particles) { - let limitsChanged = this.changeDomainLimits(limits, false); - particles.forEach((particle) => { - particle = new Particle(particle); - this.model.particles.addParticle({particle: particle}); - this.addParticle({particle: particle}); - }); - if(limitsChanged) { - this.renderLimitsView(); - } - this.renderTypesView(); - this.resetFigure(); + changeCollapseButtonText: function (e) { + app.changeCollapseButtonText(this, e) }, - addMeshDomain: function (limits, particles, types, reset) { - let limitsChanged = this.changeDomainLimits(limits, reset); - if(types) { - this.addMissingTypes(types); + changeDomainLimits: function (limits) { + limitsChanged = false; + if(this.model.x_lim[0] > limits[0][0]) { + this.model.x_lim[0] = limits[0][0]; + limitsChanged = true; } - particles.forEach((particle) => { - particle = new Particle(particle); - this.model.particles.addParticle({particle: particle}); - this.addParticle({particle: particle}); - }); - if(limitsChanged) { - this.renderLimitsView(); + if(this.model.y_lim[0] > limits[1][0]) { + this.model.y_lim[0] = limits[1][0]; + limitsChanged = true; } - this.renderTypesView(); - if(types) { - this.updateParticleViews(); + if(this.model.z_lim[0] > limits[2][0]) { + this.model.z_lim[0] = limits[2][0]; + limitsChanged = true; } - this.resetFigure(); - }, - addMissingTypes: function (typeIDs) { - typeIDs.forEach((typeID) => { - if(!this.model.types.get(typeID, 'typeID')) { - let name = this.model.types.addType(); - this.addType(name, {update: false}); - } - }); - }, - addParticle: function ({particle=this.newPart}={}) { - this.plot.data[particle.type].ids.push(particle.particle_id); - this.plot.data[particle.type].x.push(particle.point[0]); - this.plot.data[particle.type].y.push(particle.point[1]); - this.plot.data[particle.type].z.push(particle.point[2]); - }, - addType: function (name, {update=true}={}) { - let newTrace = JSON.parse(JSON.stringify(this.traceTemp)); - newTrace.name = name; - this.plot.data.push(newTrace); - if(update) { - this.updateParticleViews(); + if(this.model.x_lim[1] < limits[0][1]) { + this.model.x_lim[1] = limits[0][1]; + limitsChanged = true; } - }, - applyGeometry: function (ids, type) { - let actPart = JSON.parse(JSON.stringify(this.actPart)); - ids.forEach((id) => { - let particle = this.model.particles.get(id, 'particle_id'); - // Set active particle for updating particle in the plot - this.actPart = { - part: particle, - tn: particle.type, - pn: this.plot.data[particle.type].ids.indexOf(particle.particle_id) - } - // Update the particle attributes - particle.type = type.typeID; - particle.mass = type.mass; - particle.volume = type.volume; - particle.rho = type.rho; - particle.nu = type.nu; - particle.c = type.c; - particle.fixed = type.fixed; - // Update the particle in the figure - this.changeParticleType(type.typeID, {update: false}); - }); - if(actPart.part) { - this.actPart = { - part: this.model.particles.get(actPart.part.particle_id, "particle_id"), - tn: type.typeID, - pn: this.plot.data[type.typeID].ids.indexOf(actPart.part.particle_id) - } - if(ids.includes(actPart.part.particle_id)) { - this.renderEditParticleView(); - } - }else{ - this.actPart = actPart + if(this.model.y_lim[1] < limits[1][1]) { + this.model.y_lim[1] = limits[1][1]; + limitsChanged = true; } - this.renderTypesView(); - this.resetFigure(); - }, - changeCollapseButtonText: function (e) { - app.changeCollapseButtonText(this, e) - }, - changeDomainLimits: function (limits, reset) { - var limitsChanged = false; - if(reset) { - this.model.x_lim = limits.x_lim; - this.model.y_lim = limits.y_lim; - this.model.y_lim = limits.y_lim; + if(this.model.z_lim[1] < limits[2][1]) { + this.model.z_lim[1] = limits[2][1]; limitsChanged = true; - }else{ - if(this.model.x_lim[0] > limits.x_lim[0]) { - this.model.x_lim[0] = limits.x_lim[0]; - limitsChanged = true; - } - if(this.model.y_lim[0] > limits.y_lim[0]) { - this.model.y_lim[0] = limits.y_lim[0]; - limitsChanged = true; - } - if(this.model.z_lim[0] > limits.z_lim[0]) { - this.model.z_lim[0] = limits.z_lim[0]; - limitsChanged = true; - } - if(this.model.x_lim[1] < limits.x_lim[1]) { - this.model.x_lim[1] = limits.x_lim[1]; - limitsChanged = true; - } - if(this.model.y_lim[1] < limits.y_lim[1]) { - this.model.y_lim[1] = limits.y_lim[1]; - limitsChanged = true; - } - if(this.model.z_lim[1] < limits.z_lim[1]) { - this.model.z_lim[1] = limits.z_lim[1]; - limitsChanged = true; - } } - return limitsChanged; - }, - changeParticleLocation: function () { - this.plot.data[this.actPart.tn].x[this.actPart.pn] = this.actPart.part.point[0]; - this.plot.data[this.actPart.tn].y[this.actPart.pn] = this.actPart.part.point[1]; - this.plot.data[this.actPart.tn].z[this.actPart.pn] = this.actPart.part.point[2]; - }, - changeParticleType: function (type, {update=true}={}) { - let id = this.plot.data[this.actPart.tn].ids.splice(this.actPart.pn, 1)[0]; - let x = this.plot.data[this.actPart.tn].x.splice(this.actPart.pn, 1)[0]; - let y = this.plot.data[this.actPart.tn].y.splice(this.actPart.pn, 1)[0]; - let z = this.plot.data[this.actPart.tn].z.splice(this.actPart.pn, 1)[0]; - this.plot.data[type].ids.push(id); - this.plot.data[type].x.push(x); - this.plot.data[type].y.push(y); - this.plot.data[type].z.push(z); - if(update) { - this.resetFigure(); + if(limitsChanged) { + this.renderLimitsView(); } }, - createNewParticle: function () { + createNewAction: function () { let type = this.model.types.get(0, 'typeID'); - return new Particle({ - c: type.c, - fixed: type.fixed, - mass: type.mass, - nu: type.nu, - point: [0, 0, 0], - rho: type.rho, - type: type.typeID, - volume: type.volume + return new Action({ + type: "", scope: 'Single Particle', priority: 1, enable: true, + shape: '', transformation: '', typeID: type.typeID, + point: {x: 0, y: 0, z: 0}, newPoint: {x: 0, y: 0, z: 0}, + c: type.c, fixed: type.fixed, mass: type.mass, + nu: type.nu, rho: type.rho, vol: type.volume }); }, completeAction: function (prefix) { $(this.queryByHook(`${prefix}-in-progress`)).css("display", "none"); $(this.queryByHook(`${prefix}-complete`)).css("display", "inline-block"); + $(this.queryByHook("upp-error")).css("display", "none"); setTimeout(() => { $(this.queryByHook(`${prefix}-complete`)).css('display', 'none'); }, 5000); }, - deleteParticle: function () { - this.plot.data[this.actPart.tn].ids.splice(this.actPart.pn, 1); - this.plot.data[this.actPart.tn].x.splice(this.actPart.pn, 1); - this.plot.data[this.actPart.tn].y.splice(this.actPart.pn, 1); - this.plot.data[this.actPart.tn].z.splice(this.actPart.pn, 1); - this.resetFigure(); - }, - deleteType: function (type, {unassign=true}={}) { - if(unassign) { - this.unassignAllParticles(type, {update: false}); - }else{ - if(this.actPart.part && this.actPart.part.type === type) { - this.actPart = {"part":null, "tn":0, "pn":0}; - } - if(this.newPart && this.newPart.type === type) { - this.newPart.type = 0 - } - let particles = this.model.particles.filter((particle) => { - return particle.type === type; - }); - this.model.particles.removeParticles(particles); + displayError: function () { + let views = { + types: this.typesView, + actions: this.actionsView } - this.model.realignTypes(type); - this.plot.data.splice(type, 1); - this.renderTypesView(); - this.updateParticleViews({includeGeometry: true}); - this.resetFigure(); + views[this.model.error.type].openSection(); }, displayFigure: function () { if(this.model.particles.length > 0) { @@ -293,36 +160,28 @@ module.exports = View.extend({ $(this.elements.figure).css('display', 'none'); } }, - handleAddParticle: function () { - this.startAction("anp") - this.model.particles.addParticle({particle: this.newPart}); - this.addParticle(); - this.resetFigure(); - this.renderTypesView(); - this.newPart = this.createNewParticle(); - this.renderNewParticleView(); - this.completeAction("anp"); + errorAction: function (action) { + $(this.queryByHook("upp-in-progress")).css("display", "none"); + $(this.queryByHook("upp-action-error")).text(action); + $(this.queryByHook("upp-error")).css("display", "block"); }, handleRemoveParticle: function () { this.startAction("rsp") - this.model.particles.removeParticle(this.actPart.part); - this.deleteParticle(); - this.actPart = {"part":null, "tn":0, "pn":0}; - this.renderTypesView(); - this.renderEditParticleView(); + let action = this.actPart.action; + this.model.actions.addAction("Remove Action", {action: action}); + this.model.trigger('update-plot-preview'); + this.actPart.action = this.createNewAction(); this.completeAction("rsp") + this.renderEditParticleView(); }, handleSaveParticle: function () { this.startAction("esp"); - if(this.editParticleView.origType !== this.actPart.part.type) { - this.changeParticleType(this.actPart.part.type, {update: false}); - this.renderTypesView(); - } - if(!this.actPart.part.comparePoint(this.editParticleView.origPoint)) { - this.changeParticleLocation(); - } - this.resetFigure(); + let action = this.actPart.action; + this.model.actions.addAction("Set Action", {action: action}); + this.model.trigger('update-plot-preview'); + this.actPart.action = this.createNewAction(); this.completeAction("esp"); + this.renderEditParticleView(); }, removeFigure: function () { try { @@ -332,25 +191,25 @@ module.exports = View.extend({ return } }, - renameType: function (index, name) { - this.plot.data[index].name = name; - this.resetFigure(); - }, - renderEdit3DDomainView: function () { - if(this.edit3DDomainView) { - this.edit3DDomainView.remove(); + renderActionsView: function () { + if(this.actionsView) { + this.actionsView.remove(); } - this.edit3DDomainView = new Edit3DDomainView(); - app.registerRenderSubview(this, this.edit3DDomainView, "3d-domain-container"); + this.actionsView = new ActionsView({ + collection: this.model.actions, + readOnly: this.readOnly + }); + let hook = "domain-actions-container"; + app.registerRenderSubview(this, this.actionsView, hook); }, renderEditParticleView: function () { if(this.editParticleView) { this.editParticleView.remove(); } - let disable = this.actPart.part == null + let disable = this.actPart.action == null this.editParticleView = new EditParticleView({ - model: this.actPart.part ? this.actPart.part : this.createNewParticle(), - defaultType: this.model.types.get(this.actPart.part ? this.actPart.part.type : 0, "typeID"), + model: this.actPart.action ? this.actPart.action : this.createNewAction(), + defaultType: this.model.types.get(this.actPart.action ? this.actPart.action.typeID : 0, "typeID"), viewIndex: 1, disable: disable }); @@ -359,20 +218,6 @@ module.exports = View.extend({ $(this.queryByHook("save-selected-particle")).prop('disabled', disable); $(this.queryByHook("remove-selected-particle")).prop('disabled', disable); }, - renderFillGeometryView: function () { - if(this.fillGeometryView) { - this.fillGeometryView.remove(); - } - this.fillGeometryView = new FillGeometryView(); - app.registerRenderSubview(this, this.fillGeometryView, 'fill-geometry-container'); - }, - renderImportMeshView: function () { - if(this.importMeshView) { - this.importMeshView.remove(); - } - this.importMeshView = new ImportMeshView(); - app.registerRenderSubview(this, this.importMeshView, "import-particles-section"); - }, renderLimitsView: function () { if(this.limitsView) { this.limitsView.remove(); @@ -383,17 +228,6 @@ module.exports = View.extend({ }); app.registerRenderSubview(this, this.limitsView, "domain-limits-container"); }, - renderNewParticleView: function () { - if(this.newParticleView) { - this.newParticleView.remove(); - } - this.newParticleView = new EditParticleView({ - model: this.newPart, - defaultType: this.model.types.get(0, "typeID"), - viewIndex: 0 - }); - app.registerRenderSubview(this, this.newParticleView, "new-particle-container"); - }, renderPropertiesView: function () { if(this.propertiesView) { this.propertiesView.remove(); @@ -404,12 +238,27 @@ module.exports = View.extend({ }); app.registerRenderSubview(this, this.propertiesView, "domain-properties-container"); }, - renderTypesDescriptionView: function () { - if(this.typesDescriptionView) { - this.typesDescriptionView.remove(); + renderShapesView: function () { + if(this.shapesView) { + this.shapesView.remove(); } - this.typesDescriptionView = new TypesDescriptionView(); - app.registerRenderSubview(this, this.typesDescriptionView, "particle-types-container"); + this.shapesView = new ShapesView({ + collection: this.model.shapes, + readOnly: this.readOnly + }); + let hook = "domain-shapes-container"; + app.registerRenderSubview(this, this.shapesView, hook); + }, + renderTransformationsView: function () { + if(this.transformationsView) { + this.transformationsView.reomve(); + } + this.transformationsView = new TransformationsView({ + collection: this.model.transformations, + readOnly: this.readOnly + }); + let hook = "domain-transformations-container"; + app.registerRenderSubview(this, this.transformationsView, hook); }, renderTypesQuickview: function () { if(this.typesQuickviewView) { @@ -426,21 +275,6 @@ module.exports = View.extend({ if(this.typesView) { this.typesView.remove(); } - let particleCounts = {}; - this.model.particles.forEach((particle) => { - if(particleCounts[particle.type]) { - particleCounts[particle.type] += 1; - }else{ - particleCounts[particle.type] = 1; - } - }); - this.model.types.forEach((dType) => { - if(particleCounts[dType.typeID]) { - dType.numParticles = particleCounts[dType.typeID]; - }else{ - dType.numParticles = 0; - } - }); this.typesView = new TypesView({ collection: this.model.types, readOnly: this.readOnly @@ -453,6 +287,7 @@ module.exports = View.extend({ } this.elements.select.css('display', 'none'); this.viewParticleView = new ViewParticleView({ + parent: this, model: this.actPart.part }); app.registerRenderSubview(this.elements.particle.view, this.viewParticleView, this.elements.particle.hook); @@ -463,7 +298,16 @@ module.exports = View.extend({ }, selectParticle: function (data) { let point = data.points[0]; - this.actPart.part = this.model.particles.get(point.id, 'particle_id'); + let particle = this.model.particles.get(point.id, 'particle_id') + this.actPart.part = particle; + this.actPart.action = new Action({ + type: "", scope: 'Single Particle', priority: 1, enable: true, + shape: '', transformation: '', typeID: particle.type, + point: {x: particle.point[0], y: particle.point[1], z: particle.point[2]}, + newPoint: {x: particle.point[0], y: particle.point[1], z: particle.point[2]}, + c: particle.c, fixed: particle.fixed, mass: particle.mass, + nu: particle.nu, rho: particle.rho, vol: particle.volume + }); this.actPart.tn = point.curveNumber; this.actPart.pn = point.pointNumber; if(this.readOnly) { @@ -472,73 +316,113 @@ module.exports = View.extend({ this.renderEditParticleView(); } }, - setParticleTypes: function (typeIDs, types) { - this.addMissingTypes(typeIDs); - let actPart = JSON.parse(JSON.stringify(this.actPart)); - types.forEach((type) => { - let particle = this.model.particles.get(type.particle_id, 'particle_id'); - this.actPart = { - part: particle, - tn: particle.type, - pn: this.plot.data[particle.type].ids.indexOf(particle.particle_id) - } - this.changeParticleType(type.typeID, {update: false}); - particle.type = type.typeID; - }); - if(actPart.part && actPart.part.type === type) { - this.actPart = { - part: this.model.particles.get(actPart.part.particle_id, "particle_id"), - tn: 0, - pn: this.plot.data[0].ids.indexOf(actPart.part.particle_id) - } - this.renderEditParticleView(); - }else{ - this.actPart = actPart - } - this.resetFigure(); - this.updateParticleViews(); - }, startAction: function (prefix) { $(this.queryByHook(`${prefix}-complete`)).css('display', 'none'); $(this.queryByHook(`${prefix}-in-progress`)).css("display", "inline-block"); }, - unassignAllParticles: function (type, {update=true}={}) { - let actPart = JSON.parse(JSON.stringify(this.actPart)); - this.model.particles.forEach((particle) => { - if(particle.type === type) { - this.actPart = { - part: particle, - tn: type, - pn: this.plot.data[type].ids.indexOf(particle.particle_id) + updateParticleTypeOptions: function ({currName=null, newName=null}={}) { + if(currName === null && newName === null) { return; } + if(this.editParticleView.model.typeID === currName) { + this.editParticleView.model.typeID = newName; + } + this.editParticleView.renderTypeSelectView(); + }, + updatePlotPreview: function ({resetFigure=true}={}) { + if(resetFigure) { + this.startAction("upp"); + } + let endpoint = path.join(app.getApiPath(), "spatial-model/domain-plot"); + app.postXHR(endpoint, this.model, { + success: (err, response, body) => { + this.plot = body.fig; + this.model.particles = new Particles(body.particles); + this.typesView.updateParticleCounts(this.model.particles); + this.actionsView.toggleActionsCollectionError("vv"); + if(this.readOnly) { + this.renderTypesQuickview(); + }else{ + this.actionsView.toggleActionsCollectionError("ev"); + } + if(resetFigure) { + this.resetFigure(); + this.completeAction("upp"); + }else{ + this.displayFigure(); } - this.changeParticleType(0, {update: false}); - particle.type = 0; + this.changeDomainLimits(body.limits); + }, + error: (err, response, body) => { + this.removeFigure(); + $(this.elements.figureEmpty).css('display', 'none'); + $(this.elements.figure).css('display', 'none'); + this.errorAction(body.Message); } }); - if(actPart.part) { - this.actPart = { - part: this.model.particles.get(actPart.part.particle_id, "particle_id"), - tn: 0, - pn: this.plot.data[0].ids.indexOf(actPart.part.particle_id) + }, + updateShapeDeps: function () { + let deps = []; + let shapeNames = []; + let combForms = []; + this.model.shapes.forEach((shape) => { + shapeNames.push(shape.name); + if(shape.type === "Combinatory") { + combForms.push(shape.formula) } - if(actPart.part.type === type) { - this.renderEditParticleView(); + }); + combForms.forEach((formula) => { + formula = formula.replace(/\(/g, ' ').replace(/\)/g, ' '); + let formDeps = formula.split(' '); + shapeNames.forEach((name) => { + if(formDeps.includes(name) && !deps.includes(name)) { + deps.push(name); + } + }); + }); + this.model.actions.forEach((action) => { + let shape = action.shape; + if(shapeNames.includes(shape) && !deps.includes(shape)) { + deps.push(shape); } - }else{ - this.actPart = actPart - } - if(update) { - this.renderTypesView(); - this.resetFigure(); - } + }); + this.model.shapes.trigger('update-inuse', {deps: deps}); }, - updateParticleViews: function ({includeGeometry=false}={}) { - this.renderNewParticleView(); - this.renderEditParticleView(); - this.renderEdit3DDomainView(); - this.renderImportMeshView(); - if(includeGeometry) { - this.renderFillGeometryView(); - } + updateTransformationDeps: function () { + let deps = []; + let transNames = this.model.transformations.map((transformation) => { + return transformation.name; + }); + this.model.transformations.forEach((transformation) => { + let nestedTrans = transformation.transformation; + if(transNames.includes(nestedTrans) && !deps.includes(nestedTrans)) { + deps.push(nestedTrans); + } + }); + this.model.actions.forEach((action) => { + let transformation = action.transformation; + if(transNames.includes(transformation) && !deps.includes(transformation)) { + deps.push(transformation); + } + }); + this.model.transformations.trigger('update-inuse', {deps: deps}); + }, + updateTypeDeps: function () { + let deps = []; + let revDeps = {}; + this.model.types.forEach((type) => { + if(type.name !== "Un-Assigned") { + revDeps[type.name] = []; + } + }); + this.model.actions.forEach((action) => { + let type = this.model.types.get(action.typeID, 'typeID').name; + if(Object.keys(revDeps).includes(type)) { + revDeps[type].push(action.cid); + if(!deps.includes(type)) { + deps.push(type); + } + } + }); + console.log(revDeps); + this.model.types.trigger('update-inuse', {deps: deps}); } }); diff --git a/client/domain-view/domainView.pug b/client/domain-view/domainView.pug index 646a80baa5..3b3ed1aca5 100644 --- a/client/domain-view/domainView.pug +++ b/client/domain-view/domainView.pug @@ -2,67 +2,27 @@ div#domain-view div(data-hook="domain-properties-container") - div(data-hook="domain-limits-container") - div(data-hook="domain-types-container") - div#domain-particles-editor.card(data-hook="domain-particles-editor") - - div.card-header.pb-0 - - h3.inline.mr-3 Particles - - div.inline.mr-3 - - ul.nav.nav-tabs.card-header-tabs(id="domain-particles-tabs") - - li.nav-item - - a.nav-link.tab.active(data-hook="new-particle-tab" data-toggle="tab" href="#create-new-particle") Create New - - li.nav-item - - a.nav-link.tab(data-hook="edit-particle-tab" data-toggle="tab" href="#edit-selected-particle") Edit Selected - - li.nav-item + div(data-hook="domain-shapes-container") - a.nav-link.tab(data-hook="particle-types-tab" data-toggle="tab" href="#set-particle-types") Set Types from File + div(data-hook="domain-transformations-container") - li.nav-item + div(data-hook="domain-actions-container") - a.nav-link.tab(data-hook="3d-domain-tab" data-toggle="tab" href="#create-3d-domain") Create 3D Domain - - li.nav-item - - a.nav-link.tab(data-hook="import-particles-tab" data-toggle="tab" href="#import-particles") Import Mesh - - li.nav-item - - a.nav-link.tab(data-hook="fill-geometry-tab" data-toggle="tab" href="#fill-geometry") Fill Geometry - - button.btn.btn-outline-collapse(data-toggle="collapse" data-target="#domain-particles-section" data-hook="collapse-domain-particles") - - - div.collapse.show(id="domain-particles-section" data-hook="domain-particles-section") - - div.card-body.tab-content - - div.tab-pane.active(id="create-new-particle" data-hook="new-particle-section") - - div(data-hook="new-particle-container") - - button.btn.btn-outline-primary.box-shadow(data-hook="add-new-particle") Add Particle + div#domain-particles-editor.card(data-hook="domain-particles-editor") - div.mdl-edit-btn.saving-status.inline(data-hook="anp-in-progress") + div.card-header.pb-0 - div.spinner-grow.mr-2 + h3.inline.mr-3 Edit Single Particle - span Adding particle ... + button.btn.btn-outline-collapse(data-toggle="collapse" data-target="#domain-particles-section" data-hook="collapse-domain-particles") + - div.mdl-edit-btn.saved-status.inline(data-hook="anp-complete") + div.collapse(id="domain-particles-section" data-hook="domain-particles-section") - span Particle Added + div.card-body - div.tab-pane(id="edit-selected-particle" data-hook="edit-particle-section") + div(id="edit-selected-particle" data-hook="edit-particle-section") div.text-info.mb-4(data-hook="edit-select-message" style="display: none") | Click on a particle in the Domain section to begin editing. @@ -93,34 +53,51 @@ div#domain-view span Particle Removed - div.tab-pane(id="set-particle-types" data-hook="set-particle-types-section") + div#domain-figure-preview.card(data-hook="domain-figure-preview") - div(data-hook="particle-types-container") + div.card-header.pb-0 - div.tab-pane(id="create-3d-domain" data-hook="3d-domain-section") + h3.inline.mr-3 Domain - div(data-hook="3d-domain-container") + button.btn.btn-outline-collapse(data-toggle="collapse" data-target="#domain-figure-container" data-hook="collapse-domain-figure") - - div.tab-pane(id="import-particles" data-hook="import-particles-section") + div.card-body - div(data-hook="import-particles-container") + p.mb-0 + | Preview the domain below. - div.tab-pane(id="fill-geometry" data-hook="fill-geometry-section") + div.collapse.show(id="domain-figure-container" data-hook="domain-figure-container") - div(data-hook="fill-geometry-container") + div(id="domain-figure" data-hook="domain-figure" style="height: 800px") - div#domain-figure-preview.card(data-hook="domain-figure-preview") + div.text-danger(data-hook="domain-figure-empty") The domain currently has no particles to display + + button.btn.btn-outline-primary.box-shadow.ml-2(data-hook="update-preview-plot") Update Preview + + div.mdl-edit-btn.saving-status.inline(data-hook="upp-in-progress") + + div.spinner-grow.mr-2 + + span Updating preview ... + + div.mdl-edit-btn.saved-status.inline(data-hook="upp-complete") + + span Preview Updated + + div.mdl-edit-btn.save-error-status(data-hook="upp-error") + + span(data-hook="upp-action-error") + + div.card(data-hook="domain-view-advanced-container") div.card-header.pb-0 - h3.inline.mr-3 Domain + h3.inline Advanced - button.btn.btn-outline-collapse(data-toggle="collapse" data-target="#domain-figure-container" data-hook="collapse-domain-figure") - + button.btn.btn-outline-collapse(data-toggle="collapse" data-target="#dv-advanced-section" data-hook="collapse-dv-advanced-section") + - div.collapse.show(id="domain-figure-container" data-hook="domain-figure-container") + div.collapse(id="dv-advanced-section" data-hook="dv-advanced-section") div.card-body - div(id="domain-figure" data-hook="domain-figure" style="height: 800px") - - div.text-danger(data-hook="domain-figure-empty") The domain currently has no particles to display \ No newline at end of file + div(data-hook="domain-limits-container") diff --git a/client/domain-view/templates/actionsView.pug b/client/domain-view/templates/actionsView.pug new file mode 100644 index 0000000000..69f253e7fc --- /dev/null +++ b/client/domain-view/templates/actionsView.pug @@ -0,0 +1,96 @@ +div#domain-actions-editor.card + + div.card-header.pb-0 + + h3.inline.mr-3 Actions + + div.inline.mr-3 + + ul.nav.nav-tabs.card-header-tabs(id="actions-selection-tabs") + + li.nav-item + + a.nav-link.tab.active(data-hook="actions-edit-tab" data-toggle="tab" href="#edit-actions") Edit + + li.nav-item + + a.nav-link.tab(data-hook="actions-view-tab" data-toggle="tab" href="#view-actions") View + + button.btn.btn-outline-collapse.inline(data-toggle="collapse" data-target="#collapse-actions" data-hook="collapse") + + + div.card-body + + p.mb-0 + | Actions define changes to the particle state of a Shape/region. These changes can include the adding and removing of particles, setting particle properties, and importing particles from mesh/domain files. + + div.collapse.tab-content(id="collapse-actions" data-hook="actions-list-container") + + div.tab-pane.active(id="edit-actions" data-hook="edit-actions") + + hr + + div.mb-3.mx-1.row.head.align-items-baseline + + div.col-sm-1 + + h6.inline Edit + + div.col-sm-4 + + h6.inline Type + + div.col-sm-3 + + h6.inline Priority + + div.col-sm-2 + + h6.inline Enable + + div.col-sm-2 + + h6.inline Remove + + div.my-3(data-hook="edit-actions-list") + + div(data-hook="ev-actions-collection-error") + + p.text-danger Domains must have at least one enabled action that creates particles ('Fill Action' or 'Import Action'). + + div.dropdown + + button.btn.btn-outline-primary.dropdown-toggle.box-shadow#addActionBtn( + data-hook="add-action", + data-toggle="dropdown", + aria-haspopup="true", + aria-expanded="false", + type="button" + ) Add Action + + ul.dropdown-menu(aria-labelledby="addActionBtn") + li.dropdown-item(data-hook="fill-action" data-name="Fill Action") Fill Action + li.dropdown-item(data-hook="set-action" data-name="Set Action") Set Action + li.dropdown-item(data-hook="remove-action" data-name="Remove Action") Remove Action + li.dropdown-item(href="#") Import Action » + ul.dropdown-menu.dropdown-submenu + li.dropdown-item(data-hook="xml-mesh-action" data-name="XML Mesh") XML Mesh + li.dropdown-item(data-hook="mesh-io-action" data-name="Mesh IO") Mesh IO + li.dropdown-item(data-hook="stochss-domain-action" data-name="StochSS Domain") StochSS Domain + + div.tab-pane(id="view-actions" data-hook="view-actions") + + hr + + div.mx-1.row.head.align-items-baseline + + div.col-sm-4: h6 Type + + div.col-sm-4: h6 Priority + + div.col-sm-4: h6 Enable + + div.mt-3(data-hook="view-actions-list") + + div(data-hook="vv-actions-collection-error") + + p.text-danger Domains must have at least one enabled action that creates particles ('Fill Action' or 'Import Action'). diff --git a/client/domain-view/templates/edit3DDomainView.pug b/client/domain-view/templates/edit3DDomainView.pug deleted file mode 100644 index 7e3a60dd29..0000000000 --- a/client/domain-view/templates/edit3DDomainView.pug +++ /dev/null @@ -1,231 +0,0 @@ -div.mx-1 - - h4.mt-3 Particle Distribution - - div - - hr - - div.mb-3.mx-1.row.head.align-items-baseline - - div.col-sm-3 - - div.col-sm-2 - - h6.inline X-Plane - - div.col-sm-2 - - h6.inline Y-Plane - - div.col-sm-2 - - h6.inline Z-Plane - - div.col-sm-3 - - h6.inline Total - - div.row - - div.col-sm-3 - - h6.inline Number of Particles - - div.col-sm-2 - - div(data-target="edit-3d-domain-n" data-hook="edit-3d-domain-nx" data-key="nx") - - div.col-sm-2 - - div(data-target="edit-3d-domain-n" data-hook="edit-3d-domain-ny" data-key="ny") - - div.col-sm-2 - - div(data-target="edit-3d-domain-n" data-hook="edit-3d-domain-nz" data-key="nz") - - div.col-sm-3 - - div(data-target="3d-domain-n" data-hook="3d-domain-n") - - h4.mt-3 Domain Limits - - div - - div.mx-1.row.head.align-items-baseline - - div.col-sm-3 - - div.col-sm-3 - - h6.inline X-Axis - - div.col-sm-3 - - h6.inline Y-Axis - - div.col-sm-3 - - h6.inline Z-Axis - - div.mt-3.mx-1.row - - div.col-sm-3 - - h6.inline Minimum - - div.col-sm-3 - - div(data-hook="edit-3d-domain-x-lim-min" data-target="edit-3d-domain-limitation" data-name="xLim" data-index="0") - - div.col-sm-3 - - div(data-hook="edit-3d-domain-y-lim-min" data-target="edit-3d-domain-limitation" data-name="yLim" data-index="0") - - div.col-sm-3 - - div(data-hook="edit-3d-domain-z-lim-min" data-target="edit-3d-domain-limitation" data-name="zLim" data-index="0") - - hr - - div.mx-1.row - - div.col-sm-3 - - h6.inline Maximum - - div.col-sm-3 - - div(data-hook="edit-3d-domain-x-lim-max" data-target="edit-3d-domain-limitation" data-name="xLim" data-index="1") - - div.col-sm-3 - - div(data-hook="edit-3d-domain-y-lim-max" data-target="edit-3d-domain-limitation" data-name="yLim" data-index="1") - - div.col-sm-3 - - div(data-hook="edit-3d-domain-z-lim-max" data-target="edit-3d-domain-limitation" data-name="zLim" data-index="1") - - div.mt-3 - - h4.inline Advanced - - button.btn.btn-outline-collapse(data-toggle="collapse" data-target="#edit-3D-domain-advanced" data-hook="collapse-3D-domain-advanced") + - - div.collapse(id="edit-3D-domain-advanced" data-hook="3D-domain-advanced-container") - - div.mt-3 - - h5.inline.mr-2 Type: - - div.inline(data-hook="edit-3d-domain-type-select") - - div.mt-3 - - h5 Type Defaults - - hr - - div.mb-3.mx-1.row.head.align-items-baseline - - div.col-sm-2 - - h6.inline Mass - - div.col-sm-2 - - h6.inline Volume - - div.col-sm-2 - - h6.inline Density - - div.col-sm-2 - - h6.inline Viscosity - - div.col-sm-2 - - h6.inline Speed of Sound - - div.col-sm-2 - - h6.inline Fixed - - div.row.pl-3 - - div.col-sm-2 - - div(data-hook="edit-3d-domain-td-mass") - - div.col-sm-2 - - div(data-hook="edit-3d-domain-td-vol") - - div.col-sm-2 - - div(data-hook="edit-3d-domain-td-rho") - - div.col-sm-2 - - div(data-hook="edit-3d-domain-td-nu") - - div.col-sm-2 - - div(data-hook="edit-3d-domain-td-c") - - div.col-sm-2 - - input(type="checkbox" data-hook="edit-3d-domain-td-fixed" disabled) - - div.my-3 - - h5 Particle Transformations - - hr - - div.mb-3.mx-1.row.head.align-items-baseline - - div.col-sm-4 - - h6.inline X-Axis - - div.col-sm-4 - - h6.inline Y-Axis - - div.col-sm-4 - - h6.inline Z-Axis - - div.row - - div.col-sm-4 - - div(data-target="edit-3d-domain-trans" data-hook="edit-3d-domain-x-trans" data-index="0") - - div.col-sm-4 - - div(data-target="edit-3d-domain-trans" data-hook="edit-3d-domain-y-trans" data-index="1") - - div.col-sm-4 - - div(data-target="edit-3d-domain-trans" data-hook="edit-3d-domain-z-trans" data-index="2") - - div.inline - - button.btn.btn-outline-primary(data-hook="build-3d-domain") Build Domain - - div.mdl-edit-btn.saving-status.inline(data-hook="c3dd-in-progress") - - div.spinner-grow.mr-2 - - span Creating domain ... - - div.mdl-edit-btn.saved-status.inline(data-hook="c3dd-complete") - - span Domain successfully created - - div.mdl-edit-btn.save-error-status(data-hook="c3dd-error") - - span(data-hook="c3dd-action-error") diff --git a/client/domain-view/templates/editAction.pug b/client/domain-view/templates/editAction.pug new file mode 100644 index 0000000000..34bea6af37 --- /dev/null +++ b/client/domain-view/templates/editAction.pug @@ -0,0 +1,265 @@ +div.mx-1 + + if(this.model.collection.indexOf(this.model) !== 0) + hr + + div.row + + div.col-sm-1 + + div.pl-3 + + input(type="checkbox" data-hook="select-action" data-toggle="collapse" data-target="#collapse-action-details" + this.model.cid) + + div.col-sm-4 + + div(data-hook="select-type-container") + + div.col-sm-3 + + div.pl-2(data-hook="input-priority-container" data-target="update-preview-plot") + + div.col-sm-2 + + input(type="checkbox" data-hook="enable-action") + + div.col-sm-2 + + button.btn.btn-outline-secondary.box-shadow(data-hook="remove") X + + div-mx-1.pl-2(data-hook="action-details") + + div.collapse(id="collapse-action-details" + this.model.cid) + + hr + + div + + div.mb-3.mx-1.row.align-items-baseline + + div.col-sm-4.pl-0.hidden(data-hook="action-scope") + + h5.inline.mr-2 Scope: + + div.inline(data-hook="select-scope-container") + + div.col-sm-4.hidden(data-hook="multi-particle-shape") + + h5.inline.mr-2 Shape: + + div.inline(data-hook="shape-container") + + div.col-sm-4.hidden(data-hook="multi-particle-transformation") + + h5.inline.mr-2 Transformation: + + div.inline(data-hook="transformation-container") + + div.hidden(data-hook="single-particle-scope") + + div + + h5.inline.mr-2 Current Location: + + div.inline.mr-2 (x: + + div.inline.ml-2(data-hook="point-x-container" data-target="point" data-name="x" style="width: 25%") + + div.inline.mr-2 , y: + + div.inline.ml-2(data-hook="point-y-container" data-target="point" data-name="y" style="width: 25%") + + div.inline.mr-2 , z: + + div.inline.ml-2(data-hook="point-z-container" data-target="point" data-name="z" style="width: 25%") + + div.inline ) + + div.hidden(data-hook="new-location") + + h5.inline.mr-2 New Location: + + div.inline.mr-2 (x: + + div.inline.ml-2(data-hook="new-point-x-container" data-target="new-point" data-name="x" style="width: 25%") + + div.inline.mr-2 , y: + + div.inline.ml-2(data-hook="new-point-y-container" data-target="new-point" data-name="y" style="width: 25%") + + div.inline.mr-2 , z: + + div.inline.ml-2(data-hook="new-point-z-container" data-target="new-point" data-name="z" style="width: 25%") + + div.inline ) + + div.hidden(data-hook="particle-properties") + + h4 Particle Properties + + div + + h5.inline.mr-2 Type: + + div.inline(data-hook="particle-type") + + div + + hr + + div.mb-3.mx-1.row.head.align-items-baseline + + div.col-sm-2 + + h6.inline Mass + + div.col-sm-2 + + h6.inline Volume + + div.col-sm-2 + + h6.inline Density + + div.col-sm-2 + + h6.inline Viscosity + + div.col-sm-2 + + h6.inline Speed of Sound + + div.col-sm-2 + + h6.inline Fixed + + div.row + + div.col-sm-2 + + div.pl-2(data-hook="particle-mass" data-target="particle-property-containers") + + div.col-sm-2 + + div(data-hook="particle-vol" data-target="particle-property-containers") + + div.col-sm-2 + + div(data-hook="particle-rho" data-target="particle-property-containers") + + div.col-sm-2 + + div(data-hook="particle-nu" data-target="particle-property-containers") + + div.col-sm-2 + + div(data-hook="particle-c" data-target="particle-property-containers") + + div.col-sm-2 + + input(type="checkbox" data-hook="particle-fixed" checked=this.model.fixed) + + div.accordion.hidden(id="import-properties" + this.model.cid data-hook="import-properties") + + div.card + + div.card-header(id="importFilesHeader" + this.model.cid) + + h3.mb-0.inline="Import Files" + + button.btn.btn-outline-collapse.collapsed.inline(type="button" data-toggle="collapse" data-target="#collapseImportFiles" + this.model.cid data-hook="collapseImportFiles" aria-expanded="false" aria-controls="collapseImportFiles" style="float: right") + + div(data-hook="import-chevron") + + svg(xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 512 512") + + path(d="M233.4 406.6c12.5 12.5 32.8 12.5 45.3 0l192-192c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L256 338.7 86.6 169.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l192 192z") + + div.collapse(id="collapseImportFiles" + this.model.cid aria-labelledby="importFilesHeader" + this.model.cid data-parent="#import-properties" + this.model.cid) + + div.card-body + + div.my-3 + + span.inline(data-hook="meshfile-label" for="meshfile")=`Please specify a ${this.filetype} to import: ` + + input.ml-2(id="meshfile" data-hook="mesh-file" type="file" name="meshfile" size="30" accept=this.accept required) + + div.mb-3 + + span.inline(for=typefile) Type descriptions (optional): + + input.ml-2(id="typefile" data-hook="type-file" type="file" name="typefile" size="30" accept=".txt") + + div.inline + + button.btn.btn-outline-primary.box-shadow(data-hook="import-mesh-file" disabled style="text-transform: capitalize;")=`Import ${this.filetype}` + + div.mdl-edit-btn.saving-status.inline(data-hook="imf-in-progress") + + div.spinner-grow.mr-2 + + span Importing mesh ... + + div.mdl-edit-btn.saved-status.inline(data-hook="imf-complete") + + span + + div.mdl-edit-btn.save-error-status(data-hook="imf-error") + + span(data-hook="imf-action-error") + + div.card + + div.card-header(id="uploadedFilesHeader" + this.model.cid) + + h3.mb-0.inline="Select Files" + + button.btn.btn-outline-collapse.collapsed.inline(type="button" data-toggle="collapse" data-target="#collapseUploadedFiles" + this.model.cid data-hook="collapseUploadedFiles" aria-expanded="false" aria-controls="collapseUploadedFiles" style="float: right") + + div(data-hook="uploaded-chevron") + + svg(xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 512 512") + + path(d="M233.4 406.6c12.5 12.5 32.8 12.5 45.3 0l192-192c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L256 338.7 86.6 169.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l192 192z") + + div.collapse(id="collapseUploadedFiles" + this.model.cid aria-labelledby="uploadedFilesHeader" + this.model.cid data-parent="#import-properties" + this.model.cid) + + div.card-body + + div + + div.hidden.text-info(data-hook="mesh-location-message")=`There are multiple ${this.filetype} files with that name, please select a location` + + div + + div.inline.mr-3 + + span.inline(data-hook="action-filename-label" for="action-filename-select")=`Please specify a ${this.filetype} to import: ` + + div.inline(id="action-filename-select" data-hook="action-mesh-select") + + div.hidden.inline(data-hook="mesh-location-container") + + span.inline.mr-2(for="mesh-location-select") Location: + + div.inline(id="mesh-location-select" data-hook="mesh-location-select") + + div.hidden(data-hook="type-descriptions-prop") + + div.hidden.text-info(data-hook="types-location-message") + | There are multiple type files with that name, please select a location + + div + + div.inline.mr-3 + + span.inline(for=action-types-types-select) Type descriptions (optional): + + div.inline(id="action-types-select" data-hook="action-type-select") + + div.hidden.inline(data-hook="types-location-container") + + span.inline.mr-2(for="file-location-select") Location: + + div.inline(id="types-location-select" data-hook="type-location-select") diff --git a/client/domain-view/templates/editParticleView.pug b/client/domain-view/templates/editParticleView.pug index 0ab7f02d4e..1918a4a35e 100644 --- a/client/domain-view/templates/editParticleView.pug +++ b/client/domain-view/templates/editParticleView.pug @@ -2,31 +2,49 @@ div.mx-1 div - h4.inline.mr-2 Type: + h5.inline.mr-2 Current Location: - div.inline(data-target="type" data-hook=`particle-type-${this.viewIndex}`) + span.inline.mr-2 (x: + + div.inline=this.model.point.x + + span.inline.mr-2 , y: + + div.inline=this.model.point.y + + span.inline.mr-2 , z: + + div.inline=this.model.point.z + + span.inline ) div - h4.inline.mr-2 Location: + h5.inline.mr-2 New Location: div.inline.mr-2 (x: - div.inline.ml2(data-target=`location-${this.viewIndex}` data-hook=`x-coord-${this.viewIndex}` data-index="0") + div.inline.ml-2(data-hook="new-point-x-container" data-target="new-point" data-name="x" style="width: 25%") div.inline.mr-2 , y: - div.inline.ml2(data-target=`location-${this.viewIndex}` data-hook=`y-coord-${this.viewIndex}` data-index="1") + div.inline.ml-2(data-hook="new-point-y-container" data-target="new-point" data-name="y" style="width: 25%") div.inline.mr-2 , z: - div.inline.ml2(data-target=`location-${this.viewIndex}` data-hook=`z-coord-${this.viewIndex}` data-index="2") + div.inline.ml-2(data-hook="new-point-z-container" data-target="new-point" data-name="z" style="width: 25%") div.inline ) h4 Particle Properties - div.pl-2(data-hook="particle-details") + div + + h5.inline.mr-2 Type: + + div.inline(data-hook="particle-type") + + div hr @@ -60,24 +78,24 @@ div.mx-1 div.col-sm-2 - div(data-hook=`particle-mass-${this.viewIndex}`) + div.pl-2(data-hook="particle-mass" data-target="particle-property-containers") div.col-sm-2 - div(data-hook=`particle-vol-${this.viewIndex}`) + div(data-hook="particle-vol" data-target="particle-property-containers") div.col-sm-2 - div(data-hook=`particle-rho-${this.viewIndex}`) + div(data-hook="particle-rho" data-target="particle-property-containers") div.col-sm-2 - div(data-hook=`particle-nu-${this.viewIndex}`) + div(data-hook="particle-nu" data-target="particle-property-containers") div.col-sm-2 - div(data-hook=`particle-c-${this.viewIndex}`) + div(data-hook="particle-c" data-target="particle-property-containers") div.col-sm-2 - input(type="checkbox" data-hook=`particle-fixed-${this.viewIndex}`) \ No newline at end of file + input(type="checkbox" data-hook="particle-fixed" checked=this.model.fixed) \ No newline at end of file diff --git a/client/domain-view/templates/editShape.pug b/client/domain-view/templates/editShape.pug new file mode 100644 index 0000000000..4f5c02e978 --- /dev/null +++ b/client/domain-view/templates/editShape.pug @@ -0,0 +1,138 @@ +div.mx-1 + + if(this.model.collection.indexOf(this.model) !== 0) + hr + + div.row + + div.col-sm-1 + + div.pl-3 + + input(type="checkbox" data-hook="select-shape" data-toggle="collapse" data-target="#edit-collapse-shape-details" + this.model.cid) + + div.col-sm-2 + + div.pl-2(data-hook="input-name-container") + + div.col-sm-2 + + div(data-hook="select-geometry-container") + + div.col-sm-4 + + div(data-hook="input-formula-container") + + div.col-sm-1 + + div.pl-3 + + input(type="checkbox" data-hook="fillable-shape") + + div.col-sm-2 + + button.btn.btn-outline-secondary.box-shadow(data-hook="remove") X + + div.mx-1.pl-2(data-hook="shape-details") + + div.collapse(id="edit-collapse-shape-details" + this.model.cid data-hook="edit-collapse-shape-details" + this.model.cid) + + hr + + div.mx-1.mb-2 + + h6.inline.mr-3 Lattice: + + div.inline(data-hook="select-lattice-container") + + div.hidden(data-hook="cartesian-lattice-props") + + div.mx-1.row.head.align-items-baseline + + div.col-sm-2 + + h6.inline Length + + div.col-sm-2 + + h6.inline Height + + div.col-sm-2 + + h6.inline Depth + + div.col-sm-2 + + h6.inline X-Spacing + + div.col-sm-2 + + h6.inline Y-Spacing + + div.col-sm-2 + + h6.inline Z-Spacing + + div.mt-3.mx-1.row + + div.col-sm-2 + + div(data-hook="length-container" data-target="shape-property") + + div.col-sm-2 + + div(data-hook="height-container" data-target="shape-property") + + div.col-sm-2 + + div(data-hook="depth-container" data-target="shape-property") + + div.col-sm-2 + + div(data-hook="delta-x-container" data-target="shape-property") + + div.col-sm-2 + + div(data-hook="delta-y-container" data-target="shape-property") + + div.col-sm-2 + + div(data-hook="delta-z-container" data-target="shape-property") + + div.hidden(data-hook="circular-lattice-props") + + div.mx-1.row.head.align-items-baseline + + div.col-sm-3 + + h6.inline Radius + + div.col-sm-3.hidden(data-hook="cylinder-length-header") + + h6.inline length + + div.col-sm-3 + + h6.inline Surface Spacing + + div.col-sm-3 + + h6.inline Radial Spacing + + div.mt-3.mx-1.row + + div.col-sm-3 + + div(data-hook="radius-container" data-target="shape-property") + + div.col-sm-3.hidden(data-hook="cylinder-length-feild") + + div(data-hook="cylinder-length-container" data-target="shape-property") + + div.col-sm-3 + + div(data-hook="delta-s-container" data-target="shape-property") + + div.col-sm-3 + + div(data-hook="delta-r-container" data-target="shape-property") diff --git a/client/domain-view/templates/editTransformation.pug b/client/domain-view/templates/editTransformation.pug new file mode 100644 index 0000000000..fafab99ea8 --- /dev/null +++ b/client/domain-view/templates/editTransformation.pug @@ -0,0 +1,264 @@ +div.mx-1 + + if(this.model.collection.indexOf(this.model) !== 0) + hr + + div.row + + div.col-sm-1 + + div.pl-3 + + input(type="checkbox" data-hook="select-transformation" data-toggle="collapse" data-target="#collapse-transformation-details" + this.model.cid) + + div.col-sm-3 + + div.pl-2(data-hook="input-name-container") + + div.col-sm-6 + + div.pl-2(data-hook="select-type-container") + + div.col-sm-2 + + button.btn.btn-outline-secondary.box-shadow(data-hook="remove") X + + div-mx-1.pl-2(data-hook="transformation-details") + + div.collapse(id="collapse-transformation-details" + this.model.cid) + + div + + h6.inline.mr-3 Nested Transformation: + + div.inline(data-hook="transformation-select-container") + + div.hidden(data-hook="vector-transformation-props") + + hr + + div.mx-1.row.head.align-items-baseline + + div.col-sm-9 + + h6 Vector + + div.col-sm-3.hidden(data-hook="rotation-angle-header") + + h6 Angle (deg) + + div.mx-1.mt-3.row + + div.col-sm-9 + + div + + span.inline [(x: + + div.inline(data-hook="vector-point1-x-container" data-target="vector-point1" data-name="x" style="width: 25%") + + span.inline.mr-2 , y: + + div.inline(data-hook="vector-point1-y-container" data-target="vector-point1" data-name="y" style="width: 25%") + + span.inline.mr-2 , z: + + div.inline(data-hook="vector-point1-z-container" data-target="vector-point1" data-name="z" style="width: 25%") + + span.inline ), + + div.pl-2 + + span.inline (x: + + div.inline(data-hook="vector-point2-x-container" data-target="vector-point2" data-name="x" style="width: 25%") + + span.inline.mr-2 , y: + + div.inline(data-hook="vector-point2-y-container" data-target="vector-point2" data-name="y" style="width: 25%") + + span.inline.mr-2 , z: + + div.inline(data-hook="vector-point2-z-container" data-target="vector-point2" data-name="z" style="width: 25%") + + span.inline )] + + div.col-sm-3.hidden(data-hook="rotation-angle-prop") + + div(data-hook="rotation-angle-container") + + div.accordion.hidden(id="reflectProps" + this.model.cid data-hook="reflect-transformation-props") + + hr + + div.mx-1.my-3.row.align-items-baseline + + div.col-sm-2 + + h6 Point 1 + + div.col-sm-10 + + div + + span.inline (x: + + div.inline(data-hook="point1-x-container" data-target="reflect-point1" data-name="x" style="width: 25%") + + span.inline.mr-2 , y: + + div.inline(data-hook="point1-y-container" data-target="reflect-point1" data-name="y" style="width: 25%") + + span.inline.mr-2 , z: + + div.inline(data-hook="point1-z-container" data-target="reflect-point1" data-name="z" style="width: 25%") + + span.inline ) + + div.card + + div.card-header(id="pointNormalHeader" + this.model.cid) + + h3.mb-0.inline Point Normal Method + + button.btn.btn-outline-collapse.collapsed.inline(type="button" data-toggle="collapse" data-target="#collapsePointNormal" + this.model.cid data-hook="collapsePointNormal" aria-expanded="false" aria-controls="collapsePointNormal" style="float: right") + + div(data-hook="point-normal-chevron") + + svg(xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 512 512") + + path(d="M233.4 406.6c12.5 12.5 32.8 12.5 45.3 0l192-192c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L256 338.7 86.6 169.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l192 192z") + + div.collapse(id="collapsePointNormal" + this.model.cid aria-labelledby="pointNormalHeader" + this.model.cid data-parent="#reflectProps" + this.model.cid) + + div.card-body.px-0 + + div.mx-1.mt-3.row.align-items-baseline + + div.col-sm-2 + + h6 Normal + + div.col-sm-10 + + div + + span.inline (x: + + div.inline(data-hook="normal-x-container" data-target="reflect-normal" data-name="x" style="width: 25%") + + span.inline.mr-2 , y: + + div.inline(data-hook="normal-y-container" data-target="reflect-normal" data-name="y" style="width: 25%") + + span.inline.mr-2 , z: + + div.inline(data-hook="normal-z-container" data-target="reflect-normal" data-name="z" style="width: 25%") + + span.inline ) + + div.card + + div.card-header(id="threePointHeader" + this.model.cid) + + h3.mb-0.inline Three Point Method + + button.btn.btn-outline-collapse.collapsed.inline(type="button" data-toggle="collapse" data-target="#collapseThreePoint" + this.model.cid data-hook="collapseThreePoint" aria-expanded="false" aria-controls="collapseThreePoint" style="float: right") + + div(data-hook="three-point-chevron") + + svg(xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 512 512") + + path(d="M233.4 406.6c12.5 12.5 32.8 12.5 45.3 0l192-192c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L256 338.7 86.6 169.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l192 192z") + + div.collapse(id="collapseThreePoint" + this.model.cid aria-labelledby="threePointHeader" + this.model.cid data-parent="#reflectProps" + this.model.cid) + + div.card-body.px-0 + + div.mx-1.mt-3.row.align-items-baseline + + div.col-sm-2 + + h6 Point 2 + + div.col-sm-10 + + div + + span.inline (x: + + div.inline(data-hook="point2-x-container" data-target="reflect-point2" data-name="x" style="width: 25%") + + span.inline.mr-2 , y: + + div.inline(data-hook="point2-y-container" data-target="reflect-point2" data-name="y" style="width: 25%") + + span.inline.mr-2 , z: + + div.inline(data-hook="point2-z-container" data-target="reflect-point2" data-name="z" style="width: 25%") + + span.inline ) + + hr + + div.mx-1.mt-3.row.align-items-baseline + + div.col-sm-2 + + h6 Point 3 + + div.col-sm-10 + + div + + span.inline (x: + + div.inline(data-hook="point3-x-container" data-target="reflect-point3" data-name="x" style="width: 25%") + + span.inline.mr-2 , y: + + div.inline(data-hook="point3-y-container" data-target="reflect-point3" data-name="y" style="width: 25%") + + span.inline.mr-2 , z: + + div.inline(data-hook="point3-z-container" data-target="reflect-point3" data-name="z" style="width: 25%") + + span.inline ) + + div.hidden(data-hook="scale-transformation-props") + + hr + + div.mx-1.row.head.align-items-baseline + + div.col-sm-3 + + h6 Factor + + div.col-sm-9 + + h6 Center + + div.mx-1.mt-3.row + + div.col-sm-3 + + div(data-hook="scale-factor-container") + + div.col-sm-9 + + div + + span.inline (x: + + div.inline(data-hook="center-x-container" data-target="scale-center" data-name="x" style="width: 25%") + + span.inline.mr-2 , y: + + div.inline(data-hook="center-y-container" data-target="scale-center" data-name="y" style="width: 25%") + + span.inline.mr-2 , z: + + div.inline(data-hook="center-z-container" data-target="scale-center" data-name="z" style="width: 25%") + + span.inline ) diff --git a/client/domain-view/templates/editType.pug b/client/domain-view/templates/editType.pug index 0c630182e8..f20f091483 100644 --- a/client/domain-view/templates/editType.pug +++ b/client/domain-view/templates/editType.pug @@ -11,31 +11,17 @@ div.mx-1 input(type="checkbox" data-hook="select" data-toggle="collapse" data-target="#collapse-type-details" + this.model.typeID) - div.col-sm-3 + div.col-sm-5 div(data-hook="type-name" data-target=this.model.typeID) - div.col-sm-3 + div.col-sm-4 div=this.model.numParticles - div.col-sm-3 - - button.btn.btn-outline-secondary.box-shadow(data-type=this.model.typeID data-hook='unassign-all') Un-Assign Particles - div.col-sm-2 - button.btn.btn-outline-secondary.box-shadow.dropdown-toggle( - id="delete-domain-type", - data-toggle="dropdown", - aria-haspopup="true", - aria-expanded="false", - type="button" - ) Delete - - ul.dropdown-menu(aria-labelledby="delete-domain-type") - li.dropdown-item(data-hook="delete-type" data-type=this.model.typeID) Type - li.dropdown-item(data-hook="delete-all" data-type=this.model.typeID) Type and Particles + button.btn.btn-outline-secondary.box-shadow(data-hook="remove") X div-mx-1.pl-2(data-hook="type-details") @@ -94,35 +80,3 @@ div.mx-1 div.col-sm-2 input(type="checkbox" data-hook="td-fixed") - - hr - - div.row.align-items-baseline - - div.col-sm-10.align-items-baseline - - h6.inline Geometry: - - div.tooltip-icon.mr-3(data-html="true" data-toggle="tooltip" title=this.tooltips.geometry) - - div.inline(style="width: 80%") - - div(id="type-geometry" data-hook="type-geometry") - - div.col-sm-2 - - button.btn.btn-outline-secondary.box-shadow(data-hook="apply-geometry") Apply - - div.mdl-edit-btn.saving-status.inline(data-hook="tg-in-progress-" + this.model.typeID) - - div.spinner-grow.mr-2 - - span Applying type to particles ... - - div.mdl-edit-btn.saved-status.inline(data-hook="tg-complete-" + this.model.typeID) - - span Type successfully applied - - div.mdl-edit-btn.save-error-status(data-hook="tg-error-" + this.model.typeID) - - span(data-hook="tg-action-error-" + this.model.typeID) diff --git a/client/domain-view/templates/fillGeometryView.pug b/client/domain-view/templates/fillGeometryView.pug deleted file mode 100644 index e9f2400326..0000000000 --- a/client/domain-view/templates/fillGeometryView.pug +++ /dev/null @@ -1,167 +0,0 @@ -div.mx-1 - - div - - h4.inline.mr-2 Type: - - div.inline(data-hook="fill-geometry-type-select") - - div - - h4.inline.mr-2 Geometry: - - div.inline(data-hook="fill-geometry-type-geometry") - - div.mt-3 - - h5 Type Defaults - - hr - - div.mb-3.mx-1.row.head.align-items-baseline - - div.col-sm-2 - - h6.inline Mass - - div.col-sm-2 - - h6.inline Volume - - div.col-sm-2 - - h6.inline Density - - div.col-sm-2 - - h6.inline Viscosity - - div.col-sm-2 - - h6.inline Speed of Sound - - div.col-sm-2 - - h6.inline Fixed - - div.row.pl-3 - - div.col-sm-2 - - div(data-hook="fill-geometry-td-mass") - - div.col-sm-2 - - div(data-hook="fill-geometry-td-vol") - - div.col-sm-2 - - div(data-hook="fill-geometry-td-rho") - - div.col-sm-2 - - div(data-hook="fill-geometry-td-nu") - - div.col-sm-2 - - div(data-hook="fill-geometry-td-c") - - div.col-sm-2 - - input(type="checkbox" data-hook="fill-geometry-td-fixed" disabled) - - h4.mt-3 Domain Limits - - div - - div.mx-1.row.head.align-items-baseline - - div.col-sm-3 - - div.col-sm-3 - - h6.inline X-Axis - - div.col-sm-3 - - h6.inline Y-Axis - - div.col-sm-3 - - h6.inline Z-Axis - - div.mt-3.mx-1.row - - div.col-sm-3 - - h6.inline Minimum - - div.col-sm-3 - - div(data-hook="fill-geometry-x-lim-min" data-target="fill-geometry-limitation" data-name="xmin") - - div.col-sm-3 - - div(data-hook="fill-geometry-y-lim-min" data-target="fill-geometry-limitation" data-name="ymin") - - div.col-sm-3 - - div(data-hook="fill-geometry-z-lim-min" data-target="fill-geometry-limitation" data-name="zmin") - - hr - - div.mx-1.row - - div.col-sm-3 - - h6.inline Maximum - - div.col-sm-3 - - div(data-hook="fill-geometry-x-lim-max" data-target="fill-geometry-limitation" data-name="xmax") - - div.col-sm-3 - - div(data-hook="fill-geometry-y-lim-max" data-target="fill-geometry-limitation" data-name="ymax") - - div.col-sm-3 - - div(data-hook="fill-geometry-z-lim-max" data-target="fill-geometry-limitation" data-name="zmax") - - hr - - div.mx-1.row - - div.col-sm-3 - - h6.inline Spacing - - div.col-sm-3 - - div(data-hook="fill-geometry-deltax" data-target="fill-geometry-delta" data-name="deltax") - - div.col-sm-3 - - div(data-hook="fill-geometry-deltay" data-target="fill-geometry-delta" data-name="deltay") - - div.col-sm-3 - - div(data-hook="fill-geometry-deltaz" data-target="fill-geometry-delta" data-name="deltaz") - - div.inline - - button.btn.btn-outline-primary(data-hook="fill-geometry" disabled) Fill with Particles - - div.mdl-edit-btn.saving-status.inline(data-hook="fg-in-progress") - - div.spinner-grow.mr-2 - - span Creating particles ... - - div.mdl-edit-btn.saved-status.inline(data-hook="fg-complete") - - span Particles successfully created - - div.mdl-edit-btn.save-error-status(data-hook="fg-error") - - span(data-hook="fg-action-error") diff --git a/client/domain-view/templates/importMeshView.pug b/client/domain-view/templates/importMeshView.pug deleted file mode 100644 index 000a38acc3..0000000000 --- a/client/domain-view/templates/importMeshView.pug +++ /dev/null @@ -1,137 +0,0 @@ -div.mx-1 - - div.my-3 - - span.inline.mr-2(for="meshfile") Please specify a mesh to import: - - input(id="meshfile" type="file" name="meshfile" size="30" accept=".xml" required) - - div.mb-3 - - span.inline.mr-2(for=typefile) Type descriptions (optional): - - input(id="typefile" type="file" name="typefile" size="30" accept=".txt") - - div.mt-3 - - h4.inline Advanced - - button.btn.btn-outline-collapse(data-toggle="collapse" data-target="#import-mesh-advanced" data-hook="collapse-import-mesh-advanced") + - - div.collapse(id="import-mesh-advanced" data-hook="import-mesh-container") - - div.mt-3 - - h5.inline.mr-2 Type: - - div.inline(data-hook="import-mesh-type-select") - - div.mt-3 - - h5 Type Defaults - - hr - - div.mb-3.mx-1.row.head.align-items-baseline - - div.col-sm-2 - - h6.inline Mass - - div.col-sm-2 - - h6.inline Volume - - div.col-sm-2 - - h6.inline Density - - div.col-sm-2 - - h6.inline Viscosity - - div.col-sm-2 - - h6.inline Speed of Sound - - div.col-sm-2 - - h6.inline Fixed - - div.row.pl-3 - - div.col-sm-2 - - div(data-hook="import-mesh-td-mass") - - div.col-sm-2 - - div(data-hook="import-mesh-td-vol") - - div.col-sm-2 - - div(data-hook="import-mesh-td-rho") - - div.col-sm-2 - - div(data-hook="import-mesh-td-nu") - - div.col-sm-2 - - div(data-hook="import-mesh-td-c") - - div.col-sm-2 - - input(type="checkbox" data-hook="import-mesh-td-fixed" disabled) - - div.my-3 - - h5 Particle Transformations - - hr - - div.mb-3.mx-1.row.head.align-items-baseline - - div.col-sm-4 - - h6.inline X-Axis - - div.col-sm-4 - - h6.inline Y-Axis - - div.col-sm-4 - - h6.inline Z-Axis - - div.row - - div.col-sm-4 - - div(data-target="import-mesh-trans" data-hook="import-mesh-x-trans" data-index="0") - - div.col-sm-4 - - div(data-target="import-mesh-trans" data-hook="import-mesh-y-trans" data-index="1") - - div.col-sm-4 - - div(data-target="import-mesh-trans" data-hook="import-mesh-z-trans" data-index="2") - - div.inline - - button.btn.btn-outline-primary.box-shadow(data-hook="import-mesh-particles" disabled) Import Mesh - - div.mdl-edit-btn.saving-status.inline(data-hook="imp-in-progress") - - div.spinner-grow.mr-2 - - span Importing mesh ... - - div.mdl-edit-btn.saved-status.inline(data-hook="imp-complete") - - span - - div.mdl-edit-btn.save-error-status(data-hook="imp-error") - - span(data-hook="imp-action-error") \ No newline at end of file diff --git a/client/domain-view/templates/limitsView.pug b/client/domain-view/templates/limitsView.pug index 3fb1868a60..c339118baa 100644 --- a/client/domain-view/templates/limitsView.pug +++ b/client/domain-view/templates/limitsView.pug @@ -16,11 +16,14 @@ div#domain-limits-editor.card a.nav-link.tab(data-hook="domain-limits-view-tab" data-toggle="tab" href="#view-domain-limits") View - button.btn.btn-ouline-collapse(data-toggle="collapse" data-target="#domain-limits-section" data-hook="collapse-domain-limits") - + button.btn.btn-ouline-collapse(data-toggle="collapse" data-target="#domain-limits-section" data-hook="collapse-domain-limits") + - div.collapse.show(id="domain-limits-section" data-hook="domain-limits-section") + div.card-body - div.card-body.tab-content + p.mb-0 + | Visualization limits for the domain. + + div.collapse.tab-content(id="domain-limits-section" data-hook="domain-limits-section") div.tab-pane.active(id="edit-domain-limits" data-hook="edit-domain-limits") diff --git a/client/domain-view/templates/propertiesView.pug b/client/domain-view/templates/propertiesView.pug index b6bc871a94..90d806f91c 100644 --- a/client/domain-view/templates/propertiesView.pug +++ b/client/domain-view/templates/propertiesView.pug @@ -18,37 +18,39 @@ div#domain-properties-editor.card button.btn.btn-outline-collapse(data-toggle="collapse" data-target="#domain-properties-section" data-hook="collapse-domain-properties") - - div.collapse.show(id="domain-properties-section" data-hook="domain-properties-section") + div.card-body + p.mb-0 + | Set system wide domain properties/initial values. - div.card-body.tab-content + div.collapse.tab-content.show(id="domain-properties-section" data-hook="domain-properties-section") div.tab-pane.active(id="edit-domain-properties" data-hook="edit-domain-properties") - - div - - span.mr-3(for="#static-domain") Static Domain: - - input(type="checkbox" id="static-domain" data-hook="static-domain") hr div.mx-1.row.head.align-items-baseline - div.col-sm-3 + div.col-sm-1 + + h6.inline Static Domain + + div.tooltip-icon(data-html="true" data-toggle="tooltip" title=this.tooltips.staticDomain) + + div.col-sm-2 h6.inline Density - div.col-sm-3 + div.col-sm-5 h6.inline Gravity - div.col-sm-3 + div.col-sm-2 h6.inline Pressure div.tooltip-icon(data-html="true" data-toggle="tooltip" title=this.tooltips.pressure) - div.col-sm-3 + div.col-sm-2 h6.inline Speed of Sound @@ -56,68 +58,82 @@ div#domain-properties-editor.card div.mt-3.mx-1.row - div.col-sm-3 + div.col-sm-1 + + input(type="checkbox" id="static-domain" data-hook="static-domain") + + div.col-sm-2 div(data-hook="density") - div.col-sm-3 + div.col-sm-5 - div(data-target="gravity" data-hook="gravity-x" data-index="0") - - div(data-target="gravity" data-hook="gravity-y" data-index="1") - - div(data-target="gravity" data-hook="gravity-z" data-index="2") + span.inline.mr-2 (x: - div.col-sm-3 + div.inline(data-target="gravity" data-hook="gravity-x" data-index="0" style="width: 25%") - div(data-hook="pressure") + span.inline.mr-2 , y: - div.col-sm-3 + div.inline(data-target="gravity" data-hook="gravity-y" data-index="1" style="width: 25%") - div(data-hook="speed") + span.inline.mr-2 , z: - div.tab-pane(id="view-domain-properties" data-hook="view-domain-properties") + div.inline(data-target="gravity" data-hook="gravity-z" data-index="2" style="width: 25%") + + span.inline ) + + div.col-sm-2 - div + div(data-hook="pressure") - span.mr-3(for="#view-static-domain") Static Domain: + div.col-sm-2 - input(type="checkbox" id="view-static-domain" data-hook="view-static-domain" disabled) + div(data-hook="speed") + + div.tab-pane(id="view-domain-properties" data-hook="view-domain-properties") hr div.mx-1.row.head.align-items-baseline - div.col-sm-3 + div.col-sm-1 + + h6.inline Static Domain + + div.col-sm-2 h6.inline Density - div.col-sm-3 + div.col-sm-5 h6.inline Gravity - div.col-sm-3 + div.col-sm-2 h6.inline Pressure - div.col-sm-3 + div.col-sm-2 h6.inline Speed of Sound div.mt-3.mx-1.row - div.col-sm-3 + div.col-sm-1 + + input(type="checkbox" id="view-static-domain" data-hook="view-static-domain" disabled) + + div.col-sm-2 div=this.model.rho_0 - div.col-sm-3 + div.col-sm-5 div=`(X: ${this.model.gravity[0]}, Y: ${this.model.gravity[1]}, Z: ${this.model.gravity[2]})` - div.col-sm-3 + div.col-sm-2 div=this.model.p_0 - div.col-sm-3 + div.col-sm-2 div=this.model.c_0 \ No newline at end of file diff --git a/client/domain-view/templates/shapesView.pug b/client/domain-view/templates/shapesView.pug new file mode 100644 index 0000000000..7939c2a959 --- /dev/null +++ b/client/domain-view/templates/shapesView.pug @@ -0,0 +1,98 @@ +div#domain-geometries-editor.card + + div.card-header.pb-0 + + h3.inline.mr-3 Shapes + + div.inline.mr-3 + + ul.nav.nav-tabs.card-header-tabs(id="shapes-section-tabs") + + li.nav-item + + a.nav-link.tab.active(data-hook="shapes-edit-tab" data-toggle="tab" href="#edit-shapes") Edit + + li.nav-item + + a.nav-link.tab(data-hook="shapes-view-tab" data-toggle="tab" href="#view-shapes") View + + button.btn.btn-outline-collapse.inline(data-toggle="collapse" data-target="#collapse-shapes" data-hook="collapse") + + + div.card-body + p.mb-0 + | Shapes to define regions in space. Formulas define a boolean expression that evaluates as True when inside the region of space, False otherwise. + + ul.mb-0 + li Formulas for 'Standard Geometry' are in terms of the variables 'x', 'y', and 'z'. + li Formulas for 'Combinatory Geometry' are in terms of other 'Shapes'. + li To use a 'Shape' in a 'Fill Action' 'Fillable' must be checked. + li All 'Shapes' may be used in 'Set Actions', 'Remove Actions', or in a 'Combinatory Geometry' formula. + + div.collapse.tab-content(id="collapse-shapes" data-hook="shapes-list-container") + + div.tab-pane.active(id="edit-shapes" data-hook="edit-shapes") + + hr + + div.mb-3.mx-1.row.head.align-items-baseline + + div.col-sm-1 + + h6.inline Edit + + div.col-sm-2 + + h6.inline Name + + div.tooltip-icon.mr-3(data-html="true" data-toggle="tooltip" title=this.tooltips.name) + + div.col-sm-2 + + h6.inline Geometry + + div.col-sm-4 + + h6.inline Formula + + div.tooltip-icon.mr-3(data-html="true" data-toggle="tooltip" title=this.tooltips.geometry) + + div.col-sm-1 + + h6.inline Fillable + + div.col-sm-2 + + h6.inline Remove + + div.my-3(data-hook="edit-shapes-list") + + div.dropdown + + button.btn.btn-outline-primary.dropdown-toggle.box-shadow#addShapeBtn( + data-hook="add-shape", + data-toggle="dropdown", + aria-haspopup="true", + aria-expanded="false", + type="button" + ) Add Shape + + ul.dropdown-menu(aria-labelledby="addShapeBtn") + li.dropdown-item(data-hook="cartesian-lattice" data-name="Cartesian Lattice") Cartesian Lattice + li.dropdown-item(data-hook="spherical-lattice" data-name="Spherical Lattice") Spherical Lattice + li.dropdown-item(data-hook="cylindrical-lattice" data-name="Cylindrical Lattice") Cylindrical Lattice + + div.tab-pane(id="view-shapes" data-hook="view-shapes") + + hr + + div.mx-1.row.head.align-items-baseline + + div.col-sm-2: h6 Name + + div.col-sm-2: h6 Geometry + + div.col-sm-7: h6 Formula + + div.col-sm-1: h6 Fillable + + div.mt-3(data-hook="view-shapes-list") diff --git a/client/domain-view/templates/transformationsView.pug b/client/domain-view/templates/transformationsView.pug new file mode 100644 index 0000000000..b9f1eadb47 --- /dev/null +++ b/client/domain-view/templates/transformationsView.pug @@ -0,0 +1,82 @@ +div#domain-transformation-editor.card + + div.card-header.pb-0 + + h3.inline.mr-3 Transformations + + div.inline.mr-3 + + ul.nav.nav-tabs.card-header-tabs(id="transformations-selection-tabs") + + li.nav-item + + a.nav-link.tab.active(data-hook="transformations-edit-tab" data-toggle="tab" href="#edit-transformations") Edit + + li.nav-item + + a.nav-link.tab(data-hook="transformations-view-tab" data-toggle="tab" href="#view-transformations") View + + button.btn.btn-outline-collapse.inline(data-toggle="collapse" data-target="#collapse-transformations" data-hook="collapse") + + + div.card-body + + p.mb-0 + | Transformations define a method of manipulation to be performed on a region within the domain defined by a Shape. + + div.collapse.tab-content(id="collapse-transformations" data-hook="transformations-list-container") + + div.tab-pane.active(id="edit-transformations" data-hook="edit-transformations") + + hr + + div.mb-3.mx-1.row.head.align-items-baseline + + div.col-sm-1 + + h6.inline Edit + + div.col-sm-3 + + h6.inline Name + + div.tooltip-icon.mr-3(data-html="true" data-toggle="tooltip" title=this.tooltips.name) + + div.col-sm-6 + + h6.inline Type + + div.col-sm-2 + + h6.inline Remove + + div.my-3(data-hook="edit-transformations-list") + + div.dropdown + + button.btn.btn-outline-primary.dropdown-toggle.box-shadow#addLatticeBtn( + data-hook="add-transformation", + data-toggle="dropdown", + aria-haspopup="true", + aria-expanded="false", + type="button" + ) Add Transformation + + ul.dropdown-menu(aria-labelledby="addLatticeBtn") + li.dropdown-item(data-hook="translate-transformation" data-name="Translate Transformation") Translate Transformation + li.dropdown-item(data-hook="rotate-transformation" data-name="Rotate Transformation") Rotate Transformation + li.dropdown-item(data-hook="reflect-transformation" data-name="Reflect Transformation") Reflect Transformation + li.dropdown-item(data-hook="scale-transformation" data-name="Scale Transformation") Scale Transformation + + div.tab-pane(id="view-transformations" data-hook="view-transformations") + + hr + + div.mx-1.row.head.align-items-baseline + + div.col-sm-4: h6 Name + + div.col-sm-4: h6 Type + + div.col-sm-4: h6 Nested Transformation + + div.mt-3(data-hook="view-transformations-list") diff --git a/client/domain-view/templates/typesDescriptionView.pug b/client/domain-view/templates/typesDescriptionView.pug deleted file mode 100644 index f4f2ab190e..0000000000 --- a/client/domain-view/templates/typesDescriptionView.pug +++ /dev/null @@ -1,36 +0,0 @@ -div.mx-1 - - div.text-info(data-hook="type-location-message" style="display: none") - | There are multiple type files with that name, please select a location - - div - - div.inline.mr-3 - - span.inline.mr-2(for="file-select") Select type description file: - - div.inline(id="file-select" data-hook="file-select") - - div.inline(data-hook="file-location-container" style="display: none") - - span.inlinemr-2(for="file-location-select") Location: - - div.inline(id="file-location-select" data-hook="file-location-select") - - div.my-3 - - button.btn.btn-outline-primary.box-shadow(data-hook="set-particle-types-btn" disabled) Set Types - - div.mdl-edit-btn.saving-status.inline(data-hook="st-in-progress") - - div.spinner-grow.mr-2 - - span Setting types ... - - div.mdl-edit-btn.saved-status.inline(data-hook="st-complete") - - span - - div.mdl-edit-btn.save-error-status(data-hook="st-error") - - span(data-hook="st-error-action") \ No newline at end of file diff --git a/client/domain-view/templates/typesView.pug b/client/domain-view/templates/typesView.pug index d51f270214..442bc5459a 100644 --- a/client/domain-view/templates/typesView.pug +++ b/client/domain-view/templates/typesView.pug @@ -16,11 +16,15 @@ div#domain-types-editor.card a.nav-link.tab(data-hook="domain-types-view-tab" data-toggle="tab" href="#view-domain-types") View - button.btn.btn-outline-collapse(data-toggle="collapse" data-target="#domain-types-section" data-hook="collapse-domain-types") - + button.btn.btn-outline-collapse(data-toggle="collapse" data-target="#domain-types-section" data-hook="collapse-domain-types") + - div.collapse.show(id="domain-types-section" data-hook="domain-types-section") + div.card-body + p.mb-0 + | Classifications of matter within the domain. Types can have different properties (mass, volume, etc...), and can be set as movable or Fixed. - div.card-body.tab-content + div.collapse.tab-content(id="domain-types-section" data-hook="domain-types-section") + + hr div @@ -42,16 +46,24 @@ div#domain-types-editor.card h6.inline Edit - div.col-sm-3 + div.col-sm-5 h6.inline Name - div.col-sm-8 + div.col-sm-4 h6.inline Number of Particles + div.col-sm-2 + + h6.inline Remove + div.my-3(data-hook="edit-domain-types-list") + div(data-hook="ev-types-collection-error") + + p.text-danger Domains must have at least one type for matter classification. + button.btn.btn-outline-primary.box-shadow(data-hook="add-domain-type") Add Type div.tab-pane(id="view-domain-types" data-hook="view-domain-types") @@ -66,10 +78,14 @@ div#domain-types-editor.card div.col-sm-3 - h6.inline Numbe of Particles + h6.inline Number of Particles div.col-sm-6 h6.inline Defaults - div.my-3(data-hook="view-domain-types-list") \ No newline at end of file + div.my-3(data-hook="view-domain-types-list") + + div(data-hook="vv-types-collection-error") + + p.text-danger Domains must have at least one type for matter classification. \ No newline at end of file diff --git a/client/domain-view/templates/viewAction.pug b/client/domain-view/templates/viewAction.pug new file mode 100644 index 0000000000..51bc203ff4 --- /dev/null +++ b/client/domain-view/templates/viewAction.pug @@ -0,0 +1,160 @@ +div.mx-1 + + if(this.model.collection.indexOf(this.model) !== 0) + hr + + div.row + + div.col-sm-4 + + div.pl-2=this.model.type + + div.col-sm-4 + + div.pl-2=this.model.priority + + div.col-sm-4 + + input(type="checkbox" checked=this.model.enable disabled) + + hr + + div + + div.mb-3.mx-1.row.align-items-baseline + + div.col-sm-4.pl-0.hidden(data-hook="action-scope") + + h5.inline.mr-2 Scope: + + div.inline=this.model.scope + + div.col-sm-4.hidden(data-hook="multi-particle-shape") + + h5.inline.mr-2 Shape: + + div.inline=this.model.shape ? this.model.shape : 'None' + + div.col-sm-4.hidden(data-hook="multi-particle-transformation") + + h5.inline.mr-2 Transformation: + + div.inline=this.model.transformation ? this.model.transformation : 'None' + + div.hidden(data-hook="single-particle-scope") + + div + + h5.inline.mr-2 Current Location: + + span.inline.mr-2 (x: + + div.inline=this.model.point.x + + span.inline.mr-2 , y: + + div.inline=this.model.point.y + + span.inline.mr-2 , z: + + div.inline=this.model.point.z + + span.inline ) + + div.hidden(data-hook="new-location") + + h5.inline.mr-2 New Location: + + span.inline.mr-2 (x: + + div.inline=this.model.newPoint.x + + span.inline.mr-2 , y: + + div.inline=this.model.newPoint.y + + span.inline.mr-2 , z: + + div.inline=this.model.newPoint.z + + span.inline ) + + div.hidden(data-hook="particle-properties") + + h4 Particle Properties + + div + + h5.inline.mr-2 Type: + + div.inline=this.model.collection.parent.types.get(this.model.typeID, 'typeID').name + + div + + hr + + div.mb-3.mx-1.row.head.align-items-baseline + + div.col-sm-2 + + h6.inline Mass + + div.col-sm-2 + + h6.inline Volume + + div.col-sm-2 + + h6.inline Density + + div.col-sm-2 + + h6.inline Viscosity + + div.col-sm-2 + + h6.inline Speed of Sound + + div.col-sm-2 + + h6.inline Fixed + + div.row + + div.col-sm-2 + + div.pl-2=this.model.mass + + div.col-sm-2 + + div=this.model.vol + + div.col-sm-2 + + div=this.model.rho + + div.col-sm-2 + + div=this.model.nu + + div.col-sm-2 + + div=this.model.c + + div.col-sm-2 + + input(type="checkbox" checked=this.model.fixed disabled) + + div.hidden(data-hook="import-properties") + + div + + h6.mr-2.inline Filename: + + div.inline=this.model.filename ? `'${this.model.filename}'` : 'None' + + div.hidden(data-hook="type-descriptions-prop") + + h6.mr-2.inline Type Descriptions: + + div.inline=this.model.subdomainFile ? `'${this.model.subdomainFile}'` : 'None' diff --git a/client/domain-view/templates/viewShape.pug b/client/domain-view/templates/viewShape.pug new file mode 100644 index 0000000000..3b476eef7f --- /dev/null +++ b/client/domain-view/templates/viewShape.pug @@ -0,0 +1,128 @@ +div.mx-1 + + if(this.model.collection.indexOf(this.model) !== 0) + hr + + div.row + + div.col-sm-2 + + div.pl-2=this.model.name + + div.col-sm-2 + + div.pl-2=this.model.type + + div.col-sm-7 + + div=this.model.formula + + div.col-sm-1 + + div.pl-3 + + input(type="checkbox" checked=this.model.fillable disabled) + + div.mx-1.pl-2(data-hook="shape-details") + + div.collapse(id="view-collapse-shape-details" + this.model.cid data-hook="view-collapse-shape-details" + this.model.cid) + + hr + + div.mx-1.mb-2 + + h6.inline.mr-3 Lattice: + + div.inline=this.model.lattice + + div.hidden(data-hook="cartesian-lattice-props") + + div.mx-1.row.head.align-items-baseline + + div.col-sm-2 + + h6.inline Length + + div.col-sm-2 + + h6.inline Height + + div.col-sm-2 + + h6.inline Depth + + div.col-sm-2 + + h6.inline X-Spacing + + div.col-sm-2 + + h6.inline Y-Spacing + + div.col-sm-2 + + h6.inline Z-Spacing + + div.mt-3.mx-1.row + + div.col-sm-2 + + div=this.model.length + + div.col-sm-2 + + div=this.model.height + + div.col-sm-2 + + div=this.model.depth + + div.col-sm-2 + + div=this.model.deltax + + div.col-sm-2 + + div=this.model.deltay + + div.col-sm-2 + + div=this.model.deltaz + + div.hidden(data-hook="circular-lattice-props") + + div.mx-1.row.head.align-items-baseline + + div.col-sm-3 + + h6.inline Radius + + div.col-sm-3.hidden(data-hook="cylinder-length-header") + + h6.inline length + + div.col-sm-3 + + h6.inline Surface Spacing + + div.col-sm-3 + + h6.inline Radial Spacing + + div.mt-3.mx-1.row + + div.col-sm-3 + + div=this.model.radius + + div.col-sm-3.hidden(data-hook="cylinder-length-feild") + + div=this.model.length + + div.col-sm-3 + + div=this.model.deltas + + div.col-sm-3 + + div=this.model.deltar diff --git a/client/domain-view/templates/viewTransformation.pug b/client/domain-view/templates/viewTransformation.pug new file mode 100644 index 0000000000..51261a5d7a --- /dev/null +++ b/client/domain-view/templates/viewTransformation.pug @@ -0,0 +1,206 @@ +div.mx-1 + + if(this.model.collection.indexOf(this.model) !== 0) + hr + + div.row + + div.col-sm-4 + + div.pl-2=this.model.name + + div.col-sm-4 + + div=this.model.type + + div.col-sm-4 + + div=this.model.transformation ? this.model.transformation : 'None' + + div.hidden(data-hook="vector-transformation-props") + + hr + + div.mx-1.row.head.align-items-baseline + + div.col-sm-9 + + h6 Vector + + div.col-sm-3.hidden(data-hook="rotation-angle-header") + + h6 Angle (deg) + + div.mx-1.mt-3.row + + div.col-sm-9 + + div.inline + + span.inline [(x: + + div.ml-2.inline=this.model.vector[0].x + + span.inline.mr-2 , y: + + div.ml-2.inline=this.model.vector[0].y + + span.inline.mr-2 , z: + + div.ml-2.inline=this.model.vector[0].z + + span.inline ), + + div.pl-2.inline + + span.inline (x: + + div.ml-2.inline=this.model.vector[1].x + + span.inline.mr-2 , y: + + div.ml-2.inline=this.model.vector[1].y + + span.inline.mr-2 , z: + + div.ml-2.inline=this.model.vector[1].z + + span.inline )] + + div.col-sm-3.hidden(data-hook="rotation-angle-prop") + + div=this.model.angle + + div.hidden(data-hook="reflect-transformation-props") + + hr + + div.mx-1.my-3.row.head.align-items-baseline + + div.col-sm-4 + + h6 Point 1 + + div.col-sm-4.hidden(data-hook="view-normal-header") + + h6 Normal + + div.col-sm-4.hidden(data-hook="view-point2-header") + + h6 Point 2 + + div.col-sm-4.hidden(data-hook="view-point3-header") + + h6 Point 3 + + div.mx-1.my-3.row.align-items-baseline + + div.col-sm-4 + + div + + span.inline (x: + + div.inline=this.model.point1.x + + span.inline.mr-2 , y: + + div.inline=this.model.point1.y + + span.inline.mr-2 , z: + + div.inline=this.model.point1.z + + span.inline ) + + div.col-sm-4.hidden(data-hook="view-reflect-normal") + + div + + span.inline (x: + + div.inline=this.model.normal.x + + span.inline.mr-2 , y: + + div.inline=this.model.normal.y + + span.inline.mr-2 , z: + + div.inline=this.model.normal.z + + span.inline ) + + div.col-sm-4.hidden(data-hook="view-reflect-point2") + + div + + span.inline (x: + + div.inline=this.model.point2.x + + span.inline.mr-2 , y: + + div.inline=this.model.point2.y + + span.inline.mr-2 , z: + + div.inline=this.model.point2.z + + span.inline ) + + div.col-sm-4.hidden(data-hook="view-reflect-point3") + + div + + span.inline (x: + + div.inline=this.model.point3.x + + span.inline.mr-2 , y: + + div.inline=this.model.point3.y + + span.inline.mr-2 , z: + + div.inline=this.model.point3.z + + span.inline ) + + div.hidden(data-hook="scale-transformation-props") + + div.mt-3 + + div.mx-1.row.head.align-items-baseline + + div.col-sm-3 + + h6 Factor + + div.col-sm-9 + + h6 Center + + div.mx-1.mt-3.row + + div.col-sm-3 + + div=this.model.factor + + div.col-sm-9 + + div + + span.inline (x: + + div.ml-2.inline=this.model.center.x + + span.inline.mr-2 , y: + + div.ml-2.inline=this.model.center.y + + span.inline.mr-2 , z: + + div.ml-2.inline=this.model.center.z + + span.inline ) diff --git a/client/domain-view/templates/viewType.pug b/client/domain-view/templates/viewType.pug index 5a0277bf4a..ab91d16c0a 100644 --- a/client/domain-view/templates/viewType.pug +++ b/client/domain-view/templates/viewType.pug @@ -17,12 +17,6 @@ div.mx-1 div=this.model.numParticles - hr - - h6.inline.mr-2 Geometry: - - div.inline=this.model.geometry - div.col-sm-6 div.row diff --git a/client/domain-view/views/action-view.js b/client/domain-view/views/action-view.js new file mode 100644 index 0000000000..4fea443dba --- /dev/null +++ b/client/domain-view/views/action-view.js @@ -0,0 +1,815 @@ +/* +StochSS is a platform for simulating biochemical systems +Copyright (C) 2019-2022 StochSS developers. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +let $ = require('jquery'); +let path = require('path'); +let _ = require('underscore'); +//support files +let app = require('../../app'); +let tests = require('../../views/tests'); +//views +let View = require('ampersand-view'); +let InputView = require('../../views/input'); +let SelectView = require('ampersand-select-view'); +//templates +let editTemplate = require('../templates/editAction.pug'); +let viewTemplate = require('../templates/viewAction.pug'); + +module.exports = View.extend({ + bindings: { + 'model.selected' : { + type: function (el, value, previousValue) { + el.checked = value; + }, + hook: 'select-action' + }, + 'model.enable' : { + type: function (el, value, previousValue) { + el.checked = value; + }, + hook: 'enable-action' + } + }, + events: { + 'change [data-hook=select-type-container]' : 'selectActionType', + 'change [data-hook=select-scope-container]' : 'selectActionScope', + 'change [data-target=update-preview-plot]' : 'updatePreviewPlot', + 'change [data-hook=shape-container]' : 'selectShape', + 'change [data-hook=transformation-container]' : 'selectTransformation', + 'change [data-target=point]' : 'setPoint', + 'change [data-target=new-point]' : 'setNewPoint', + 'change [data-hook=particle-type]' : 'setParticleType', + 'change [data-target=particle-property-containers]' : 'updateViewers', + 'change [data-hook=particle-fixed]' : 'setParticleFixed', + 'change [data-hook=mesh-file]' : 'setMeshFile', + 'change [data-hook=type-file]' : 'setTypeFile', + 'change [data-hook=action-mesh-select]' : 'selectMeshFile', + 'change [data-hook=mesh-location-select]' : 'selectMeshLocation', + 'change [data-hook=action-type-select]' : 'selectTypeFile', + 'change [data-hook=type-location-select]' : 'selectFileLocation', + 'click [data-hook=select-action]' : 'selectAction', + 'click [data-hook=enable-action]' : 'enableAction', + 'click [data-hook=remove]' : 'removeAction', + 'click [data-hook=collapseImportFiles]' : 'toggleImportFiles', + 'click [data-hook=collapseUploadedFiles]' : 'toggleUploadedFiles', + 'click [data-hook=import-mesh-file]' : 'handleImportMesh' + }, + initialize: function (attrs, options) { + View.prototype.initialize.apply(this, arguments); + this.viewMode = attrs.viewMode ? attrs.viewMode : false; + this.chevrons = { + hide: ` + + + + `, + show: ` + + + + ` + } + this.accepts = {"XML Mesh": ".xml", "Mesh IO": ".msh", "StochSS Domain": ".domn"}; + this.accept = Object.keys(this.accepts).includes(this.model.type) ? this.accepts[this.model.type] : null; + this.filetype = this.model.type === "StochSS Domain" ? 'domain' : 'mesh'; + this.meshFile = null; + this.typeFile = null; + }, + render: function () { + this.template = this.viewMode ? viewTemplate : editTemplate; + View.prototype.render.apply(this, arguments); + this.details = { + 'Multi Particle': { + 'Fill Action': [ + $(this.queryByHook('action-scope')), + $(this.queryByHook('multi-particle-shape')), + $(this.queryByHook('multi-particle-transformation')), + $(this.queryByHook('particle-properties')) + ], + 'Set Action': [ + $(this.queryByHook('action-scope')), + $(this.queryByHook('multi-particle-shape')), + $(this.queryByHook('multi-particle-transformation')), + $(this.queryByHook('particle-properties')) + ], + 'Remove Action': [ + $(this.queryByHook('action-scope')), + $(this.queryByHook('multi-particle-shape')), + $(this.queryByHook('multi-particle-transformation')) + ] + }, + 'Single Particle': { + 'Fill Action': [ + $(this.queryByHook('action-scope')), + $(this.queryByHook('single-particle-scope')), + $(this.queryByHook('particle-properties')) + ], + 'Set Action': [ + $(this.queryByHook('action-scope')), + $(this.queryByHook('single-particle-scope')), + $(this.queryByHook('new-location')), + $(this.queryByHook('particle-properties')) + ], + 'Remove Action': [ + $(this.queryByHook('action-scope')), + $(this.queryByHook('single-particle-scope')) + ] + }, + 'XML Mesh': [ + $(this.queryByHook('multi-particle-transformation')), + $(this.queryByHook('import-properties')), + $(this.queryByHook('type-descriptions-prop')) + ], + 'Mesh IO': [ + $(this.queryByHook('multi-particle-transformation')), + $(this.queryByHook('import-properties')), + $(this.queryByHook('type-descriptions-prop')) + ], + 'StochSS Domain': [ + $(this.queryByHook('multi-particle-transformation')), + $(this.queryByHook('import-properties')) + ] + } + app.documentSetup(); + if(!this.viewMode){ + if(this.model.selected) { + setTimeout(_.bind(this.openDetails, this), 1); + } + this.model.on('change', _.bind(this.updateViewer, this)); + this.renderShapeSelectView(); + this.renderTransformationSelectView(); + this.renderNewLocationViews(); + this.renderTypeSelectView(); + this.renderParticleProperties(); + if(Object.keys(this.accepts).includes(this.model.type)) { + this.meshFiles = null; + this.typeFiles = null; + this.renderActionFileSelects(); + } + this.toggleEnable(); + } + this.displayDetails(); + }, + completeAction: function () { + $(this.queryByHook("imf-in-progress")).css("display", "none"); + $(this.queryByHook("imf-complete")).css("display", "inline-block"); + setTimeout(() => { + $(this.queryByHook("imf-complete")).css("display", "none"); + }, 5000); + }, + displayDetails: function () { + let importTypes = ['XML Mesh', 'Mesh IO', 'StochSS Domain'] + let elements = importTypes.includes(this.model.type) ? + this.details[this.model.type] : this.details[this.model.scope][this.model.type]; + elements.forEach((element) => { + element.css('display', 'block'); + }); + }, + enableAction: function () { + this.model.enable = !this.model.enable; + this.collection.parent.trigger('update-plot-preview'); + }, + errorAction: function (action) { + $(this.queryByHook("imf-in-progress")).css("display", "none"); + $(this.queryByHook("imf-action-error")).text(action); + $(this.queryByHook("imf-error")).css("display", "block"); + }, + getShapeOptions: function () { + let options = []; + if(this.model.type === "Fill Action" && this.model.scope === "Multi Particle") { + this.model.collection.parent.shapes.forEach((shape) => { + if(shape.fillable) { options.push(shape.name); } + }); + }else { + this.model.collection.parent.shapes.forEach((shape) => { + options.push(shape.name); + }); + } + return options; + }, + getTransformationOptions: function () { + return this.model.collection.parent.transformations.map((transformation) => { + return transformation.name; + }); + }, + handleImportMesh: function () { + this.startAction(); + let formData = new FormData(); + var filePath = this.model.collection.parent.directory; + if(filePath === null) { + filePath = this.parent.parent.parent.model.directory; + } + formData.append("path", filePath); + formData.append("datafile", this.meshFile); + if(this.typeFile) { + formData.append("typefile", this.typeFile); + } + let endpoint = path.join(app.getApiPath(), 'spatial-model/import-mesh'); + app.postXHR(endpoint, formData, { + success: (err, response, body) => { + body = JSON.parse(body); + this.model.filename = path.join(body.meshPath, body.meshFile); + if(Object.keys(body).includes("typesPath")) { + this.model.subdomainFile = path.join(body.typesPath, body.typesFile); + this.typeFiles = null; + } + this.completeAction(); + $(this.queryByHook('collapseUploadedFiles')).click(); + this.renderActionFileSelects(); + }, + error: (err, response, body) => { + body = JSON.parse(body); + this.errorAction(body.Message); + } + }, false); + }, + hideDetails: function () { + let importTypes = ['XML Mesh', 'Mesh IO', 'StochSS Domain'] + let elements = importTypes.includes(this.model.type) ? + this.details[this.model.type] : this.details[this.model.scope][this.model.type]; + elements.forEach((element) => { + element.css('display', 'none'); + }); + }, + openDetails: function () { + $("#collapse-action-details" + this.model.cid).collapse("show"); + }, + removeAction: function () { + let actions = this.collection; + let enabled = this.model.enable; + actions.removeAction(this.model); + actions.parent.trigger('update-shape-deps'); + if(enabled) { + actions.parent.trigger('update-plot-preview'); + } + }, + renderActionFileSelects: function () { + var queryStr = `?ext=${this.accept}`; + if(this.typeFiles === null) { + queryStr += `${queryStr}&includeTypes=True`; + } + let endpoint = `${path.join(app.getApiPath(), 'spatial-model/lattice-files')}${queryStr}`; + app.getXHR(endpoint, {success: (err, response, body) => { + this.meshFiles = body.meshFiles; + this.renderMeshSelectView(); + if(Object.keys(body).includes('typeFiles')) { + this.typeFiles = body.typeFiles; + this.renderTypeFileSelectView(); + } + }}); + }, + renderDensityPropertyView: function () { + if(this.densityPropertyView) { + this.densityPropertyView.remove(); + } + this.densityPropertyView = new InputView({ + parent: this, + required: true, + name: 'density', + tests: tests.valueTests, + valueType: 'number', + modelKey: 'rho', + value: this.model.rho + }); + let hook = "particle-rho"; + app.registerRenderSubview(this, this.densityPropertyView, hook); + }, + renderMassPropertyView: function () { + if(this.massPropertyView) { + this.massPropertyView.remove(); + } + this.massPropertyView = new InputView({ + parent: this, + required: true, + name: 'mass', + tests: tests.valueTests, + valueType: 'number', + modelKey: 'mass', + value: this.model.mass + }); + let hook = "particle-mass"; + app.registerRenderSubview(this, this.massPropertyView, hook); + }, + renderMeshLocationSelectView: function (index) { + if(this.meshLocationSelectView) { + this.meshLocationSelectView.remove(); + } + let value = Boolean(this.model.filename) ? this.model.filename : ""; + this.meshLocationSelectView = new SelectView({ + name: 'mesh-locations', + required: false, + idAttributes: 'cid', + options: this.meshFiles.paths[index], + value: value, + unselectedText: "-- Select Mesh File Location --" + }); + let hook = "mesh-location-select"; + app.registerRenderSubview(this, this.meshLocationSelectView, hook); + }, + renderMeshSelectView: function () { + if(this.meshSelectView) { + this.meshSelectView.remove(); + } + let files = this.meshFiles.files.filter((file) => { + if(file[1] === this.model.filename.split('/').pop()) { + return file; + } + }); + let value = files.length > 0 ? files[0] : ""; + this.meshSelectView = new SelectView({ + name: 'mesh-files', + required: false, + idAttributes: 'cid', + options: this.meshFiles.files, + value: value, + unselectedText: "-- Select Mesh File --" + }); + let hook = "action-mesh-select"; + app.registerRenderSubview(this, this.meshSelectView, hook); + if(value !== "" && this.meshFiles.paths[value[0]].length > 1) { + this.renderMeshLocationSelectView(value[0]); + $(this.queryByHook("mesh-location-container")).css("display", "inline-block"); + } + }, + renderNewLocationViews: function () { + if(this.newLocationViews) { + this.newLocationViews['x'].remove(); + this.newLocationViews['y'].remove(); + this.newLocationViews['z'].remove(); + }else{ + this.newLocationViews = {}; + } + this.newLocationViews['x'] = new InputView({ + parent: this, + required: true, + name: 'new-point-x', + tests: [tests.nanValue], + valueType: 'number', + value: this.model.newPoint.x + }); + let hookX = "new-point-x-container"; + app.registerRenderSubview(this, this.newLocationViews['x'], hookX); + this.newLocationViews['y'] = new InputView({ + parent: this, + required: true, + name: 'new-point-y', + tests: [tests.nanValue], + valueType: 'number', + value: this.model.newPoint.y + }); + let hookY = "new-point-y-container"; + app.registerRenderSubview(this, this.newLocationViews['y'], hookY); + this.newLocationViews['z'] = new InputView({ + parent: this, + required: true, + name: 'new-point-z', + tests: [tests.nanValue], + valueType: 'number', + value: this.model.newPoint.z + }); + let hookZ = "new-point-z-container"; + app.registerRenderSubview(this, this.newLocationViews['z'], hookZ); + }, + renderParticleProperties: function () { + this.renderMassPropertyView(); + this.renderVolumePropertyView(); + this.renderDensityPropertyView(); + this.renderViscosityPropertyView(); + this.renderSOSPropertyView(); + }, + renderShapeSelectView: function () { + if(this.shapeSelectView) { + this.shapeSelectView.remove(); + } + let options = this.getShapeOptions(); + this.shapeSelectView = new SelectView({ + name: 'shape', + required: true, + options: options, + value: this.model.shape, + unselectedText: "-- Select Shape --" + }); + let hook = "shape-container"; + app.registerRenderSubview(this, this.shapeSelectView, hook); + }, + renderSOSPropertyView: function () { + if(this.sOSPropertyView) { + this.sOSPropertyView.remove(); + } + this.sOSPropertyView = new InputView({ + parent: this, + required: true, + name: 'speed-of-sound', + tests: tests.valueTests, + valueType: 'number', + modelKey: 'c', + value: this.model.c + }); + let hook = "particle-c"; + app.registerRenderSubview(this, this.sOSPropertyView, hook); + }, + renderTransformationSelectView: function () { + if(this.transformationSelectView) { + this.transformationSelectView.remove(); + } + let options = this.getTransformationOptions(); + this.transformationSelectView = new SelectView({ + name: 'transformation', + required: true, + options: options, + value: this.model.transformation, + unselectedText: "-- Select Transformation --" + }); + let hook = "transformation-container"; + app.registerRenderSubview(this, this.transformationSelectView, hook); + }, + renderTypeLocationSelectView: function (index) { + if(this.typeLocationSelectView) { + this.typeLocationSelectView.remove(); + } + let value = Boolean(this.model.subdomainFile) ? this.model.subdomainFile : ""; + this.typeLocationSelectView = new SelectView({ + name: 'type-locations', + required: false, + idAttributes: 'cid', + options: this.typeFiles.paths[index], + value: value, + unselectedText: "-- Select Type File Location --" + }); + let hook = "type-location-select"; + app.registerRenderSubview(this, this.typeLocationSelectView, hook); + }, + renderTypeFileSelectView: function () { + if(this.typeSelectView) { + this.typeSelectView.remove(); + } + var file = this.typeFiles.files.filter((file) => { + if(file[1] === this.model.subdomainFile.split('/').pop()) { + return file; + } + }); + let value = file.length > 0 ? file[0] : ""; + this.typeSelectView = new SelectView({ + name: 'type-files', + required: false, + idAttributes: 'cid', + options: this.typeFiles.files, + value: value, + unselectedText: "-- Select Type File --" + }); + let hook = "action-type-select"; + app.registerRenderSubview(this, this.typeSelectView, hook); + if(value !== "" && this.typeFiles.paths[value[0]].length > 1) { + this.renderTypeLocationSelectView(value[0]); + $(this.queryByHook("types-location-container")).css("display", "inline-block"); + } + }, + renderTypeSelectView: function () { + if(this.typeSelectView) { + this.typeSelectView.remove(); + } + let options = this.model.collection.parent.types.map((type) => { + return [type.typeID, type.name]; + }); + this.typeSelectView = new SelectView({ + name: 'type', + required: true, + options: options, + value: this.model.typeID, + }); + let hook = "particle-type"; + app.registerRenderSubview(this, this.typeSelectView, hook); + }, + renderViscosityPropertyView: function () { + if(this.viscosityPropertyView) { + this.viscosityPropertyView.remove(); + } + this.viscosityPropertyView = new InputView({ + parent: this, + required: true, + name: 'viscosity', + tests: tests.valueTests, + valueType: 'number', + modelKey: 'nu', + value: this.model.nu + }); + let hook = "particle-nu"; + app.registerRenderSubview(this, this.viscosityPropertyView, hook); + }, + renderVolumePropertyView: function () { + if(this.volumePropertyView) { + this.volumePropertyView.remove(); + } + this.volumePropertyView = new InputView({ + parent: this, + required: true, + name: 'volume', + tests: tests.valueTests, + valueType: 'number', + modelKey: 'vol', + value: this.model.vol + }); + let hook = "particle-vol"; + app.registerRenderSubview(this, this.volumePropertyView, hook); + }, + selectAction: function () { + this.model.selected = !this.model.selected; + }, + selectActionScope: function (e) { + this.hideDetails(); + this.model.scope = e.target.value; + this.displayDetails(); + this.toggleEnable(); + this.updatePreviewPlot(); + }, + selectActionType: function (e) { + this.hideDetails(); + this.model.type = e.target.value; + this.displayDetails(); + let types = { + 'XML Mesh': '.xml', 'Mesh IO': '.msh', 'StochSS Domain': '.domn' + }; + if(Object.keys(types).includes(this.model.type)) { + this.accept = types[this.model.type] + $(this.queryByHook('mesh-file')).prop('accept', this.accept); + this.filetype = this.model.type === "StochSS Domain" ? 'domain' : 'mesh'; + $(this.queryByHook('meshfile-label')).text(`Please specify a ${this.filetype} to import: `); + $(this.queryByHook('action-filename-label')).text(`Please specify a ${this.filetype} to import: `); + $(this.queryByHook('mesh-location-message')).text( + `There are multiple ${this.filetype} files with that name, please select a location` + ); + this.renderActionFileSelects(); + } + this.toggleEnable(); + this.updatePreviewPlot(); + }, + selectFileLocation: function (e) { + this.model.subdomainFile = e.target.value ? e.target.value : ""; + }, + selectMeshFile: function (e) { + let value = e.target.value; + var msgDisplay = "none"; + var contDisplay = "none"; + if(value) { + if(this.meshFiles.paths[value].length > 1) { + msgDisplay = "block"; + contDisplay = "inline-block"; + this.renderMeshLocationSelectView(value); + this.model.filename = ""; + }else{ + this.model.filename = this.meshFiles.paths[value][0]; + } + }else{ + this.model.filename = ""; + } + $(this.queryByHook("mesh-location-message")).css('display', msgDisplay); + $(this.queryByHook("mesh-location-container")).css("display", contDisplay); + }, + selectMeshLocation: function (e) { + this.model.filename = e.target.value ? e.target.value : ""; + }, + selectShape: function (e) { + this.model.shape = e.target.value; + this.toggleEnable(); + this.model.collection.parent.trigger('update-shape-deps'); + this.updateViewer(); + this.updatePreviewPlot(); + }, + selectTypeFile: function (e) { + let value = e.target.value; + var msgDisplay = "none"; + var contDisplay = "none"; + if(value) { + if(this.typeFiles.paths[value].length > 1) { + msgDisplay = "block"; + contDisplay = "inline-block"; + this.renderTypeLocationSelectView(value); + this.model.subdomainFile = ""; + }else{ + this.model.subdomainFile = this.typeFiles.paths[value][0]; + } + }else{ + this.model.subdomainFile = ""; + } + $(this.queryByHook("types-location-message")).css('display', msgDisplay); + $(this.queryByHook("types-location-container")).css("display", contDisplay); + }, + selectTransformation: function (e) { + this.model.transformation = e.target.value; + this.model.collection.parent.trigger('update-transformation-deps'); + this.updateViewer(); + this.updatePreviewPlot(); + }, + setMeshFile: function (e) { + this.meshFile = e.target.files[0]; + this.updateViewer(); + $(this.queryByHook("import-mesh-file")).prop('disabled', !this.meshFile); + }, + setNewPoint: function (e) { + let key = e.target.parentElement.parentElement.dataset.name; + let value = Number(e.target.value); + this.model.newPoint[key] = value; + this.updateViewer(); + this.updatePreviewPlot(); + }, + setParticleFixed: function (e) { + this.model.fixed = !this.model.fixed; + this.updateViewer(); + this.updatePreviewPlot(); + }, + setParticleType: function (e) { + let value = Number(e.target.value); + let currType = this.model.collection.parent.types.get(this.model.typeID, "typeID"); + let newType = this.model.collection.parent.types.get(value, "typeID"); + this.updatePropertyDefaults(currType, newType); + this.model.typeID = value; + this.model.collection.parent.trigger('update-type-deps'); + this.updateViewer(); + this.updatePreviewPlot(); + }, + setPoint: function (e) { + let key = e.target.parentElement.parentElement.dataset.name; + let value = Number(e.target.value); + if(this.model.newPoint[key] === this.model.point[key]) { + this.model.newPoint[key] = value; + this.renderNewLocationViews(); + } + this.model.point[key] = value; + this.updateViewer(); + this.updatePreviewPlot(); + }, + setTypeFile: function (e) { + this.typeFile = e.target.files[0]; + this.updateViewer(); + }, + startAction: function () { + $(this.queryByHook("imf-complete")).css("display", "none"); + $(this.queryByHook("imf-error")).css("display", "none"); + $(this.queryByHook("imf-in-progress")).css("display", "inline-block"); + }, + toggleEnable: function () { + let noShape = this.model.shape === ""; + let mltPart = this.model.scope === "Multi Particle"; + let noImprt = ["Fill Action", "Set Action", "Remove Action"].includes(this.model.type); + let disable = noShape && mltPart && noImprt + $(this.queryByHook("enable-action")).prop("disabled", disable); + }, + toggleImportFiles: function (e) { + let classes = $(this.queryByHook('collapseImportFiles')).attr("class").split(/\s+/); + $(this.queryByHook('uploaded-chevron')).html(this.chevrons.hide); + if(classes.includes('collapsed')) { + $(this.queryByHook('import-chevron')).html(this.chevrons.show); + }else{ + $(this.queryByHook('import-chevron')).html(this.chevrons.hide); + } + }, + toggleUploadedFiles: function (e) { + let classes = $(this.queryByHook('collapseUploadedFiles')).attr("class").split(/\s+/); + $(this.queryByHook('import-chevron')).html(this.chevrons.hide); + if(classes.includes('collapsed')) { + $(this.queryByHook('uploaded-chevron')).html(this.chevrons.show); + }else{ + $(this.queryByHook('uploaded-chevron')).html(this.chevrons.hide); + } + }, + update: function () {}, + updatePreviewPlot: function () { + if(!this.model.enable) { return } + if(this.model.type === "Fill Action" && this.model.scope === "Multi Particle" && this.model.shape === "") { + return + } + this.collection.parent.trigger('update-plot-preview'); + }, + updatePropertyDefaults: function (currType, newType) { + if(this.model.mass === currType.mass) { + this.model.mass = newType.mass; + } + if(this.model.vol === currType.volume) { + this.model.vol = newType.volume; + } + if(this.model.rho === currType.rho) { + this.model.rho = newType.rho; + } + if(this.model.nu === currType.nu) { + this.model.nu = newType.nu; + } + if(this.model.c === currType.c) { + this.model.c = newType.c; + } + if(this.model.fixed === currType.fixed) { + this.model.fixed = newType.fixed; + } + this.renderParticleProperties(); + $(this.queryByHook('particle-fixed')).prop('checked', this.model.fixed); + }, + updateValid: function () {}, + updateViewer: function () { + this.parent.renderViewActionsView(); + }, + updateViewers: function () { + this.updatePreviewPlot(); + this.updateViewer(); + }, + subviews: { + inputPriority: { + hook: 'input-priority-container', + prepareView: function (el) { + return new InputView({ + parent: this, + required: true, + name: 'priority', + tests: tests.valueTests, + modelKey: 'priority', + valueType: 'number', + value: this.model.priority + }); + } + }, + selectType: { + hook: 'select-type-container', + prepareView: function (el) { + let groupOptions = [ + {groupName: "Shape Actions", options: ['Fill Action', 'Set Action', 'Remove Action']}, + {groupName: "Import Actions", options: ['XML Mesh', 'Mesh IO', 'StochSS Domain']} + ] + return new SelectView({ + name: 'type', + required: true, + idAttributes: 'cid', + groupOptions: groupOptions, + value: this.model.type + }); + } + }, + selectScope: { + hook: 'select-scope-container', + prepareView: function (el) { + let options = [ + 'Multi Particle', + 'Single Particle' + ]; + return new SelectView({ + name: 'scope', + required: true, + idAttributes: 'cid', + options: options, + value: this.model.scope + }); + } + }, + inputPointX: { + hook: 'point-x-container', + prepareView: function (el) { + return new InputView({ + parent: this, + required: true, + name: 'point-x', + tests: [tests.nanValue], + valueType: 'number', + value: this.model.point.x + }); + } + }, + inputPointY: { + hook: 'point-y-container', + prepareView: function (el) { + return new InputView({ + parent: this, + required: true, + name: 'point-y', + tests: [tests.nanValue], + valueType: 'number', + value: this.model.point.y + }); + } + }, + inputPointZ: { + hook: 'point-z-container', + prepareView: function (el) { + return new InputView({ + parent: this, + required: true, + name: 'point-z', + tests: [tests.nanValue], + valueType: 'number', + value: this.model.point.z + }); + } + } + } +}); \ No newline at end of file diff --git a/client/domain-view/views/actions-view.js b/client/domain-view/views/actions-view.js new file mode 100644 index 0000000000..9122f820f3 --- /dev/null +++ b/client/domain-view/views/actions-view.js @@ -0,0 +1,148 @@ +/* +StochSS is a platform for simulating biochemical systems +Copyright (C) 2019-2022 StochSS developers. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +let $ = require('jquery'); +//support files +let app = require('../../app'); +let Tooltips = require('../../tooltips'); +//views +let View = require('ampersand-view'); +let EditActionView = require('./action-view'); +//templates +let template = require('../templates/actionsView.pug'); + +module.exports = View.extend({ + template: template, + events: { + 'click [data-hook=collapse]' : 'changeCollapseButtonText', + 'click [data-hook=fill-action]' : 'addAction', + 'click [data-hook=set-action]' : 'addAction', + 'click [data-hook=remove-action]' : 'addAction', + 'click [data-hook=xml-mesh-action]' : 'addAction', + 'click [data-hook=mesh-io-action]' : 'addAction', + 'click [data-hook=stochss-domain-action]' : 'addAction' + }, + initialize: function (attrs, options) { + View.prototype.initialize.apply(this, arguments); + this.readOnly = attrs.readOnly ? attrs.readOnly : false; + this.tooltips = Tooltips.domainAction; + }, + render: function (attrs, options) { + View.prototype.render.apply(this, arguments); + if(this.readOnly) { + $(this.queryByHook('actions-edit-tab')).addClass("disabled"); + $(".nav .disabled>a").on("click", function(e) { + e.preventDefault(); + return false; + }); + $(this.queryByHook('actions-view-tab')).tab('show'); + $(this.queryByHook('edit-actions')).removeClass('active'); + $(this.queryByHook('view-actions')).addClass('active'); + }else{ + this.renderEditActionsView(); + this.toggleActionsCollectionError("ev"); + this.collection.on('add remove', () => { + this.toggleActionsCollectionError("ev"); + this.toggleActionsCollectionError("vv"); + }, this); + this.collection.on('update-shape-options', this.updateShapeOptions, this); + this.collection.on('update-transformation-options', this.updateTransformationOptions, this); + this.collection.on('update-type-options', this.updateTypeOptions, this); + } + this.renderViewActionsView(); + this.toggleActionsCollectionError("vv"); + }, + addAction: function (e) { + let type = e.target.dataset.name; + this.collection.addAction(type); + }, + changeCollapseButtonText: function (e) { + app.changeCollapseButtonText(this, e); + }, + openSection: function () { + if(!$(this.queryByHook("actions-list-container")).hasClass("show")) { + let actionsCollapseBtn = $(this.queryByHook("collapse")); + actionsCollapseBtn.click(); + actionsCollapseBtn.html('-'); + } + }, + renderEditActionsView: function ({key=null, attr=null}={}) { + if(this.editActionsView) { + this.editActionsView.remove(); + } + let options = {filter: (model) => { return model.contains(attr, key); }} + this.editActionsView = this.renderCollection( + this.collection, + EditActionView, + this.queryByHook('edit-actions-list'), + options + ); + }, + renderViewActionsView: function ({key=null, attr=null}={}) { + if(this.viewActionsView) { + this.viewActionsView.remove(); + } + let options = { + viewOptions: {viewMode: true}, + filter: (model) => { return model.contains(attr, key); } + } + this.viewActionsView = this.renderCollection( + this.collection, + EditActionView, + this.queryByHook('view-actions-list'), + options + ); + }, + toggleActionsCollectionError: function (viewCode) { + let errorMsg = $(this.queryByHook(`${viewCode}-actions-collection-error`)); + if(!this.collection.validateCollection()) { + errorMsg.addClass('component-invalid'); + errorMsg.removeClass('component-valid'); + }else{ + errorMsg.addClass('component-valid'); + errorMsg.removeClass('component-invalid'); + } + }, + updateShapeOptions: function ({currName=null, newName=null}={}) { + if(currName === null && newName === null) { return; } + this.editActionsView.views.forEach((view) => { + if(view.model.shape === currName) { + view.model.shape = newName; + } + view.renderShapeSelectView(); + }); + }, + updateTransformationOptions: function ({currName=null, newName=null}={}) { + if(currName === null && newName === null) { return; } + this.editActionsView.views.forEach((view) => { + if(view.model.transformation === currName && newName !== null) { + view.model.transformation = newName; + } + view.renderTransformationSelectView(); + }); + }, + updateTypeOptions: function ({currName=null, newName=null}={}) { + if(currName === null && newName === null) { return; } + this.editActionsView.views.forEach((view) => { + if(view.model.typeID === currName) { + view.model.typeID = newName; + } + view.renderTypeSelectView(); + }); + } +}); \ No newline at end of file diff --git a/client/domain-view/views/edit-3D-domain-view.js b/client/domain-view/views/edit-3D-domain-view.js deleted file mode 100644 index cfd4c503b4..0000000000 --- a/client/domain-view/views/edit-3D-domain-view.js +++ /dev/null @@ -1,302 +0,0 @@ -/* -StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -*/ - -let $ = require('jquery'); -let path = require('path'); -//support files -let app = require('../../app'); -let tests = require('../../views/tests'); -//views -let View = require('ampersand-view'); -let InputView = require('../../views/input'); -let SelectView = require('ampersand-select-view'); -//templates -let template = require('../templates/edit3DDomainView.pug'); - -module.exports = View.extend({ - template: template, - events: { - 'change [data-target=edit-3d-domain-n]' : 'handleNumParticlesUpdate', - 'change [data-target=edit-3d-domain-limitation]' : 'handleLimitsUpdate', - 'change [data-hook=edit-3d-domain-type-select]' : 'handleTypeUpdate', - 'change [data-target=edit-3d-domain-trans]' : 'handleTransformationupdate', - 'click [data-hook=collapse-3D-domain-advanced]' : 'changeCollapseButtonText', - 'click [data-hook=build-3d-domain]' : 'handleBuildDomain' - }, - initialize: function (attrs, options) { - View.prototype.initialize.apply(this, arguments); - this.data = { - "nx":1, "ny":1, "nz":1, - "xLim":[0, 0], "yLim":[0, 0], "zLim":[0, 0], - "type": null, "transformation": null - } - this.transformation = [0, 0, 0]; - }, - render: function (attrs, options) { - View.prototype.render.apply(this, arguments); - this.type = this.parent.model.types.get(0, "typeID"); - this.updateTotalParticles(); - this.updateTypeDefaults(); - }, - changeCollapseButtonText: function (e) { - app.changeCollapseButtonText(this, e); - }, - completeAction: function () { - $(this.queryByHook("c3dd-in-progress")).css("display", "none"); - $(this.queryByHook("c3dd-complete")).css("display", "inline-block"); - setTimeout(() => { - $(this.queryByHook("c3dd-complete")).css("display", "none"); - }, 5000); - }, - errorAction: function (action) { - $(this.queryByHook("c3dd-in-progress")).css("display", "none"); - $(this.queryByHook("c3dd-action-error")).text(action); - $(this.queryByHook("c3dd-error")).css("display", "block"); - }, - handleBuildDomain: function () { - this.startAction(); - this.transformation.every((value) => { - if(value !== 0) { - this.data.transformation = this.transformation; - return false; - } - return true; - }); - this.data.domainExists = this.parent.model.particles.length > 0; - let endpoint = path.join(app.getApiPath(), "spatial-model/3d-domain"); - app.postXHR(endpoint, this.data, { - success: (err, response, body) => { - this.parent.add3DDomain(body.limits, body.particles); - this.completeAction(); - $('html, body').animate({ - scrollTop: $("#domain-figure").offset().top - }, 20); - }, - error: (err, response, body) => { - this.errorAction(body.Message); - } - }); - }, - handleLimitsUpdate: function (e) { - let key = e.target.parentElement.parentElement.dataset.name; - let index = Number(e.target.parentElement.parentElement.dataset.index); - this.data[key][index] = Number(e.target.value); - }, - handleNumParticlesUpdate: function (e) { - let key = e.target.parentElement.parentElement.dataset.key; - this.data[key] = Number(e.target.value); - this.updateTotalParticles(); - }, - handleTransformationupdate: function (e) { - let index = e.target.parentElement.parentElement.dataset.index; - this.transformation[index] = Number(e.target.value); - }, - handleTypeUpdate: function (e) { - let typeID = Number(e.target.value); - this.type = this.parent.model.types.get(typeID, "typeID"); - this.updateTypeDefaults(); - }, - startAction: function () { - $(this.queryByHook("c3dd-complete")).css("display", "none"); - $(this.queryByHook("c3dd-error")).css("display", "none"); - $(this.queryByHook("c3dd-in-progress")).css("display", "inline-block"); - }, - update: function () {}, - updateValid: function () {}, - updateTotalParticles: function () { - let n = this.data.nx * this.data.ny * this.data.nz; - $(this.queryByHook("3d-domain-n")).text(n); - $(this.queryByHook("build-3d-domain")).prop("disabled", n <= 0); - }, - updateTypeDefaults: function () { - this.data.typeID = this.type.typeID; - this.data.type = { - "type_id": this.type.name, "mass": this.type.mass, "rho": this.type.rho, - "nu": this.type.nu, "c": this.type.c, "fixed": this.type.fixed - } - $(this.queryByHook("edit-3d-domain-td-mass")).text(this.type.mass); - $(this.queryByHook("edit-3d-domain-td-vol")).text(this.type.volume); - $(this.queryByHook("edit-3d-domain-td-rho")).text(this.type.rho); - $(this.queryByHook("edit-3d-domain-td-nu")).text(this.type.nu); - $(this.queryByHook("edit-3d-domain-td-c")).text(this.type.c); - $(this.queryByHook("edit-3d-domain-td-fixed")).prop('checked', this.type.fixed); - }, - subviews: { - nXInputView: { - hook: "edit-3d-domain-nx", - prepareView: function (el) { - return new InputView({ - parent: this, - required: true, - name: 'nx', - valueType: 'number', - tests: tests.valueTests, - value: this.data.nx - }); - } - }, - nYInputView: { - hook: "edit-3d-domain-ny", - prepareView: function (el) { - return new InputView({ - parent: this, - required: true, - name: 'ny', - valueType: 'number', - tests: tests.valueTests, - value: this.data.ny - }); - } - }, - nZInputView: { - hook: "edit-3d-domain-nz", - prepareView: function (el) { - return new InputView({ - parent: this, - required: true, - name: 'nz', - valueType: 'number', - tests: tests.valueTests, - value: this.data.nz - }); - } - }, - xMinLimInputView: { - hook: "edit-3d-domain-x-lim-min", - prepareView: function (el) { - return new InputView({ - parent: this, - required: true, - name: 'x-lim-min', - valueType: 'number', - value: this.data.xLim[0] - }); - } - }, - yMinLimInputView: { - hook: "edit-3d-domain-y-lim-min", - prepareView: function (el) { - return new InputView({ - parent: this, - required: true, - name: 'y-lim-min', - valueType: 'number', - value: this.data.yLim[0] - }); - } - }, - zMinLimInputView: { - hook: "edit-3d-domain-z-lim-min", - prepareView: function (el) { - return new InputView({ - parent: this, - required: true, - name: 'z-lim-min', - valueType: 'number', - value: this.data.zLim[0] - }); - } - }, - xMaxLimInputView: { - hook: "edit-3d-domain-x-lim-max", - prepareView: function (el) { - return new InputView({ - parent: this, - required: true, - name: 'x-lim-max', - valueType: 'number', - value: this.data.xLim[1] - }); - } - }, - yMaxLimInputView: { - hook: "edit-3d-domain-y-lim-max", - prepareView: function (el) { - return new InputView({ - parent: this, - required: true, - name: 'y-lim-max', - valueType: 'number', - value: this.data.yLim[1] - }); - } - }, - zMaxLimInputView: { - hook: "edit-3d-domain-z-lim-max", - prepareView: function (el) { - return new InputView({ - parent: this, - required: true, - name: 'z-lim-max', - valueType: 'number', - value: this.data.zLim[1] - }); - } - }, - typeSelectView: { - hook: "edit-3d-domain-type-select", - prepareView: function (el) { - return new SelectView({ - name: 'type', - required: true, - idAttribute: 'typeID', - textAttribute: 'name', - eagerValidate: true, - options: this.parent.model.types, - value: this.type - }); - } - }, - xTransInputView: { - hook: "edit-3d-domain-x-trans", - prepareView: function (el) { - return new InputView({ - parent: this, - required: true, - name: 'x-transformation', - valueType: 'number', - value: this.transformation[0] - }); - } - }, - yTransInputView: { - hook: "edit-3d-domain-y-trans", - prepareView: function (el) { - return new InputView({ - parent: this, - required: true, - name: 'y-transformation', - valueType: 'number', - value: this.transformation[1] - }); - } - }, - zTransInputView: { - hook: "edit-3d-domain-z-trans", - prepareView: function (el) { - return new InputView({ - parent: this, - required: true, - name: 'z-transformation', - valueType: 'number', - value: this.transformation[2] - }); - } - } - } -}); \ No newline at end of file diff --git a/client/domain-view/views/fill-geometry-view.js b/client/domain-view/views/fill-geometry-view.js deleted file mode 100644 index f9f507f3a9..0000000000 --- a/client/domain-view/views/fill-geometry-view.js +++ /dev/null @@ -1,267 +0,0 @@ -/* -StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -*/ - -let $ = require('jquery'); -let path = require('path'); -//support files -let app = require('../../app'); -let tests = require('../../views/tests'); -//views -let View = require('ampersand-view'); -let InputView = require('../../views/input'); -let SelectView = require('ampersand-select-view'); -//templates -let template = require('../templates/fillGeometryView.pug'); - -module.exports = View.extend({ - template: template, - events: { - 'change [data-hook=fill-geometry-type-select]' : 'handleTypeUpdate', - 'change [data-target=fill-geometry-limitation]' : 'handleDataUpdate', - 'change [data-target=fill-geometry-delta]' : 'handleDataUpdate', - 'click [data-hook=fill-geometry]' : 'handleFillGeometry' - }, - initialize: function (attrs, options) { - View.prototype.initialize.apply(this, arguments); - this.data = { - "xmin":0, "ymin":0, "zmin":0, "xmax":0, "ymax":0, - "zmax":0, "deltax": 0, "deltay": 0, "deltaz": 0 - } - this.type = null; - }, - render: function (attrs, options) { - View.prototype.render.apply(this, arguments); - this.getTypes(); - this.renderTypeSelectView(); - }, - completeAction: function () { - $(this.queryByHook("fg-in-progress")).css("display", "none"); - $(this.queryByHook("fg-complete")).css("display", "inline-block"); - setTimeout(() => { - $(this.queryByHook("fg-complete")).css("display", "none"); - }, 5000); - }, - disable: function () { - if(this.data.xmin === this.data.xmax) { return true; } - if(this.data.ymin === this.data.ymax) { return true; } - if(this.data.zmin === this.data.zmax) { return true; } - if(this.data.deltax === 0) { return true; } - if(this.data.deltay === 0) { return true; } - if(this.data.deltaz === 0) { return true; } - if(!this.type) { return true; } - return false; - }, - errorAction: function (action) { - $(this.queryByHook("fg-in-progress")).css("display", "none"); - $(this.queryByHook("fg-action-error")).html(action); - $(this.queryByHook("fg-error")).css("display", "block"); - }, - getTypes: function () { - this.types = this.parent.model.types.filter((type) => { - return Boolean(type.geometry); - }); - }, - handleDataUpdate: function (e) { - let key = e.target.parentElement.parentElement.dataset.name; - this.data[key] = Number(e.target.value); - this.toggleFillGeometry(); - }, - handleFillGeometry: function () { - this.startAction(); - let data = {kwargs: this.data, type: this.type} - let endpoint = path.join(app.getApiPath(), 'spatial-model/fill-geometry'); - app.postXHR(endpoint, data, { - success: (err, response, body) => { - this.parent.add3DDomain(body.limits, body.particles); - this.completeAction(); - }, - error: (err, response, body) => { - if(body.Traceback.includes("SyntaxError")) { - var tracePart = body.Traceback.split('\n').slice(6) - tracePart.splice(2, 2) - tracePart[1] = tracePart[1].replace(new RegExp(' ', 'g'), ' ') - var errorBlock = `

${body.Message}
${tracePart.join('
')}

` - }else{ - var errorBlock = body.Message - } - this.errorAction(errorBlock); - } - }); - }, - handleTypeUpdate: function (e) { - if(e.target.value) { - let typeID = Number(e.target.value); - this.type = this.parent.model.types.get(typeID, "typeID"); - }else{ - this.type = null; - } - this.updateTypeDefaults(); - this.toggleFillGeometry(); - }, - renderTypeSelectView: function () { - if(this.typeSelectView) { - this.typeSelectView.remove(); - } - if(this.types) { - var options = this.types.map((type) => { - return [type.typeID, type.name]; - }); - }else{ - var options = []; - } - this.typeSelectView = new SelectView({ - name: 'type', - required: false, - idAttribute: 'typeID', - textAttribute: 'name', - eagerValidate: true, - options: options, - unselectedText: "Select a Type" - }); - app.registerRenderSubview(this, this.typeSelectView, "fill-geometry-type-select"); - }, - startAction: function () { - $(this.queryByHook("fg-complete")).css("display", "none"); - $(this.queryByHook("fg-error")).css("display", "none"); - $(this.queryByHook("fg-in-progress")).css("display", "inline-block"); - }, - toggleFillGeometry: function () { - $(this.queryByHook('fill-geometry')).prop('disabled', this.disable()); - }, - update: function() {}, - updateTypeDefaults: function () { - $(this.queryByHook("fill-geometry-type-geometry")).text(this.type.geometry || ""); - $(this.queryByHook("fill-geometry-td-mass")).text(this.type.mass || ""); - $(this.queryByHook("fill-geometry-td-vol")).text(this.type.volume || ""); - $(this.queryByHook("fill-geometry-td-rho")).text(this.type.rho || ""); - $(this.queryByHook("fill-geometry-td-nu")).text(this.type.nu || "0"); - $(this.queryByHook("fill-geometry-td-c")).text(this.type.c || ""); - $(this.queryByHook("fill-geometry-td-fixed")).prop('checked', this.type.fixed || false); - }, - updateValid: function () {}, - subviews: { - xMinLimInputView: { - hook: "fill-geometry-x-lim-min", - prepareView: function (el) { - return new InputView({ - parent: this, - required: true, - name: 'x-lim-min', - valueType: 'number', - value: this.data.xmin - }); - } - }, - yMinLimInputView: { - hook: "fill-geometry-y-lim-min", - prepareView: function (el) { - return new InputView({ - parent: this, - required: true, - name: 'y-lim-min', - valueType: 'number', - value: this.data.ymin - }); - } - }, - zMinLimInputView: { - hook: "fill-geometry-z-lim-min", - prepareView: function (el) { - return new InputView({ - parent: this, - required: true, - name: 'z-lim-min', - valueType: 'number', - value: this.data.zmin - }); - } - }, - xMaxLimInputView: { - hook: "fill-geometry-x-lim-max", - prepareView: function (el) { - return new InputView({ - parent: this, - required: true, - name: 'x-lim-max', - valueType: 'number', - value: this.data.xmax - }); - } - }, - yMaxLimInputView: { - hook: "fill-geometry-y-lim-max", - prepareView: function (el) { - return new InputView({ - parent: this, - required: true, - name: 'y-lim-max', - valueType: 'number', - value: this.data.ymax - }); - } - }, - zMaxLimInputView: { - hook: "fill-geometry-z-lim-max", - prepareView: function (el) { - return new InputView({ - parent: this, - required: true, - name: 'z-lim-max', - valueType: 'number', - value: this.data.zmax - }); - } - }, - deltaxInputView: { - hook: "fill-geometry-deltax", - prepareView: function (el) { - return new InputView({ - parent: this, - required: true, - name: 'deltax', - valueType: 'number', - value: this.data.deltax - }); - } - }, - deltayInputView: { - hook: "fill-geometry-deltay", - prepareView: function (el) { - return new InputView({ - parent: this, - required: true, - name: 'deltay', - valueType: 'number', - value: this.data.deltay - }); - } - }, - deltazInputView: { - hook: "fill-geometry-deltaz", - prepareView: function (el) { - return new InputView({ - parent: this, - required: true, - name: 'deltaz', - valueType: 'number', - value: this.data.deltaz - }); - } - }, - } -}); \ No newline at end of file diff --git a/client/domain-view/views/import-mesh-view.js b/client/domain-view/views/import-mesh-view.js deleted file mode 100644 index 29bf950f57..0000000000 --- a/client/domain-view/views/import-mesh-view.js +++ /dev/null @@ -1,187 +0,0 @@ -/* -StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -*/ - -let $ = require('jquery'); -let path = require('path'); -//support files -let app = require('../../app'); -let tests = require('../../views/tests'); -//views -let View = require('ampersand-view'); -let InputView = require('../../views/input'); -let SelectView = require('ampersand-select-view'); -//templates -let template = require('../templates/importMeshView.pug'); - -module.exports = View.extend({ - template: template, - events: { - 'change #meshfile' : 'setMeshFile', - 'change #typefile' : 'setTypeFile', - 'change [data-hook=import-mesh-type-select]' : 'handleTypeUpdate', - 'change [data-target=import-mesh-trans]' : 'handleTransformationUpdate', - 'click [data-hook=collapse-import-mesh-advanced]' : 'changeCollapseButtonText', - 'click [data-hook=import-mesh-particles]' : 'handleImportMesh' - }, - initialize: function (attrs, options) { - View.prototype.initialize.apply(this, arguments); - this.meshFile = null; - this.typeFile = null; - this.data = {"type": null, "transformation": null}; - this.transformation = [0, 0, 0]; - }, - render: function (attrs, options) { - View.prototype.render.apply(this, arguments); - this.type = this.parent.model.types.get(0, "typeID"); - this.updateTypeDefaults(); - }, - changeCollapseButtonText: function (e) { - app.changeCollapseButtonText(this, e); - }, - completeAction: function () { - $(this.queryByHook("imp-in-progress")).css("display", "none"); - $(this.queryByHook("imp-complete")).css("display", "inline-block"); - setTimeout(() => { - $(this.queryByHook("imp-complete")).css("display", "none"); - }, 5000); - }, - errorAction: function (action) { - $(this.queryByHook("imp-in-progress")).css("display", "none"); - $(this.queryByHook("imp-action-error")).text(action); - $(this.queryByHook("imp-error")).css("display", "block"); - }, - handleImportMesh: function () { - this.startAction(); - this.transformation.every((value) => { - if(value !== 0) { - this.data.transformation = this.transformation; - return false; - } - return true; - }); - let formData = new FormData(); - formData.append("datafile", this.meshFile); - formData.append("particleData", JSON.stringify(this.data)); - if(this.typeFile) { - formData.append("typefile", this.typeFile); - } - let endpoint = path.join(app.getApiPath(), 'spatial-model/import-mesh'); - app.postXHR(endpoint, formData, { - success: (err, response, body) => { - body = JSON.parse(body); - this.parent.addMeshDomain(body.limits, body.particles, body.types, this.parent.model.particles.length > 0); - this.completeAction(); - $('html, body').animate({ - scrollTop: $("#domain-figure").offset().top - }, 20); - }, - error: (err, response, body) => { - body = JSON.parse(body); - this.errorAction(body.Message); - } - }, false); - }, - handleTransformationUpdate: function (e) { - let index = e.target.parentElement.parentElement.dataset.index; - this.transformation[index] = Number(e.target.value); - }, - handleTypeUpdate: function (e) { - let typeID = Number(e.target.value); - this.type = this.parent.model.types.get(typeID, "typeID"); - this.updateTypeDefaults(); - }, - setMeshFile: function (e) { - this.meshFile = e.target.files[0]; - $(this.queryByHook("import-mesh-particles")).prop('disabled', !this.meshFile); - }, - setTypeFile: function (e) { - this.typeFile = e.target.files[0]; - }, - startAction: function () { - $(this.queryByHook("imp-complete")).css("display", "none"); - $(this.queryByHook("imp-error")).css("display", "none"); - $(this.queryByHook("imp-in-progress")).css("display", "inline-block"); - }, - update: function () {}, - updateValid: function () {}, - updateTypeDefaults: function () { - this.data.typeID = this.type.typeID; - this.data.type = { - "type_id": this.type.name, "mass": this.type.mass, "rho": this.type.rho, - "nu": this.type.nu, "c": this.type.c, "fixed": this.type.fixed - } - $(this.queryByHook("import-mesh-td-mass")).text(this.type.mass); - $(this.queryByHook("import-mesh-td-vol")).text(this.type.volume); - $(this.queryByHook("import-mesh-td-rho")).text(this.type.rho); - $(this.queryByHook("import-mesh-td-nu")).text(this.type.nu); - $(this.queryByHook("import-mesh-td-c")).text(this.type.c); - $(this.queryByHook("import-mesh-td-fixed")).prop('checked', this.type.fixed); - }, - subviews: { - typeSelectView: { - hook: "import-mesh-type-select", - prepareView: function (el) { - return new SelectView({ - name: 'type', - required: true, - idAttribute: 'typeID', - textAttribute: 'name', - eagerValidate: true, - options: this.parent.model.types, - value: this.type - }); - } - }, - xTransInputView: { - hook: "import-mesh-x-trans", - prepareView: function (el) { - return new InputView({ - parent: this, - required: true, - name: 'x-transformation', - valueType: 'number', - value: this.transformation[0] - }); - } - }, - yTransInputView: { - hook: "import-mesh-y-trans", - prepareView: function (el) { - return new InputView({ - parent: this, - required: true, - name: 'y-transformation', - valueType: 'number', - value: this.transformation[1] - }); - } - }, - zTransInputView: { - hook: "import-mesh-z-trans", - prepareView: function (el) { - return new InputView({ - parent: this, - required: true, - name: 'z-transformation', - valueType: 'number', - value: this.transformation[2] - }); - } - } - } -}); \ No newline at end of file diff --git a/client/domain-view/views/limits-view.js b/client/domain-view/views/limits-view.js index 09f740b160..c1c78089a8 100644 --- a/client/domain-view/views/limits-view.js +++ b/client/domain-view/views/limits-view.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -74,7 +74,12 @@ module.exports = View.extend({ required: true, name: 'x-lim-min', valueType: 'number', - value: this.model.x_lim[0] || 0 + value: this.model.x_lim[0] || 0, + tests: [(value) => { + if(value > this.model.x_lim[1]) { + return "X-Min cannot be greater than X-Max"; + } + }] }); app.registerRenderSubview(this, this.xMinLimitInputView, "x-lim-min"); }, @@ -87,7 +92,12 @@ module.exports = View.extend({ required: true, name: 'y-lim-min', valueType: 'number', - value: this.model.y_lim[0] || 0 + value: this.model.y_lim[0] || 0, + tests: [(value) => { + if(value > this.model.y_lim[1]) { + return "Y-Min cannot be greater than Y-Max"; + } + }] }); app.registerRenderSubview(this, this.yMinLimitInputView, "y-lim-min"); }, @@ -100,7 +110,12 @@ module.exports = View.extend({ required: true, name: 'z-lim-min', valueType: 'number', - value: this.model.z_lim[0] || 0 + value: this.model.z_lim[0] || 0, + tests: [(value) => { + if(value > this.model.z_lim[1]) { + return "Z-Min cannot be greater than Z-Max"; + } + }] }); app.registerRenderSubview(this, this.zMinLimitInputView, "z-lim-min"); }, @@ -113,7 +128,12 @@ module.exports = View.extend({ required: true, name: 'x-lim-max', valueType: 'number', - value: this.model.x_lim[1] || 0 + value: this.model.x_lim[1] || 0, + tests: [(value) => { + if(value < this.model.x_lim[0]) { + return "X-Max cannot be less than X-Min"; + } + }] }); app.registerRenderSubview(this, this.xMaxLimitInputView, "x-lim-max"); }, @@ -126,7 +146,12 @@ module.exports = View.extend({ required: true, name: 'y-lim-max', valueType: 'number', - value: this.model.y_lim[1] || 0 + value: this.model.y_lim[1] || 0, + tests: [(value) => { + if(value < this.model.y_lim[0]) { + return "Y-Max cannot be less than Y-Min"; + } + }] }); app.registerRenderSubview(this, this.yMaxLimitInputView, "y-lim-max"); }, @@ -139,7 +164,12 @@ module.exports = View.extend({ required: true, name: 'z-lim-max', valueType: 'number', - value: this.model.z_lim[1] || 0 + value: this.model.z_lim[1] || 0, + tests: [(value) => { + if(value < this.model.z_lim[0]) { + return "Z-Max cannot be less than Z-Min"; + } + }] }); app.registerRenderSubview(this, this.zMaxLimitInputView, "z-lim-max"); }, diff --git a/client/domain-view/views/particle-view.js b/client/domain-view/views/particle-view.js index 87f1a0b424..8b73b3f620 100644 --- a/client/domain-view/views/particle-view.js +++ b/client/domain-view/views/particle-view.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -19,6 +19,7 @@ along with this program. If not, see . let $ = require('jquery'); //support files let app = require('../../app'); +let tests = require('../../views/tests'); //views let View = require('ampersand-view'); let InputView = require('../../views/input'); @@ -28,202 +29,211 @@ let template = require('../templates/editParticleView.pug'); module.exports = View.extend({ template: template, - events: function () { - let events = {}; - events[`change [data-hook=particle-type-${this.viewIndex}]`] = 'handleSelectType'; - events[`change [data-target=location-${this.viewIndex}]`] = 'handleUpdateLocation'; - events[`change [data-hook=particle-fixed=${this.viewIndex}]`] = 'handleUpdateFixed'; - return events; + events: { + 'change [data-target=new-point]' : 'setNewPoint', + 'change [data-hook=particle-type]' : 'setParticleType', + 'change [data-hook=particle-fixed]' : 'setParticleFixed' }, initialize: function (attrs, options) { View.prototype.initialize.apply(this, arguments); this.viewIndex = attrs.viewIndex; this.defaultType = attrs.defaultType; this.disable = attrs.disable ? attrs.disable : false; - this.origType = this.model.type; + this.origType = this.model.typeID; this.origPoint = JSON.parse(JSON.stringify(this.model.point)); }, render: function (attrs, options) { View.prototype.render.apply(this, arguments); this.renderTypeSelectView(); this.renderParticleProperties(); - this.renderXCoordView(); - this.renderYCoordView(); - this.renderZCoordView(); + $(this.queryByHook('particle-fixed')).prop('disabled', this.disable); }, - handleSelectType: function (e) { - this.defaultType = this.parent.model.types.get(this.model.type, "typeID") - this.model.type = Number(e.target.value); - let type = this.parent.model.types.get(this.model.type, "typeID") - if(this.model.mass === this.defaultType.mass) { - this.model.mass = type.mass; + renderDensityPropertyView: function () { + if(this.densityPropertyView) { + this.densityPropertyView.remove(); } - if(this.model.volume === this.defaultType.volume) { - this.model.volume = type.volume; - } - if(this.model.rho === this.defaultType.rho) { - this.model.rho = type.rho; - } - if(this.model.nu === this.defaultType.nu) { - this.model.nu = type.nu; - } - if(this.model.c === this.defaultType.c) { - this.model.c = type.c; - } - if(this.model.fixed === this.defaultType.fixed) { - this.model.fixed = type.fixed; - } - this.renderParticleProperties(); - }, - handleUpdateFixed: function (e) { - this.model.fixed = e.target.checked; - }, - handleUpdateLocation: function (e) { - let index = Number(e.target.parentElement.parentElement.dataset.index); - this.model.point[index] = Number(e.target.value); - }, - renderCInputView: function () { - if(this.cInputView) { - this.cInputView.remove(); - } - this.cInputView = new InputView({ + this.densityPropertyView = new InputView({ parent: this, required: true, - name: 'speed-of-sound', - disabled: this.disable, - modelKey: 'c', + name: 'density', + tests: tests.valueTests, valueType: 'number', - value: this.model.c + modelKey: 'rho', + value: this.model.rho }); - app.registerRenderSubview(this, this.cInputView, `particle-c-${this.viewIndex}`); + let hook = "particle-rho"; + app.registerRenderSubview(this, this.densityPropertyView, hook); }, - renderParticleProperties: function () { - this.renderCInputView(); - this.renderMassInputView(); - this.renderNuInputView(); - this.renderRhoInputView(); - this.renderVolumeInputView(); - $(this.queryByHook(`particle-fixed-${this.viewIndex}`)).prop('checked', this.model.fixed); - $(this.queryByHook(`particle-fixed-${this.viewIndex}`)).prop('disabled', this.disable); - }, - renderMassInputView: function () { - if(this.massInputView) { - this.massInputView.remove(); + renderMassPropertyView: function () { + if(this.massPropertyView) { + this.massPropertyView.remove(); } - this.massInputView = new InputView({ + this.massPropertyView = new InputView({ parent: this, required: true, name: 'mass', - disabled: this.disable, - modelKey: 'mass', + tests: tests.valueTests, valueType: 'number', + modelKey: 'mass', value: this.model.mass }); - app.registerRenderSubview(this, this.massInputView, `particle-mass-${this.viewIndex}`); + let hook = "particle-mass"; + app.registerRenderSubview(this, this.massPropertyView, hook); }, - renderNuInputView: function () { - if(this.nuInputView) { - this.nuInputView.remove(); - } - this.nuInputView = new InputView({ - parent: this, - required: true, - name: 'viscosity', - disabled: this.disable, - modelKey: 'nu', - valueType: 'number', - value: this.model.nu - }); - app.registerRenderSubview(this, this.nuInputView, `particle-nu-${this.viewIndex}`); - }, - renderRhoInputView: function () { - if(this.rhoInputView) { - this.rhoInputView.remove(); + renderParticleProperties: function () { + this.renderMassPropertyView(); + this.renderVolumePropertyView(); + this.renderDensityPropertyView(); + this.renderViscosityPropertyView(); + this.renderSOSPropertyView(); + }, + renderSOSPropertyView: function () { + if(this.sOSPropertyView) { + this.sOSPropertyView.remove(); } - this.rhoInputView = new InputView({ + this.sOSPropertyView = new InputView({ parent: this, required: true, - name: 'density', - disabled: this.disable, - modelKey: 'rho', + name: 'speed-of-sound', + tests: tests.valueTests, valueType: 'number', - value: this.model.rho + modelKey: 'c', + value: this.model.c }); - app.registerRenderSubview(this, this.rhoInputView, `particle-rho-${this.viewIndex}`); + let hook = "particle-c"; + app.registerRenderSubview(this, this.sOSPropertyView, hook); }, renderTypeSelectView: function () { if(this.typeSelectView) { this.typeSelectView.remove(); } + let options = this.parent.model.types.map((type) => { + return [type.typeID, type.name]; + }); this.typeSelectView = new SelectView({ name: 'type', required: true, - idAttribute: 'typeID', - textAttribute: 'name', - eagerValidate: true, - options: this.parent.model.types, - value: this.parent.model.types.get(this.model.type, "typeID") + options: options, + value: this.model.typeID, }); - app.registerRenderSubview(this, this.typeSelectView, `particle-type-${this.viewIndex}`) - $(this.queryByHook(`particle-type-${this.viewIndex}`)).find('select').prop('disabled', this.disable); + let hook = "particle-type"; + app.registerRenderSubview(this, this.typeSelectView, hook); + $(this.queryByHook(hook)).find('select').prop('disabled', this.disable); }, - renderVolumeInputView: function () { - if(this.volumeInputView) { - this.volumeInputView.remove(); + renderViscosityPropertyView: function () { + if(this.viscosityPropertyView) { + this.viscosityPropertyView.remove(); } - this.volumeInputView = new InputView({ + this.viscosityPropertyView = new InputView({ parent: this, required: true, - name: 'volume', - disabled: this.disable, - modelKey: 'volume', + name: 'viscosity', + tests: tests.valueTests, valueType: 'number', - value: this.model.volume + modelKey: 'nu', + value: this.model.nu }); - app.registerRenderSubview(this, this.volumeInputView, `particle-vol-${this.viewIndex}`); + let hook = "particle-nu"; + app.registerRenderSubview(this, this.viscosityPropertyView, hook); }, - renderXCoordView: function () { - if(this.xCoordView) { - this.xCoordView.remove(); + renderVolumePropertyView: function () { + if(this.volumePropertyView) { + this.volumePropertyView.remove(); } - this.xCoordView = new InputView({ + this.volumePropertyView = new InputView({ parent: this, required: true, - name: 'x-coord', - disabled: this.disable, + name: 'volume', + tests: tests.valueTests, valueType: 'number', - value: this.model.point[0] + modelKey: 'vol', + value: this.model.vol }); - app.registerRenderSubview(this, this.xCoordView, `x-coord-${this.viewIndex}`); + let hook = "particle-vol"; + app.registerRenderSubview(this, this.volumePropertyView, hook); }, - renderYCoordView: function () { - if(this.yCoordView) { - this.yCoordView.remove(); - } - this.yCoordView = new InputView({ - parent: this, - required: true, - name: 'y-coord', - disabled: this.disable, - valueType: 'number', - value: this.model.point[1] - }); - app.registerRenderSubview(this, this.yCoordView, `y-coord-${this.viewIndex}`); + setNewPoint: function (e) { + let key = e.target.parentElement.parentElement.dataset.name; + let value = Number(e.target.value); + this.model.newPoint[key] = value; }, - renderZCoordView: function () { - if(this.zCoordView) { - this.zCoordView.remove(); - } - this.zCoordView = new InputView({ - parent: this, - required: true, - name: 'z-coord', - disabled: this.disable, - valueType: 'number', - value: this.model.point[2] - }); - app.registerRenderSubview(this, this.zCoordView, `z-coord-${this.viewIndex}`); + setParticleFixed: function (e) { + this.model.fixed = !this.model.fixed; + }, + setParticleType: function (e) { + let value = Number(e.target.value); + console.log(value); + let currType = this.parent.model.types.get(this.model.typeID, "typeID"); + let newType = this.parent.model.types.get(value, "typeID"); + this.updatePropertyDefaults(currType, newType); + this.model.typeID = value; }, update: function (e) {}, - updateValid: function (e) {} + updatePropertyDefaults: function (currType, newType) { + if(this.model.mass === currType.mass) { + this.model.mass = newType.mass; + } + if(this.model.vol === currType.volume) { + this.model.vol = newType.volume; + } + if(this.model.rho === currType.rho) { + this.model.rho = newType.rho; + } + if(this.model.nu === currType.nu) { + this.model.nu = newType.nu; + } + if(this.model.c === currType.c) { + this.model.c = newType.c; + } + if(this.model.fixed === currType.fixed) { + this.model.fixed = newType.fixed; + } + this.renderParticleProperties(); + $(this.queryByHook('particle-fixed')).prop('checked', this.model.fixed); + }, + updateValid: function (e) {}, + subviews: { + inputNewPointX: { + hook: 'new-point-x-container', + prepareView: function (el) { + return new InputView({ + parent: this, + required: true, + name: 'new-point-x', + disabled: this.disable, + tests: [tests.nanValue], + valueType: 'number', + value: this.model.newPoint.x + }); + } + }, + inputNewPointY: { + hook: 'new-point-y-container', + prepareView: function (el) { + return new InputView({ + parent: this, + required: true, + name: 'new-point-y', + disabled: this.disable, + tests: [tests.nanValue], + valueType: 'number', + value: this.model.newPoint.y + }); + } + }, + inputNewPointZ: { + hook: 'new-point-z-container', + prepareView: function (el) { + return new InputView({ + parent: this, + required: true, + name: 'new-point-z', + disabled: this.disable, + tests: [tests.nanValue], + valueType: 'number', + value: this.model.newPoint.z + }); + } + } + } }); \ No newline at end of file diff --git a/client/domain-view/views/properties-view.js b/client/domain-view/views/properties-view.js index f45575c57c..9b030d8eec 100644 --- a/client/domain-view/views/properties-view.js +++ b/client/domain-view/views/properties-view.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -114,8 +114,7 @@ module.exports = View.extend({ required: true, name: 'gravity-x', valueType: 'number', - value: this.model.gravity[0], - label: "X: " + value: this.model.gravity[0] }); app.registerRenderSubview(this, this.xGravityInputView, "gravity-x"); }, @@ -128,8 +127,7 @@ module.exports = View.extend({ required: true, name: 'gravity-y', valueType: 'number', - value: this.model.gravity[1], - label: "Y: " + value: this.model.gravity[1] }); app.registerRenderSubview(this, this.yGravityInputView, "gravity-y"); }, @@ -142,8 +140,7 @@ module.exports = View.extend({ required: true, name: 'gravity-z', valueType: 'number', - value: this.model.gravity[2], - label: "Z: " + value: this.model.gravity[2] }); app.registerRenderSubview(this, this.zGravityInputView, "gravity-z"); }, diff --git a/client/domain-view/views/quickview-type.js b/client/domain-view/views/quickview-type.js index 1e0f17bc6c..dd986ff802 100644 --- a/client/domain-view/views/quickview-type.js +++ b/client/domain-view/views/quickview-type.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/domain-view/views/shape-view.js b/client/domain-view/views/shape-view.js new file mode 100644 index 0000000000..4aa4450146 --- /dev/null +++ b/client/domain-view/views/shape-view.js @@ -0,0 +1,363 @@ +/* +StochSS is a platform for simulating biochemical systems +Copyright (C) 2019-2022 StochSS developers. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ +let $ = require('jquery'); +let _ = require('underscore'); +//support files +let app = require('../../app'); +let tests = require('../../views/tests'); +//views +let View = require('ampersand-view'); +let InputView = require('../../views/input'); +let SelectView = require('ampersand-select-view'); +//templates +let editTemplate = require('../templates/editShape.pug'); +let viewTemplate = require('../templates/viewShape.pug'); + +module.exports = View.extend({ + events: { + 'change [data-hook=input-name-container]' : 'updateDepsOptions', + 'change [data-hook=select-geometry-container]' : 'selectGeometryType', + 'change [data-hook=input-formula-container]' : 'updateGeometriesInUse', + 'change [data-hook=select-lattice-container]' : 'selectLatticeType', + 'change [data-target=shape-property]' : 'updateViewer', + 'click [data-hook=select-shape]' : 'selectShape', + 'click [data-hook=fillable-shape]' : 'selectFillable', + 'click [data-hook=remove]' : 'removeShape' + }, + bindings: { + 'model.selected' : { + type: function (el, value, previousValue) { + el.checked = value; + }, + hook: 'select-shape' + }, + 'model.fillable' : { + type: function (el, value, previousValue) { + el.checked = value; + }, + hook: 'fillable-shape' + }, + 'model.notEditable' : { + hook: 'select-shape', + type: function (el, value, previousValue) { + el.disabled = !value; + }, + name: 'disabled' + }, + 'model.inUse': { + hook: 'remove', + type: 'booleanAttribute', + name: 'disabled' + } + }, + initialize: function (attrs, options) { + View.prototype.initialize.apply(this, arguments); + this.viewMode = attrs.viewMode ? attrs.viewMode : false; + }, + render: function () { + this.template = this.viewMode ? viewTemplate : editTemplate; + View.prototype.render.apply(this, arguments); + this.details = { + 'Cartesian Lattice': [ + $(this.queryByHook('cartesian-lattice-props')) + ], + 'Spherical Lattice': [ + $(this.queryByHook('circular-lattice-props')) + ], + 'Cylindrical Lattice': [ + $(this.queryByHook('circular-lattice-props')), + $(this.queryByHook('cylinder-length-header')), + $(this.queryByHook('cylinder-length-feild')) + ] + } + app.documentSetup(); + if(!this.viewMode){ + if(this.model.selected) { + setTimeout(_.bind(this.openDetails, this), 1); + } + this.renderFormulaInputView(); + this.displayDetails(); + }else if(this.model.fillable) { + setTimeout(_.bind(this.openDetails, this), 1); + this.displayDetails(); + } + }, + displayDetails: function () { + this.details[this.model.lattice].forEach((element) => { + element.css('display', 'block'); + }); + }, + hideDetails: function () { + this.details[this.model.lattice].forEach((element) => { + element.css('display', 'none'); + }); + }, + openDetails: function () { + $(this.queryByHook("view-collapse-shape-details" + this.model.cid)).collapse("show"); + }, + removeShape: function () { + let name = this.model.name; + let actions = this.model.collection.parent.actions; + this.collection.removeShape(this.model); + actions.trigger('update-shape-options', {currName: name}); + }, + renderFormulaInputView: function () { + if(this.formulaInputView) { + this.formulaInputView.remove(); + } + let placeholder = this.model.type === "Standard" ? + "-- Boolean Formula in terms of 'x', 'y', 'z' --" : + "-- Boolean Formula in terms of other shapes --"; + this.formulaInputView = new InputView({ + parent: this, required: this.model.type === "Combinatory", name: 'formula', modelKey: 'formula', + valueType: 'string', value: this.model.formula, placeholder: placeholder + }); + let hook = 'input-formula-container'; + app.registerRenderSubview(this, this.formulaInputView, hook); + }, + selectGeometryType: function (e) { + this.model.type = e.target.value; + this.renderFormulaInputView(); + this.updateViewer(); + }, + selectShape: function () { + this.model.selected = !this.model.selected; + }, + selectFillable: function () { + this.model.fillable = !this.model.fillable; + if(!this.model.fillable && this.model.selected) { + this.model.selected = false; + $(this.queryByHook("edit-collapse-shape-details" + this.model.cid)).collapse("hide"); + } + this.updateViewer(); + this.model.collection.parent.actions.trigger('update-shape-options', {currName: name}); + }, + selectLatticeType: function (e) { + this.hideDetails(); + this.model.lattice = e.target.value; + this.displayDetails(); + this.updateViewer(); + }, + update: function () {}, + updateDepsOptions: function (e) { + let name = this.model.name; + this.model.name = e.target.value; + this.updateViewer(); + this.model.collection.parent.actions.trigger( + 'update-shape-options', {currName: name, newName: this.model.name} + ); + }, + updateGeometriesInUse: function () { + if(this.model.type === "Combinatory") { + this.model.collection.parent.trigger('update-shape-deps'); + } + this.updateViewer(); + }, + updateValid: function () {}, + updateViewer: function () { + this.parent.renderViewShapesView(); + }, + subviews: { + inputName: { + hook: 'input-name-container', + prepareView: function (el) { + return new InputView({ + parent: this, + required: true, + name: 'name', + tests: tests.nameTests, + valueType: 'string', + value: this.model.name + }); + } + }, + selectGeometry: { + hook: 'select-geometry-container', + prepareView: function (el) { + let options = [ + 'Standard', + 'Combinatory' + ]; + return new SelectView({ + name: 'geometry', + required: true, + idAttributes: 'cid', + options: options, + value: this.model.type + }); + } + }, + selectLattice: { + hook: 'select-lattice-container', + prepareView: function (el) { + let options = [ + 'Cartesian Lattice', + 'Spherical Lattice', + 'Cylindrical Lattice' + ]; + return new SelectView({ + name: 'lattice', + required: true, + idAttributes: 'cid', + options: options, + value: this.model.lattice + }); + } + }, + lengthInputView: { + hook: "length-container", + prepareView: function (el) { + return new InputView({ + parent: this, + required: true, + name: 'length', + modelKey: 'length', + valueType: 'number', + value: this.model.length, + tests: tests.valueTests + }); + } + }, + heightInputView: { + hook: "height-container", + prepareView: function (el) { + return new InputView({ + parent: this, + required: false, + name: 'height', + modelKey: 'height', + valueType: 'number', + value: this.model.height, + tests: tests.valueTests + }); + } + }, + depthInputView: { + hook: "depth-container", + prepareView: function (el) { + return new InputView({ + parent: this, + required: false, + name: 'depth', + modelKey: 'depth', + valueType: 'number', + value: this.model.depth, + tests: tests.valueTests + }); + } + }, + deltaxInputView: { + hook: "delta-x-container", + prepareView: function (el) { + return new InputView({ + parent: this, + required: true, + name: 'deltax', + modelKey: 'deltax', + valueType: 'number', + value: this.model.deltax, + tests: tests.valueTests + }); + } + }, + deltayInputView: { + hook: "delta-y-container", + prepareView: function (el) { + return new InputView({ + parent: this, + required: false, + name: 'deltay', + modelKey: 'deltay', + valueType: 'number', + value: this.model.deltay, + tests: tests.valueTests + }); + } + }, + deltazInputView: { + hook: "delta-z-container", + prepareView: function (el) { + return new InputView({ + parent: this, + required: false, + name: 'deltaz', + modelKey: 'deltaz', + valueType: 'number', + value: this.model.deltaz, + tests: tests.valueTests + }); + } + }, + radiusInputView: { + hook: "radius-container", + prepareView: function (el) { + return new InputView({ + parent: this, + required: true, + name: 'radius', + modelKey: 'radius', + valueType: 'number', + value: this.model.radius, + tests: tests.valueTests + }); + } + }, + cylinderLengthInputView: { + hook: "cylinder-length-container", + prepareView: function (el) { + return new InputView({ + parent: this, + required: true, + name: 'length', + modelKey: 'length', + valueType: 'number', + value: this.model.length, + tests: tests.valueTests + }); + } + }, + deltasInputView: { + hook: "delta-s-container", + prepareView: function (el) { + return new InputView({ + parent: this, + required: true, + name: 'deltas', + modelKey: 'deltas', + valueType: 'number', + value: this.model.deltas, + tests: tests.valueTests + }); + } + }, + deltarInputView: { + hook: "delta-r-container", + prepareView: function (el) { + return new InputView({ + parent: this, + required: false, + name: 'deltar', + modelKey: 'deltar', + valueType: 'number', + value: this.model.deltar, + tests: tests.valueTests + }); + } + } + } +}); diff --git a/client/domain-view/views/shapes-view.js b/client/domain-view/views/shapes-view.js new file mode 100644 index 0000000000..26aa414bc1 --- /dev/null +++ b/client/domain-view/views/shapes-view.js @@ -0,0 +1,101 @@ +/* +StochSS is a platform for simulating biochemical systems +Copyright (C) 2019-2022 StochSS developers. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +let $ = require('jquery'); +//support files +let app = require('../../app'); +let Tooltips = require('../../tooltips'); +//views +let View = require('ampersand-view'); +let EditShapesView = require('./shape-view'); +//templates +let template = require('../templates/shapesView.pug'); + +module.exports = View.extend({ + template: template, + events: { + 'click [data-hook=collapse]' : 'changeCollapseButtonText', + 'click [data-hook=cartesian-lattice]' : 'addShape', + 'click [data-hook=spherical-lattice]' : 'addShape', + 'click [data-hook=cylindrical-lattice]' : 'addShape' + }, + initialize: function (attrs, options) { + View.prototype.initialize.apply(this, arguments); + this.readOnly = attrs.readOnly ? attrs.readOnly : false; + this.tooltips = Tooltips.domainGeometry; + }, + render: function (attrs, options) { + View.prototype.render.apply(this, arguments); + if(this.readOnly) { + $(this.queryByHook('shapes-edit-tab')).addClass("disabled"); + $(".nav .disabled>a").on("click", function(e) { + e.preventDefault(); + return false; + }); + $(this.queryByHook('shapes-view-tab')).tab('show'); + $(this.queryByHook('edit-shapes')).removeClass('active'); + $(this.queryByHook('view-shapes')).addClass('active'); + }else{ + this.renderEditShapesView(); + this.collection.on("update-inuse", this.updateInUse, this); + this.collection.parent.trigger('update-shape-deps'); + } + this.renderViewShapesView(); + }, + addShape: function (e) { + let type = e.target.dataset.name; + let name = this.collection.addShape(type); + this.collection.parent.actions.trigger('update-shape-options', {currName: name}); + }, + changeCollapseButtonText: function (e) { + app.changeCollapseButtonText(this, e); + }, + renderEditShapesView: function ({key=null, attr=null}={}) { + if(this.editShapesView) { + this.editShapesView.remove(); + } + let options = {filter: (model) => { return model.contains(attr, key); }} + this.editShapesView = this.renderCollection( + this.collection, + EditShapesView, + this.queryByHook('edit-shapes-list'), + options + ); + }, + renderViewShapesView: function ({key=null, attr=null}={}) { + if(this.viewShapesView) { + this.viewShapesView.remove(); + } + let options = { + viewOptions: {viewMode: true}, + filter: (model) => { return model.contains(attr, key); } + } + this.viewShapesView = this.renderCollection( + this.collection, + EditShapesView, + this.queryByHook('view-shapes-list'), + options + ); + }, + updateInUse: function ({deps=null}={}) { + if(deps === null) { return; } + this.collection.forEach((shape) => { + shape.inUse = deps.includes(shape.name); + }); + } +}); diff --git a/client/domain-view/views/transformation-view.js b/client/domain-view/views/transformation-view.js new file mode 100644 index 0000000000..bff8ad6e91 --- /dev/null +++ b/client/domain-view/views/transformation-view.js @@ -0,0 +1,550 @@ +/* +StochSS is a platform for simulating biochemical systems +Copyright (C) 2019-2022 StochSS developers. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +let $ = require('jquery'); +let _ = require('underscore'); +//support files +let app = require('../../app'); +let tests = require('../../views/tests'); +//views +let View = require('ampersand-view'); +let InputView = require('../../views/input'); +let SelectView = require('ampersand-select-view'); +//templates +let editTemplate = require('../templates/editTransformation.pug'); +let viewTemplate = require('../templates/viewTransformation.pug'); + +module.exports = View.extend({ + bindings: { + 'model.selected' : { + type: function (el, value, previousValue) { + el.checked = value; + }, + hook: 'select-transformation' + }, + 'model.inUse': { + hook: 'remove', + type: 'booleanAttribute', + name: 'disabled', + } + }, + events: { + 'change [data-hook=input-name-container]' : 'updateDepsOptions', + 'change [data-hook=select-type-container]' : 'selectTransformationType', + 'change [data-hook=transformation-select-container]' : 'selectNestedTransformation', + 'change [data-target=vector-point1]' : 'setVectorPoint1', + 'change [data-target=vector-point2]' : 'setVectorPoint2', + 'change [data-target=reflect-normal]' : 'setReflectNormal', + 'change [data-target=reflect-point1]' : 'setReflectPoint1', + 'change [data-target=reflect-point2]' : 'setReflectPoint2', + 'change [data-target=reflect-point3]' : 'setReflectPoint3', + 'change [data-target=scale-center]' : 'setCenter', + 'click [data-hook=select-transformation]' : 'selectTransformation', + 'click [data-hook=remove]' : 'removeTransformation', + 'click [data-hook=collapsePointNormal]' : 'togglePointNormal', + 'click [data-hook=collapseThreePoint]' : 'toggleThreePoint' + }, + initialize: function (attrs, options) { + View.prototype.initialize.apply(this, arguments); + this.viewMode = attrs.viewMode ? attrs.viewMode : false; + this.chevrons = { + hide: ` + + + + `, + show: ` + + + + ` + } + }, + render: function () { + this.template = this.viewMode ? viewTemplate : editTemplate; + View.prototype.render.apply(this, arguments); + this.details = { + 'Translate Transformation': [ + $(this.queryByHook('vector-transformation-props')) + ], + 'Rotate Transformation': [ + $(this.queryByHook('vector-transformation-props')), + $(this.queryByHook('rotation-angle-header')), + $(this.queryByHook('rotation-angle-prop')) + ], + 'Reflect Transformation': [ + $(this.queryByHook('reflect-transformation-props')) + ], + 'Scale Transformation': [ + $(this.queryByHook('scale-transformation-props')) + ] + } + app.documentSetup(); + if(this.viewMode){ + if(this.model.point2.inUse || this.model.point3.inUse) { + $(this.queryByHook('view-point2-header')).css('display', 'inline-block'); + $(this.queryByHook('view-point3-header')).css('display', 'inline-block'); + $(this.queryByHook('view-reflect-point2')).css('display', 'inline-block'); + $(this.queryByHook('view-reflect-point3')).css('display', 'inline-block'); + }else{ + $(this.queryByHook('view-normal-header')).css('display', 'inline-block'); + $(this.queryByHook('view-reflect-normal')).css('display', 'inline-block'); + } + }else{ + if(this.model.selected) { + setTimeout(_.bind(this.openDetails, this), 1); + } + this.model.on('change', _.bind(this.updateViewer, this)); + this.renderTransformationSelectView(); + } + this.displayDetails(); + }, + displayDetails: function () { + this.details[this.model.type].forEach((element) => { + element.css('display', 'block'); + }); + }, + hideDetails: function () { + this.details[this.model.type].forEach((element) => { + element.css('display', 'none'); + }); + }, + openDetails: function () { + $("#collapse-transformation-details" + this.model.cid).collapse("show"); + }, + removeTransformation: function () { + let name = this.model.name; + let collection = this.model.collection; + let actions = this.model.collection.parent.actions; + collection.removeTransformation(this.model); + collection.trigger('update-transformation-options', {currName: name}); + actions.trigger('update-transformation-options', {currName: name}); + }, + renderTransformationSelectView: function () { + if(this.transformationSelectView) { + this.transformationSelectView.remove(); + } + let options = []; + this.model.collection.parent.transformations.forEach((transformation) => { + if(transformation.name !== this.model.name) { + options.push(transformation.name); + } + }); + this.transformationSelectView = new SelectView({ + name: 'transformation', + required: false, + eagerValidate: false, + options: options, + value: this.model.transformation, + unselectedText: '-- Select a Transformation --' + }); + let hook = "transformation-select-container"; + app.registerRenderSubview(this, this.transformationSelectView, hook); + }, + selectNestedTransformation: function (e) { + this.model.transformation = e.target.value; + this.model.collection.parent.trigger('update-transformation-deps'); + }, + selectTransformation: function () { + this.model.selected = !this.model.selected; + }, + selectTransformationType: function (e) { + this.hideDetails(); + this.model.type = e.target.value; + this.displayDetails(); + }, + setCenter: function (e) { + let key = e.target.parentElement.parentElement.dataset.name; + this.model.center[key] = Number(e.target.value); + this.model.trigger('change'); + }, + setReflectNormal: function (e) { + let key = e.target.parentElement.parentElement.dataset.name; + this.model.normal[key] = Number(e.target.value); + this.model.trigger('change'); + }, + setReflectPoint1: function (e) { + let key = e.target.parentElement.parentElement.dataset.name; + this.model.point1[key] = Number(e.target.value); + this.model.trigger('change'); + }, + setReflectPoint2: function (e) { + let key = e.target.parentElement.parentElement.dataset.name; + this.model.point2[key] = Number(e.target.value); + this.model.trigger('change'); + }, + setReflectPoint3: function (e) { + let key = e.target.parentElement.parentElement.dataset.name; + this.model.point3[key] = Number(e.target.value); + this.model.trigger('change'); + }, + setVectorPoint1: function (e) { + let key = e.target.parentElement.parentElement.dataset.name; + this.model.vector[0][key] = Number(e.target.value); + this.model.trigger('change'); + }, + setVectorPoint2: function (e) { + let key = e.target.parentElement.parentElement.dataset.name; + this.model.vector[1][key] = Number(e.target.value); + this.model.trigger('change'); + }, + togglePointNormal: function () { + let classes = $(this.queryByHook('collapsePointNormal')).attr("class").split(/\s+/); + $(this.queryByHook('three-point-chevron')).html(this.chevrons.hide); + if(classes.includes('collapsed')) { + $(this.queryByHook('point-normal-chevron')).html(this.chevrons.show); + }else{ + $(this.queryByHook('point-normal-chevron')).html(this.chevrons.hide); + } + }, + toggleThreePoint: function () { + let classes = $(this.queryByHook('collapseThreePoint')).attr("class").split(/\s+/); + $(this.queryByHook('point-normal-chevron')).html(this.chevrons.hide); + if(classes.includes('collapsed')) { + $(this.queryByHook('three-point-chevron')).html(this.chevrons.show); + }else{ + $(this.queryByHook('three-point-chevron')).html(this.chevrons.hide); + } + }, + update: function () {}, + updateDepsOptions: function (e) { + let name = this.model.name; + this.model.name = e.target.value; + this.model.collection.parent.transformations.trigger( + 'update-transformation-options', {currName: name, newName: this.model.name} + ); + this.model.collection.parent.actions.trigger( + 'update-transformation-options', {currName: name, newName: this.model.name} + ); + }, + updateValid: function () {}, + updateViewer: function () { + this.parent.renderViewTransformationsView(); + }, + subviews: { + inputNameView: { + hook: 'input-name-container', + prepareView: function (el) { + return new InputView({ + parent: this, + required: true, + name: 'name', + tests: tests.nameTests, + valueType: 'string', + value: this.model.name + }); + } + }, + selectTypeView: { + hook: 'select-type-container', + prepareView: function (el) { + let options = [ + 'Translate Transformation', + 'Rotate Transformation', + 'Reflect Transformation', + 'Scale Transformation' + ]; + return new SelectView({ + name: 'type', + required: true, + idAttributes: 'cid', + options: options, + value: this.model.type + }); + } + }, + vectorPoint1XInputView: { + hook: 'vector-point1-x-container', + prepareView: function (el) { + return new InputView({ + parent: this, + name: 'vector-point1-x', + tests: [tests.nanValue], + valueType: 'number', + value: this.model.vector[0].x + }); + } + }, + vectorPoint1YInputView: { + hook: 'vector-point1-y-container', + prepareView: function (el) { + return new InputView({ + parent: this, + name: 'vector-point1-x', + tests: [tests.nanValue], + valueType: 'number', + value: this.model.vector[0].y + }); + } + }, + vectorPoint1ZInputView: { + hook: 'vector-point1-z-container', + prepareView: function (el) { + return new InputView({ + parent: this, + name: 'vector-point1-z', + tests: [tests.nanValue], + valueType: 'number', + value: this.model.vector[0].z + }); + } + }, + vectorPoint2XInputView: { + hook: 'vector-point2-x-container', + prepareView: function (el) { + return new InputView({ + parent: this, + name: 'vector-point2-x', + tests: [tests.nanValue], + valueType: 'number', + value: this.model.vector[1].x + }); + } + }, + vectorPoint2YInputView: { + hook: 'vector-point2-y-container', + prepareView: function (el) { + return new InputView({ + parent: this, + name: 'vector-point2-y', + tests: [tests.nanValue], + valueType: 'number', + value: this.model.vector[1].y + }); + } + }, + vectorPoint2ZInputView: { + hook: 'vector-point2-z-container', + prepareView: function (el) { + return new InputView({ + parent: this, + name: 'vector-point2-z', + tests: [tests.nanValue], + valueType: 'number', + value: this.model.vector[1].z + }); + } + }, + rotationAngleInputView: { + hook: 'rotation-angle-container', + prepareView: function (el) { + return new InputView({ + parent: this, + name: 'angle', + modelKey: 'angle', + tests: [tests.nanValue], + valueType: 'number', + value: this.model.angle + }); + } + }, + scalingFactorInputView: { + hook: 'scale-factor-container', + prepareView: function (el) { + return new InputView({ + parent: this, + name: 'factor', + modelKey: 'factor', + tests: [tests.nanValue], + valueType: 'number', + value: this.model.factor + }); + } + }, + centerXInputView: { + hook: 'center-x-container', + prepareView: function (el) { + return new InputView({ + parent: this, + name: 'center-x', + tests: [tests.nanValue], + valueType: 'number', + value: this.model.center.x + }); + } + }, + centerYInputView: { + hook: 'center-y-container', + prepareView: function (el) { + return new InputView({ + parent: this, + name: 'center-y', + tests: [tests.nanValue], + valueType: 'number', + value: this.model.center.y + }); + } + }, + centerZInputView: { + hook: 'center-z-container', + prepareView: function (el) { + return new InputView({ + parent: this, + name: 'center-z', + tests: [tests.nanValue], + valueType: 'number', + value: this.model.center.z + }); + } + }, + normalXInputView: { + hook: 'normal-x-container', + prepareView: function (el) { + return new InputView({ + parent: this, + name: 'normal-x', + tests: [tests.nanValue], + valueType: 'number', + value: this.model.normal.x + }); + } + }, + normalYInputView: { + hook: 'normal-y-container', + prepareView: function (el) { + return new InputView({ + parent: this, + name: 'normal-y', + tests: [tests.nanValue], + valueType: 'number', + value: this.model.normal.y + }); + } + }, + normalZInputView: { + hook: 'normal-z-container', + prepareView: function (el) { + return new InputView({ + parent: this, + name: 'normal-z', + tests: [tests.nanValue], + valueType: 'number', + value: this.model.normal.z + }); + } + }, + point1XInputView: { + hook: 'point1-x-container', + prepareView: function (el) { + return new InputView({ + parent: this, + name: 'point1-x', + tests: [tests.nanValue], + valueType: 'number', + value: this.model.point1.x + }); + } + }, + point1YInputView: { + hook: 'point1-y-container', + prepareView: function (el) { + return new InputView({ + parent: this, + name: 'point1-y', + tests: [tests.nanValue], + valueType: 'number', + value: this.model.point1.y + }); + } + }, + point1ZInputView: { + hook: 'point1-z-container', + prepareView: function (el) { + return new InputView({ + parent: this, + name: 'point1-z', + tests: [tests.nanValue], + valueType: 'number', + value: this.model.point1.z + }); + } + }, + point2XInputView: { + hook: 'point2-x-container', + prepareView: function (el) { + return new InputView({ + parent: this, + name: 'point2-x', + tests: [tests.nanValue], + valueType: 'number', + value: this.model.point2.x + }); + } + }, + point2YInputView: { + hook: 'point2-y-container', + prepareView: function (el) { + return new InputView({ + parent: this, + name: 'point2-y', + tests: [tests.nanValue], + valueType: 'number', + value: this.model.point2.y + }); + } + }, + point2ZInputView: { + hook: 'point2-z-container', + prepareView: function (el) { + return new InputView({ + parent: this, + name: 'point2-z', + tests: [tests.nanValue], + valueType: 'number', + value: this.model.point2.z + }); + } + }, + point3XInputView: { + hook: 'point3-x-container', + prepareView: function (el) { + return new InputView({ + parent: this, + name: 'point3-x', + tests: [tests.nanValue], + valueType: 'number', + value: this.model.point3.x + }); + } + }, + point3YInputView: { + hook: 'point3-y-container', + prepareView: function (el) { + return new InputView({ + parent: this, + name: 'point3-y', + tests: [tests.nanValue], + valueType: 'number', + value: this.model.point3.y + }); + } + }, + point3ZInputView: { + hook: 'point3-z-container', + prepareView: function (el) { + return new InputView({ + parent: this, + name: 'point3-z', + tests: [tests.nanValue], + valueType: 'number', + value: this.model.point3.z + }); + } + } + } +}); \ No newline at end of file diff --git a/client/domain-view/views/transformations-view.js b/client/domain-view/views/transformations-view.js new file mode 100644 index 0000000000..09206e4575 --- /dev/null +++ b/client/domain-view/views/transformations-view.js @@ -0,0 +1,115 @@ +/* +StochSS is a platform for simulating biochemical systems +Copyright (C) 2019-2022 StochSS developers. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +let $ = require('jquery'); +//support files +let app = require('../../app'); +let Tooltips = require('../../tooltips'); +//views +let View = require('ampersand-view'); +let EditTransformationView = require('./transformation-view'); +//templates +let template = require('../templates/transformationsView.pug'); + +module.exports = View.extend({ + template: template, + events: { + 'click [data-hook=collapse]' : 'changeCollapseButtonText', + 'click [data-hook=translate-transformation]' : 'addTransformation', + 'click [data-hook=rotate-transformation]' : 'addTransformation', + 'click [data-hook=reflect-transformation]' : 'addTransformation', + 'click [data-hook=scale-transformation]' : 'addTransformation' + }, + initialize: function (attrs, options) { + View.prototype.initialize.apply(this, arguments); + this.readOnly = attrs.readOnly ? attrs.readOnly : false; + this.tooltips = Tooltips.domainTransformation; + }, + render: function (attrs, options) { + View.prototype.render.apply(this, arguments); + if(this.readOnly) { + $(this.queryByHook('transformations-edit-tab')).addClass("disabled"); + $(".nav .disabled>a").on("click", function(e) { + e.preventDefault(); + return false; + }); + $(this.queryByHook('transformations-view-tab')).tab('show'); + $(this.queryByHook('edit-transformations')).removeClass('active'); + $(this.queryByHook('view-transformations')).addClass('active'); + }else{ + this.renderEditTransformationsView(); + this.collection.on('update-transformation-options', this.updateTransformationOptions, this); + this.collection.on('update-inuse', this.updateInUse, this); + this.collection.parent.trigger('update-transformation-deps'); + } + this.renderViewTransformationsView(); + }, + changeCollapseButtonText: function (e) { + app.changeCollapseButtonText(this, e); + }, + addTransformation: function (e) { + let type = e.target.dataset.name; + let name = this.collection.addTransformation(type); + this.collection.trigger('update-transformation-options', {currName: name}); + this.collection.parent.actions.trigger('update-transformation-options', {currName: name}); + }, + renderEditTransformationsView: function ({key=null, attr=null}={}) { + if(this.editTransformationsView) { + this.editTransformationsView.remove(); + } + let options = {filter: (model) => { return model.contains(attr, key); }} + this.editTransformationsView = this.renderCollection( + this.collection, + EditTransformationView, + this.queryByHook('edit-transformations-list'), + options + ); + }, + renderViewTransformationsView: function ({key=null, attr=null}={}) { + if(this.viewTransformationsView) { + this.viewTransformationsView.remove(); + } + let options = { + viewOptions: {viewMode: true}, + filter: (model) => { return model.contains(attr, key); } + } + this.viewTransformationsView = this.renderCollection( + this.collection, + EditTransformationView, + this.queryByHook('view-transformations-list'), + options + ); + }, + updateInUse: function ({deps=null}={}) { + if(deps === null) { return; } + this.collection.forEach((transformation) => { + transformation.inUse = deps.includes(transformation.name); + }); + }, + updateTransformationOptions: function ({currName=null, newName=null}={}) { + if(currName === null && newName === null) { return; } + this.editTransformationsView.views.forEach((view) => { + if(view.model.transformation === currName && newName !== null) { + view.model.transformation = newName + } + if((newName === null && view.model.name !== currName) || view.model.name !== newName) { + view.renderTransformationSelectView(); + } + }); + } +}); \ No newline at end of file diff --git a/client/domain-view/views/type-view.js b/client/domain-view/views/type-view.js index ea2ac60f3d..789b16f060 100644 --- a/client/domain-view/views/type-view.js +++ b/client/domain-view/views/type-view.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -37,18 +37,19 @@ module.exports = View.extend({ el.checked = value; }, hook: 'select' + }, + 'model.inUse': { + hook: 'remove', + type: 'booleanAttribute', + name: 'disabled', } }, events: { - 'change [data-hook=type-name]' : 'handleRenameType', - 'change [data-target=type-defaults]' : 'updateView', + 'change [data-hook=type-name]' : 'updateDepsOptions', + 'change [data-target=type-defaults]' : 'updateViewer', 'change [data-hook=td-fixed]' : 'setTDFixed', - 'change [data-hook=type-geometry]' : 'updateView', 'click [data-hook=select]' : 'selectType', - 'click [data-hook=unassign-all]' : 'handleUnassignParticles', - 'click [data-hook=delete-type]' : 'handleDeleteType', - 'click [data-hook=delete-all]' : 'handleDeleteTypeAndParticle', - 'click [data-hook=apply-geometry]' : 'handleApplyGeometry' + 'click [data-hook=remove]' : 'handleDeleteType', }, initialize: function (attrs, options) { View.prototype.initialize.apply(this, arguments); @@ -66,66 +67,13 @@ module.exports = View.extend({ $(this.queryByHook('view-td-fixed')).prop('checked', this.model.fixed) app.documentSetup(); }, - completeAction: function () { - $(this.queryByHook(`tg-in-progress-${this.model.typeID}`)).css("display", "none"); - $(this.queryByHook(`tg-complete-${this.model.typeID}`)).css("display", "inline-block"); - setTimeout(() => { - $(this.queryByHook(`tg-complete-${this.model.typeID}`)).css("display", "none"); - }, 5000); - }, - errorAction: function (action) { - $(this.queryByHook(`tg-in-progress-${this.model.typeID}`)).css("display", "none"); - $(this.queryByHook(`tg-action-error-${this.model.typeID}`)).html(action); - $(this.queryByHook(`tg-error-${this.model.typeID}`)).css("display", "block"); - }, - handleApplyGeometry: function (e) { - this.startAction(); - let domain = this.model.collection.parent; - let particles = domain.particles.toJSON(); - let center = [ - (domain.x_lim[1] + domain.x_lim[0]) / 2, - (domain.y_lim[1] + domain.y_lim[0]) / 2, - (domain.z_lim[1] + domain.z_lim[0]) / 2 - ]; - let data = {particles: particles, type: this.model.toJSON(), center: center} - let endpoint = path.join(app.getApiPath(), 'spatial-model/apply-geometry'); - app.postXHR(endpoint, data, { - success: (err, response, body) => { - this.parent.parent.applyGeometry(body.particles, this.model); - this.completeAction(); - }, - error: (err, response, body) => { - if(body.Traceback.includes("SyntaxError")) { - var tracePart = body.Traceback.split('\n').slice(6) - tracePart.splice(2, 2) - tracePart[1] = tracePart[1].replace(new RegExp(' ', 'g'), ' ') - var errorBlock = `

${body.Message}
${tracePart.join('
')}

` - }else{ - var errorBlock = body.Message - } - this.errorAction(errorBlock); - } - }); - }, handleDeleteType: function (e) { - let type = Number(e.target.dataset.type); - this.model.collection.removeType(this.model); - this.parent.parent.deleteType(this.model.typeID); - }, - handleDeleteTypeAndParticle: function (e) { - let type = Number(e.target.dataset.type); + let typeID = this.model.typeID; + let domain = this.model.collection.parent; + let actions = domain.actions; this.model.collection.removeType(this.model); - this.parent.parent.deleteType(this.model.typeID, {unassign: false}); - }, - handleRenameType: function (e) { - this.updateView(); - let type = Number(e.target.parentElement.parentElement.dataset.target); - let name = e.target.value; - this.parent.parent.renameType(type, name); - }, - handleUnassignParticles: function () { - this.model.numParticles = 0; - this.parent.parent.unassignAllParticles(this.model.typeID); + domain.trigger('update-particle-type-options', {currName: typeID}); + actions.trigger('update-type-options', {currName: typeID}); }, openTypeDetails: function () { $("#collapse-type-details" + this.model.typeID).collapse("show"); @@ -135,28 +83,32 @@ module.exports = View.extend({ }, setTDFixed: function (e) { this.model.fixed = e.target.checked; - this.updateView(); - }, - startAction: function () { - $(this.queryByHook(`tg-complete-${this.model.typeID}`)).css("display", "none"); - $(this.queryByHook(`tg-error-${this.model.typeID}`)).css("display", "none"); - $(this.queryByHook(`tg-in-progress-${this.model.typeID}`)).css("display", "inline-block"); + this.updateViewer(); }, update: function () {}, + updateDepsOptions: function (e) { + let typeID = this.model.typeID; + this.model.name = e.target.value; + this.updateViewer(); + this.model.collection.parent.trigger( + 'update-particle-type-options', {currName: typeID, newName: this.model.typeID} + ); + this.model.collection.parent.actions.trigger( + 'update-type-options', {currName: typeID, newName: this.model.typeID} + ); + }, updateValid: function () {}, - updateView: function () { + updateViewer: function () { this.parent.renderViewTypeView(); - this.parent.parent.updateParticleViews({includeGeometry: true}); }, subviews: { - inputTypeID: { + inputName: { hook: "type-name", prepareView: function (el) { return new InputView({ parent: this, required: true, name: 'name', - modelKey: 'name', tests: [tests.invalidChar], valueType: 'string', value: this.model.name @@ -232,20 +184,6 @@ module.exports = View.extend({ value: this.model.c }); } - }, - inputGeometry: { - hook: 'type-geometry', - prepareView: function (el) { - return new InputView({ - parent: this, - required: false, - name: 'geometry', - modelKey: 'geometry', - valueType: 'string', - value: this.model.geometry, - placeholder: "--Expression in terms of 'x', 'y', 'z'--" - }); - } } } }); \ No newline at end of file diff --git a/client/domain-view/views/types-description-view.js b/client/domain-view/views/types-description-view.js deleted file mode 100644 index 2279ff68ee..0000000000 --- a/client/domain-view/views/types-description-view.js +++ /dev/null @@ -1,128 +0,0 @@ -/* -StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -*/ - -let $ = require('jquery'); -let path = require('path'); -//support files -let app = require('../../app'); -//views -let View = require('ampersand-view'); -let SelectView = require('ampersand-select-view'); -//templates -let template = require('../templates/typesDescriptionView.pug'); - -module.exports = View.extend({ - template: template, - events: { - 'change [data-hook=file-select]' : 'selectTypeFile', - 'change [data-hook=file-location-select]' : 'selectFileLocation', - 'click [data-hook=set-particle-types-btn]' : 'getTypesFromFile' - }, - initialize: function (attrs, options) { - View.prototype.initialize.apply(this, arguments); - this.typeFile = null; - }, - render: function(attrs, options) { - View.prototype.render.apply(this, arguments); - let endpoint = path.join(app.getApiPath(), 'spatial-model/types-list'); - app.getXHR(endpoint, {success: (err, response, body) => { - this.typeFiles = body.files; - this.fileLocations = body.paths; - this.renderFileSelectView(); - }}); - }, - completeAction: function () { - $(this.queryByHook('st-in-progress')).css('display', 'none'); - $(this.queryByHook('st-complete')).css('display', 'inline-block'); - }, - errorAction: function (action) { - $(this.queryByHook('st-in-progress')).css('display', 'none'); - $(this.queryByHook('st-action-error')).text(action); - $(this.queryByHook('st-error')).css('display', 'block'); - }, - getTypesFromFile: function () { - this.startAction(); - let queryStr = `?path=${this.typeFile}`; - let endpoint = path.join(app.getApiPath(), 'spatial-model/particle-types') + queryStr; - app.getXHR(endpoint, { - success: (err, response, body) => { - this.parent.setParticleTypes(body.names, body.types); - this.completeAction(); - }, - error: (err, response, body) => { - this.errorAction(body.Message); - } - }); - }, - renderFileLocationSelectView: function (options) { - if(this.fileLocationSelectView) { - this.fileLocationSelectView.remove(); - } - this.fileLocationSelectView = new SelectView({ - name: 'type-locations', - required: false, - idAttributes: 'cid', - options: options, - unselectedText: "-- Select Location --" - }); - app.registerRenderSubview(this, this.fileLocationSelectView, "file-location-select"); - }, - renderFileSelectView: function () { - if(this.fileSelectView) { - this.fileSelectView.remove(); - } - this.fileSelectView = new SelectView({ - name: 'type-files', - required: false, - idAttributes: 'cid', - options: this.typeFiles, - unselectedText: "-- Select Type File --", - }); - app.registerRenderSubview(this, this.fileSelectView, "file-select"); - }, - selectFileLocation: function (e) { - let value = e.target.value; - this.typeFile = value ? value : null; - $(this.queryByHook("set-particle-types-btn")).prop("disabled", this.typeFile === null); - }, - selectTypeFile: function (e) { - let value = e.target.value; - if(value) { - if(this.fileLocations[value].length > 1) { - $(this.queryByHook("type-location-message")).css('display', "block"); - $(this.queryByHook("file-location-container")).css("display", "inline-block"); - this.renderFileLocationSelectView(this.fileLocations[value]); - this.typeFile = null; - }else{ - $(this.queryByHook("type-location-message")).css('display', "none"); - $(this.queryByHook("file-location-container")).css("display", "none"); - this.typeFile = this.fileLocations[value][0]; - } - }else{ - $(this.queryByHook("type-location-message")).css('display', "none"); - $(this.queryByHook("file-location-container")).css("display", "none"); - this.typeFile = null; - } - $(this.queryByHook("set-particle-types-btn")).prop("disabled", this.typeFile === null); - }, - startAction: function () { - $(this.queryByHook("st-complete")).css('display', 'none'); - $(this.queryByHook("st-error")).css('display', 'none'); - $(this.queryByHook("st-in-progress")).css("display", "inline-block"); - }, -}); \ No newline at end of file diff --git a/client/domain-view/views/types-view.js b/client/domain-view/views/types-view.js index b5351ed9e9..fc60f115af 100644 --- a/client/domain-view/views/types-view.js +++ b/client/domain-view/views/types-view.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -48,8 +48,16 @@ module.exports = View.extend({ $(this.queryByHook('view-domain-types')).addClass('active'); }else{ this.renderEditTypeView(); + this.collection.on("update-inuse", this.updateInUse, this); + this.collection.parent.trigger('update-type-deps'); + this.toggleTypesCollectionError("ev"); + this.collection.on('add remove', () => { + this.toggleTypesCollectionError("ev"); + this.toggleTypesCollectionError("vv"); + }, this); } this.toggleDomainError(); + this.toggleTypesCollectionError("vv"); this.renderViewTypeView(); }, changeCollapseButtonText: function (e) { @@ -57,7 +65,15 @@ module.exports = View.extend({ }, handleAddDomainType: function () { let name = this.collection.addType(); - this.parent.addType(name); + this.collection.parent.trigger('update-particle-type-options', {currName: name}); + this.collection.parent.actions.trigger('update-type-options', {currName: name}); + }, + openSection: function () { + if(!$(this.queryByHook("domain-types-section")).hasClass("show")) { + let typesCollapseBtn = $(this.queryByHook("collapse-domain-types")); + typesCollapseBtn.click(); + typesCollapseBtn.html('-'); + } }, renderEditTypeView: function () { if(this.editTypeView) { @@ -93,13 +109,47 @@ module.exports = View.extend({ ); }, toggleDomainError: function () { - let errorMsg = $(this.queryByHook('domain-error')) - if(!this.collection.parent.valid) { + let errorMsg = $(this.queryByHook('domain-error')); + if(this.collection.models[0].numParticles > 0) { + errorMsg.addClass('component-invalid'); + errorMsg.removeClass('component-valid'); + }else{ + errorMsg.addClass('component-valid'); + errorMsg.removeClass('component-invalid'); + } + }, + toggleTypesCollectionError: function (viewCode) { + let errorMsg = $(this.queryByHook(`${viewCode}-types-collection-error`)); + if(this.collection.length == 1) { errorMsg.addClass('component-invalid'); errorMsg.removeClass('component-valid'); }else{ errorMsg.addClass('component-valid'); errorMsg.removeClass('component-invalid'); } + }, + updateInUse: function ({deps=null}={}) { + if(deps === null) { return; } + this.collection.forEach((type) => { + type.inUse = deps.includes(type.name); + }); + }, + updateParticleCounts: function (particles) { + let particleCounts = {}; + particles.forEach((particle) => { + if(particleCounts[particle.type]) { + particleCounts[particle.type] += 1; + }else{ + particleCounts[particle.type] = 1; + } + }); + this.collection.forEach((type) => { + type.numParticles = particleCounts[type.typeID] ? particleCounts[type.typeID] : 0; + }); + $(this.queryByHook('unassigned-type-count')).text(this.collection.models[0].numParticles); + this.renderEditTypeView(); + this.renderViewTypeView(); + this.collection.parent.updateValid(); + this.toggleDomainError(); } }); \ No newline at end of file diff --git a/client/domain-view/views/view-particle.js b/client/domain-view/views/view-particle.js index 68cd5c7d7a..6aa4a89830 100644 --- a/client/domain-view/views/view-particle.js +++ b/client/domain-view/views/view-particle.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -24,6 +24,6 @@ let template = require('../templates/viewParticle.pug'); module.exports = View.extend({ template: template, initialize: function (attrs, options) { - this.type = this.model.collection.parent.types.get(this.model.type, "typeID").name; + this.type = this.parent.model.types.get(this.model.type, "typeID").name; } }); \ No newline at end of file diff --git a/client/file-config.js b/client/file-config.js index b1541e2e74..d1db755112 100644 --- a/client/file-config.js +++ b/client/file-config.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/graphics.js b/client/graphics.js index 6739f854c9..aa0be73137 100644 --- a/client/graphics.js +++ b/client/graphics.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/job-view/job-view.js b/client/job-view/job-view.js index 5d4fd9955e..439a4684f4 100644 --- a/client/job-view/job-view.js +++ b/client/job-view/job-view.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/job-view/templates/spatialResultsView.pug b/client/job-view/templates/spatialResultsView.pug index d2c4aee411..98ef61d0ae 100644 --- a/client/job-view/templates/spatialResultsView.pug +++ b/client/job-view/templates/spatialResultsView.pug @@ -20,21 +20,47 @@ div#workflow-results.card div(id="title" data-hook="title") - div.inline.horizontal-space + div.mx-1.row.head.align-items-baseline - span.inline(for="target-of-interest") Target of Interest: + div.col-sm-2 - div.tooltip-icon(data-html="true" data-toggle="tooltip" title=this.tooltips.spatialTarget) + h6.inline Target of Interest - div.inline(id="target-of-interest" data-hook="target-of-interest-list") + div.tooltip-icon(data-html="true" data-toggle="tooltip" title=this.tooltips.spatialTarget) - div.inline.horizontal-space.hidden(data-hook="job-results-mode") + div.col-sm-4.hidden(data-hook="job-results-mode-header") + + h6.inline Mode - span.inline(for="target-of-interest") Mode: + div.tooltip-icon(data-html="true" data-toggle="tooltip" title=this.tooltips.mode) - div.tooltip-icon(data-html="true" data-toggle="tooltip" title=this.tooltips.mode) + div.col-sm-6.hidden(data-hook="spatial-trajectory-header") - div.inline(id="target-mode" data-hook="target-mode-list") + h6 + + div.inline.mr-2 Trajectory: + + div.inline(data-hook="trajectory-index-value")=this.trajectoryIndex + + div.mx-1.my-3.row + + div.col-sm-2 + + div.inline(id="target-of-interest" data-hook="target-of-interest-list") + + div.col-sm-4.hidden(data-hook="job-results-mode-container") + + div.inline(id="target-mode" data-hook="target-mode-list") + + div.col-sm-6.hidden(data-hook="spatial-trajectory-container") + + input.custom-range( + type="range" + min="1" + max=`${this.model.settings.simulationSettings.realizations}` + value=this.trajectoryIndex + data-hook="trajectory-index-slider" + ) div.card diff --git a/client/job-view/views/job-logs-view.js b/client/job-view/views/job-logs-view.js index 7facc18b4a..bc71947c04 100644 --- a/client/job-view/views/job-logs-view.js +++ b/client/job-view/views/job-logs-view.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/job-view/views/job-results-view.js b/client/job-view/views/job-results-view.js index 67676a7c2b..55f6328ac6 100644 --- a/client/job-view/views/job-results-view.js +++ b/client/job-view/views/job-results-view.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -42,6 +42,7 @@ module.exports = View.extend({ 'change [data-hook=yaxis]' : 'setYAxis', 'change [data-hook=target-of-interest-list]' : 'getPlotForTarget', 'change [data-hook=target-mode-list]' : 'getPlotForTargetMode', + 'change [data-hook=trajectory-index-slider]' : 'getPlotForTrajectory', 'change [data-hook=specie-of-interest-list]' : 'getPlotForSpecies', 'change [data-hook=feature-extraction-list]' : 'getPlotForFeatureExtractor', 'change [data-hook=ensemble-aggragator-list]' : 'getPlotForEnsembleAggragator', @@ -55,7 +56,8 @@ module.exports = View.extend({ 'click [data-target=download-plot-csv]' : 'handlePlotCSVClick', 'click [data-hook=convert-to-notebook]' : 'handleConvertToNotebookClick', 'click [data-hook=download-results-csv]' : 'handleFullCSVClick', - 'click [data-hook=job-presentation]' : 'handlePresentationClick' + 'click [data-hook=job-presentation]' : 'handlePresentationClick', + 'input [data-hook=trajectory-index-slider]' : 'viewTrajectoryIndex' }, initialize: function (attrs, options) { View.prototype.initialize.apply(this, arguments); @@ -65,6 +67,7 @@ module.exports = View.extend({ this.tooltips = Tooltips.jobResults; this.plots = {}; this.plotArgs = {}; + this.trajectoryIndex = 1; }, render: function (attrs, options) { let isEnsemble = this.model.settings.simulationSettings.realizations > 1 && @@ -113,6 +116,10 @@ module.exports = View.extend({ this.targetIndex = null; this.targetMode = "discrete"; this.renderTargetOfInterestView(); + if(this.model.settings.simulationSettings.realizations > 1) { + $(this.queryByHook("spatial-trajectory-header")).css("display", "inline-block"); + $(this.queryByHook("spatial-trajectory-container")).css("display", "inline-block"); + } $(this.queryByHook("spatial-plot-csv")).css('display', 'none'); } this.getPlot(type); @@ -218,7 +225,7 @@ module.exports = View.extend({ }else if(type === "spatial") { data['sim_type'] = "SpatialPy"; data['data_keys'] = { - target: this.spatialTarget, index: this.targetIndex, mode: this.targetMode + target: this.spatialTarget, index: this.targetIndex, mode: this.targetMode, trajectory: this.trajectoryIndex - 1 }; data['plt_key'] = type; }else { @@ -259,10 +266,12 @@ module.exports = View.extend({ this.targetIndex = null; } if(!["type", "v", "nu", "rho", "mass"].includes(this.spatialTarget)) { - $(this.queryByHook('job-results-mode')).css('display', 'inline-block'); + $(this.queryByHook('job-results-mode-header')).css('display', 'inline-block'); + $(this.queryByHook('job-results-mode-container')).css('display', 'inline-block'); this.renderTargetModeView(); }else{ - $(this.queryByHook('job-results-mode')).css('display', 'none'); + $(this.queryByHook('job-results-mode-header')).css('display', 'none'); + $(this.queryByHook('job-results-mode-container')).css('display', 'none'); } this.getPlot("spatial"); }, @@ -270,6 +279,10 @@ module.exports = View.extend({ this.targetMode = e.target.value; this.getPlot("spatial"); }, + getPlotForTrajectory: function (e) { + this.trajectoryIndex = Number(e.target.value); + this.getPlot('spatial'); + }, getPlotKey: function (type) { if(type === "psweep") { let realizations = this.model.settings.simulationSettings.realizations; @@ -579,6 +592,9 @@ module.exports = View.extend({ }, update: function () {}, updateValid: function () {}, + viewTrajectoryIndex: function (e) { + $(this.queryByHook("trajectory-index-value")).html(e.target.value); + }, subviews: { inputTitle: { hook: 'title', diff --git a/client/job-view/views/sweep-parameter-range-view.js b/client/job-view/views/sweep-parameter-range-view.js index 05d50e04ff..e2c1f25ed9 100644 --- a/client/job-view/views/sweep-parameter-range-view.js +++ b/client/job-view/views/sweep-parameter-range-view.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/modals.js b/client/modals.js index 0f711d6436..11f76f7034 100644 --- a/client/modals.js +++ b/client/modals.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/model-view/model-view.js b/client/model-view/model-view.js index d259bc279f..48ea47eba4 100644 --- a/client/model-view/model-view.js +++ b/client/model-view/model-view.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -43,6 +43,7 @@ let template = require('./modelView.pug'); module.exports = View.extend({ template: template, events: { + 'change [data-hook=model-filter]' : 'filterModel', 'change [data-hook=all-continuous]' : 'setDefaultMode', 'change [data-hook=all-discrete]' : 'setDefaultMode', 'change [data-hook=advanced]' : 'setDefaultMode', @@ -134,9 +135,25 @@ module.exports = View.extend({ this.setInitialDefaultMode(modal, "dynamic"); }); }, + filterModel: function (e) { + var key = e.target.value === "" ? null : e.target.value; + var attr = null; + if(key && key.includes(':')) { + let attrKey = key.split(':'); + attr = attrKey[0].toLowerCase().replace(/ /g, ''); + key = attrKey[1]; + } + this.renderSpeciesView({'key': key, 'attr': attr}); + this.renderInitialConditionsView({'key': key, 'attr': attr}); + this.renderParametersView({'key': key, 'attr': attr}); + this.renderReactionsView({'key': key, 'attr': attr}); + this.renderEventsView({'key': key, 'attr': attr}); + this.renderRulesView({'key': key, 'attr': attr}); + this.renderBoundaryConditionsView({'key': key, 'attr': attr}); + this.renderSbmlComponentView({'key': key, 'attr': attr}); + }, openAdvancedSection: function () { if(!$(this.queryByHook("me-advanced-section")).hasClass("show")) { - console.log("opening advanced section") let advCollapseBtn = $(this.queryByHook("collapse-mv-advanced-section")); advCollapseBtn.click(); advCollapseBtn.html('-'); @@ -157,7 +174,8 @@ module.exports = View.extend({ process: this.reactionsView, event: this.eventsView, rule: this.rulesView, - domain: this.domainViewer + domain: this.domainViewer, + initialCondition: this.initialConditionsView } if(["reaction", "process", "event"].includes(error.type)) { views[error.type].openSection(error); @@ -166,17 +184,23 @@ module.exports = View.extend({ } } }, - renderBoundaryConditionsView: function () { + renderBoundaryConditionsView: function ({key=null, attr=null}={}) { if(!this.model.is_spatial) { return }; + let opened = $(this.queryByHook("boundary-conditions-container")).hasClass("show") if(this.boundaryConditionsView) { this.boundaryConditionsView.remove(); } this.boundaryConditionsView = new BoundaryConditionsView({ collection: this.model.boundaryConditions, - readOnly: this.readOnly + readOnly: this.readOnly, + attr: attr, + key: key }); let hook = "boundary-conditions-view-container"; app.registerRenderSubview(this, this.boundaryConditionsView, hook); + if(opened) { + this.boundaryConditionsView.openSection(); + } }, renderDomainViewer: function (domainPath=null) { if(!this.model.is_spatial) { return }; @@ -192,77 +216,113 @@ module.exports = View.extend({ }); app.registerRenderSubview(this, this.domainViewer, 'domain-viewer-container'); }, - renderEventsView: function () { + renderEventsView: function ({key=null, attr=null}={}) { if(this.model.is_spatial) { return }; + let opened = $(this.queryByHook("events")).hasClass("show"); if(this.eventsView) { this.eventsView.remove(); } this.eventsView = new EventsView({ collection: this.model.eventsCollection, - readOnly: this.readOnly + readOnly: this.readOnly, + attr: attr, + key: key }); let hook = "events-view-container"; app.registerRenderSubview(this, this.eventsView, hook); + if(opened) { + this.eventsView.openSection(); + } }, - renderInitialConditionsView: function () { + renderInitialConditionsView: function ({key=null, attr=null}={}) { if(!this.model.is_spatial) { return }; + let opened = $(this.queryByHook("initial-conditions")).hasClass("show") if(this.initialConditionsView) { this.initialConditionsView.remove(); } this.initialConditionsView = new InitialConditionsView({ collection: this.model.initialConditions, - readOnly: this.readOnly + readOnly: this.readOnly, + attr: attr, + key: key }); let hook = "initial-conditions-view-container"; app.registerRenderSubview(this, this.initialConditionsView, hook); + if(opened) { + this.initialConditionsView.openSection(); + } }, - renderParametersView: function () { + renderParametersView: function ({key=null, attr=null}={}) { + let opened = $(this.queryByHook("parameters-list-container")).hasClass("show"); if(this.parametersView) { this.parametersView.remove(); } this.parametersView = new ParametersView({ collection: this.model.parameters, - readOnly: this.readOnly + readOnly: this.readOnly, + attr: attr, + key: key }); let hook = "parameters-view-container"; app.registerRenderSubview(this, this.parametersView, hook); + if(opened) { + this.parametersView.openSection(); + } }, - renderReactionsView: function () { + renderReactionsView: function ({key=null, attr=null}={}) { + let opened = $(this.queryByHook("reactions-list-container")).hasClass("show"); if(this.reactionsView) { this.reactionsView.remove(); } this.reactionsView = new ReactionsView({ collection: this.model.reactions, - readOnly: this.readOnly + readOnly: this.readOnly, + attr: attr, + key: key }); let hook = "reactions-view-container"; app.registerRenderSubview(this, this.reactionsView, hook); + if(opened) { + this.reactionsView.openSection(); + } }, - renderRulesView: function () { + renderRulesView: function ({key=null, attr=null}={}) { if(this.model.is_spatial) { return }; + let opened = $(this.queryByHook("rules-list-container")).hasClass("show"); if(this.rulesView) { this.rulesView.remove(); } this.rulesView = new RulesView({ collection: this.model.rules, - readOnly: this.readOnly + readOnly: this.readOnly, + attr: attr, + key: key }); let hook = "rules-view-container"; app.registerRenderSubview(this, this.rulesView, hook); + if(opened) { + this.rulesView.openSection(); + } }, - renderSbmlComponentView: function () { + renderSbmlComponentView: function ({key=null, attr=null}={}) { if(this.model.is_spatial || !this.model.functionDefinitions.length) { return }; + let opened = $(this.queryByHook("function-definitions-list-container")).hasClass("show") if(this.sbmlComponentView) { this.sbmlComponentView.remove(); } this.sbmlComponentView = new SBMLComponentsView({ functionDefinitions: this.model.functionDefinitions, - readOnly: this.readOnly + readOnly: this.readOnly, + attr: attr, + key: key }); let hook = "sbml-components-view-container"; app.registerRenderSubview(this, this.sbmlComponentView, hook); + if(opened) { + this.sbmlComponentView.openSection(); + } }, - renderSpeciesView: function () { + renderSpeciesView: function ({key=null, attr=null}={}) { if(this.speciesView) { this.speciesView.remove(); } @@ -270,7 +330,9 @@ module.exports = View.extend({ collection: this.model.species, spatial: this.model.is_spatial, defaultMode: this.model.defaultMode, - readOnly: this.readOnly + readOnly: this.readOnly, + attr: attr, + key: key }); let hook = "species-view-container"; app.registerRenderSubview(this, this.speciesView, hook); @@ -410,5 +472,18 @@ module.exports = View.extend({ updateValid: function () {}, updateVolumeViewer: function (e) { $(this.queryByHook("view-volume")).html("Volume: " + this.model.volume); + }, + subviews: { + filter: { + hook: 'model-filter', + prepareView: function (el) { + return new InputView({ + parent: this, + required: false, + name: 'filter', + valueType: 'string' + }); + } + } } }); \ No newline at end of file diff --git a/client/model-view/modelView.pug b/client/model-view/modelView.pug index 8fea01cd20..d03bbc6b46 100644 --- a/client/model-view/modelView.pug +++ b/client/model-view/modelView.pug @@ -1,5 +1,11 @@ div#model-view + div + + h5.inline.mr-2 Filter: + + div.inline(data-hook="model-filter") + div.card div.card-header.pb-0 diff --git a/client/model-view/templates/boundaryConditionsView.pug b/client/model-view/templates/boundaryConditionsView.pug index bc2792c802..d0eb9e5eaf 100644 --- a/client/model-view/templates/boundaryConditionsView.pug +++ b/client/model-view/templates/boundaryConditionsView.pug @@ -25,7 +25,7 @@ div#boundary-conditions.card p.mb-0 | Set spatial regions of the domain where a property of particles are held constant. - div.collapse.tab-content(id="collapse-boundary-conditions") + div.collapse.tab-content(id="collapse-boundary-conditions" data-hook="boundary-conditions-container") div.tab-pane.active(id="edit-boundary-conditions" data-hook="edit-boundary-conditions") diff --git a/client/model-view/templates/domainViewer.pug b/client/model-view/templates/domainViewer.pug index 26866a60f5..eef5c604cb 100644 --- a/client/model-view/templates/domainViewer.pug +++ b/client/model-view/templates/domainViewer.pug @@ -20,13 +20,17 @@ div#domain-viewer.card div.collapse(id="collapse-domain" data-hook="domain-container") - div.card-body.tab-content + div.card-body.tab-content.mx-4 div(data-hook="external-domains-container") - button.btn.btn-outline-secondary.box-shadow.mb-3(data-hook="select-external-domain") View External Domain + button.btn.btn-outline-primary.box-shadow.mb-3(data-hook="edit-domain-btn") Edit Domain - div(data-hook="external-domain-select" style="display: none") + button.btn.btn-outline-primary.box-shadow.mb-3.ml-2(data-hook="create-domain") Build New Domain + + button.btn.btn-outline-primary.box-shadow.mb-3.ml-2(data-hook="select-external-domain") View External Domain + + div.hidden(data-hook="domain-interact-section") div.text-info(data-hook="select-location-message" style="display: none") | There are multiple domain files with that name, please select a location @@ -41,17 +45,13 @@ div#domain-viewer.card div.inline(data-hook="select-location") - div(data-hook="domain-view-container") - - div.tab-pane.active(id="edit-domain" data-hook="edit-domain") - - div.mt-3 + div.mb-3.inline - button.btn.btn-outline-primary.box-shadow(data-hook="edit-domain-btn") Edit Domain + button.btn.btn-outline-primary.box-shadow.ml-2(data-hook="save-to-model" disabled) Import into Model - button.btn.btn-outline-primary.box-shadow.ml-2(data-hook="create-domain") Build New Domain + div(data-hook="domain-view-container") - button.btn.btn-outline-primary.box-shadow.ml-2(data-hook="save-to-model" disabled) Import into Model + div.tab-pane.active(id="edit-domain" data-hook="edit-domain") div.tab-pane(id="view-domain" data-hook="view-domain") diff --git a/client/model-view/templates/editEvent.pug b/client/model-view/templates/editEvent.pug new file mode 100644 index 0000000000..882eed7a37 --- /dev/null +++ b/client/model-view/templates/editEvent.pug @@ -0,0 +1,127 @@ +div.mx-1 + + if(this.model.collection.indexOf(this.model) !== 0) + hr + + div.row.align-items-baseline + + div.col-sm-1 + + div.pl-3: input(type="checkbox" data-hook="select" data-toggle="collapse" data-target="#collapse-event-details" + this.model.compID) + + div.col-sm-7(data-hook="event-name-container") + + div.col-sm-2 + + div.tooltip-icon-large(data-hook="annotation-tooltip" data-html="true" data-toggle="tooltip" title=this.model.annotation || "Click 'Add' to add an annotation") + + button.btn.btn-outline-secondary.btn-sm.box-shadow(data-hook="edit-annotation-btn") Edit + + div.col-sm-2 + + button.btn.btn-outline-secondary.box-shadow(data-hook="remove") X + + div.mx-1.pl-2(data-hook="event-details") + + div.collapse(id="collapse-event-details" + this.model.compID) + + hr + + div.mx-1.row.align-items-baseline + + div.col-sm-6 + + div + + h5.inline + + span(for="trigger-input") Trigger Expression: + + div.tooltip-icon(data-html="true" data-hook="rate-parameter-tooltip" data-toggle="tooltip" title=this.parent.tooltips.triggerExpression) + + div + + div(id="event-trigger-expression" data-hook="event-trigger-expression") + + div.my-1 + + h6.inline Advanced + button.btn.btn-outline-collapse(data-toggle='collapse' data-target='#advanced-events' + this.model.compID data-hook='advanced-event-button') + + + div.collapse(id="advanced-events" + this.model.compID data-hook="advanced-event-section") + + div.mr-2.inline + + h5.inline + + span(for="event-delay") Delay: + + div.tooltip-icon(data-html="true" data-toggle="tooltip" title=this.parent.tooltips.delay) + + div.inline(style="width: 80%") + + div(id="event-delay" data-hook="event-delay") + + div.mr-2.inline + + h5.inline + + span(for="event-trigger-expression") Priority: + + div.tooltip-icon(data-html="true" data-toggle="tooltip" title=this.parent.tooltips.priority) + + div.inline(style="width: 80%") + + div(id="event-priority" data-hook="event-priority") + + div.row + + div.col-lg-6.col-md-12.verticle-space + + div.mr-2.inline + + h5.inline + + span.checkbox(for="initial-value") Initial Value: + + div.tooltip-icon(data-html="true" data-toggle="tooltip" title=this.parent.tooltips.initialValue) + + input(type="checkbox" id="initial-value" data-hook="event-trigger-init-value") + + div.col-lg-6.col-md-12.verticle-space + + div.mr-2.inline + + h5.inline + + span.checkbox(for="persistent") Persistent: + + div.tooltip-icon(data-html="true" data-toggle="tooltip" title=this.parent.tooltips.persistent) + + input(type="checkbox" id="persistent", data-hook="event-trigger-persistent") + + div.mr-2.inline + + h5.inline + + span.checkbox(for="use-values-from-trigger-time") Use Values From: + + div.tooltip-icon(data-html="true" data-toggle="tooltip" title=this.parent.tooltips.useValuesFromTriggerTime) + + div.row + + div.col-lg-6.col-md-12.verticle-space + + div.horizontal-space.inline + + input(type="radio" name="edit-use-values-from" + this.model.compID data-hook="edit-trigger-time" data-name="trigger") + | Trigger Time + + div.col-lg-6.col-md-12.verticle-space + + div.horizontal-space.inline + + input(type="radio" name="edit-use-values-from" + this.model.compID data-hook="edit-assignment-time" data-name="assignment") + | Assignment Time + + div.col-sm-6(data-hook="assignments-container") diff --git a/client/model-view/templates/editEventAssignment.pug b/client/model-view/templates/editEventAssignment.pug index ecc8b48dda..1b5276ece2 100644 --- a/client/model-view/templates/editEventAssignment.pug +++ b/client/model-view/templates/editEventAssignment.pug @@ -15,4 +15,4 @@ div.mx-1 div.col-sm-2 - button.btn.btn-outline-secondary(data-hook="remove") X + button.btn.btn-outline-secondary(data-hook="remove-assignment") X diff --git a/client/model-view/templates/editInitialCondition.pug b/client/model-view/templates/editInitialCondition.pug index 3b29326c07..6c24123f16 100644 --- a/client/model-view/templates/editInitialCondition.pug +++ b/client/model-view/templates/editInitialCondition.pug @@ -16,15 +16,27 @@ div.mx-1 div.col-sm-2 div(data-hook="place-details") + span Location: + div + div.inline(data-hook="x-container") + div.inline(data-hook="y-container") + div.inline(data-hook="z-container") + div(data-hook="scatter-details") + span Active in Types: + div(data-hook="initial-condition-types") + div(data-hook="restict-to-error") + + p.text-danger Species must be active in at least one 'Type'. + div.col-sm-2 div.tooltip-icon-large(data-hook="annotation-tooltip" data-html="true" data-toggle="tooltip" title=this.model.annotation || "Click 'Add' to add an annotation") diff --git a/client/model-view/templates/editSpatialSpecie.pug b/client/model-view/templates/editSpatialSpecie.pug index ae78da2e75..f9f21cd282 100644 --- a/client/model-view/templates/editSpatialSpecie.pug +++ b/client/model-view/templates/editSpatialSpecie.pug @@ -17,6 +17,10 @@ div.mx-1 div.row(data-hook="species-types") + div(data-hook="restict-to-error") + + p.text-danger Species must be active in at least one 'Type'. + div.col-sm-2 div.tooltip-icon-large(data-hook="annotation-tooltip" data-html="true" data-toggle="tooltip" title=this.model.annotation || "Click 'Add' to add an annotation") diff --git a/client/model-view/templates/eventAssignmentsView.pug b/client/model-view/templates/eventAssignmentsView.pug index f32e4cf6da..7cfca19d08 100644 --- a/client/model-view/templates/eventAssignmentsView.pug +++ b/client/model-view/templates/eventAssignmentsView.pug @@ -10,11 +10,11 @@ div#event-assignments-editor div.tooltip-icon(data-html="true" data-toggle="tooltip" title=this.tooltips.assignments) - button.btn.btn-outline-collapse(data-toggle="collapse" data-target="#collapse-event-assignments" data-hook="collapse-assignments") - + button.btn.btn-outline-collapse(data-toggle="collapse" data-target="#collapse-event-assignments" + this.collection.parent.compID data-hook="collapse-assignments") - div - div.collapse.show.tab-content(id="collapse-event-assignments" data-hook="event-assignments-list-container") + div.collapse.show.tab-content(id="collapse-event-assignments" + this.collection.parent.compID data-hook="event-assignments-list-container") div.tab-pane.active(id="edit-event-assignments" data-hook="edit-event-assignments") diff --git a/client/model-view/templates/eventDetails.pug b/client/model-view/templates/eventDetails.pug deleted file mode 100644 index c473f51381..0000000000 --- a/client/model-view/templates/eventDetails.pug +++ /dev/null @@ -1,74 +0,0 @@ -div(data-hook="event-details") - - div.row - - div - - span(for="event-trigger-expression") Trigger Expression: - - div.tooltip-icon(data-html="true" data-toggle="tooltip" title=this.parent.tooltips.triggerExpression) - - div.col-md-8(id="event-trigger-expression" data-hook="event-trigger-expression") - - div(data-hook="event-assignments") - - div.my-1 - - h6.inline Advanced - button.btn.btn-outline-collapse(data-toggle='collapse', data-target='#advanced-events', data-hook='advanced-event-button') + - - div.collapse(id="advanced-events" data-hook="advanced-event-section") - - div.row - - div.col-md-6 - - span(for="event-trigger-expression") Delay: - - div.tooltip-icon(data-html="true" data-toggle="tooltip" title=this.parent.tooltips.delay) - - div.col-md-9.inline(id="event-delay" data-hook="event-delay") - - div.col-md-6 - - span(for="event-trigger-expression") Priority: - - div.tooltip-icon(data-html="true" data-toggle="tooltip" title=this.parent.tooltips.priority) - - div.col-md-8.inline(data-hook="event-priority") - - div.row - - div.col-md-6.verticle-space - - span.checkbox(for="initial-value") Initial Value: - - div.tooltip-icon(data-html="true" data-toggle="tooltip" title=this.parent.tooltips.initialValue) - - input(type="checkbox", id="initial-value", data-hook="event-trigger-init-value") - - div.col-md-6.verticle-space - - span.checkbox(for="persistent") Persistent: - - div.tooltip-icon(data-html="true" data-toggle="tooltip" title=this.parent.tooltips.persistent) - - input(type="checkbox", id="persistent", data-hook="event-trigger-persistent") - - div.row - - div.col-md-12.verticle-space - - span.checkbox(for="use-values-from-trigger-time") Use Values From: - - div.tooltip-icon(data-html="true" data-toggle="tooltip" title=this.parent.tooltips.useValuesFromTriggerTime) - - div.horizontal-space.inline - - input(type="radio", name="edit-use-values-from" data-hook="edit-trigger-time" data-name="trigger") - | Trigger Time - - div.horizontal-space.inline - - input(type="radio", name="edit-use-values-from" data-hook="edit-assignment-time" data-name="assignment") - | Assignment Time diff --git a/client/model-view/templates/eventListing.pug b/client/model-view/templates/eventListing.pug deleted file mode 100644 index f67a2ecc19..0000000000 --- a/client/model-view/templates/eventListing.pug +++ /dev/null @@ -1,22 +0,0 @@ -div.mx-1 - - if(this.model.collection.indexOf(this.model) !== 0) - hr - - div.row.align-items-baseline - - div.col-md-2 - - div.pl-3: input(type="radio", data-hook="select", name="event-select") - - div.col-md-5(data-hook="event-name-container") - - div.col-md-3 - - div.tooltip-icon-large(data-hook="annotation-tooltip" data-html="true" data-toggle="tooltip" title=this.model.annotation || "Click 'Add' to add an annotation") - - button.btn.btn-outline-secondary.btn-sm.box-shadow(data-hook="edit-annotation-btn") Edit - - div.col-md-2 - - button.btn.btn-outline-secondary.box-shadow(data-hook="remove") X diff --git a/client/model-view/templates/eventsView.pug b/client/model-view/templates/eventsView.pug index 85fef5ae21..afccb32798 100644 --- a/client/model-view/templates/eventsView.pug +++ b/client/model-view/templates/eventsView.pug @@ -29,37 +29,31 @@ div#events.card div.tab-pane.active(id="edit-events" data-hook="edit-events") - div.row.mb-3.align-items-baseline - - div.col-md-6.container-part - - hr - - div.mx-1.row.head.align-items-baseline + hr - div.col-md-2 + div.mx-1.row.head.align-items-baseline - h6 Edit + div.col-sm-1 - div.col-md-5 + h6 Edit - h6.inline Name + div.col-sm-7 - div.tooltip-icon(data-html="true" data-toggle="tooltip" title=this.tooltips.name) + h6.inline Name - div.col-md-3 + div.tooltip-icon(data-html="true" data-toggle="tooltip" title=this.tooltips.name) - h6.inline Annotation + div.col-sm-2 - div.tooltip-icon(data-html="true" data-toggle="tooltip" title=this.tooltips.annotation) + h6.inline Annotation - div.col-md-2 + div.tooltip-icon(data-html="true" data-toggle="tooltip" title=this.tooltips.annotation) - h6 Remove + div.col-sm-2 - div.mt-3(data-hook="edit-event-listing-container") + h6 Remove - div.col-md-6.container-part(data-hook="event-details-container") + div.mt-3(data-hook="edit-event-container") button.btn.btn-outline-primary.box-shadow(data-hook="add-event") Add Event @@ -89,4 +83,4 @@ div#events.card h6 Annotation - div.mt-3(data-hook="view-event-listing-container") + div.mt-3(data-hook="view-event-container") diff --git a/client/model-view/templates/reactionRestrictTo.pug b/client/model-view/templates/reactionRestrictTo.pug index 6c810ba00a..3c9d70fa1d 100644 --- a/client/model-view/templates/reactionRestrictTo.pug +++ b/client/model-view/templates/reactionRestrictTo.pug @@ -2,4 +2,8 @@ div.mb-3 span Types the reaction can occur in: - div.mt-1.row(data-hook="reaction-types-container") \ No newline at end of file + div.mt-1.row(data-hook="reaction-types-container") + + div(data-hook="restict-to-error") + + p.text-danger Reactions must be active in at least one 'Type'. \ No newline at end of file diff --git a/client/model-view/templates/sbmlComponentsView.pug b/client/model-view/templates/sbmlComponentsView.pug index 84ac3d3fb5..e3fe619412 100644 --- a/client/model-view/templates/sbmlComponentsView.pug +++ b/client/model-view/templates/sbmlComponentsView.pug @@ -6,7 +6,7 @@ div#sbml-components.card button.btn.btn-outline-collapse(data-toggle="collapse", data-target="#collapse-sbml-component", data-hook="collapse") + - div.collapse(id="collapse-sbml-component") + div.collapse(id="collapse-sbml-component" data-hook="sbml-component-container") div.card-body @@ -30,11 +30,13 @@ div#sbml-components.card button.btn.btn-outline-collapse(data-toggle="collapse" data-target="#collapse-function-definitions" data-hook="collapse-function-definitions") - + div.inline(data-hook="function-definition-filter" style="float: right;") + div.card-body p.mb-0 | Object representation defining an evaluable function to be used during simulation. - div.collapse.tab-content(class="show" id="collapse-function-definitions") + div.collapse.tab-content(class="show" id="collapse-function-definitions" data-hook="function-definitions-list-container") div.tab-pane.active(id="edit-function-definitions" data-hook="edit-function-definitions") diff --git a/client/model-view/templates/viewEvent.pug b/client/model-view/templates/viewEvent.pug index ef20893766..54a65d8abd 100644 --- a/client/model-view/templates/viewEvent.pug +++ b/client/model-view/templates/viewEvent.pug @@ -11,7 +11,7 @@ div.mx-1 div.col-sm-2=this.model.triggerExpression - div.col-sm-4(data-hook="assignment-viewer") + div.col-sm-4(data-hook="assignments-container") div.col-sm-2 @@ -35,13 +35,13 @@ div.mx-1 div - input(type="radio", id="trigger-time", name="view-use-values-from" data-hook="view-trigger-time" disabled) + input(type="radio" id="trigger-time" name="view-use-values-from" + this.model.compID data-hook="view-trigger-time" disabled) span.inline.horizontal-space(for="trigger-time") Trigger Time div - input(type="radio", id="assignment-time", name="view-use-values-from" data-hook="view-assignment-time" disabled) + input(type="radio" id="assignment-time" name="view-use-values-from" + this.model.compID data-hook="view-assignment-time" disabled) span.inline.horizontal-space(for="assignment-time") Assignment Time diff --git a/client/model-view/views/boundary-condition-view.js b/client/model-view/views/boundary-condition-view.js index f1c5ab8116..c21620b63d 100644 --- a/client/model-view/views/boundary-condition-view.js +++ b/client/model-view/views/boundary-condition-view.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/model-view/views/boundary-conditions-view.js b/client/model-view/views/boundary-conditions-view.js index e2f4831a94..2c42dd65de 100644 --- a/client/model-view/views/boundary-conditions-view.js +++ b/client/model-view/views/boundary-conditions-view.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -56,6 +56,8 @@ module.exports = View.extend({ View.prototype.initialize.apply(this, arguments); this.readOnly = attrs.readOnly ? attrs.readOnly : false; this.tooltips = Tooltips.boundaryConditionsEditor; + this.filterAttr = attrs.attr; + this.filterKey = attrs.key; this.setDefaultBC(); }, render: function (attrs, options) { @@ -70,10 +72,10 @@ module.exports = View.extend({ $(this.queryByHook('edit-boundary-conditions')).removeClass('active'); $(this.queryByHook('view-boundary-conditions')).addClass('active'); }else{ - this.renderEditBoundaryConditionView(); + this.renderEditBoundaryConditionView({'key': this.filterKey, 'attr': this.filterAttr}); this.toggleAddNewBCButton(); } - this.renderViewBoundaryConditionView(); + this.renderViewBoundaryConditionView({'key': this.filterKey, 'attr': this.filterAttr}); }, changeCollapseButtonText: function (e) { app.changeCollapseButtonText(this, e); @@ -93,17 +95,16 @@ module.exports = View.extend({ }, handleAddBCClick: function (e) { let endpoint = path.join(app.getApiPath(), "model/new-bc"); - let self = this; - if(this.newBC.property === "v") { + if(this.newBC.target === "v") { this.newBC.value = this.newBCVector; } let data = {model_path: this.collection.parent.directory, kwargs: this.newBC}; app.postXHR(endpoint, data, { - success: function (err, response, body) { - self.collection.addNewBoundaryCondition(self.newBCName, body.expression); - self.setDefaultBC(); - self.resetNewBCViews(); + success: (err, response, body) => { + this.collection.addNewBoundaryCondition(this.newBCName, body.expression); + this.setDefaultBC(); + this.resetNewBCViews(); } }); }, @@ -121,15 +122,15 @@ module.exports = View.extend({ $(this.queryByHook("new-bc-velocity-value")).css("display", "none"); $(this.queryByHook("new-bc-other-value")).css("display", "block"); } - this.newBC.property = target; - this.newBC.species = null; + this.newBC.target = target; $(this.queryByHook("new-bc-deterministic")).prop("disabled", true); }else{ - let species = this.collection.parent.species.filter(function (specie) { + $(this.queryByHook("new-bc-velocity-value")).css("display", "none"); + $(this.queryByHook("new-bc-other-value")).css("display", "block"); + let species = this.collection.parent.species.filter((specie) => { return specie.compID === Number(target); })[0].name; - this.newBC.property = null; - this.newBC.species = species; + this.newBC.target = species; $(this.queryByHook("new-bc-deterministic")).prop("disabled", false); } }, @@ -143,18 +144,25 @@ module.exports = View.extend({ this.newBC[key] = this.validateNewBCCondition(key, value); }else if(key === "value") { if(e.delegateTarget.dataset.hook.endsWith("x")) { - this.newBCVector[0] = value === "" ? 0 : value; + this.newBCVector[0] = value === "" ? 0 : Number(value); }else if(e.delegateTarget.dataset.hook.endsWith("y")) { - this.newBCVector[1] = value === "" ? 0 : value; + this.newBCVector[1] = value === "" ? 0 : Number(value); }else if(e.delegateTarget.dataset.hook.endsWith("z")) { - this.newBCVector[2] = value === "" ? 0 : value; + this.newBCVector[2] = value === "" ? 0 : Number(value); }else{ - this.newBC[key] = value === "" ? null : value; + this.newBC[key] = value === "" ? null : Number(value); } } } this.toggleAddNewBCButton(); }, + openSection: function ({editMode=true}={}) { + if(!$(this.queryByHook("boundary-conditions-container")).hasClass("show")) { + let boundCondCollapseBtn = $(this.queryByHook("collapse-bc")); + boundCondCollapseBtn.click(); + boundCondCollapseBtn.html('-'); + } + }, renderEditBoundaryConditionView: function ({key=null, attr=null}={}) { if(this.editBoundaryConditionView) { this.editBoundaryConditionView.remove(); @@ -193,7 +201,7 @@ module.exports = View.extend({ resetNewBCViews: function () { $(this.queryByHook("new-bc-deterministic")).prop("checked", this.newBC.deterministic); $(this.queryByHook("new-bc-name")).find("input").val(this.newBCName); - $(this.queryByHook("new-bc-target")).find("select").val(this.newBC.property); + $(this.queryByHook("new-bc-target")).find("select").val(this.newBC.target); $(this.queryByHook("new-bc-type")).find("input").val(this.newBC.type_id); $(this.queryByHook("new-bc-value")).find("input").val(this.newBC.value); $(this.queryByHook("new-bc-x-min")).find("input").val(this.newBC.xmin); @@ -207,14 +215,14 @@ module.exports = View.extend({ }, setDefaultBC: function () { this.newBCName = ""; - this.newBC = {"species": null, "property": "v", "value": null, "deterministic": true, "type_id": null, + this.newBC = {"target": "v", "value": null, "deterministic": true, "type_id": null, "xmin": null, "ymin": null, "zmin": null, "xmax": null, "ymax": null, "zmax": null}; this.newBCVector = [0, 0, 0]; this.setConditions = []; }, toggleAddNewBCButton: function () { let invalidName = this.newBCName === ""; - let invalidValue = this.newBC.property === "v" ? this.newBCVector === [0, 0, 0] : this.newBC.value === null; + let invalidValue = this.newBC.target === "v" ? this.newBCVector === [0, 0, 0] : this.newBC.value === null; let disabled = invalidName || invalidValue || !this.setConditions.length; $(this.queryByHook("add-new-bc")).prop("disabled", disabled); }, @@ -229,7 +237,7 @@ module.exports = View.extend({ }else if(!this.setConditions.includes(key)){ this.setConditions.push(key); } - return value; + return key === "type_id" ? value : Number(value); }, subviews: { filter: { @@ -240,6 +248,7 @@ module.exports = View.extend({ required: false, name: 'filter', valueType: 'string', + disabled: this.filterKey !== null, placeholder: 'filter' }); } @@ -271,7 +280,7 @@ module.exports = View.extend({ name: 'target', eagerValidate: true, groupOptions: options, - value: this.newBC.property + value: this.newBC.target }); } }, @@ -333,7 +342,7 @@ module.exports = View.extend({ parent: this, required: false, name: 'xmin', - tests: tests.valueTests, + tests: tests.intTests, valueType: 'number', value: this.newBC.xmin }); @@ -346,7 +355,7 @@ module.exports = View.extend({ parent: this, required: false, name: 'ymin', - tests: tests.valueTests, + tests: tests.intTests, valueType: 'number', value: this.newBC.ymin }); @@ -359,7 +368,7 @@ module.exports = View.extend({ parent: this, required: false, name: 'zmin', - tests: tests.valueTests, + tests: tests.intTests, valueType: 'number', value: this.newBC.zmin }); @@ -372,7 +381,7 @@ module.exports = View.extend({ parent: this, required: false, name: 'xmax', - tests: tests.valueTests, + tests: tests.intTests, valueType: 'number', value: this.newBC.xmax }); @@ -385,7 +394,7 @@ module.exports = View.extend({ parent: this, required: false, name: 'ymax', - tests: tests.valueTests, + tests: tests.intTests, valueType: 'number', value: this.newBC.ymax }); @@ -398,7 +407,7 @@ module.exports = View.extend({ parent: this, required: false, name: 'zmax', - tests: tests.valueTests, + tests: tests.intTests, valueType: 'number', value: this.newBC.zmax }); @@ -411,8 +420,8 @@ module.exports = View.extend({ parent: this, required: false, name: 'type', - tests: tests.valueTests, - valueType: 'number', + tests: tests.optionalNameTests, + valueType: 'string', value: this.newBC.type_id }); } diff --git a/client/model-view/views/component-types.js b/client/model-view/views/component-types.js index 57ca64b996..9fbf89431f 100644 --- a/client/model-view/views/component-types.js +++ b/client/model-view/views/component-types.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -47,5 +47,6 @@ module.exports = View.extend({ this.parent.model.types.splice(index, 1); } this.parent.model.trigger('change'); + this.parent.model.trigger('types-changed'); } }); \ No newline at end of file diff --git a/client/model-view/views/domain-viewer.js b/client/model-view/views/domain-viewer.js index 720501ba4c..fffed1e405 100644 --- a/client/model-view/views/domain-viewer.js +++ b/client/model-view/views/domain-viewer.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -128,7 +128,7 @@ module.exports = View.extend({ this.domainPath = null; $(this.queryByHook('select-location-message')).css('display', 'none'); $(this.queryByHook('select-domain-location')).css('display', 'none'); - $(this.queryByHook('external-domain-select')).css('display', 'none'); + $(this.queryByHook('domain-interact-section')).css('display', 'none'); $(this.queryByHook('save-to-model')).prop('disabled', true); this.renderDomainView(); } @@ -162,6 +162,7 @@ module.exports = View.extend({ domainCollapseBtn.html('-'); } app.switchToEditTab(this, "domain"); + this.domainView.displayError(); }, renderDomainFileSelectView: function () { if(this.domainFileSelectView) { @@ -175,7 +176,7 @@ module.exports = View.extend({ unselectedText: '-- Select Domain --' }); app.registerRenderSubview(this, this.domainFileSelectView, 'select-domain'); - $(this.queryByHook('external-domain-select')).css('display', 'block'); + $(this.queryByHook('domain-interact-section')).css('display', 'block'); }, renderDomainLocationSelectView: function (options) { if(this.domainLocationSelectView) { diff --git a/client/model-view/views/event-assignment-view.js b/client/model-view/views/event-assignment-view.js index 2e00163ef0..1871f79911 100644 --- a/client/model-view/views/event-assignment-view.js +++ b/client/model-view/views/event-assignment-view.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -29,7 +29,7 @@ let viewTemplate = require('../templates/viewEventAssignment.pug'); module.exports = View.extend({ events: { - 'click [data-hook=remove]' : 'removeAssignment', + 'click [data-hook=remove-assignment]' : 'removeAssignment', 'change [data-hook=event-assignment-variable]' : 'selectAssignmentVariable', 'change [data-hook=event-assignment-expression]' : 'updateViewer' }, diff --git a/client/model-view/views/event-assignments-view.js b/client/model-view/views/event-assignments-view.js index 483df74ee3..d473af8828 100644 --- a/client/model-view/views/event-assignments-view.js +++ b/client/model-view/views/event-assignments-view.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/model-view/views/event-details.js b/client/model-view/views/event-details.js deleted file mode 100644 index 5bd52cf5ee..0000000000 --- a/client/model-view/views/event-details.js +++ /dev/null @@ -1,138 +0,0 @@ -/* -StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -*/ - -let $ = require('jquery'); -//support files -let app = require('../../app'); -let tests = require('../../views/tests'); -//views -let InputView = require('../../views/input'); -let View = require('ampersand-view'); -let EventAssignment = require('./event-assignments-view'); -//templates -let template = require('../templates/eventDetails.pug'); - -module.exports = View.extend({ - template: template, - bindings: { - 'model.initialValue': { - hook: 'event-trigger-init-value', - type: 'booleanAttribute', - name: 'checked' - }, - 'model.persistent': { - hook: 'event-trigger-persistent', - type: 'booleanAttribute', - name: 'checked' - }, - }, - events: { - 'change [data-hook=event-trigger-init-value]' : 'setTriggerInitialValue', - 'change [data-hook=event-trigger-persistent]' : 'setTriggerPersistent', - 'change [data-hook=edit-trigger-time]' : 'setUseValuesFromTriggerTime', - 'change [data-hook=edit-assignment-time]' : 'setUseValuesFromTriggerTime', - 'click [data-hook=advanced-event-button]' : 'changeCollapseButtonText' - }, - initialize: function (attrs, options) { - View.prototype.initialize.apply(this, arguments); - }, - render: function () { - View.prototype.render.apply(this, arguments); - this.renderEventAssignments(); - if(this.model.useValuesFromTriggerTime){ - $(this.queryByHook('edit-trigger-time')).prop('checked', true); - }else{ - $(this.queryByHook('edit-assignment-time')).prop('checked', true); - } - app.tooltipSetup(); - }, - changeCollapseButtonText: function (e) { - app.changeCollapseButtonText(this, e); - }, - openAdvancedSection: function () { - if(this.model.advanced_error && !$(this.queryByHook("advanced-event-section")).hasClass('show')) { - let advCollapseBtn = $(this.queryByHook("advanced-event-button")); - advCollapseBtn.click(); - advCollapseBtn.html('-'); - } - }, - renderEventAssignments: function () { - if(this.eventAssignmentsView){ - this.eventAssignmentsView.remove(); - } - this.eventAssignmentsView = new EventAssignment({ - collection: this.model.eventAssignments, - tooltips: this.parent.tooltips - }); - app.registerRenderSubview(this, this.eventAssignmentsView, 'event-assignments'); - }, - setTriggerInitialValue: function (e) { - this.model.initialValue = e.target.checked; - }, - setTriggerPersistent: function (e) { - this.model.persistent = e.target.checked; - }, - setUseValuesFromTriggerTime: function (e) { - this.model.useValuesFromTriggerTime = e.target.dataset.name === "trigger"; - }, - update: function () {}, - updateValid: function () {}, - subviews: { - inputDelay: { - hook: 'event-delay', - prepareView: function (el) { - return new InputView({ - parent: this, - required: false, - name: 'delay', - placeholder: '---No Expression Entered---', - modelKey: 'delay', - valueType: 'string', - value: this.model.delay - }); - } - }, - inputPriority: { - hook: 'event-priority', - prepareView: function (el) { - return new InputView({ - parent: this, - required: true, - name: 'priority', - modelKey: 'priority', - valueType: 'string', - value: this.model.priority - }); - } - }, - inputTriggerExpression: { - hook: 'event-trigger-expression', - prepareView: function (el) { - return new InputView({ - parent: this, - required: true, - name: 'trigger-expression', - placeholder: '---No Expression Entered---', - modelKey: 'triggerExpression', - valueType: 'string', - value: this.model.triggerExpression - }); - } - } - } -}); \ No newline at end of file diff --git a/client/model-view/views/event-listing.js b/client/model-view/views/event-listing.js deleted file mode 100644 index 6ab4cea796..0000000000 --- a/client/model-view/views/event-listing.js +++ /dev/null @@ -1,141 +0,0 @@ -/* -StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -*/ - -let $ = require('jquery'); -let _ = require('underscore'); -//support files -let app = require('../../app'); -let tests = require('../../views/tests'); -let modals = require('../../modals'); -//views -let InputView = require('../../views/input'); -let View = require('ampersand-view'); -let EventAssignment = require('./event-assignments-view'); -//templates -let viewTemplate = require('../templates/viewEvent.pug'); -let editTemplate = require('../templates/eventListing.pug'); - -module.exports = View.extend({ - bindings: { - 'model.selected' : { - type: function (el, value, previousValue) { - el.checked = value; - }, - hook: 'select' - }, - 'model.initialValue': { - hook: 'event-trigger-init-value', - type: 'booleanAttribute', - name: 'checked' - }, - 'model.persistent': { - hook: 'event-trigger-persistent', - type: 'booleanAttribute', - name: 'checked' - } - }, - events: { - 'click [data-hook=select]' : 'selectEvent', - 'click [data-hook=edit-annotation-btn]' : 'editAnnotation', - 'click [data-hook=remove]' : 'removeEvent' - }, - initialize: function (attrs, options) { - View.prototype.initialize.apply(this, arguments); - this.viewMode = attrs.viewMode ? attrs.viewMode : false; - if(this.viewMode) { - this.delay = this.model.delay === "" ? "None" : this.model.delay; - }else{ - this.model.on("change", _.bind(this.updateViewer, this)); - } - }, - render: function () { - this.template = this.viewMode ? viewTemplate : editTemplate; - View.prototype.render.apply(this, arguments); - app.documentSetup(); - if(!this.model.annotation){ - $(this.queryByHook('edit-annotation-btn')).text('Add'); - } - if(this.viewMode) { - if(this.model.useValuesFromTriggerTime) { - $(this.queryByHook("view-trigger-time")).prop('checked', true); - }else{ - $(this.queryByHook("view-assignment-time")).prop('checked', true); - } - } - }, - editAnnotation: function () { - if(document.querySelector('#eventAnnotationModal')) { - document.querySelector('#eventAnnotationModal').remove(); - } - let self = this; - let name = this.model.name; - let annotation = this.model.annotation; - let modal = $(modals.annotationModalHtml("event", name, annotation)).modal(); - let okBtn = document.querySelector('#eventAnnotationModal .ok-model-btn'); - let input = document.querySelector('#eventAnnotationModal #eventAnnotationInput'); - input.addEventListener("keyup", function (event) { - if(event.keyCode === 13){ - event.preventDefault(); - okBtn.click(); - } - }); - okBtn.addEventListener('click', function (e) { - self.model.annotation = input.value.trim(); - self.parent.renderEditEventListingsView(); - modal.modal('hide'); - }); - }, - removeEvent: function () { - this.remove(); - this.collection.removeEvent(this.model); - }, - selectEvent: function (e) { - this.model.collection.trigger("select", this.model); - }, - update: function () {}, - updateValid: function () {}, - updateViewer: function () { - this.parent.renderViewEventListingView(); - }, - subviews: { - inputName: { - hook: 'event-name-container', - prepareView: function (el) { - return new InputView({ - parent: this, - required: true, - name: 'name', - tests: tests.nameTests, - modelKey: 'name', - valueType: 'string', - value: this.model.name - }); - } - }, - viewEventAssignmentsView: { - hook: "assignment-viewer", - prepareView: function (el) { - return new EventAssignment({ - collection: this.model.eventAssignments, - tooltips: this.parent.tooltips, - readOnly: true - }); - } - } - } -}); \ No newline at end of file diff --git a/client/model-view/views/event-view.js b/client/model-view/views/event-view.js new file mode 100644 index 0000000000..b7179de850 --- /dev/null +++ b/client/model-view/views/event-view.js @@ -0,0 +1,221 @@ +/* +StochSS is a platform for simulating biochemical systems +Copyright (C) 2019-2023 StochSS developers. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +let $ = require('jquery'); +let _ = require('underscore'); +//support files +let app = require('../../app'); +let modals = require('../../modals'); +let tests = require('../../views/tests'); +//views +let View = require('ampersand-view'); +let InputView = require('../../views/input'); +let EventAssignmentsView = require('./event-assignments-view'); +//templates +let editTemplate = require('../templates/editEvent.pug'); +let viewTemplate = require('../templates/viewEvent.pug'); + +module.exports = View.extend({ + bindings: { + 'model.selected' : { + type: function (el, value, previousValue) { + el.checked = value; + }, + hook: 'select' + }, + 'model.initialValue': { + hook: 'event-trigger-init-value', + type: 'booleanAttribute', + name: 'checked' + }, + 'model.persistent': { + hook: 'event-trigger-persistent', + type: 'booleanAttribute', + name: 'checked' + } + }, + events: { + 'change [data-hook=event-trigger-init-value]' : 'setTriggerInitialValue', + 'change [data-hook=event-trigger-persistent]' : 'setTriggerPersistent', + 'change [data-hook=edit-trigger-time]' : 'setUseValuesFromTriggerTime', + 'change [data-hook=edit-assignment-time]' : 'setUseValuesFromTriggerTime', + 'click [data-hook=select]' : 'selectEvent', + 'click [data-hook=edit-annotation-btn]' : 'editAnnotation', + 'click [data-hook=remove]' : 'removeEvent', + 'click [data-hook=advanced-event-button]' : 'changeCollapseButtonText' + }, + initialize: function (attrs, options) { + View.prototype.initialize.apply(this, arguments); + this.viewMode = attrs.viewMode ? attrs.viewMode : false; + if(this.viewMode) { + this.delay = !this.model.delay ? "None" : this.model.delay; + }else{ + this.model.on("change", _.bind(this.updateViewer, this)); + this.model.on("change-event", () => { + this.renderEventAssignmentsView({override: true}); + }); + } + }, + render: function () { + this.template = this.viewMode ? viewTemplate : editTemplate; + View.prototype.render.apply(this, arguments); + app.documentSetup(); + if(this.viewMode) { + var uvfView = "view" + this.renderEventAssignmentsView(); + }else{ + var uvfView = "edit" + if(this.model.selected) { + setTimeout(_.bind(this.openEventDetails, this), 1); + } + if(!this.model.annotation){ + $(this.queryByHook('edit-annotation-btn')).text('Add'); + } + } + if(this.model.useValuesFromTriggerTime){ + $(this.queryByHook(`${uvfView}-trigger-time`)).prop('checked', true); + }else{ + $(this.queryByHook(`${uvfView}-assignment-time`)).prop('checked', true); + } + }, + changeCollapseButtonText: function (e) { + app.changeCollapseButtonText(this, e); + }, + editAnnotation: function () { + if(document.querySelector('#eventAnnotationModal')) { + document.querySelector('#eventAnnotationModal').remove(); + } + let name = this.model.name; + let annotation = this.model.annotation; + let modal = $(modals.annotationModalHtml("event", name, annotation)).modal(); + let okBtn = document.querySelector('#eventAnnotationModal .ok-model-btn'); + let input = document.querySelector('#eventAnnotationModal #eventAnnotationInput'); + input.addEventListener("keyup", (event) => { + if(event.keyCode === 13){ + event.preventDefault(); + okBtn.click(); + } + }); + okBtn.addEventListener('click', (e) => { + modal.modal('hide'); + this.model.annotation = input.value.trim(); + this.parent.renderEditEventView(); + }); + }, + openEventDetails: function () { + $("#collapse-event-details" + this.model.compID).collapse("show"); + this.renderDetailsSection(); + }, + removeEvent: function () { + this.collection.removeEvent(this.model); + }, + renderDetailsSection: function () { + this.renderEventAssignmentsView(); + }, + renderEventAssignmentsView: function ({override=false}={}) { + if(override && this.eventAssignmentsView) { + this.eventAssignmentsView.remove(); + } + + if(!this.eventAssignmentsView || override) { + this.eventAssignmentsView = new EventAssignmentsView({ + collection: this.model.eventAssignments, + tooltips: this.parent.tooltips, + readOnly: this.viewMode + }); + app.registerRenderSubview(this, this.eventAssignmentsView, "assignments-container"); + } + }, + selectEvent: function (e) { + this.model.selected = !this.model.selected; + this.renderDetailsSection(); + }, + setTriggerInitialValue: function (e) { + this.model.initialValue = e.target.checked; + }, + setTriggerPersistent: function (e) { + this.model.persistent = e.target.checked; + }, + setUseValuesFromTriggerTime: function (e) { + this.model.useValuesFromTriggerTime = e.target.dataset.name === "trigger"; + }, + update: function () {}, + updateValid: function () {}, + updateViewer: function (event) { + if(!event || !("selected" in event._changed)){ + this.parent.renderViewEventView(); + } + }, + subviews: { + inputName: { + hook: 'event-name-container', + prepareView: function (el) { + return new InputView({ + parent: this, + required: true, + name: 'name', + tests: tests.nameTests, + modelKey: 'name', + valueType: 'string', + value: this.model.name + }); + } + }, + inputTriggerExpression: { + hook: 'event-trigger-expression', + prepareView: function (el) { + return new InputView({ + parent: this, + required: true, + name: 'trigger-expression', + placeholder: '---No Expression Entered---', + modelKey: 'triggerExpression', + valueType: 'string', + value: this.model.triggerExpression + }); + } + }, + inputDelay: { + hook: 'event-delay', + prepareView: function (el) { + return new InputView({ + parent: this, + required: false, + name: 'delay', + placeholder: '---No Expression Entered---', + modelKey: 'delay', + valueType: 'string', + value: this.model.delay + }); + } + }, + inputPriority: { + hook: 'event-priority', + prepareView: function (el) { + return new InputView({ + parent: this, + required: true, + name: 'priority', + modelKey: 'priority', + valueType: 'string', + value: this.model.priority + }); + } + } + } +}); \ No newline at end of file diff --git a/client/model-view/views/events-view.js b/client/model-view/views/events-view.js index 7b07de89db..d8fc9bbc59 100644 --- a/client/model-view/views/events-view.js +++ b/client/model-view/views/events-view.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -22,17 +22,15 @@ let app = require('../../app'); let Tooltips = require("../../tooltips"); //views let View = require('ampersand-view'); +let EventView = require('./event-view'); let InputView = require('../../views/input'); -let EventDetails = require('./event-details'); -let EventListings = require('./event-listing'); -let ViewSwitcher = require('ampersand-view-switcher'); //templates let template = require('../templates/eventsView.pug'); module.exports = View.extend({ template: template, events: { - 'click [data-hook=add-event]' : 'addEvent', + 'click [data-hook=add-event]' : 'handleAddEvent', 'click [data-hook=collapse]' : 'changeCollapseButtonText', 'change [data-hook=event-filter]' : 'filterEvents' }, @@ -40,26 +38,8 @@ module.exports = View.extend({ View.prototype.initialize.apply(this, arguments); this.readOnly = attrs.readOnly ? attrs.readOnly : false; this.tooltips = Tooltips.eventsEditor; - if(!this.readOnly) { - this.collection.on("select", function (event) { - this.setSelectedEvent(event); - this.setDetailsView(event); - }, this); - this.collection.on("remove", function (event) { - // Select the last event by default - // But only if there are other events other than the one we're removing - if (event.detailsView){ - event.detailsView.remove(); - } - this.collection.removeEvent(event); - if (this.collection.length) { - let selected = this.collection.at(this.collection.length-1); - this.collection.trigger("select", selected); - } - }, this); - this.collection.parent.species.on('add remove', this.toggleAddEventButton, this); - this.collection.parent.parameters.on('add remove', this.toggleAddEventButton, this); - } + this.filterAttr = attrs.attr; + this.filterKey = attrs.key; }, render: function () { View.prototype.render.apply(this, arguments); @@ -73,24 +53,10 @@ module.exports = View.extend({ $(this.queryByHook('edit-events')).removeClass('active'); $(this.queryByHook('view-events')).addClass('active'); }else { - this.renderEditEventListingsView(); - this.detailsContainer = this.queryByHook('event-details-container'); - this.detailsViewSwitcher = new ViewSwitcher({ - el: this.detailsContainer - }); - if(this.collection.length) { - this.setSelectedEvent(this.collection.at(0)); - this.collection.trigger("select", this.selectedEvent); - } + this.renderEditEventView({'key': this.filterKey, 'attr': this.filterAttr}); this.toggleAddEventButton(); } - this.renderViewEventListingView(); - }, - addEvent: function () { - let event = this.collection.addEvent(); - event.detailsView = this.newDetailsView(event); - this.collection.trigger("select", event); - app.tooltipSetup(); + this.renderViewEventView({'key': this.filterKey, 'attr': this.filterAttr}); }, changeCollapseButtonText: function (e) { app.changeCollapseButtonText(this, e); @@ -104,14 +70,20 @@ module.exports = View.extend({ key = attrKey[1]; } if(!this.readOnly) { - this.renderEditEventListingsView({'key': key, 'attr': attr}); + this.renderEditEventView({'key': key, 'attr': attr}); } - this.renderViewEventListingView({'key': key, 'attr': attr}); + this.renderViewEventView({'key': key, 'attr': attr}); }, - newDetailsView: function (event) { - let detailsView = new EventDetails({ model: event }); - detailsView.parent = this; - return detailsView; + handleAddEvent: function () { + let event = this.collection.addEvent(); + app.tooltipSetup(); + }, + openAdvancedSection: function () { + if(this.model.advanced_error && !$(this.queryByHook("advanced-event-section")).hasClass('show')) { + let advCollapseBtn = $(this.queryByHook("advanced-event-button")); + advCollapseBtn.click(); + advCollapseBtn.html('-'); + } }, openSection: function (error) { if(!$(this.queryByHook("events")).hasClass("show")) { @@ -119,29 +91,34 @@ module.exports = View.extend({ evtCollapseBtn.click(); evtCollapseBtn.html('-'); } - app.switchToEditTab(this, "events"); - var event = this.collection.filter((e) => { - return e.compID === error.id; - })[0]; - this.collection.trigger("select", event); - event.detailsView.openAdvancedSection(); + if(!this.readOnly) { + app.switchToEditTab(this, "events"); + } + if(error) { + let eventView = this.editEventView.views.filter((eventView) => { + return eventView.model.compID === error.id; + })[0]; + eventView.model.selected = true; + eventView.openEventDetails(); + eventView.openAdvancedSection(); + } }, - renderEditEventListingsView: function ({key=null, attr=null}={}) { - if(this.editEventListingsView){ - this.editEventListingsView.remove(); + renderEditEventView: function ({key=null, attr=null}={}) { + if(this.editEventsView){ + this.editEventsView.remove(); } let options = {filter: (model) => { return model.contains(attr, key); }} - this.editEventListingsView = this.renderCollection( + this.editEventsView = this.renderCollection( this.collection, - EventListings, - this.queryByHook('edit-event-listing-container'), + EventView, + this.queryByHook('edit-event-container'), options ); app.tooltipSetup(); }, - renderViewEventListingView: function ({key=null, attr=null}={}) { - if(this.viewEventListingsView) { - this.viewEventListingsView.remove(); + renderViewEventView: function ({key=null, attr=null}={}) { + if(this.viewEventsView) { + this.viewEventsView.remove(); } this.containsMdlWithAnn = this.collection.filter(function (model) {return model.annotation}).length > 0; if(!this.containsMdlWithAnn) { @@ -153,29 +130,15 @@ module.exports = View.extend({ viewOptions: {parent: this, viewMode: true}, filter: (model) => { return model.contains(attr, key); } }; - this.viewEventListingsView = this.renderCollection( + this.viewEventsView = this.renderCollection( this.collection, - EventListings, - this.queryByHook('view-event-listing-container'), + EventView, + this.queryByHook('view-event-container'), options ); app.tooltipSetup(); }, - setDetailsView: function (event) { - event.detailsView = event.detailsView || this.newDetailsView(event); - this.detailsViewSwitcher.set(event.detailsView); - }, - setSelectedEvent: function (event) { - this.collection.each(function (m) { m.selected = false; }); - event.selected = true; - this.selectedEvent = event; - }, toggleAddEventButton: function () { - this.collection.map(function (event) { - if(event.detailsView && event.selected){ - event.detailsView.renderEventAssignments(); - } - }); let numSpecies = this.collection.parent.species.length; let numParameters = this.collection.parent.parameters.length; let disabled = numSpecies <= 0 && numParameters <= 0; @@ -192,6 +155,7 @@ module.exports = View.extend({ required: false, name: 'filter', valueType: 'string', + disabled: this.filterKey !== null, placeholder: 'filter' }); } diff --git a/client/model-view/views/function-definition-view.js b/client/model-view/views/function-definition-view.js index 4eccb14246..185dda0125 100644 --- a/client/model-view/views/function-definition-view.js +++ b/client/model-view/views/function-definition-view.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/model-view/views/initial-condition-view.js b/client/model-view/views/initial-condition-view.js index 1e9c013193..b76bd00e60 100644 --- a/client/model-view/views/initial-condition-view.js +++ b/client/model-view/views/initial-condition-view.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -57,6 +57,8 @@ module.exports = View.extend({ app.documentSetup(); if(!this.viewMode) { this.model.on('change', _.bind(this.updateViewer, this)); + this.model.on('types-changed', this.toggleRestrictToError, this); + this.toggleRestrictToError(); } if(!this.model.annotation){ $(this.queryByHook('edit-annotation-btn')).text('Add'); @@ -106,6 +108,7 @@ module.exports = View.extend({ this.model.icType = newType; if(currentType === "Place" || newType === "Place"){ this.toggleDetailsView(); + this.toggleRestrictToError(); } }, toggleDetailsView: function () { @@ -117,6 +120,16 @@ module.exports = View.extend({ $(this.queryByHook("scatter-details")).css("display", "block"); } }, + toggleRestrictToError: function () { + let errorMsg = $(this.queryByHook("restict-to-error")); + if(!this.model.validateComponent()) { + errorMsg.addClass('component-invalid'); + errorMsg.removeClass('component-valid'); + }else{ + errorMsg.addClass('component-valid'); + errorMsg.removeClass('component-invalid'); + } + }, update: function () {}, updateValid: function () {}, updateViewer: function () { diff --git a/client/model-view/views/initial-conditions-view.js b/client/model-view/views/initial-conditions-view.js index 3a43c7fcff..aa2ecb2616 100644 --- a/client/model-view/views/initial-conditions-view.js +++ b/client/model-view/views/initial-conditions-view.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -41,6 +41,8 @@ module.exports = View.extend({ View.prototype.initialize.apply(this, arguments); this.tooltips = Tooltips.initialConditionsEditor; this.readOnly = attrs.readOnly ? attrs.readOnly : false; + this.filterAttr = attrs.attr; + this.filterKey = attrs.key; }, render: function () { View.prototype.render.apply(this, arguments); @@ -54,9 +56,9 @@ module.exports = View.extend({ $(this.queryByHook('edit-initial-conditions')).removeClass('active'); $(this.queryByHook('view-initial-conditions')).addClass('active'); }else{ - this.renderEditInitialConditionsView(); + this.renderEditInitialConditionsView({'key': this.filterKey, 'attr': this.filterAttr}); } - this.renderViewInitialConditionsView(); + this.renderViewInitialConditionsView({'key': this.filterKey, 'attr': this.filterAttr}); }, addInitialCondition: function (e) { var initialConditionType = e.target.textContent; @@ -86,6 +88,13 @@ module.exports = View.extend({ } this.renderViewInitialConditionsView({'key': key, 'attr': attr}); }, + openSection: function ({editMode=true}={}) { + if(!$(this.queryByHook("initial-conditions")).hasClass("show")) { + let initCondCollapseBtn = $(this.queryByHook("initial-condition-button")); + initCondCollapseBtn.click(); + initCondCollapseBtn.html('-'); + } + }, renderEditInitialConditionsView: function ({key=null, attr=null}={}) { if(this.editInitialConditionView) { this.editInitialConditionView.remove(); @@ -132,6 +141,7 @@ module.exports = View.extend({ required: false, name: 'filter', valueType: 'string', + disabled: this.filterKey !== null, placeholder: 'filter' }); } diff --git a/client/model-view/views/parameter-view.js b/client/model-view/views/parameter-view.js index 075d250095..9db707c3b1 100644 --- a/client/model-view/views/parameter-view.js +++ b/client/model-view/views/parameter-view.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/model-view/views/parameters-view.js b/client/model-view/views/parameters-view.js index 68e122eb1d..2a8eceb8fd 100644 --- a/client/model-view/views/parameters-view.js +++ b/client/model-view/views/parameters-view.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -38,9 +38,10 @@ module.exports = View.extend({ View.prototype.initialize.apply(this, arguments); this.readOnly = attrs.readOnly ? attrs.readOnly : false; this.tooltips = Tooltips.parametersEditor; - let self = this; - this.collection.on('update-parameters', function (compID, parameter) { - self.collection.parent.reactions.map(function (reaction) { + this.filterAttr = attrs.attr; + this.filterKey = attrs.key; + this.collection.on('update-parameters', (compID, parameter) => { + this.collection.parent.reactions.map((reaction) => { if(reaction.rate && reaction.rate.compID === compID){ reaction.rate = parameter; if(reaction.reactionType !== 'custom-propensity') { @@ -48,22 +49,23 @@ module.exports = View.extend({ } } }); - self.collection.parent.eventsCollection.map(function (event) { - event.eventAssignments.map(function (assignment) { + this.collection.parent.eventsCollection.map((event) => { + event.eventAssignments.map((assignment) => { if(assignment.variable.compID === compID) { assignment.variable = parameter; } }) - if(event.selected) - event.detailsView.renderEventAssignments(); + if(event.selected) { + event.trigger('change-event'); + } }); - self.collection.parent.rules.map(function (rule) { + this.collection.parent.rules.map((rule) => { if(rule.variable.compID === compID) { rule.variable = parameter; } }); - if(self.parent.rulesView) { - self.parent.rulesView.renderEditRules(); + if(this.parent.rulesView) { + this.parent.rulesView.renderEditRules(); } }); }, @@ -79,9 +81,9 @@ module.exports = View.extend({ $(this.queryByHook('edit-parameters')).removeClass('active'); $(this.queryByHook('view-parameters')).addClass('active'); }else{ - this.renderEditParameter(); + this.renderEditParameter({'key': this.filterKey, 'attr': this.filterAttr}); } - this.renderViewParameter(); + this.renderViewParameter({'key': this.filterKey, 'attr': this.filterAttr}); }, addParameter: function () { this.collection.addParameter(); @@ -109,7 +111,9 @@ module.exports = View.extend({ paramCollapseBtn.click(); paramCollapseBtn.html('-'); } - app.switchToEditTab(this, "parameters"); + if(!this.readOnly) { + app.switchToEditTab(this, "parameters"); + } }, renderEditParameter: function ({key=null, attr=null}={}) { if(this.editParameterView){ @@ -155,6 +159,7 @@ module.exports = View.extend({ required: false, name: 'filter', valueType: 'string', + disabled: this.filterKey !== null, placeholder: 'filter' }); } diff --git a/client/model-view/views/reactant-product.js b/client/model-view/views/reactant-product.js index f3b82d54b9..17205590ea 100644 --- a/client/model-view/views/reactant-product.js +++ b/client/model-view/views/reactant-product.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/model-view/views/reaction-restrict-to.js b/client/model-view/views/reaction-restrict-to.js index 7f48966fc8..bc070c9bf6 100644 --- a/client/model-view/views/reaction-restrict-to.js +++ b/client/model-view/views/reaction-restrict-to.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -15,7 +15,7 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ - +let $ = require('jquery'); //views let View = require('ampersand-view'); let DomainTypesView = require('./component-types'); @@ -32,6 +32,8 @@ module.exports = View.extend({ render: function () { View.prototype.render.apply(this, arguments); this.renderDomainTypes(); + this.toggleRestrictToError(); + this.model.on('types-changed', this.toggleRestrictToError, this); }, renderDomainTypes: function () { if(this.domainTypesView) { @@ -45,5 +47,15 @@ module.exports = View.extend({ return model.typeID != 0; }} ); + }, + toggleRestrictToError: function () { + let errorMsg = $(this.queryByHook("restict-to-error")); + if(this.model.types.length <= 0) { + errorMsg.addClass('component-invalid'); + errorMsg.removeClass('component-valid'); + }else{ + errorMsg.addClass('component-valid'); + errorMsg.removeClass('component-invalid'); + } } }); \ No newline at end of file diff --git a/client/model-view/views/reaction-view.js b/client/model-view/views/reaction-view.js index 30f8042e32..9c4b81c066 100644 --- a/client/model-view/views/reaction-view.js +++ b/client/model-view/views/reaction-view.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/model-view/views/reactions-view.js b/client/model-view/views/reactions-view.js index 8efb1b97c7..6ed6424e31 100644 --- a/client/model-view/views/reactions-view.js +++ b/client/model-view/views/reactions-view.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -48,6 +48,8 @@ module.exports = View.extend({ View.prototype.initialize.apply(this, arguments); this.tooltips = Tooltips.reactionsEditor; this.readOnly = attrs.readOnly ? attrs.readOnly : false; + this.filterAttr = attrs.attr; + this.filterKey = attrs.key; if(!this.readOnly) { this.collection.parent.species.on('add remove', this.toggleAddReactionButton, this); this.collection.parent.parameters.on('add remove', this.updateMAState, this); @@ -66,7 +68,7 @@ module.exports = View.extend({ $(this.queryByHook('edit-reactions')).removeClass('active'); $(this.queryByHook('view-reactions')).addClass('active'); }else{ - this.renderEditReactionView(); + this.renderEditReactionView({'key': this.filterKey, 'attr': this.filterAttr}); this.toggleAddReactionButton(); this.updateMAState(); this.renderReactionTypes(); @@ -76,7 +78,7 @@ module.exports = View.extend({ displayMode: false, output: 'html', }); - this.renderViewReactionView(); + this.renderViewReactionView({'key': this.filterKey, 'attr': this.filterAttr}); }, changeCollapseButtonText: function (e) { app.changeCollapseButtonText(this, e); @@ -126,8 +128,10 @@ module.exports = View.extend({ reacCollapseBtn.click(); reacCollapseBtn.html('-'); } - app.switchToEditTab(this, "reactions"); - if(error.type !== "process") { + if(!this.readOnly) { + app.switchToEditTab(this, "reactions"); + } + if(error && error.type !== "process") { let reactionView = this.editReactionView.views.filter((reactView) => { return reactView.model.compID === error.id; })[0]; @@ -226,6 +230,7 @@ module.exports = View.extend({ required: false, name: 'filter', valueType: 'string', + disabled: this.filterKey !== null, placeholder: 'filter' }); } diff --git a/client/model-view/views/rule-view.js b/client/model-view/views/rule-view.js index 38a2d3c27e..0723cc05f8 100644 --- a/client/model-view/views/rule-view.js +++ b/client/model-view/views/rule-view.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/model-view/views/rules-view.js b/client/model-view/views/rules-view.js index 00870f4377..c8f9c89858 100644 --- a/client/model-view/views/rules-view.js +++ b/client/model-view/views/rules-view.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -42,6 +42,8 @@ module.exports = View.extend({ this.collection.parent.species.on('add remove', this.toggleAddRuleButton, this); this.collection.parent.parameters.on('add remove', this.toggleAddRuleButton, this); this.tooltips = Tooltips.rulesEditor; + this.filterAttr = attrs.attr; + this.filterKey = attrs.key; }, render: function () { View.prototype.render.apply(this, arguments); @@ -56,10 +58,10 @@ module.exports = View.extend({ $(this.queryByHook('edit-rules')).removeClass('active'); $(this.queryByHook('view-rules')).addClass('active'); }else { - this.renderEditRules(); + this.renderEditRules({'key': this.filterKey, 'attr': this.filterAttr}); this.toggleAddRuleButton(); } - this.renderViewRules(); + this.renderViewRules({'key': this.filterKey, 'attr': this.filterAttr}); }, addRule: function (e) { let type = e.target.dataset.name; @@ -88,7 +90,9 @@ module.exports = View.extend({ ruleCollapseBtn.click(); ruleCollapseBtn.html('-'); } - app.switchToEditTab(this, "rules"); + if(!this.readOnly) { + app.switchToEditTab(this, "rules"); + } }, renderDocs: function () { let options = { @@ -135,7 +139,7 @@ module.exports = View.extend({ app.tooltipSetup(); }, toggleAddRuleButton: function () { - this.renderEditRules(); + this.renderEditRules({'key': this.filterKey, 'attr': this.filterAttr}); let numSpecies = this.collection.parent.species.length; let numParameters = this.collection.parent.parameters.length; let disabled = numSpecies <= 0 && numParameters <= 0; @@ -152,6 +156,7 @@ module.exports = View.extend({ required: false, name: 'filter', valueType: 'string', + disabled: this.filterKey !== null, placeholder: 'filter' }); } diff --git a/client/model-view/views/sbml-components-view.js b/client/model-view/views/sbml-components-view.js index 1fa52cde0c..79ee6ca7cc 100644 --- a/client/model-view/views/sbml-components-view.js +++ b/client/model-view/views/sbml-components-view.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -22,6 +22,7 @@ let app = require('../../app'); let Tooltips = require('../../tooltips'); //views let View = require('ampersand-view'); +let InputView = require('../../views/input'); let EditFunctionDefinition = require('./function-definition-view'); //templates let template = require('../templates/sbmlComponentsView.pug'); @@ -30,13 +31,16 @@ module.exports = View.extend({ template: template, events: { 'click [data-hook=collapse]' : 'changeCollapseButtonText', - 'click [data-hook=collapse-function-definitions]' : 'changeCollapseButtonText' + 'click [data-hook=collapse-function-definitions]' : 'changeCollapseButtonText', + 'change [data-hook=function-definition-filter]' : 'functionDefinitionRules' }, initialize: function (attrs, options) { View.prototype.initialize.apply(this, arguments); this.tooltips = Tooltips.sbmlComponentsEditor this.functionDefinitions = attrs.functionDefinitions; this.readOnly = attrs.readOnly ? attrs.readOnly : false; + this.filterAttr = attrs.attr; + this.filterKey = attrs.key; }, render: function () { View.prototype.render.apply(this, arguments); @@ -50,35 +54,70 @@ module.exports = View.extend({ $(this.queryByHook('edit-function-definitions')).removeClass('active'); $(this.queryByHook('view-function-definitions')).addClass('active'); }else { - this.renderEditFunctionDefinitionView(); + this.renderEditFunctionDefinitionView({'key': this.filterKey, 'attr': this.filterAttr}); } - this.renderViewFunctionDefinitionView(); + this.renderViewFunctionDefinitionView({'key': this.filterKey, 'attr': this.filterAttr}); }, changeCollapseButtonText: function (e) { app.changeCollapseButtonText(this, e); }, - renderEditFunctionDefinitionView: function () { + openFuncDefSection: function () { + if(!$(this.queryByHook("function-definitions-list-container")).hasClass("show")) { + let funcDefCollapseBtn = $(this.queryByHook("collapse-function-definitions")); + funcDefCollapseBtn.click(); + funcDefCollapseBtn.html('-'); + } + if(!this.readOnly) { + app.switchToEditTab(this, "function-definitions"); + } + }, + openSection: function () { + if(!$(this.queryByHook("sbml-comonent-container")).hasClass("show")) { + let funcDefCollapseBtn = $(this.queryByHook("collapse")); + funcDefCollapseBtn.click(); + funcDefCollapseBtn.html('-'); + } + }, + functionDefinitionRules: function (e) { + var key = e.target.value === "" ? null : e.target.value; + var attr = null; + if(key && key.includes(':')) { + let attrKey = key.split(':'); + attr = attrKey[0].toLowerCase().replace(/ /g, ''); + key = attrKey[1]; + } + if(!this.readOnly) { + this.renderEditFunctionDefinitionView({'key': key, 'attr': attr}); + } + this.renderViewFunctionDefinitionView({'key': key, 'attr': attr}); + }, + renderEditFunctionDefinitionView: function ({key=null, attr=null}={}) { if(this.editFunctionDefinitionView){ this.editFunctionDefinitionView.remove(); } + let options = {filter: (model) => { return model.contains(attr, key); }} this.editFunctionDefinitionView = this.renderCollection( this.functionDefinitions, EditFunctionDefinition, - this.queryByHook('edit-function-definition-list') + this.queryByHook('edit-function-definition-list'), + options ); app.tooltipSetup(); }, - renderViewFunctionDefinitionView: function () { + renderViewFunctionDefinitionView: function ({key=null, attr=null}={}) { if(this.viewFunctionDefinitionView) { this.viewFunctionDefinitionView.remove(); } - let options = {viewOptions: {viewMode: true}}; this.containsMdlWithAnn = this.functionDefinitions.filter(function (model) {return model.annotation}).length > 0; if(!this.containsMdlWithAnn) { $(this.queryByHook("function-definition-annotation-header")).css("display", "none"); }else{ $(this.queryByHook("function-definition-annotation-header")).css("display", "block"); } + let options = { + viewOptions: {viewMode: true, hasAnnotations: this.containsMdlWithAnn}, + filter: (model) => { return model.contains(attr, key); } + } this.viewFunctionDefinitionView = this.renderCollection( this.functionDefinitions, EditFunctionDefinition, @@ -86,5 +125,22 @@ module.exports = View.extend({ options ); app.tooltipSetup(); + }, + update: function () {}, + updateValid: function () {}, + subviews: { + filter: { + hook: 'function-definition-filter', + prepareView: function (el) { + return new InputView({ + parent: this, + required: false, + name: 'filter', + valueType: 'string', + disabled: this.filterKey !== null, + placeholder: 'filter' + }); + } + } } }); \ No newline at end of file diff --git a/client/model-view/views/specie-view.js b/client/model-view/views/specie-view.js index ae1949d887..3fc2cafb69 100644 --- a/client/model-view/views/specie-view.js +++ b/client/model-view/views/specie-view.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -102,8 +102,10 @@ module.exports = View.extend({ this.updateInputValidation(); if(!this.viewMode){ this.model.on('change', _.bind(this.updateViewer, this)); + this.model.on('types-changed', this.toggleRestrictToError, this); if(this.parent.spatial) { this.renderTypes(); + this.toggleRestrictToError(); } } }, @@ -170,6 +172,16 @@ module.exports = View.extend({ this.updateInputValidation(); this.toggleSwitchingSettingsInput(); }, + toggleRestrictToError: function () { + let errorMsg = $(this.queryByHook("restict-to-error")); + if(this.model.types.length <= 0) { + errorMsg.addClass('component-invalid'); + errorMsg.removeClass('component-valid'); + }else{ + errorMsg.addClass('component-valid'); + errorMsg.removeClass('component-invalid'); + } + }, toggleSwitchingSettings: function () { if(this.model.mode === "dynamic"){ $(this.queryByHook('switching-tol')).prop('disabled', false); diff --git a/client/model-view/views/species-view.js b/client/model-view/views/species-view.js index b55220fd05..f0bb0eb25a 100644 --- a/client/model-view/views/species-view.js +++ b/client/model-view/views/species-view.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -41,17 +41,18 @@ module.exports = View.extend({ this.template = this.spatial ? spatialSpeciesTemplate : speciesTemplate; this.tooltips = Tooltips.speciesEditor; this.defaultMode = attrs.defaultMode; - let self = this; - this.collection.on('update-species', function (compID, specie, isNameUpdate, isDefaultMode) { - self.collection.parent.reactions.forEach(function (reaction) { + this.filterAttr = attrs.attr; + this.filterKey = attrs.key; + this.collection.on('update-species', (compID, specie, isNameUpdate, isDefaultMode) => { + this.collection.parent.reactions.forEach((reaction) => { var changedReaction = false; - reaction.reactants.forEach(function (reactant) { + reaction.reactants.forEach((reactant) => { if(reactant.specie.compID === compID) { reactant.specie = specie; changedReaction = true; } }); - reaction.products.forEach(function (product) { + reaction.products.forEach((product) => { if(product.specie.compID === compID) { product.specie = specie; changedReaction = true; @@ -61,23 +62,23 @@ module.exports = View.extend({ reaction.trigger('change-reaction'); } }); - self.collection.parent.eventsCollection.forEach(function (event) { - event.eventAssignments.forEach(function (assignment) { + this.collection.parent.eventsCollection.forEach((event) => { + event.eventAssignments.forEach((assignment) => { if(assignment.variable.compID === compID) { assignment.variable = specie; } }) if(isNameUpdate && event.selected) { - event.detailsView.renderEventAssignments(); + event.trigger('change-event'); } }); - self.collection.parent.rules.forEach(function (rule) { + this.collection.parent.rules.forEach((rule) => { if(rule.variable.compID === compID) { rule.variable = specie; } }); if(isNameUpdate) { - self.parent.rulesView.renderEditRules(); + this.parent.rulesView.renderEditRules(); } }); }, @@ -94,9 +95,9 @@ module.exports = View.extend({ $(this.queryByHook('view-species')).addClass('active'); }else{ this.toggleSpeciesCollectionError(); - this.renderEditSpeciesView(); + this.renderEditSpeciesView({'key': this.filterKey, 'attr': this.filterAttr}); } - this.renderViewSpeciesView(); + this.renderViewSpeciesView({'key': this.filterKey, 'attr': this.filterAttr}); }, addSpecies: function () { if(this.parent.model.domain.types) { @@ -206,6 +207,7 @@ module.exports = View.extend({ required: false, name: 'filter', valueType: 'string', + disabled: this.filterKey !== null, placeholder: 'filter' }); } diff --git a/client/model-view/views/stoich-species-view.js b/client/model-view/views/stoich-species-view.js index 01df300036..b0a19a1e3b 100644 --- a/client/model-view/views/stoich-species-view.js +++ b/client/model-view/views/stoich-species-view.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/models/action.js b/client/models/action.js new file mode 100644 index 0000000000..1e7a862e76 --- /dev/null +++ b/client/models/action.js @@ -0,0 +1,94 @@ +/* +StochSS is a platform for simulating biochemical systems +Copyright (C) 2019-2022 StochSS developers. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +//models +let Point = require('./point'); +let State = require('ampersand-state'); + +module.exports = State.extend({ + props: { + c: 'number', + enable: 'boolean', + filename: 'string', + fixed: 'boolean', + mass: 'number', + nu: 'number', + priority: 'number', + rho: 'number', + scope: 'string', + shape: 'string', + subdomainFile: 'string', + transformation: 'string', + type: 'string', + typeID: 'number', + vol: 'number' + }, + children: { + point: Point, + newPoint: Point + }, + session: { + selected: { + type: 'boolean', + default: false + }, + }, + initialize: function (attrs, options) { + State.prototype.initialize.apply(this, arguments); + }, + contains: function (attr, key) { + if(key === null) { return true; } + + let checks = { + 'c': this.c === key, + 'disabled': !this.enable, + 'enable': this.enable, + 'filename': this.filename.includes(key), + 'fixed': this.fixed, + 'mass': this.mass === key, + 'nu': this.nu === key, + 'priority': this.priority === key, + 'rho': this.rho === key, + 'scope': this.scope === key, + 'shape': this.shape.includes(key), + 'subdomainfile': this.subdomainFile.includes(key), + 'transformation': this.transformation.includes(key), + 'type': this.type === key, + 'typeid': this.collection.parent.types.get(this.typeID, 'typeID').name === key, + 'vol': this.vol === key + } + + if(attr !== null) { + let otherAttrs = { + 'speedofsound': 'c', 'static': 'fixed', 'viscosity': 'nu', + 'density': 'rho', 'type_id': 'typeid', 'volume': 'vol' + } + if(Object.keys(otherAttrs).includes(attr)) { + attr = otherAttrs[attr]; + } + return checks[attr]; + } + for(let attribute in checks) { + if(checks[attribute]) { return true; } + } + return false; + }, + validate: function () { + return true; + } +}); \ No newline at end of file diff --git a/client/models/actions.js b/client/models/actions.js new file mode 100644 index 0000000000..7f0d788633 --- /dev/null +++ b/client/models/actions.js @@ -0,0 +1,67 @@ +/* +StochSS is a platform for simulating biochemical systems +Copyright (C) 2019-2022 StochSS developers. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +let _ = require('underscore'); +//models +let Point = require('./point'); +let Action = require('./action'); +//collections +let Collection = require('ampersand-collection'); + +module.exports = Collection.extend({ + model: Action, + addAction: function (type, {action=null}={}) { + if(action) { + action.type = type; + }else{ + action = new Action({ + c: 10, + enable: false, + filename: '', + fixed: false, + mass: 1.0, + nu: 0.0, + priority: 1, + rho: 1.0, + scope: 'Multi Particle', + shape: '', + subdomainFile: '', + transformation: '', + type: type, + typeID: 0, + vol: 1.0 + }); + action.selected = true; + action.point = this.getNewPoint(); + action.newPoint = this.getNewPoint(); + } + this.add(action); + }, + getNewPoint: function (action) { + return new Point({x: 0, y: 0, z: 0}); + }, + removeAction: function (action) { + this.remove(action); + }, + validateCollection: function () { + if(this.length <= 0) { return false; } + if(this.parent.particles === undefined) { return false; } + if(this.parent.particles.length <= 0) { return false; } + return true; + } +}); diff --git a/client/models/boundary-condition.js b/client/models/boundary-condition.js index 0fc3db918b..c95f3d0ab4 100644 --- a/client/models/boundary-condition.js +++ b/client/models/boundary-condition.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/models/boundary-conditions.js b/client/models/boundary-conditions.js index f6e5110d32..5f1c4d17ab 100644 --- a/client/models/boundary-conditions.js +++ b/client/models/boundary-conditions.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/models/creator.js b/client/models/creator.js index e92b96392d..f38e6c3363 100644 --- a/client/models/creator.js +++ b/client/models/creator.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/models/creators.js b/client/models/creators.js index ca80dec981..e10b8184af 100644 --- a/client/models/creators.js +++ b/client/models/creators.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/models/domain-type.js b/client/models/domain-type.js index 16584666e4..1e34db83e1 100644 --- a/client/models/domain-type.js +++ b/client/models/domain-type.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -23,7 +23,6 @@ module.exports = State.extend({ props: { c: 'number', fixed: 'boolean', - geometry: 'string', mass: 'number', name: 'string', nu: 'number', @@ -32,6 +31,10 @@ module.exports = State.extend({ volume: 'number' }, session: { + inUse: { + type: 'boolean', + default: false, + }, numParticles: 'number', selected: { type: 'boolean', diff --git a/client/models/domain-types.js b/client/models/domain-types.js index 9ec4e5ac59..0d786c271a 100644 --- a/client/models/domain-types.js +++ b/client/models/domain-types.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -30,7 +30,6 @@ module.exports = Collection.extend({ let type = new Type({ c: 10, fixed: false, - geometry: "", mass: 1.0, name: name, nu: 0.0, @@ -46,6 +45,8 @@ module.exports = Collection.extend({ this.remove(type); }, validateCollection: function () { + if(this.models[0].numParticles > 0) { return false; } + if(this.length <= 1) { return false; } return true; } }); \ No newline at end of file diff --git a/client/models/domain.js b/client/models/domain.js index 137e9c38f4..00c06d3d9b 100644 --- a/client/models/domain.js +++ b/client/models/domain.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -17,10 +17,12 @@ along with this program. If not, see . */ //models -var State = require('ampersand-state'); +let State = require('ampersand-state'); //collections +let Shapes = require('./shapes'); +let Actions = require('./actions'); let Types = require('./domain-types'); -let Particles = require('./particles'); +let Transformations = require('./transformations'); module.exports = State.extend({ props: { @@ -37,37 +39,28 @@ module.exports = State.extend({ template_version: 'number' }, collections: { - types: Types, - particles: Particles + actions: Actions, + shapes: Shapes, + transformations: Transformations, + types: Types }, session: { def_particle_id: 'number', def_type_id: 'number', directory: 'string', - dirname: 'string' + dirname: 'string', + particles: 'object', + error: 'object' }, initialize: function (attrs, options) { State.prototype.initialize.apply(this, arguments) - this.particles.on('add change remove', this.updateValid, this); - this.def_particle_id = this.particles.length; this.def_type_id = this.types.length; - let self = this; - this.particles.forEach(function (particle) { - if(particle.particle_id >= self.def_particle_id) { - self.def_particle_id = particle.particle_id + 1; - } - }); - this.types.forEach(function (type) { - if(type.typeID >= self.def_type_id) { - self.def_type_id = type.typeID + 1; + this.types.forEach((type) => { + if(type.typeID >= this.def_type_id) { + this.def_type_id = type.typeID + 1; } }); }, - getDefaultID: function () { - let id = this.def_particle_id; - this.def_particle_id += 1; - return id; - }, getDefaultTypeID: function () { let id = this.def_type_id; this.def_type_id += 1; @@ -84,14 +77,16 @@ module.exports = State.extend({ type.typeID = id; } }); - this.particles.forEach((particle) => { - if(particle.type > oldType) { - particle.type -= 1; - } - }); }, validateModel: function () { - if(!this.particles.validateCollection()) return false; + if(!this.types.validateCollection()) { + this.error = {"type":"types"}; + return false; + } + if(!this.actions.validateCollection()) { + this.error = {"type":"actions"}; + return false; + } return true; }, updateValid: function () { diff --git a/client/models/event-assignment.js b/client/models/event-assignment.js index fe7f72f5e8..ae5db1463c 100644 --- a/client/models/event-assignment.js +++ b/client/models/event-assignment.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/models/event-assignments.js b/client/models/event-assignments.js index c26e83c17a..e769035304 100644 --- a/client/models/event-assignments.js +++ b/client/models/event-assignments.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/models/event.js b/client/models/event.js index 78a2126b55..b3994ae12b 100644 --- a/client/models/event.js +++ b/client/models/event.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -39,7 +39,7 @@ module.exports = State.extend({ session: { selected: { type: 'boolean', - default: true, + default: false, }, advanced_error: 'boolean' }, diff --git a/client/models/events.js b/client/models/events.js index a22303eb39..113458faae 100644 --- a/client/models/events.js +++ b/client/models/events.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -37,6 +37,7 @@ module.exports = Collection.extend({ initialValue: false, persistent: false, useValuesFromTriggerTime: false, + selected: true }); event.eventAssignments.addEventAssignment() this.parent.updateValid() diff --git a/client/models/function-definition.js b/client/models/function-definition.js index 9fb082b46f..5e9adf21b2 100644 --- a/client/models/function-definition.js +++ b/client/models/function-definition.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -28,5 +28,22 @@ module.exports = State.extend({ variables: 'string', signature: 'string', annotation: 'string' + }, + contains: function (attr, key) { + if(key === null) { return true; } + + let checks = { + 'name': this.name.includes(key), + 'signature': this.name.includes(key), + 'expression': this.expression.includes(key) + } + + if(attr !== null) { + return checks[attr]; + } + for(let attribute in checks) { + if(checks[attribute]) { return true; } + } + return false; } }); \ No newline at end of file diff --git a/client/models/function-definitions.js b/client/models/function-definitions.js index 143b2086a3..cecb4297b2 100644 --- a/client/models/function-definitions.js +++ b/client/models/function-definitions.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/models/initial-condition.js b/client/models/initial-condition.js index 7d8cfd9659..ed09f7393f 100644 --- a/client/models/initial-condition.js +++ b/client/models/initial-condition.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -22,6 +22,7 @@ var Specie = require('./specie'); module.exports = State.extend({ props: { + compID: 'number', icType: 'string', annotation: 'string', count: 'any', @@ -71,5 +72,9 @@ module.exports = State.extend({ if(checks[attribute]) { return true; } } return false; + }, + validateComponent: function () { + if(this.icType !== "Place" && this.types.length <= 0) { return false; } + return true; } }); \ No newline at end of file diff --git a/client/models/initial-conditions.js b/client/models/initial-conditions.js index f1f037498d..b7194e34cb 100644 --- a/client/models/initial-conditions.js +++ b/client/models/initial-conditions.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -23,8 +23,11 @@ var InitialCondition = require('./initial-condition'); module.exports = Collection.extend({ model: InitialCondition, + indexes: ['compID'], addInitialCondition: function (initialConditionType, types) { + var id = this.parent.getDefaultID(); var initialCondition = new InitialCondition({ + compID: id, icType: initialConditionType, annotation: "", types: types, @@ -43,4 +46,13 @@ module.exports = Collection.extend({ removeInitialCondition: function (initialCondition) { this.remove(initialCondition); }, + validateCollection: function () { + for(var i = 0; i < this.length; i++) { + if(!this.models[i].validateComponent()) { + this.parent.error = {'id':this.models[i].compID,'type':'initialCondition'}; + return false; + } + } + return true; + } }); \ No newline at end of file diff --git a/client/models/job.js b/client/models/job.js index 40cb8db071..97d7b8317e 100644 --- a/client/models/job.js +++ b/client/models/job.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/models/jobs.js b/client/models/jobs.js index eb1fcc99a3..c42797fb52 100644 --- a/client/models/jobs.js +++ b/client/models/jobs.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/models/me.js b/client/models/me.js index c88cc9ed35..54b239e7e1 100644 --- a/client/models/me.js +++ b/client/models/me.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/models/metadata.js b/client/models/metadata.js index 8d7318f8de..bca5325c0b 100644 --- a/client/models/metadata.js +++ b/client/models/metadata.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/models/model.js b/client/models/model.js index c993e86c91..7d126dae57 100644 --- a/client/models/model.js +++ b/client/models/model.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -96,32 +96,33 @@ module.exports = Model.extend({ this.rules.on('add change remove', this.updateValid, this); }, validateModel: function () { - if(!this.species.validateCollection(this.is_spatial)) return false; - if(!this.parameters.validateCollection()) return false; - if(!this.reactions.validateCollection()) return false; - if(!this.eventsCollection.validateCollection()) return false; - if(!this.rules.validateCollection()) return false; + if(this.is_spatial && this.domain.validateModel() === false) { + this.error = {"type":"domain"}; + return false; + }; + if(!this.species.validateCollection(this.is_spatial)) { return false; } + if(this.is_spatial && !this.initialConditions.validateCollection()) { return false; } + if(!this.parameters.validateCollection()) { return false; } + if(!this.reactions.validateCollection(this.is_spatial)) { return false; } + if(!this.eventsCollection.validateCollection()) { return false; } + if(!this.rules.validateCollection()) { return false; } if(!this.is_spatial && this.reactions.length <= 0 && this.eventsCollection.length <= 0 && this.rules.length <= 0) { - this.error = {"type":"process"} + this.error = {"type":"process"}; return false; } if(!this.volume === "" || isNaN(this.volume)) { - this.error = {"type":"volume"} - return false - }; - if(this.domain.validateModel() === false) { - this.error = {"type":"domain"} - return false + this.error = {"type":"volume"}; + return false; }; - if(this.modelSettings.validate() === false) { - this.error = {"type":"timespan"} - return false + if(this.modelSettings.validate(this.is_spatial) === false) { + this.error = {"type":"timespan"}; + return false; }; return true; }, updateValid: function () { - this.error = {} - this.valid = this.validateModel() + this.error = {}; + this.valid = this.validateModel(); }, getDefaultID: function () { var id = this.defaultID; diff --git a/client/models/parameter-sweep-settings.js b/client/models/parameter-sweep-settings.js index 073bc1f1bf..b8e78b9b66 100644 --- a/client/models/parameter-sweep-settings.js +++ b/client/models/parameter-sweep-settings.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/models/parameter.js b/client/models/parameter.js index a617b43bb7..33ff770af1 100644 --- a/client/models/parameter.js +++ b/client/models/parameter.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/models/parameters.js b/client/models/parameters.js index 2279bc59ee..904bb01353 100644 --- a/client/models/parameters.js +++ b/client/models/parameters.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/models/particle.js b/client/models/particle.js index 0a685631bf..82f1ad2b11 100644 --- a/client/models/particle.js +++ b/client/models/particle.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/models/particles.js b/client/models/particles.js index 968f1f6ff5..ccb13f5a51 100644 --- a/client/models/particles.js +++ b/client/models/particles.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -24,40 +24,4 @@ var Collection = require('ampersand-collection'); module.exports = Collection.extend({ model: Particle, indexes: ['particle_id'], - addParticle: function ({particle=null, point=[0,0,0], vol=1, mass=1, type=0, nu=0, fixed=false, c=10, rho=1}={}) { - let id = this.parent.getDefaultID(); - if(particle) { - particle.particle_id = id; - }else{ - particle = new Particle({ - c: c, - fixed: fixed, - mass: mass, - nu: nu, - particle_id: id, - point: point, - rho: rho, - type: type, - volume: vol - }); - } - this.add(particle); - }, - removeParticle: function (particle) { - this.remove(particle); - }, - removeParticles: function (particles) { - let self = this; - particles.forEach(function (particle) { - self.removeParticle(particle) - }); - }, - validateCollection: function () { - for(var i = 0; i < this.length; i++) { - if(this.models[i].type < 1) { - return false; - } - } - return true; - } }); \ No newline at end of file diff --git a/client/models/point.js b/client/models/point.js new file mode 100644 index 0000000000..cc4cf57193 --- /dev/null +++ b/client/models/point.js @@ -0,0 +1,75 @@ +/* +StochSS is a platform for simulating biochemical systems +Copyright (C) 2019-2022 StochSS developers. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +//models +let State = require('ampersand-state'); + +module.exports = State.extend({ + props: { + x: 'number', + y: 'number', + z: 'number', + }, + initialize: function (attrs, options) { + State.prototype.initialize.apply(this, arguments); + }, + contains: function (attr, key) { + if(key === null) { return true; } + + let centers = [`[${this.x}, ${this.y}, ${this.z}]`, `[${this.x},${this.y},${this.z}]`]; + + let checks = { + 'center': centers.includes(key), + 'x': this.x === key, + 'y': this.y === key, + 'z': this.z === key, + } + + if(attr !== null) { + let otherAttrs = { + 'point': 'center', 'normal': 'center', + 'point1': 'center','point2': 'center','point3': 'center' + } + if(Object.keys(otherAttrs).includes(attr)) { + attr = otherAttrs[attr]; + } + return checks[attr]; + } + for(let attribute in checks) { + if(checks[attribute]) { return true; } + } + return false + }, + validate: function () { + return true; + }, + derived: { + inUse: { + deps: ['x', 'y', 'z'], + fn: function () { + if(this.x !== 0) { + return true; + } + if(this.y !== 0) { + return true; + } + return this.z !== 0; + } + } + } +}); \ No newline at end of file diff --git a/client/models/presentation.js b/client/models/presentation.js index a3799b596a..8d9347afd4 100644 --- a/client/models/presentation.js +++ b/client/models/presentation.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/models/project.js b/client/models/project.js index a8db193934..358457ad75 100644 --- a/client/models/project.js +++ b/client/models/project.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/models/reaction.js b/client/models/reaction.js index 67685f921d..9709d2963a 100644 --- a/client/models/reaction.js +++ b/client/models/reaction.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -73,11 +73,19 @@ module.exports = State.extend({ if(this.reactionType === "custom-propensity") { return } var odePropensity = this.rate.name; var propensity = this.rate.name; - + + let reactants = {} this.reactants.forEach((stoichSpecies) => { - let name = stoichSpecies.specie.name; - if(stoichSpecies.ratio == 2) { - odePropensity += ` * ${name} * ${name}`; + if(Object.keys(reactants).includes(stoichSpecies.specie.name)) { + reactants[stoichSpecies.specie.name] += stoichSpecies.ratio; + }else{ + reactants[stoichSpecies.specie.name] = stoichSpecies.ratio; + } + }); + + Object.keys(reactants).forEach((name) => { + if(reactants[name] == 2) { + odePropensity = `${propensity} * ${name} * ${name}`; propensity = `${propensity} * ${name} * (${name} - 1) / vol`; }else{ odePropensity += ` * ${name}`; @@ -85,7 +93,7 @@ module.exports = State.extend({ } }) - let order = this.reactants.length; + let order = reactants.length; if(order == 2) { propensity += " / vol"; }else if(order == 0) { @@ -237,13 +245,14 @@ module.exports = State.extend({ } return "custom-massaction" }, - validateComponent: function () { - if(!this.name.trim() || this.name.match(/^\d/)) return false; - if((!/^[a-zA-Z0-9_]+$/.test(this.name))) return false; - if(!this.propensity.trim() && this.reactionType === "custom-propensity") return false; + validateComponent: function (isSpatial) { + if(!this.name.trim() || this.name.match(/^\d/)) { return false; } + if((!/^[a-zA-Z0-9_]+$/.test(this.name))) { return false; } + if(!this.propensity.trim() && this.reactionType === "custom-propensity") { return false; } if(this.reactionType.startsWith('custom')) { - if(this.reactants.length <= 0 && this.products.length <= 0) return false; + if(this.reactants.length <= 0 && this.products.length <= 0) { return false; } } + if(isSpatial && this.types.length <= 0) { return false; } return true; } }); \ No newline at end of file diff --git a/client/models/reactions.js b/client/models/reactions.js index f609f339c5..9fdc867292 100644 --- a/client/models/reactions.js +++ b/client/models/reactions.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -85,14 +85,14 @@ Reactions = Collection.extend({ this.remove(reaction); this.parent.updateValid() }, - validateCollection: function () { + validateCollection: function (isSpatial) { for(var i = 0; i < this.length; i++) { - if(!this.models[i].validateComponent()) { - this.parent.error = {'id':this.models[i].compID,'type':'reaction'} - return false + if(!this.models[i].validateComponent(isSpatial)) { + this.parent.error = {'id':this.models[i].compID,'type':'reaction'}; + return false; } } - return true + return true; } }); diff --git a/client/models/results-settings.js b/client/models/results-settings.js index e41e7e37a4..a9799161f1 100644 --- a/client/models/results-settings.js +++ b/client/models/results-settings.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/models/rule.js b/client/models/rule.js index aeebe6bea0..0bfc19b1f3 100644 --- a/client/models/rule.js +++ b/client/models/rule.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/models/rules.js b/client/models/rules.js index 09d6b9c846..215ba88710 100644 --- a/client/models/rules.js +++ b/client/models/rules.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/models/settings.js b/client/models/settings.js index 85f9cfd0ee..5dba94e6ed 100644 --- a/client/models/settings.js +++ b/client/models/settings.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/models/shape.js b/client/models/shape.js new file mode 100644 index 0000000000..ad6c2dce2b --- /dev/null +++ b/client/models/shape.js @@ -0,0 +1,95 @@ +/* +StochSS is a platform for simulating biochemical systems +Copyright (C) 2019-2022 StochSS developers. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +//models +let State = require('ampersand-state'); + +module.exports = State.extend({ + props: { + deltar: 'number', + deltas: 'number', + deltax: 'number', + deltay: 'number', + deltaz: 'number', + depth: 'number', + fillable: 'boolean', + formula: 'string', + height: 'number', + lattice: 'string', + length: 'number', + name: 'string', + radius: 'number', + type: 'string' + }, + session: { + selected: { + type: 'boolean', + default: false + }, + inUse: { + type: 'boolean', + default: false + } + }, + derived: { + notEditable: { + deps: ['fillable'], + fn: function () { + return this.fillable; + } + } + }, + initialize: function (attrs, options) { + State.prototype.initialize.apply(this, arguments); + }, + contains: function (attr, key) { + if(key === null) { return true; } + + let checks = { + 'deltar': ["Spherical Lattice", "Cylindrical Lattice"].includes(this.lattice) && + this.deltar === key, + 'deltas': ["Spherical Lattice", "Cylindrical Lattice"].includes(this.lattice) && + this.deltas === key, + 'deltax': this.lattice === "Cartesian Lattice" && this.deltax === key, + 'deltay': this.lattice === "Cartesian Lattice" && this.deltay === key, + 'deltaz': this.lattice === "Cartesian Lattice" && this.deltaz === key, + 'depth': this.lattice === "Cartesian Lattice" && this.depth === key, + 'formula': this.formula.includes(key), + 'height': this.lattice === "Cartesian Lattice" && this.height === key, + 'lattice': this.lattice === key, + 'length': ["Cartesian Lattice", "Cylindrical Lattice"].includes(this.lattice) && + this.length === key, + 'name': this.name.includes(key), + 'radius': ["Spherical Lattice", "Cylindrical Lattice"].includes(this.lattice) && + this.radius === key, + 'type': this.type === key, + } + + if(attr !== null) { + return checks[attr]; + } + for(let attribute in checks) { + if(checks[attribute]) { return true; } + } + return false + }, + validate: function () { + if((!/^[a-zA-Z0-9_]+$/.test(this.name))) return false; + return true; + } +}); diff --git a/client/models/shapes.js b/client/models/shapes.js new file mode 100644 index 0000000000..173f78b87f --- /dev/null +++ b/client/models/shapes.js @@ -0,0 +1,65 @@ +/* +StochSS is a platform for simulating biochemical systems +Copyright (C) 2019-2022 StochSS developers. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +let _ = require('underscore'); +//models +let Shape = require('./shape'); +//collections +let Collection = require('ampersand-collection'); + +module.exports = Collection.extend({ + model: Shape, + indexes: ['name'], + addShape: function (type) { + let name = this.getDefaultName(); + let shape = new Shape({ + deltar: 0, + deltas: 0, + deltax: 0, + deltay: 0, + deltaz: 0, + depth: 0, + fillable: false, + formula: '', + height: 0, + lattice: type, + length: 0, + name: name, + radius: 0, + type: 'Standard' + }); + this.add(shape); + return name; + }, + getDefaultName: function () { + var i = this.length + 1; + var name = `shape${i}`; + var names = this.map((shape) => { return shape.name; }); + while(_.contains(names, name)) { + i += 1; + name = `shape${i}`; + } + return name; + }, + removeShape: function (shape) { + this.remove(shape); + }, + validateCollection: function () { + return true; + } +}); diff --git a/client/models/simulation-settings.js b/client/models/simulation-settings.js index b49870d447..bd091c4fc0 100644 --- a/client/models/simulation-settings.js +++ b/client/models/simulation-settings.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/models/specie.js b/client/models/specie.js index 698a32ebe6..a8b2a9d674 100644 --- a/client/models/specie.js +++ b/client/models/specie.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -78,14 +78,15 @@ module.exports = State.extend({ } return false; }, - validateComponent: function () { - if(!this.name.trim() || this.name.match(/^\d/)) return false; - if((!/^[a-zA-Z0-9_]+$/.test(this.name))) return false; - if(this.value === "" || isNaN(this.value)) return false; + validateComponent: function (isSpatial) { + if(!this.name.trim() || this.name.match(/^\d/)) { return false; } + if((!/^[a-zA-Z0-9_]+$/.test(this.name))) { return false; } + if(this.value === "" || isNaN(this.value)) { return false; } if(this.mode === "dynamic") { - if(this.isSwitchTol && (this.switchTol === "" || isNaN(this.switchTol))) return false; - if(!this.isSwitchTol && (this.switchMin === "" || isNaN(this.switchMin))) return false; + if(this.isSwitchTol && (this.switchTol === "" || isNaN(this.switchTol))) { return false; } + if(!this.isSwitchTol && (this.switchMin === "" || isNaN(this.switchMin))) { return false; } } + if(isSpatial && this.types.length <= 0) { return false; } return true; } }); \ No newline at end of file diff --git a/client/models/species.js b/client/models/species.js index dcba8eadc2..c35b3bd2b5 100644 --- a/client/models/species.js +++ b/client/models/species.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -58,13 +58,13 @@ module.exports = Collection.extend({ }, validateCollection: function (isSpatial) { if(this.length <= 0 && !isSpatial) { - this.parent.error = {'type':'species'} + this.parent.error = {'type':'species'}; return false; } for(var i = 0; i < this.length; i++) { - if(!this.models[i].validateComponent()) { - this.parent.error = {'id':this.models[i].compID,'type':'species'} - return false + if(!this.models[i].validateComponent(isSpatial)) { + this.parent.error = {'id':this.models[i].compID,'type':'species'}; + return false; } } return true; diff --git a/client/models/stoich-specie.js b/client/models/stoich-specie.js index 293062fa3f..ff9abe9be3 100644 --- a/client/models/stoich-specie.js +++ b/client/models/stoich-specie.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/models/stoich-species.js b/client/models/stoich-species.js index 4fc9006f68..ff9fc034c0 100644 --- a/client/models/stoich-species.js +++ b/client/models/stoich-species.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/models/sweep-parameter.js b/client/models/sweep-parameter.js index 13b45fbad1..2506e45e19 100644 --- a/client/models/sweep-parameter.js +++ b/client/models/sweep-parameter.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/models/sweep-parameters.js b/client/models/sweep-parameters.js index 68c58af6d2..65e51e57a8 100644 --- a/client/models/sweep-parameters.js +++ b/client/models/sweep-parameters.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/models/timespan-settings.js b/client/models/timespan-settings.js index 78618766e3..cf8dc643a0 100644 --- a/client/models/timespan-settings.js +++ b/client/models/timespan-settings.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -39,9 +39,10 @@ module.exports = State.extend({ initialize: function (attrs, options) { State.prototype.initialize.apply(this, arguments) }, - validate: function () { - if(this.endSim === "" || isNaN(this.endSim)) return false; - if(this.timeStep === "" || isNaN(this.timeStep)) return false; + validate: function (isSpatial) { + if(this.endSim === "" || isNaN(this.endSim)) { return false; } + if(this.timeStep === "" || isNaN(this.timeStep)) { return false; } + if(isSpatial && this.timestepSize > this.timeStep) { return false; } return true; } }); \ No newline at end of file diff --git a/client/models/transformation.js b/client/models/transformation.js new file mode 100644 index 0000000000..5d4be22b8a --- /dev/null +++ b/client/models/transformation.js @@ -0,0 +1,109 @@ +/* +StochSS is a platform for simulating biochemical systems +Copyright (C) 2019-2022 StochSS developers. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +//models +let Point = require('./point'); +let State = require('ampersand-state'); + +module.exports = State.extend({ + props: { + angle: 'number', + factor: 'number', + name: 'string', + transformation: 'string', + type: 'string', + vector: 'object' + }, + children: { + center: Point, + normal: Point, + point1: Point, + point2: Point, + point3: Point + }, + session: { + selected: { + type: 'boolean', + default: false + }, + inUse: { + type: 'boolean', + default: false + } + }, + initialize: function (attrs, options) { + State.prototype.initialize.apply(this, arguments); + }, + contains: function (attr, key) { + if(key === null) { return true; } + + let checks = { + 'angle': this.angle === Number(key), + 'factor': this.factor === Number(key), + 'name': this.name.includes(key), + 'transformation': this.transformation.includes(key), + 'type': this.type === key + } + + if(attr !== null) { + let otherAttrs = { + 'centerx': 'x', 'centery': 'y', 'centerz': 'z', + 'normalx': 'x', 'normaly': 'y', 'normalz': 'z', + 'point1x': 'x', 'point1y': 'y', 'point1z': 'z', + 'point2x': 'x', 'point2y': 'y', 'point2z': 'z', + 'point3x': 'x', 'point3y': 'y', 'point3z': 'z' + } + let pointCheck = this.setPointCheck(attr) + if(Object.keys(otherAttrs).includes(attr)) { + attr = otherAttrs[attr]; + } + checks['center'] = this.center.contains(attr, key); + checks['normal'] = this.normal.contains(attr, key); + checks['point1'] = this.point1.contains(attr, key); + checks['point2'] = this.point2.contains(attr, key); + checks['point3'] = this.point3.contains(attr, key); + if(['x', 'y', 'z'].includes(attr)) { + if(pointCheck !== null) { + return checks[pointCheck] + } + let points = ['center', 'normal', 'point1', 'point2', 'point3']; + for(let attribute in points) { + if(checks[attribute]) { return true; } + } + return false; + } + return checks[attr]; + } + for(let attribute in checks) { + if(checks[attribute]) { return true; } + } + return false; + }, + setPointCheck: function (attr) { + if(attr.includes('center')) { return "center"; } + if(attr.includes('normal')) { return "normal"; } + if(attr.includes('point1')) { return "point1"; } + if(attr.includes('point2')) { return "point2"; } + if(attr.includes('point3')) { return "point3"; } + return null; + }, + validate: function () { + if((!/^[a-zA-Z0-9_]+$/.test(this.name))) return false; + return true; + } +}); \ No newline at end of file diff --git a/client/models/transformations.js b/client/models/transformations.js new file mode 100644 index 0000000000..3bf5ca2afe --- /dev/null +++ b/client/models/transformations.js @@ -0,0 +1,70 @@ +/* +StochSS is a platform for simulating biochemical systems +Copyright (C) 2019-2022 StochSS developers. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +let _ = require('underscore'); +//models +let Point = require('./point'); +let Transformation = require('./transformation'); +//collections +let Collection = require('ampersand-collection'); + +module.exports = Collection.extend({ + model: Transformation, + indexes: ['name'], + addTransformation: function (type) { + let name = this.getDefaultName(); + let transformation = new Transformation({ + angle: 0, + factor: 1, + name: name, + transformation: "", + type: type, + }); + transformation.selected = true; + transformation.vector = this.getNewVector(); + transformation.center = this.getNewPoint(); + transformation.normal = this.getNewPoint(); + transformation.point1 = this.getNewPoint(); + transformation.point2 = this.getNewPoint(); + transformation.point3 = this.getNewPoint(); + this.add(transformation); + return name; + }, + getDefaultName: function () { + var i = this.length + 1; + var name = 'transformation' + i; + var names = this.map(function (transformation) {return transformation.name; }); + while(_.contains(names, name)) { + i += 1; + name = 'transformation' + i; + } + return name; + }, + getNewPoint: function () { + return new Point({x: 0, y: 0, z: 0}); + }, + getNewVector: function () { + return [this.getNewPoint(), this.getNewPoint()]; + }, + removeTransformation: function (transformation) { + this.remove(transformation); + }, + validateCollection: function () { + return true; + } +}); diff --git a/client/models/user-settings.js b/client/models/user-settings.js new file mode 100644 index 0000000000..44cc65bc3d --- /dev/null +++ b/client/models/user-settings.js @@ -0,0 +1,47 @@ +/* +StochSS is a platform for simulating biochemical systems +Copyright (C) 2019-2022 StochSS developers. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +let path = require('path'); +//support files +let app = require('../app'); +//models +let Model = require('ampersand-model'); + +module.exports = Model.extend({ + url: function () { + return path.join(app.getApiPath(), "user-settings"); + }, + props: { + awsAccessKeyID: 'string', + awsRegion: 'string', + headNode: 'string', + userLogs: 'boolean' + }, + session: { + awsHeadNodeStatus: 'string', + awsSecretKey: 'string', + modelLoaded: 'boolean' + }, + applySettings: function (cb, {secretKey=null}={}) { + let data = { + settings: this.toJSON(), + secret_key: secretKey + } + app.postXHR(this.url(), data, { success: cb }); + } +}); diff --git a/client/models/workflow-group.js b/client/models/workflow-group.js index b1e86e8e92..73acf20ddb 100644 --- a/client/models/workflow-group.js +++ b/client/models/workflow-group.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/models/workflow-groups.js b/client/models/workflow-groups.js index c64bb2dbec..1edd573b48 100644 --- a/client/models/workflow-groups.js +++ b/client/models/workflow-groups.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/models/workflow.js b/client/models/workflow.js index b82d02340f..e84f875fe2 100644 --- a/client/models/workflow.js +++ b/client/models/workflow.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/models/workflows.js b/client/models/workflows.js index d783c5e3c0..8be760cffb 100644 --- a/client/models/workflows.js +++ b/client/models/workflows.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/page-help.js b/client/page-help.js index 5fdd5407dc..8f48813aee 100644 --- a/client/page-help.js +++ b/client/page-help.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/pages/base.js b/client/pages/base.js index c588e1ef5f..9377244414 100644 --- a/client/pages/base.js +++ b/client/pages/base.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/pages/browser.js b/client/pages/browser.js index 463af56413..9f22e6ee74 100644 --- a/client/pages/browser.js +++ b/client/pages/browser.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/pages/domain-editor.js b/client/pages/domain-editor.js index 9a489a61e9..f8a6e64af8 100644 --- a/client/pages/domain-editor.js +++ b/client/pages/domain-editor.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -29,6 +29,7 @@ let DomainView = require('../domain-view/domain-view'); let PageView = require('../pages/base'); //templates let template = require('../templates/pages/domainEditor.pug'); +let errorTemplate = require('../templates/pages/errorTemplate.pug'); import initPage from './page.js'; @@ -64,6 +65,12 @@ let DomainEditor = PageView.extend({ this.domain.dirname = newDomain && !modelPath ? domainPath : null this.model = this.buildModel(body.model, modelPath); this.renderSubviews(); + }, + error: (err, response, body) => { + this.title = `${response.statusCode} ${body.Reason.toUpperCase()}`; + this.errMsg = body.Message; + this.logoPath = "/static/stochss-logo.png"; + this.renderErrorView(); } }); }, @@ -172,6 +179,10 @@ let DomainEditor = PageView.extend({ }); app.registerRenderSubview(this, this.domainView, "domain-view-container"); }, + renderErrorView: function () { + this.template = errorTemplate; + PageView.prototype.render.apply(this, arguments); + }, renderSubviews: function () { let breadData = this.getBreadcrumbData(); if(breadData.model && breadData.project) { diff --git a/client/pages/example-library.js b/client/pages/example-library.js index 6f0b74b4ff..be644fb069 100644 --- a/client/pages/example-library.js +++ b/client/pages/example-library.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/pages/home.js b/client/pages/home.js index 0dc762a273..d0c9d62aee 100644 --- a/client/pages/home.js +++ b/client/pages/home.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/pages/job-presentation.js b/client/pages/job-presentation.js index b59e979bc7..ad0c95aa82 100644 --- a/client/pages/job-presentation.js +++ b/client/pages/job-presentation.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -57,6 +57,9 @@ let JobPresentationPage = PageView.extend({ this.renderSubviews(false); }, error: (err, response, body) => { + this.logoPath = "/hub/static/stochss-logo.png"; + this.title = "404 PAGE NOT FOUND"; + this.errMsg = `This ${this.fileType} presentation was removed by the author and is no longer available.`; this.renderSubviews(true); } }); diff --git a/client/pages/loading-page.js b/client/pages/loading-page.js index 70eb4b8891..17ba1bddae 100644 --- a/client/pages/loading-page.js +++ b/client/pages/loading-page.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -22,9 +22,9 @@ let path = require('path'); let app = require('../app'); let modals = require('../modals'); // views -var PageView = require('./base'); +let PageView = require('./base'); // templates -var template = require('../templates/pages/loadingPage.pug'); +let template = require('../templates/pages/loadingPage.pug'); import initPage from './page.js'; @@ -39,7 +39,6 @@ let LoadingPage = PageView.extend({ }, render: function (attrs, options) { PageView.prototype.render.apply(this, arguments); - let self = this; $(document.querySelector("div[data-hook=side-navbar]")).css("display", "none"); $(document.querySelector("main[data-hook=page-main]")).removeClass().addClass("col-md-12 body"); $(this.queryByHook("loading-spinner")).css("display", "block"); @@ -56,75 +55,73 @@ let LoadingPage = PageView.extend({ $(this.queryByHook("loading-target")).css("display", "none") let message = `If the file is a Project, Workflow, Model, Domain, or Notebook it will be opened when the upload has completed.`; $(this.queryByHook("loading-message")).html(message); - let self = this; - let queryStr = "?path=" + filePath + "&cmd=validate"; + let queryStr = `?path=${filePath}&cmd=validate`; let endpoint = path.join(app.getApiPath(), 'file/upload-from-link') + queryStr; app.getXHR(endpoint, { - success: function (err, response, body) { + success: (err, response, body) => { if(!body.exists) { - self.uploadFileFromLink(filePath, false); + this.uploadFileFromLink(filePath, false); }else{ let title = "File Already Exists"; let message = "A file with that name already exists, do you wish to overwrite this file?"; let modal = $(modals.uploadFileExistsHtml(title, message)).modal(); let yesBtn = document.querySelector("#uploadFileExistsModal .yes-modal-btn"); let noBtn = document.querySelector("#uploadFileExistsModal .btn-secondary") - yesBtn.addEventListener('click', function (e) { + yesBtn.addEventListener('click', (e) => { modal.modal('hide'); - self.uploadFileFromLink(filePath, true); + this.uploadFileFromLink(filePath, true); }); - noBtn.addEventListener('click', function (e) { - window.location.href = self.homeLink; + noBtn.addEventListener('click', (e) => { + window.location.href = this.homeLink; }); } }, - error: function(err, response, body) { + error: (err, response, body) => { if(document.querySelector("#errorModal")) { document.querySelector("#errorModal").remove(); } - $(self.queryByHook("loading-spinner")).css("display", "none"); + $(this.queryByHook("loading-spinner")).css("display", "none"); let modal = $(modals.errorHtml(body.Reason, body.Message)).modal(); - modal.on('hidden.bs.modal', function (e) { - window.location.href = self.homeLink; + modal.on('hidden.bs.modal', (e) => { + window.location.href = this.homeLink; }); } }); }, getUploadResponse: function () { - let self = this; - setTimeout(function () { - let queryStr = "?path=" + self.responsePath + "&cmd=read"; + setTimeout(() => { + let queryStr = `?path=${this.responsePath}&cmd=read`; let endpoint = path.join(app.getApiPath(), 'file/upload-from-link') + queryStr; - let errorCB = function (err, response, body) { + let errorCB = (err, response, body) => { if(document.querySelector("#errorModal")) { document.querySelector("#errorModal").remove(); } - $(self.queryByHook("loading-spinner")).css("display", "none"); + $(this.queryByHook("loading-spinner")).css("display", "none"); let modal = $(modals.errorHtml(body.reason, body.message)).modal(); - modal.on('hidden.bs.modal', function (e) { - window.location.href = self.homeLink; + modal.on('hidden.bs.modal', (e) => { + window.location.href = this.homeLink; }); } app.getXHR(endpoint, { - success: function (err, response, body) { + success: (err, response, body) => { if(Object.keys(body).includes("reason")) { errorCB(err, response, body); }else if(body.done) { if(body.file_path.endsWith(".proj")){ - self.openStochSSPage("stochss/project/manager", body.file_path); + this.openStochSSPage("stochss/project/manager", body.file_path); }else if(body.file_path.endsWith(".wkfl")){ - self.openStochSSPage("stochss/workflow/edit", body.file_path); + this.openStochSSPage("stochss/workflow/edit", body.file_path); }else if(body.file_path.endsWith(".mdl")){ - self.openStochSSPage("stochss/models/edit", body.file_path); + this.openStochSSPage("stochss/models/edit", body.file_path); }else if(body.file_path.endsWith(".domn")){ - self.openStochSSPage("stochss/domain/edit", body.file_path); + this.openStochSSPage("stochss/domain/edit", body.file_path); }else if(body.file_path.endsWith(".ipynb")){ - self.openNotebookFile(body.file_path); + this.openNotebookFile(body.file_path); }else{ - self.openStochSSPage('stochss/files'); + this.openStochSSPage('stochss/files'); } }else{ - self.getUploadResponse(); + this.getUploadResponse(); } }, error: errorCB @@ -147,13 +144,12 @@ let LoadingPage = PageView.extend({ $(this.queryByHook("loading-header")).html("Updating Format"); $(this.queryByHook("loading-target")).html(filePath.split('/').pop()); $(this.queryByHook("loading-message")).html(message); - let self = this; - let queryStr = "?path=" + filePath; + let queryStr = `?path=${filePath}`; let endpoint = path.join(app.getApiPath(), target, "update-format") + queryStr; app.getXHR(endpoint, { - success: function (err, response, body) { + success: (err, response, body) => { let dst = target === "workflow" ? body : filePath; - self.openStochSSPage(identifier, dst); + this.openStochSSPage(identifier, dst); } }); }, @@ -168,19 +164,18 @@ let LoadingPage = PageView.extend({ this.updateFormat(filePath, message, "workflow", identifier); }, uploadFileFromLink: function (filePath, overwrite) { - setTimeout(function () { - $(self.queryByHook("loading-problem").css("display", "block")); + setTimeout(() => { + $(this.queryByHook("loading-problem")).css("display", "block"); }, 30000); - let self = this; - var queryStr = "?path=" + filePath; + var queryStr = `?path=${filePath}`; if(overwrite) { - queryStr += "&overwrite=" + overwrite; + queryStr += `&overwrite=${overwrite}`; } let endpoint = path.join(app.getApiPath(), 'file/upload-from-link') + queryStr; app.getXHR(endpoint, { - success: function (err, response, body) { - self.responsePath = body.responsePath; - self.getUploadResponse(); + success: (err, response, body) => { + this.responsePath = body.responsePath; + this.getUploadResponse(); } }); } diff --git a/client/pages/model-editor.js b/client/pages/model-editor.js index ab48e71a30..8fd76438b9 100644 --- a/client/pages/model-editor.js +++ b/client/pages/model-editor.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -138,7 +138,10 @@ let ModelEditor = PageView.extend({ }, 5000); }, focusOnError: function (e) { - let mdlSections = ["species", "parameter", "reaction", "process", "event", "rule", "volume", "domain"]; + let mdlSections = [ + "species", "parameter", "reaction", "process", "event", + "rule", "volume", "domain", "initialCondition" + ]; if(this.model.error) { if(this.model.error.type === "timespan") { this.openTimespanSection(); @@ -148,6 +151,7 @@ let ModelEditor = PageView.extend({ setTimeout(() => { let inputErrors = this.queryAll(".input-invalid"); let componentErrors = this.queryAll(".component-invalid"); + console.log(inputErrors, componentErrors) if(componentErrors.length > 0) { componentErrors[0].scrollIntoView({'block':"center"}); }else if(inputErrors.length > 0) { @@ -251,6 +255,7 @@ let ModelEditor = PageView.extend({ }, handleSimulateClick: function (e) { let errorMsg = $(this.queryByHook("error-detected-msg")); + this.model.updateValid(); if(!this.model.valid) { this.displayError(errorMsg, e); }else{ @@ -303,7 +308,7 @@ let ModelEditor = PageView.extend({ tspnCollapseBtn.click(); tspnCollapseBtn.html('-'); } - this.switchToEditTab(this.modelSettings, "timespan"); + app.switchToEditTab(this.modelSettings, "timespan"); }, plotResults: function (data) { this.ran(true); diff --git a/client/pages/model-presentation.js b/client/pages/model-presentation.js index fe3d43eac3..2c8fb413dd 100644 --- a/client/pages/model-presentation.js +++ b/client/pages/model-presentation.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -24,6 +24,7 @@ let bootstrap = require('bootstrap'); let app = require('../app'); //models let Model = require('../models/model'); +let Particles = require('../models/particles'); //views let PageView = require('./base'); let ModelView = require('../model-view/model-view'); @@ -52,11 +53,17 @@ let ModelPresentationPage = PageView.extend({ app.getXHR(endpoint, { success: (err, response, body) => { this.model.set(body.model); + if(Object.keys(body.model.domain).includes("particles")) { + let particles = new Particles(body.model.domain.particles); + this.model.domain.particles = particles; + } let domainPlot = Boolean(body.domainPlot) ? body.domainPlot : null; this.renderSubviews(false, domainPlot); }, error: (err, response, body) => { - this.notFound = true; + this.logoPath = "/hub/static/stochss-logo.png"; + this.title = "404 PAGE NOT FOUND"; + this.errMsg = `This ${this.fileType} presentation was removed by the author and is no longer available.`; this.renderSubviews(true); } }); diff --git a/client/pages/multiple-plots.js b/client/pages/multiple-plots.js index 908e86c0cb..48e1508cde 100644 --- a/client/pages/multiple-plots.js +++ b/client/pages/multiple-plots.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/pages/notebook-presentation.js b/client/pages/notebook-presentation.js index 7553762842..35714cc268 100644 --- a/client/pages/notebook-presentation.js +++ b/client/pages/notebook-presentation.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -41,16 +41,18 @@ let NotebookPresentationPage = PageView.extend({ let owner = urlParams.get("owner"); let file = urlParams.get("file"); this.fileType = "Notebook"; - let self = this; let queryStr = "?owner=" + owner + "&file=" + file; let endpoint = "api/notebook/load" + queryStr; app.getXHR(endpoint, { - success: function (err, response, body) { - self.name = body.file.split('/').pop().split('.ipynb')[0]; - self.renderSubviews(false, body.html); + success: (err, response, body) => { + this.name = body.file.split('/').pop().split('.ipynb')[0]; + this.renderSubviews(false, body.html); }, - error: function (err, response, body) { - self.renderSubviews(true, null); + error: (err, response, body) => { + this.logoPath = "/hub/static/stochss-logo.png"; + this.title = "404 PAGE NOT FOUND"; + this.errMsg = `This ${this.fileType} presentation was removed by the author and is no longer available.`; + this.renderSubviews(true, null); } }); let downloadStart = "/stochss/notebook/download_presentation"; diff --git a/client/pages/page.js b/client/pages/page.js index a91d6a2bcf..e8855e89a5 100644 --- a/client/pages/page.js +++ b/client/pages/page.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/pages/project-manager.js b/client/pages/project-manager.js index e2122d1060..2346697474 100644 --- a/client/pages/project-manager.js +++ b/client/pages/project-manager.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/pages/quickstart.js b/client/pages/quickstart.js index fea0c53566..2ca96e3b8e 100644 --- a/client/pages/quickstart.js +++ b/client/pages/quickstart.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/pages/user-settings.js b/client/pages/user-settings.js new file mode 100644 index 0000000000..40be934556 --- /dev/null +++ b/client/pages/user-settings.js @@ -0,0 +1,297 @@ +/* +StochSS is a platform for simulating biochemical systems +Copyright (C) 2019-2022 StochSS developers. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +let $ = require('jquery'); +let path = require('path'); +// support files +let app = require('../app'); +let tests = require('../views/tests'); +// models +let Settings = require('../models/user-settings'); +// views +let PageView = require('./base'); +let InputView = require('../views/input'); +let SelectView = require('ampersand-select-view'); +// templates +let template = require('../templates/pages/userSettings.pug'); + +import initPage from './page.js'; + +let userSettings = PageView.extend({ + template: template, + events: { + 'change [data-hook=user-logs]' : 'toggleUserLogs', + 'change [data-target=aws-credentials]' : 'toggleAWSComputeNodeSection', + 'change [data-hook=aws-secretaccesskey-container]' : 'handleSetSecretKey', + 'change [data-hook=aws-instancetype-container]' : 'handleSelectInstanceType', + 'change [data-hook=aws-instancesize-container]' : 'handleSelectInstanceSize', + 'click [data-hook=refresh-aws-status]' : 'handleRefreshAWSStatus', + 'click [data-hook=launch-aws-cluster]' : 'handleLaunchAWSCluster', + 'click [data-hook=terminate-aws-cluster]' : 'handleTerminateAWSCluster', + 'click [data-hook=apply-user-settings]' : 'handleApplyUserSettings' + }, + initialize: function (attrs, options) { + PageView.prototype.initialize.apply(this, arguments); + let urlParams = new URLSearchParams(window.location.search); + this.path = urlParams.has('continue') ? urlParams.get('continue') : null; + this.model = new Settings(); + this.secretKey = null; + app.getXHR(this.model.url(), { + success: (err, response, body) => { + this.model.set(body.settings); + this.model.modelLoaded = true; + this.instances = body.instances; + if(this.model.headNode === "") { + this.awsType = ""; + this.awsSize = ""; + }else{ + let data = this.model.headNode.split('.') + this.awsType = data[0]; + this.awsSize = data[1]; + this.renderAWSInstanceSizesView(); + } + $(this.queryByHook('user-logs')).prop('checked', this.model.userLogs); + this.renderAWSInstanceTypesView(); + this.toggleAWSComputeNodeSection(); + this.refreshAWSStatus(); + } + }); + }, + render: function (attrs, options) { + PageView.prototype.render.apply(this, arguments); + if(this.path !== null) { + $(this.queryByHook('aws-config-msg')).css('display', 'block'); + } + }, + completeAction: function () { + $(this.queryByHook("usa-in-progress")).css("display", "none"); + $(this.queryByHook("usa-complete")).css("display", "inline-block"); + setTimeout(() => { + $(this.queryByHook("usa-complete")).css("display", "none"); + }, 5000); + }, + disables: function (btnType, status) { + let disables = { + instance: !['not configured', 'not launched', 'terminated'].includes(status), + launch: !['not launched', 'terminated'].includes(status), + refresh: status === "not configured", + terminate: ['not configured', 'not launched', 'terminated'].includes(status) + } + return disables[btnType]; + }, + errorAction: function (action) { + $(this.queryByHook("usa-in-progress")).css("display", "none"); + $(this.queryByHook("usa-action-error")).text(action); + $(this.queryByHook("usa-error")).css("display", "block"); + }, + handleApplyUserSettings: function ({cb=null}={}) { + this.startAction(); + if(cb === null) { + if(this.path === null) { + cb = () => { + this.completeAction(); + this.refreshAWSStatus(); + } + }else{ + cb = () => { + window.location.href = this.path; + } + } + } + let options = this.secretKey !== null ? {secretKey: this.secretKey} : {}; + this.model.applySettings(cb, options); + }, + handleLaunchAWSCluster: function () { + this.handleApplyUserSettings({cb: () => { + this.completeAction(); + this.launchAWSCluster(); + }}); + }, + handleRefreshAWSStatus: function () { + this.handleApplyUserSettings({cb: () => { + this.completeAction(); + this.refreshAWSStatus(); + }}); + }, + handleSelectInstanceSize: function (e) { + this.awsSize = e.target.value; + this.model.headNode = this.awsSize === "" ? "" : `${this.awsType}.${this.awsSize}`; + this.updateAWSStatus(); + }, + handleSelectInstanceType: function (e) { + this.awsType = e.target.value; + this.awsSize = ""; + this.model.headNode = ""; + this.renderAWSInstanceSizesView(); + this.updateAWSStatus(); + }, + handleSetSecretKey: function (e) { + this.secretKey = e.target.value; + this.model.awsSecretKey = this.secretKey ? "set" : null; + this.toggleAWSComputeNodeSection(); + }, + handleTerminateAWSCluster: function () { + this.handleApplyUserSettings({cb: () => { + this.completeAction(); + this.terminateAWSCluster(); + }}); + }, + launchAWSCluster: function () { + let endpoint = path.join(app.getApiPath(), 'aws/launch-cluster'); + app.getXHR(endpoint, { + success: (err, response, body) => { + this.model.awsHeadNodeStatus = body.settings.awsHeadNodeStatus; + this.updateAWSStatus(); + } + }); + }, + refreshAWSStatus: function () { + if(this.model.headNode === "") { return } + let endpoint = path.join(app.getApiPath(), 'aws/cluster-status'); + app.getXHR(endpoint, { + success: (err, response, body) => { + this.model.awsHeadNodeStatus = body.settings.awsHeadNodeStatus; + this.updateAWSStatus(); + } + }); + }, + renderAWSInstanceSizesView: function () { + if(this.awsInstanceSizesView) { + this.awsInstanceSizesView.remove(); + } + if(this.awsType === "") { return } + let options = this.instances[this.awsType]; + this.awsInstanceSizesView = new SelectView({ + name: 'aws-instance-size', + required: false, + idAttributes: 'cid', + options: options, + value: this.awsSize, + unselectedText: "-- Select Instance Size --" + }); + let hook = "aws-instancesize-container"; + app.registerRenderSubview(this, this.awsInstanceSizesView, hook); + }, + renderAWSInstanceTypesView: function () { + if(this.awsInstanceTypesView) { + this.awsInstanceTypesView.remove(); + } + let options = Object.keys(this.instances); + this.awsInstanceTypesView = new SelectView({ + name: 'aws-instance-type', + required: false, + idAttributes: 'cid', + options: options, + value: this.awsType, + unselectedText: "-- Select Instance Type --" + }); + let hook = "aws-instancetype-container"; + app.registerRenderSubview(this, this.awsInstanceTypesView, hook); + }, + startAction: function () { + $(this.queryByHook("usa-complete")).css("display", "none"); + $(this.queryByHook("usa-error")).css("display", "none"); + $(this.queryByHook("usa-in-progress")).css("display", "inline-block"); + }, + terminateAWSCluster: function () { + let endpoint = path.join(app.getApiPath(), 'aws/terminate-cluster'); + app.getXHR(endpoint, { + success: (err, response, body) => { + this.model.awsHeadNodeStatus = body.settings.awsHeadNodeStatus; + this.updateAWSStatus(); + } + }); + }, + toggleAWSComputeNodeSection: function () { + let regionSet = this.model.awsRegion !== ""; + let accessKeySet = this.model.awsAccessKeyID !== ""; + let secretKeySet = this.model.awsSecretKey !== null; + let display = regionSet && accessKeySet && secretKeySet ? "block" : "none"; + $(this.queryByHook('aws-compute-node-section')).css('display', display); + if(display === "block") { + this.updateAWSStatus(); + } + }, + toggleUserLogs: function (e) { + this.model.userLogs = e.target.checked; + }, + update: function () {}, + updateAWSStatus: function () { + let awsStatus = this.model.headNode === "" ? "not configured" : this.model.awsHeadNodeStatus; + $(this.queryByHook('aws-instancetype-container').firstChild.children[1]).prop( + 'disabled', this.disables('instance', awsStatus) + ); + if(this.awsType !== "") { + $(this.queryByHook('aws-instancesize-container').firstChild.children[1]).prop( + 'disabled', this.disables('instance', awsStatus) + ); + } + $(this.queryByHook('refresh-user-settings')).prop('disabled', this.disables('refresh', awsStatus)); + $(this.queryByHook('aws-headnode-status')).text(awsStatus); + $(this.queryByHook('launch-aws-cluster')).prop('disabled', this.disables('launch', awsStatus)); + $(this.queryByHook('terminate-aws-cluster')).prop('disabled', this.disables('terminate', awsStatus)); + }, + updateValid: function () {}, + subviews: { + awsRegionInputView: { + hook: 'aws-region-container', + waitFor: 'model.modelLoaded', + prepareView: function (el) { + return new InputView({ + parent: this, + required: false, + name: 'aws-region', + modelKey: 'awsRegion', + valueType: 'string', + value: this.model.awsRegion, + placeholder: "-- i.e. us-east-2 --" + }); + } + }, + awsAccessKeyID: { + hook: 'aws-accesskeyid-container', + waitFor: 'model.modelLoaded', + prepareView: function (el) { + return new InputView({ + parent: this, + required: false, + name: 'aws-access-key-id', + modelKey: 'awsAccessKeyID', + valueType: 'string', + value: this.model.awsAccessKeyID + }); + } + }, + awsSecretAccessKey: { + hook: 'aws-secretaccesskey-container', + waitFor: 'model.modelLoaded', + prepareView: function (el) { + return new InputView({ + parent: this, + type: 'password', + required: false, + name: 'aws-secret-access-key', + valueType: 'string', + value: this.model.awsSecretKey + }); + } + } + } +}); + +initPage(userSettings); diff --git a/client/pages/users-home.js b/client/pages/users-home.js index b099ad41e5..acda68093c 100644 --- a/client/pages/users-home.js +++ b/client/pages/users-home.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/pages/workflow-manager.js b/client/pages/workflow-manager.js index 07acc47fd8..63b6d16b60 100644 --- a/client/pages/workflow-manager.js +++ b/client/pages/workflow-manager.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -46,7 +46,7 @@ let WorkflowManager = PageView.extend({ 'click [data-hook=save-model]' : 'handleSaveWorkflow', 'click [data-hook=collapse-settings]' : 'changeCollapseButtonText', 'click [data-hook=save]' : 'clickSaveHandler', - 'click [data-hook=start-job]' : 'clickStartJobHandler', + 'click [data-target=start-job]' : 'clickStartJobHandler', 'click [data-hook=edit-model]' : 'clickEditModelHandler', 'click [data-hook=collapse-jobs]' : 'changeCollapseButtonText', 'click [data-hook=return-to-project-btn]' : 'handleReturnToProject' @@ -91,7 +91,7 @@ let WorkflowManager = PageView.extend({ app.changeCollapseButtonText(this, e); }, clickEditModelHandler: function (e) { - this.handleSaveWorkflow(e, _.bind(function () { + this.handleSaveWorkflow(_.bind(function () { let queryStr = "?path=" + this.model.model; let endpoint = path.join(app.getBasePath(), "stochss/models/edit") + queryStr; window.location.href = endpoint; @@ -99,33 +99,28 @@ let WorkflowManager = PageView.extend({ }, clickSaveHandler: function (e) { this.saving(); - this.handleSaveWorkflow(e, _.bind(this.saved, this)); + this.handleSaveWorkflow(_.bind(this.saved, this)); }, clickStartJobHandler: function (e) { this.saving(); - let types = { - "Ensemble Simulation": "gillespy", - "Parameter Sweep": "parameterSweep", - "Spatial Ensemble Simulation": "spatial" - }; - let data = { - "settings": this.model.settings.toJSON(), "mdl_path": this.model.model, - "type": types[this.model.type], "time_stamp": this.getTimeStamp() - }; - let queryStr = `?path=${this.model.directory}&data=${JSON.stringify(data)}`; - let initEndpoint = `${path.join(app.getApiPath(), "workflow/init-job")}${queryStr}`; - app.getXHR(initEndpoint, { - success: (err, response, body) => { - this.saved(); - let runQuery = `?path=${body}&type=${data.type}`; - let runEndpoint = `${path.join(app.getApiPath(), "workflow/run-job")}${runQuery}`; - app.getXHR(runEndpoint, { - success: (err, response, body) => { - this.updateWorkflow(true); - } - }); - } - }); + if(e.target.dataset['computeenv'] === "local") { + this.startJob() + }else if(e.target.dataset['computeenv'] === "aws") { + let endpoint = path.join(app.getApiPath(), 'aws/job-config-check'); + app.getXHR(endpoint, { + success: (err, response, body) => { + this.startJob({compute: "AWS"}); + }, + error: (err, response, body) => { + this.handleSaveWorkflow(() => { + this.saved(); + let contEndpoint = `${path.join(app.getBasePath(), 'stochss/workflow/edit')}${window.location.search}`; + let endpoint = `${path.join(app.getBasePath(), 'stochss/settings')}?continue=${contEndpoint}`; + window.location.href = endpoint; + }); + } + }); + } }, getTimeStamp: function () { if(!this.model.newFormat) { @@ -178,7 +173,7 @@ let WorkflowManager = PageView.extend({ }, handleReturnToProject: function (e) { if(this.model.activeJob.model.directory){ - this.handleSaveWorkflow(e, _.bind(function () { + this.handleSaveWorkflow(_.bind(function () { let queryStr = "?path=" + this.projectPath; let endpoint = path.join(app.getBasePath(), "stochss/project/manager") + queryStr; window.location.href = endpoint; @@ -189,18 +184,16 @@ let WorkflowManager = PageView.extend({ window.location.href = endpoint; } }, - handleSaveWorkflow: function (e, cb) { - let self = this; - let endpoint = this.model.url(); - app.postXHR(endpoint, this.model.toJSON(), { - success: function (err, response, body) { + handleSaveWorkflow: function (cb) { + app.postXHR(this.model.url(), this.model.toJSON(), { + success: (err, response, body) => { if(cb) { cb(); }else{ - $(self.queryByHook("src-model")).css("display", "none"); - let oldFormRdyState = !self.model.newFormat && self.model.activeJob.status === "ready"; - if(oldFormRdyState || self.model.newFormat) { - self.setupSettingsView(); + $(this.queryByHook("src-model")).css("display", "none"); + let oldFormRdyState = !this.model.newFormat && this.model.activeJob.status === "ready"; + if(oldFormRdyState || this.model.newFormat) { + this.setupSettingsView(); } } } @@ -313,10 +306,12 @@ let WorkflowManager = PageView.extend({ this.renderActiveJob(); }, setupSettingsView: function () { - if(this.model.newFormat) { - $(this.queryByHook("start-job")).text("Start New Job"); + if(!this.model.newFormat) { + $(this.queryByHook("of-start-job")).css('display', 'inline-block'); + $(this.queryByHook("nf-start-job")).css('display', 'none'); } if(this.model.type === "Parameter Sweep"){ + $(this.queryByHook("aws-start-job")).addClass("disabled") if(this.model.settings.parameterSweepSettings.parameters.length < 1) { $(this.queryByHook("start-job")).prop("disabled", true); } @@ -345,6 +340,32 @@ let WorkflowManager = PageView.extend({ this.renderSettingsView(options); } }, + startJob: function ({compute="Local"}={}) { + let types = { + "Ensemble Simulation": "gillespy", + "Parameter Sweep": "parameterSweep", + "Spatial Ensemble Simulation": "spatial" + }; + let data = { + "settings": this.model.settings.toJSON(), "mdl_path": this.model.model, + "type": types[this.model.type], "time_stamp": this.getTimeStamp(), + "compute": compute + }; + let queryStr = `?path=${this.model.directory}&data=${JSON.stringify(data)}`; + let initEndpoint = `${path.join(app.getApiPath(), "workflow/init-job")}${queryStr}`; + app.getXHR(initEndpoint, { + success: (err, response, body) => { + this.saved(); + let runQuery = `?path=${body}&type=${data.type}`; + let runEndpoint = `${path.join(app.getApiPath(), "workflow/run-job")}${runQuery}`; + app.getXHR(runEndpoint, { + success: (err, response, body) => { + this.updateWorkflow(true); + } + }); + } + }); + }, updateWorkflow: function (newJob) { let self = this; if(this.model.newFormat) { diff --git a/client/pages/workflow-selection.js b/client/pages/workflow-selection.js index c334af64f4..6752d15993 100644 --- a/client/pages/workflow-selection.js +++ b/client/pages/workflow-selection.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -24,6 +24,7 @@ var app = require('../app'); let Model = require('../models/model'); //views let PageView = require('./base'); +let SelectView = require('ampersand-select-view'); //templates let template = require('../templates/pages/workflowSelection.pug'); @@ -32,6 +33,7 @@ import initPage from './page.js'; let workflowSelection = PageView.extend({ template: template, events: { + "change [data-hook=compute-env-select]" : "handleSelectComputeEnv", "click [data-hook=ensemble-simulation]" : "notebookWorkflow", "click [data-hook=spatial-simulation]" : "notebookWorkflow", "click [data-hook=oned-parameter-sweep]" : "notebookWorkflow", @@ -41,7 +43,6 @@ let workflowSelection = PageView.extend({ }, initialize: function (attrs, options) { PageView.prototype.initialize.apply(this, arguments); - let self = this; let urlParams = new URLSearchParams(window.location.search); this.modelDir = urlParams.get('path'); if(urlParams.has('parentPath')){ @@ -57,33 +58,37 @@ let workflowSelection = PageView.extend({ this.projectName = this.projectPath.split('/').pop().split('.proj')[0]; this.workflowGroupName = this.parentPath.split('/').pop().split('.wkgp')[0]; } + this.computeEnv = "StochSS"; this.model = new Model({ directory: this.modelDir, isPreview: false, for: "wkfl", }); app.getXHR(this.model.url(), { - success: function (err, response, body) { - self.model.set(body) - $(self.queryByHook("wkfl-selection-header")).text("Workflow Selection for " + self.model.name); - if(self.modelDir.includes(".proj")) { - self.queryByHook("workflow-selection-breadcrumb-links").style.display = "block"; + success: (err, response, body) => { + this.model.set(body) + $(this.queryByHook("wkfl-selection-header")).text(`Workflow Selection for ${this.model.name}`); + if(this.modelDir.includes(".proj")) { + this.queryByHook("workflow-selection-breadcrumb-links").style.display = "block"; } - self.validateWorkflows() + this.validateWorkflows() } }); }, + handleSelectComputeEnv: function (e) { + this.computeEnv = e.target.value; + }, notebookWorkflow: function (e) { let type = e.target.dataset.type; - let queryString = "?type=" + type + "&path=" + this.modelDir + "&parentPath=" + this.parentPath - let endpoint = path.join(app.getApiPath(), "workflow/notebook") + queryString + let queryString = `?type=${type}&path=${this.modelDir}&parentPath=${this.parentPath}&compute=${this.computeEnv}`; + let endpoint = `${path.join(app.getApiPath(), "workflow/notebook")}${queryString}`; app.getXHR(endpoint, { - success: function (err, response, body) { + success: (err, response, body) => { let notebookPath = path.join(app.getBasePath(), "notebooks", body.FilePath) window.open(notebookPath, "_blank") window.history.back(); } - }) + }); }, validateWorkflows: function () { this.model.updateValid(); @@ -98,7 +103,7 @@ let workflowSelection = PageView.extend({ if(dimensions < 2) { $(this.queryByHook('psweep-workflow-message')).css('display', 'block'); if(dimensions === 1) { - $(this.queryByHook('psweep-workflow-message')).text('2D Parameter Sweep workflows require at least two parameters'); + $(this.queryByHook('psweep-workflow-message')).html('2D Parameter Sweep workflows require at least two parameters'); } } $(this.queryByHook('ensemble-simulation')).prop('disabled', invalid || this.model.is_spatial); @@ -107,6 +112,22 @@ let workflowSelection = PageView.extend({ $(this.queryByHook('oned-parameter-sweep')).prop('disabled', invalid || this.model.is_spatial || dimensions < 1); $(this.queryByHook('twod-parameter-sweep')).prop('disabled', invalid || this.model.is_spatial || dimensions < 2); $(this.queryByHook('sciope-model-exploration')).prop('disabled', invalid || this.model.is_spatial); + $(this.queryByHook('compute-env-select').firstChild.children[1]).prop('disabled', this.model.is_spatial); + }, + subviews: { + computeEnvSelect: { + hook: "compute-env-select", + prepareView: function (el) { + let options = ["StochSS", "AWS Cloud"]; + return new SelectView({ + name: 'compute-environment', + required: true, + idAttributes: 'cid', + options: options, + value: this.computeEnv + }); + } + } } }); diff --git a/client/project-config.js b/client/project-config.js index 3bfc24ab2c..939dfd068b 100644 --- a/client/project-config.js +++ b/client/project-config.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/reaction-types.js b/client/reaction-types.js index f8a2e3f8e4..672f53c309 100644 --- a/client/reaction-types.js +++ b/client/reaction-types.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/settings-view/settings-view.js b/client/settings-view/settings-view.js index 00b37ea448..0eb4c271f6 100644 --- a/client/settings-view/settings-view.js +++ b/client/settings-view/settings-view.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/settings-view/templates/timespanSettingsView.pug b/client/settings-view/templates/timespanSettingsView.pug index 8947e27f52..f45dd2d67d 100644 --- a/client/settings-view/templates/timespanSettingsView.pug +++ b/client/settings-view/templates/timespanSettingsView.pug @@ -52,35 +52,38 @@ div#preview-settings.card div.col-sm-3(data-hook="time-units") - div.col-sm-6 + div.col-sm-6.hidden(data-hook="timestep-size-container") - if this.isSpatial - div + div + + div.row + + div.col-sm-1 + + span.inline 1 - div.row + div.col-sm-8 - div.col-sm-1 - - span.inline 1 + span.mr-2.inline Current Value: - div.col-sm-8 + div.inline(data-hook="timestep-size-value") - span.mr-2.inline Current Value: + div.col-sm-3.d-flex.justify-content-end - div.inline(data-hook="timestep-size-value") + span.inline 1e-5 - div.col-sm-3.d-flex.justify-content-end + input.custom-range( + type="range" + min="0" + max="5" + step="1" + value=this.tssValue + data-hook="timestep-size-slider" + ) - span.inline 1e-5 + div(data-hook="timestep-size-error") - input.custom-range( - type="range" - min="0" - max="5" - step="1" - value=this.tssValue - data-hook="timestep-size-slider" - ) + p.text-danger 'Timestep Size' can't be greater than 'Sample Frequency'. div.tab-pane(id=this.model.elementID + "-view-timespan" data-hook=this.model.elementID + "-view-timespan") diff --git a/client/settings-view/views/parameter-settings-view.js b/client/settings-view/views/parameter-settings-view.js index eddad1520c..d69b7b92a4 100644 --- a/client/settings-view/views/parameter-settings-view.js +++ b/client/settings-view/views/parameter-settings-view.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/settings-view/views/spatial-settings-view.js b/client/settings-view/views/spatial-settings-view.js index 7850abaeff..edd84e50b7 100644 --- a/client/settings-view/views/spatial-settings-view.js +++ b/client/settings-view/views/spatial-settings-view.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/settings-view/views/sweep-parameter-view.js b/client/settings-view/views/sweep-parameter-view.js index 8e9df918fd..34e7dc65d1 100644 --- a/client/settings-view/views/sweep-parameter-view.js +++ b/client/settings-view/views/sweep-parameter-view.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/settings-view/views/timespan-settings-view.js b/client/settings-view/views/timespan-settings-view.js index f8e0de13f3..8dc37c42d2 100644 --- a/client/settings-view/views/timespan-settings-view.js +++ b/client/settings-view/views/timespan-settings-view.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -33,7 +33,7 @@ module.exports = View.extend({ 'click [data-hook=collapse]' : 'changeCollapseButtonText', 'input [data-hook=timestep-size-slider]' : 'viewTimestepValue', 'change [data-hook=preview-time]' : 'updateViewer', - 'change [data-hook=time-units]' : 'updateViewer', + 'change [data-hook=time-units]' : 'updateTimeStep', 'change [data-hook=timestep-size-slider]' : 'setTimestepSize' }, initialize: function (attrs, options) { @@ -55,8 +55,13 @@ module.exports = View.extend({ $(this.queryByHook(this.model.elementID + '-timespan-view-tab')).tab('show'); $(this.queryByHook(this.model.elementID + '-edit-timespan')).removeClass('active'); $(this.queryByHook(this.model.elementID + '-view-timespan')).addClass('active'); + }else{ + this.toggleTimestepSizeError(); + } + if(this.isSpatial) { + $(this.queryByHook("timestep-size-container")).css("display", "inline-block"); + $(this.queryByHook("timestep-size-value")).html(this.model.timestepSize); } - $(this.queryByHook("timestep-size-value")).html(this.model.timestepSize); }, changeCollapseButtonText: function (e) { app.changeCollapseButtonText(this, e); @@ -64,8 +69,23 @@ module.exports = View.extend({ setTimestepSize: function (e) { this.model.timestepSize = Number("1e-" + e.target.value); $(this.queryByHook("view-timestep-size")).html(this.model.timestepSize); + this.toggleTimestepSizeError(); + }, + toggleTimestepSizeError: function () { + let errorMsg = $(this.queryByHook("timestep-size-error")); + if(this.isSpatial && this.model.timestepSize > this.model.timeStep) { + errorMsg.addClass('component-invalid'); + errorMsg.removeClass('component-valid'); + }else{ + errorMsg.addClass('component-valid'); + errorMsg.removeClass('component-invalid'); + } }, update: function (e) {}, + updateTimeStep: function () { + this.updateViewer(); + this.toggleTimestepSizeError(); + }, updateValid: function () {}, updateViewer: function (e) { $(this.queryByHook("view-end-sim")).html("0 to " + this.model.endSim); diff --git a/client/settings-view/views/well-mixed-settings-view.js b/client/settings-view/views/well-mixed-settings-view.js index 0409cd14c5..1f93e4ec87 100644 --- a/client/settings-view/views/well-mixed-settings-view.js +++ b/client/settings-view/views/well-mixed-settings-view.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/styles/styles.css b/client/styles/styles.css index 3f3e7906a3..c6eb3bbd9d 100644 --- a/client/styles/styles.css +++ b/client/styles/styles.css @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -27,6 +27,10 @@ img.quickstart { margin: 2em auto; } +svg { + pointer-events: none; +} + .page { padding-bottom: 150px; } @@ -43,7 +47,7 @@ img.quickstart { .user-logs { white-space: nowrap; - max-height: 250px; + max-height: 335px; padding: 5px; margin-top: 10px; width: 135%; @@ -196,10 +200,25 @@ a.anchor { white-space: normal; } +.dropdown-menu li { + position: relative; +} + .dropdown-menu li:hover { cursor: pointer; } +.dropdown-menu .dropdown-submenu { + display: none; + position: absolute; + left: 100%; + top: -7px; +} + +.dropdown-menu > li:hover > .dropdown-submenu { + display: block; +} + .name { width: 8rem; } @@ -385,6 +404,10 @@ input[type="file"]::-ms-browse { color: #0056b3; } +.full-btn { + width: 100%; +} + #collapse-description { padding: 0rem 2rem; font-size: 12px; @@ -762,4 +785,67 @@ span.checkbox { .hidden { display: none; -} \ No newline at end of file +} + +.custom-range { + height: auto; +} + +.switch { + position: relative; + display: inline-block; + width: 52px; + height: 26px; +} + +.switch input { + opacity: 0; + width: 0; + height: 0; +} + +.slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + -webkit-transition: .4s; + transition: .4s; +} + +.slider:before { + position: absolute; + content: ""; + height: 20px; + width: 20px; + left: 4px; + bottom: 4px; + background-color: white; + -webkit-transition: .4s; + transition: .4s; +} + +input:checked + .slider { + background-color: #2196F3; +} + +input:focus + .slider { + box-shadow: 0 0 1px #2196F3; +} + +input:checked + .slider:before { + -webkit-transform: translateX(24px); + -ms-transform: translateX(24px); + transform: translateX(24px); +} + +.slider.round { + border-radius: 34px; +} + +.slider.round:before { + border-radius: 50%; +} diff --git a/client/templates/body.pug b/client/templates/body.pug index 4d4844508b..1456df92ce 100644 --- a/client/templates/body.pug +++ b/client/templates/body.pug @@ -8,27 +8,42 @@ body div#navbar.collapse.navbar-collapse.justify-content-end(role="navigation") ul.navbar-nav.float-right - li.nav-item: a.nav-link(href="http://www.stochss.org", target="_blank") - | About + li.nav-item: a.nav-link(href="http://www.stochss.org" title="About" target="_blank") + svg(xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 512 512") + path(d="M256 512c141.4 0 256-114.6 256-256S397.4 0 256 0S0 114.6 0 256S114.6 512 256 512zM216 336h24V272H216c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24H216c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-144c-17.7 0-32-14.3-32-32s14.3-32 32-32s32 14.3 32 32s-14.3 32-32 32z") - li.nav-item: a.nav-link(href="https://docs.google.com/forms/d/12tAH4f8CJ-3F-lK44Q9uQHFio_mGoK0oY829q5lD7i4/viewform?embedded=true", target="_blank") - | Register + li.nav-item: a.nav-link(href="https://docs.google.com/forms/d/12tAH4f8CJ-3F-lK44Q9uQHFio_mGoK0oY829q5lD7i4/viewform?embedded=true" title="Register" target="_blank") + svg(xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 384 512") + path(d="M101.5 64C114.6 26.7 150.2 0 192 0s77.4 26.7 90.5 64H320c35.3 0 64 28.7 64 64V448c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V128C0 92.7 28.7 64 64 64h37.5zM224 96c0-17.7-14.3-32-32-32s-32 14.3-32 32s14.3 32 32 32s32-14.3 32-32zM160 368c0 8.8 7.2 16 16 16H304c8.8 0 16-7.2 16-16s-7.2-16-16-16H176c-8.8 0-16 7.2-16 16zM96 392c13.3 0 24-10.7 24-24s-10.7-24-24-24s-24 10.7-24 24s10.7 24 24 24zm64-120c0 8.8 7.2 16 16 16H304c8.8 0 16-7.2 16-16s-7.2-16-16-16H176c-8.8 0-16 7.2-16 16zM96 296c13.3 0 24-10.7 24-24s-10.7-24-24-24s-24 10.7-24 24s10.7 24 24 24z") - li.nav-item: a.nav-link(href="http://www.stochss.org/documentation", target="_blank") - | Documentation + li.nav-item: a.nav-link(href="http://www.stochss.org/documentation" title="Documentation" target="_blank") + svg(xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 448 512") + path(d="M96 0C43 0 0 43 0 96V416c0 53 43 96 96 96H384h32c17.7 0 32-14.3 32-32s-14.3-32-32-32V384c17.7 0 32-14.3 32-32V32c0-17.7-14.3-32-32-32H384 96zm0 384H352v64H96c-17.7 0-32-14.3-32-32s14.3-32 32-32zm32-240c0-8.8 7.2-16 16-16H336c8.8 0 16 7.2 16 16s-7.2 16-16 16H144c-8.8 0-16-7.2-16-16zm16 48H336c8.8 0 16 7.2 16 16s-7.2 16-16 16H144c-8.8 0-16-7.2-16-16s7.2-16 16-16z") - li.nav-item: a.nav-link(href="https://forms.gle/hpGJ1ruxR7wTT43h8" target="_blank") - | Submit Feedback + li.nav-item + a.nav-link(href="https://discord.gg/QhMeFCJJ" title="Discord" target="_blank") + svg(xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-discord" viewBox="0 0 16 16") + path(d="M13.545 2.907a13.227 13.227 0 0 0-3.257-1.011.05.05 0 0 0-.052.025c-.141.25-.297.577-.406.833a12.19 12.19 0 0 0-3.658 0 8.258 8.258 0 0 0-.412-.833.051.051 0 0 0-.052-.025c-1.125.194-2.22.534-3.257 1.011a.041.041 0 0 0-.021.018C.356 6.024-.213 9.047.066 12.032c.001.014.01.028.021.037a13.276 13.276 0 0 0 3.995 2.02.05.05 0 0 0 .056-.019c.308-.42.582-.863.818-1.329a.05.05 0 0 0-.01-.059.051.051 0 0 0-.018-.011 8.875 8.875 0 0 1-1.248-.595.05.05 0 0 1-.02-.066.051.051 0 0 1 .015-.019c.084-.063.168-.129.248-.195a.05.05 0 0 1 .051-.007c2.619 1.196 5.454 1.196 8.041 0a.052.052 0 0 1 .053.007c.08.066.164.132.248.195a.051.051 0 0 1-.004.085 8.254 8.254 0 0 1-1.249.594.05.05 0 0 0-.03.03.052.052 0 0 0 .003.041c.24.465.515.909.817 1.329a.05.05 0 0 0 .056.019 13.235 13.235 0 0 0 4.001-2.02.049.049 0 0 0 .021-.037c.334-3.451-.559-6.449-2.366-9.106a.034.034 0 0 0-.02-.019Zm-8.198 7.307c-.789 0-1.438-.724-1.438-1.612 0-.889.637-1.613 1.438-1.613.807 0 1.45.73 1.438 1.613 0 .888-.637 1.612-1.438 1.612Zm5.316 0c-.788 0-1.438-.724-1.438-1.612 0-.889.637-1.613 1.438-1.613.807 0 1.451.73 1.438 1.613 0 .888-.631 1.612-1.438 1.612Z") + + li.nav-item + a.nav-link(href="https://forms.gle/hpGJ1ruxR7wTT43h8" title="Submit Feedback" target="_blank") + svg(xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 512 512") + path(d="M256 448c141.4 0 256-93.1 256-208S397.4 32 256 32S0 125.1 0 240c0 45.1 17.7 86.8 47.7 120.9c-1.9 24.5-11.4 46.3-21.4 62.9c-5.5 9.2-11.1 16.6-15.2 21.6c-2.1 2.5-3.7 4.4-4.9 5.7c-.6 .6-1 1.1-1.3 1.4l-.3 .3 0 0 0 0 0 0 0 0c-4.6 4.6-5.9 11.4-3.4 17.4c2.5 6 8.3 9.9 14.8 9.9c28.7 0 57.6-8.9 81.6-19.3c22.9-10 42.4-21.9 54.3-30.6c31.8 11.5 67 17.9 104.1 17.9zM128 272c-17.7 0-32-14.3-32-32s14.3-32 32-32s32 14.3 32 32s-14.3 32-32 32zm128 0c-17.7 0-32-14.3-32-32s14.3-32 32-32s32 14.3 32 32s-14.3 32-32 32zm160-32c0 17.7-14.3 32-32 32s-32-14.3-32-32s14.3-32 32-32s32 14.3 32 32z") + + li.nav-item + a.nav-link(href="https://github.com/StochSS/stochss/issues/new" title="Report an Issue" target="_blank") + svg(xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 496 512") + path(d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z") + + li.nav-item + a.nav-link(href="/stochss/settings" title="Settings") + svg(xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 512 512") + path(d="M495.9 166.6c3.2 8.7 .5 18.4-6.4 24.6l-43.3 39.4c1.1 8.3 1.7 16.8 1.7 25.4s-.6 17.1-1.7 25.4l43.3 39.4c6.9 6.2 9.6 15.9 6.4 24.6c-4.4 11.9-9.7 23.3-15.8 34.3l-4.7 8.1c-6.6 11-14 21.4-22.1 31.2c-5.9 7.2-15.7 9.6-24.5 6.8l-55.7-17.7c-13.4 10.3-28.2 18.9-44 25.4l-12.5 57.1c-2 9.1-9 16.3-18.2 17.8c-13.8 2.3-28 3.5-42.5 3.5s-28.7-1.2-42.5-3.5c-9.2-1.5-16.2-8.7-18.2-17.8l-12.5-57.1c-15.8-6.5-30.6-15.1-44-25.4L83.1 425.9c-8.8 2.8-18.6 .3-24.5-6.8c-8.1-9.8-15.5-20.2-22.1-31.2l-4.7-8.1c-6.1-11-11.4-22.4-15.8-34.3c-3.2-8.7-.5-18.4 6.4-24.6l43.3-39.4C64.6 273.1 64 264.6 64 256s.6-17.1 1.7-25.4L22.4 191.2c-6.9-6.2-9.6-15.9-6.4-24.6c4.4-11.9 9.7-23.3 15.8-34.3l4.7-8.1c6.6-11 14-21.4 22.1-31.2c5.9-7.2 15.7-9.6 24.5-6.8l55.7 17.7c13.4-10.3 28.2-18.9 44-25.4l12.5-57.1c2-9.1 9-16.3 18.2-17.8C227.3 1.2 241.5 0 256 0s28.7 1.2 42.5 3.5c9.2 1.5 16.2 8.7 18.2 17.8l12.5 57.1c15.8 6.5 30.6 15.1 44 25.4l55.7-17.7c8.8-2.8 18.6-.3 24.5 6.8c8.1 9.8 15.5 20.2 22.1 31.2l4.7 8.1c6.1 11 11.4 22.4 15.8 34.3zM256 336c44.2 0 80-35.8 80-80s-35.8-80-80-80s-80 35.8-80 80s35.8 80 80 80z") li.nav-item - div.dropdown - button.dropdown-toggle.nav-item.nav-link.dropbtn Contact - div.dropdown-content - a.nav-link(href="https://gitter.im/StochSS/community#", target="_blank") Community Chat - a.nav-link(href="https://github.com/StochSS/stochss/issues/new", target="_blank") Report Issue - - li.nav-item: a.nav-link(href="/logout") - | Sign Out + a.nav-link(href="/logout" title="Sign Out") + svg(xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 576 512") + path(d="M534.6 278.6c12.5-12.5 12.5-32.8 0-45.3l-128-128c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L434.7 224 224 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l210.7 0-73.4 73.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l128-128zM192 96c17.7 0 32-14.3 32-32s-14.3-32-32-32l-64 0c-53 0-96 43-96 96l0 256c0 53 43 96 96 96l64 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-64 0c-17.7 0-32-14.3-32-32l0-256c0-17.7 14.3-32 32-32l64 0z") div.container-fluid div.row diff --git a/client/templates/includes/editDomainType.pug b/client/templates/includes/editDomainType.pug deleted file mode 100644 index 887eab716a..0000000000 --- a/client/templates/includes/editDomainType.pug +++ /dev/null @@ -1,28 +0,0 @@ -tr - - td: div(data-target='type-name' data-hook='type-' + this.model.typeID) - - td=this.model.numParticles - - td - div="mass: " + this.model.mass - div="vol: " + this.model.volume - div="viscosity: " + this.model.nu - div="fixed: " + this.model.fixed - - td: button.btn.btn-outline-secondary.box-shadow(data-type=this.model.typeID data-hook='edit-defaults-btn') Edit Defaults - - td: button.btn.btn-outline-secondary.box-shadow(data-type=this.model.typeID data-hook='unassign-all') Un-Assign Particles - - td - button.btn.btn-outline-secondary.box-shadow.dropdown-toggle( - id="delete-domain-type", - data-toggle="dropdown", - aria-haspopup="true", - aria-expanded="false", - type="button" - ) Delete - - ul.dropdown-menu(aria-labelledby="delete-domain-type") - li.dropdown-item(data-hook="delete-type" data-type=this.model.typeID) Type - li.dropdown-item(data-hook="delete-all" data-type=this.model.typeID) Type and Particles diff --git a/client/templates/pages/domainEditor.pug b/client/templates/pages/domainEditor.pug index d97d85b6c2..ae0b5bc2e5 100644 --- a/client/templates/pages/domainEditor.pug +++ b/client/templates/pages/domainEditor.pug @@ -2,6 +2,13 @@ section.page div: h2.inline Domain Editor + ul.mb-0 + li Use the domain editor to create complex 3D 'Shapes'. + li Use 'Shapes' to define regions in space. + li Use 'Transformations' to move (rotate, etc...) 'Shapes' in space. + li Use 'Types' to classify particles (e.g. Cell_Wall, Cytoplasm, Nucleus). + li Use 'Actions' to fill (set properties, remove, import) 'Shapes' with particles. + div(data-hook="two-parent-breadcrumb-links" style="display: none") h5 nav(aria-label="breadcrumb") diff --git a/client/templates/pages/errorTemplate.pug b/client/templates/pages/errorTemplate.pug index 2a66c42c7b..e27e4202d5 100644 --- a/client/templates/pages/errorTemplate.pug +++ b/client/templates/pages/errorTemplate.pug @@ -3,12 +3,12 @@ section.page div#page-error.container.centered div.logo(id="stochss-logo" data-hook="stochss-logo") - img(src="/hub/static/stochss-logo.png") + img(src=this.logoPath) div.presentation-header - h1 404 PAGE NOT FOUND + h1=this.title div - h5="This " + this.fileType + " presentation was removed by the author and is no longer available." \ No newline at end of file + h5=errMsg \ No newline at end of file diff --git a/client/templates/pages/exampleLibrary.pug b/client/templates/pages/exampleLibrary.pug index f86ccf2180..97ed66634a 100644 --- a/client/templates/pages/exampleLibrary.pug +++ b/client/templates/pages/exampleLibrary.pug @@ -12,7 +12,19 @@ section.page div.collapse.show(id="collapse-well-mixed") - div.card-body(data-hook="well-mixed-examples") + div.card-body + + div.mx-1.row.head.align-items-baseline + + div.col-sm-3 + + h6 Name + + div.col-sm-9 + + h6 Description + + div.my-3(data-hook="well-mixed-examples") div.card.mt-4#spatial-examples @@ -24,4 +36,16 @@ section.page div.collapse.show(id="collapse-spatial") - div.card-body(data-hook="spatial-examples") + div.card-body + + div.mx-1.row.head.align-items-baseline + + div.col-sm-3 + + h6 Name + + div.col-sm-9 + + h6 Description + + div.my-3(data-hook="spatial-examples") diff --git a/client/templates/pages/userSettings.pug b/client/templates/pages/userSettings.pug new file mode 100644 index 0000000000..c5abde0fec --- /dev/null +++ b/client/templates/pages/userSettings.pug @@ -0,0 +1,142 @@ +section.page + + div.no-gutters + + div + + div.mx-1.head.align-items-baseline + + h2 General + + div.mt-3.row + + div.col-sm-1 + + label.switch + + input(data-hook="user-logs" type="checkbox" checked) + + span.slider.round + + div.col-sm-11 + + h5(style="verticle-align: middle;") User Logs + + div + + div.mt-5.mx-1.head.align-items-baseline + + h2 AWS + + div.mt-3.pl-3.py-3.hidden(data-hook="aws-config-msg" style="background-color: rgba(220, 53, 69, 0.5) !important;") + + div AWS has not been setup to run jobs, check the following: + ul.mb-0 + li Check that your credentials have been entered correctly. + li Ensure you have a compute node type and size are selected. + li Launch the compute node and ensure the status is running. + + div.mt-3.mx-1.head.align-items-baseline + + h3 Credentials + + p See AWS Configuration Documentation for details on where your credetials can be found. + + div.my-3.mx-1.row.head.align-items-baseline + + div.col-sm-3 + + h6 Region + + div.col-sm-4 + + h6 Access Key ID + + div.col-sm-4 + + h6 Secret Access Key + + div.row.my-2 + + div.col-sm-3 + + div.pl-2(data-hook="aws-region-container" data-target="aws-credentials") + + div.col-sm-4 + + div(data-hook="aws-accesskeyid-container" data-target="aws-credentials") + + div.col-sm-4 + + div(data-hook="aws-secretaccesskey-container") + + div.hidden(data-hook="aws-compute-node-section") + + hr + + div.mx-1.row.head.align-items-baseline + + h3 Compute Node + + div.my-3.mx-1.row.head.align-items-baseline + + div.col-sm-2 + + h6 Type + + div.col-sm-2 + + h6 Size + + div.col-sm-4 + + h6 Status + + div.row.my-2 + + div.col-sm-2 + + div(data-hook="aws-instancetype-container") + + div.col-sm-2 + + div(data-hook="aws-instancesize-container") + + div.col-sm-4 + + button.btn.btn-outline-collapse.mr-2.inline(data-hook="refresh-aws-status") + + svg(xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 512 512") + + path(d="M105.1 202.6c7.7-21.8 20.2-42.3 37.8-59.8c62.5-62.5 163.8-62.5 226.3 0L386.3 160H336c-17.7 0-32 14.3-32 32s14.3 32 32 32H463.5c0 0 0 0 0 0h.4c17.7 0 32-14.3 32-32V64c0-17.7-14.3-32-32-32s-32 14.3-32 32v51.2L414.4 97.6c-87.5-87.5-229.3-87.5-316.8 0C73.2 122 55.6 150.7 44.8 181.4c-5.9 16.7 2.9 34.9 19.5 40.8s34.9-2.9 40.8-19.5zM39 289.3c-5 1.5-9.8 4.2-13.7 8.2c-4 4-6.7 8.8-8.1 14c-.3 1.2-.6 2.5-.8 3.8c-.3 1.7-.4 3.4-.4 5.1V448c0 17.7 14.3 32 32 32s32-14.3 32-32V396.9l17.6 17.5 0 0c87.5 87.4 229.3 87.4 316.7 0c24.4-24.4 42.1-53.1 52.9-83.7c5.9-16.7-2.9-34.9-19.5-40.8s-34.9 2.9-40.8 19.5c-7.7 21.8-20.2 42.3-37.8 59.8c-62.5 62.5-163.8 62.5-226.3 0l-.1-.1L125.6 352H176c17.7 0 32-14.3 32-32s-14.3-32-32-32H48.4c-1.6 0-3.2 .1-4.8 .3s-3.1 .5-4.6 1z") + + div.inline(data-hook="aws-headnode-status") + + div.col-sm-2 + + button.btn.btn-outline-secondary.box-shadow(data-hook="launch-aws-cluster" disabled) Launch + + div.col-sm-2 + + button.btn.btn-outline-secondary.box-shadow(data-hook="terminate-aws-cluster" disabled) Terminate + + div.mt-3(style="float: right;") + + if this.path === null + button.btn.btn-primary.box-shadow(data-hook="apply-user-settings") Apply + else + button.btn.btn-primary.box-shadow(data-hook="apply-user-settings") Apply & Continue + + div.mdl-edit-btn.saving-status.inline(data-hook="usa-in-progress") + + div.spinner-grow.mr-2 + + span Applying settings ... + + div.mdl-edit-btn.saved-status.inline(data-hook="usa-complete") + + span Settings successfully applied + + div.mdl-edit-btn.save-error-status(data-hook="usa-error") + + span(data-hook="usa-action-error") diff --git a/client/templates/pages/workflowManager.pug b/client/templates/pages/workflowManager.pug index 2285ddb5b8..05582cab73 100644 --- a/client/templates/pages/workflowManager.pug +++ b/client/templates/pages/workflowManager.pug @@ -48,21 +48,36 @@ section.page div.mdl-edit-btn - button.btn.btn-primary.box-shadow(data-hook="save") Save + button.btn.btn-primary.box-shadow.inline(data-hook="save") Save - div.mdl-edit-btn.saving-status(data-hook="saving-workflow") + div.mdl-edit-btn.saving-status.inline(data-hook="saving-workflow") div.spinner-grow span Saving... - div.mdl-edit-btn.saved-status(data-hook="saved-workflow") + div.mdl-edit-btn.saved-status.inline(data-hook="saved-workflow") span Saved - button.btn.btn-primary.box-shadow(data-hook="start-job") Start Workflow + div.inline.hidden(data-hook="of-start-job") + + button.btn.btn-primary.box-shadow(data-target="start-job" data-computeenv="local") Start Workflow + + div.inline(data-hook="nf-start-job") + + button.btn.btn-primary.dropdown-toggle.box-shadow#startNewJobBtn( + data-toggle="dropdown", + aria-haspopup="true", + aria-expanded="false", + type="button" + ) Start New Job using + + ul.dropdown-menu(aria-labelledby="startNewJobBtn") + li.dropdown-item(data-target="start-job" data-computeenv="local") StochSS + li.dropdown-item(data-hook="aws-start-job" data-target="start-job" data-computeenv="aws") AWS Cloud - button.btn.btn-primary.box-shadow(data-hook="edit-model") Edit Model + button.btn.btn-primary.box-shadow.inline(data-hook="edit-model") Edit Model div(data-hook="status-container") diff --git a/client/templates/pages/workflowSelection.pug b/client/templates/pages/workflowSelection.pug index 5316f86ec1..ab308488dc 100644 --- a/client/templates/pages/workflowSelection.pug +++ b/client/templates/pages/workflowSelection.pug @@ -9,11 +9,21 @@ section.page li.breadcrumb-item a(href="stochss/project/manager?path="+this.projectPath data-hook="project-breadcrumb")=this.projectName - div.alert-danger(data-hook="invalid-model-message" style="display: none;") - | You cannot create a workflow if your model does not have at least one Variable and one Reaction, Event, or Rule + div.alert-danger.hidden(data-hook="invalid-model-message") + | You cannot create a workflow if your model does not have at least one Variable and one Reaction, Event, or Rule - div.alert-info(data-hook="psweep-workflow-message" style="display: none;") - | Parameter Sweep workflows require at least one parameter + div.alert-info.hidden(data-hook="psweep-workflow-message") + | Parameter Sweep workflows require at least one parameter + + div + + div.alert-info AWS Cloud is only supported in the Ensemble Simulation workflow + + div.my-2 + + span.mr-2.inline(for="#compute-env-select") Compute Environment: + + div.inline(id="compute-env-select" data-hook="compute-env-select") table.table thead diff --git a/client/tooltips.js b/client/tooltips.js index 09895c828a..8165377942 100644 --- a/client/tooltips.js +++ b/client/tooltips.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -179,14 +179,24 @@ module.exports = { parameterSweep: "Produced and compare results from model simulations by varying parameters over a range." }, domainEditor: { + staticDomain: "If true, particles are fixed system wide. If false, Fixed property of particles is used.", + pressure: "Atmospheric or background pressure.", - speed: "Approximate or artificial speed of sound" + speed: "Approximate or artificial speed of sound." + }, + domainGeometry: { + name: "Unique identifier for Geometry. Cannot share a name with other model components.", + + geometry: "The geometry formula can be any mathematical expression which evaluates to a boolean value in a python environment (i.e. x==5). This "+ + "expression is evaluable within the a limited namespace, and only lower case variables (x, y, z), particles location in standard geometries, "+ + "or other geometry names, combinatory geometries, can be referenced in the expression." + }, + domainLattice: { + name: "Unique identifier for Lattice. Cannot share a name with other model components." }, - domainType: { - geometry: "The geometry expression can be any mathematical expression which evaluates to a boolean value in a python environment (i.e. x==5). This "+ - "expression is evaluable within the a limited namespace, and only lower case variables (x, y, z), particles location, and (cx, cy, cz), "+ - "center of the geometry, can be referenced in the expression." + domainTransformation: { + name: "Unique identifier for Transformation. Cannot share a name with other model components." }, boundaryConditionsEditor: { annotation: "An optional note about a boundary condition." diff --git a/client/views/archive-listing.js b/client/views/archive-listing.js index 8b89566052..54c28cebcc 100644 --- a/client/views/archive-listing.js +++ b/client/views/archive-listing.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/views/creator-listing.js b/client/views/creator-listing.js index 19fa76b160..3debb9c27c 100644 --- a/client/views/creator-listing.js +++ b/client/views/creator-listing.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/views/edit-domain-type.js b/client/views/edit-domain-type.js deleted file mode 100644 index bfd9bd4be6..0000000000 --- a/client/views/edit-domain-type.js +++ /dev/null @@ -1,84 +0,0 @@ -/* -StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -*/ - -//support files -let app = require('../app'); -//views -var View = require('ampersand-view'); -var InputView = require('./input'); -//templates -let editTemplate = require('../templates/includes/editDomainType.pug'); -let viewTemplate = require('../templates/includes/viewDomainType.pug'); - -module.exports = View.extend({ - events: { - 'click [data-hook=unassign-all]' : 'handleUnassignParticles', - 'click [data-hook=delete-type]' : 'handleDeleteType', - 'click [data-hook=delete-all]' : 'handleDeleteTypeAndParticle', - 'click [data-hook=edit-defaults-btn]' : 'handleEditDefaults', - 'change [data-target=type-name]' : 'handleRenameType' - }, - initialize: function (attrs, options) { - View.prototype.initialize.apply(this, arguments); - this.viewMode = attrs.viewMode ? attrs.viewMode : false; - }, - render: function (attrs, options) { - this.template = this.viewMode ? viewTemplate : editTemplate; - View.prototype.render.apply(this, arguments); - this.renderNameView() - }, - handleDeleteType: function (e) { - let type = Number(e.target.dataset.type); - this.parent.deleteType(type); - this.parent.renderDomainTypes(); - }, - handleDeleteTypeAndParticle: function (e) { - let type = Number(e.target.dataset.type); - this.parent.deleteTypeAndParticles(type); - this.parent.renderDomainTypes(); - }, - handleEditDefaults: function (e) { - this.parent.selectedType = this.model.typeID; - this.parent.renderEditTypeDefaults(); - }, - handleRenameType: function (e) { - let dataset = e.target.parentElement.parentElement.dataset; - let type = Number(dataset.hook.split('-').pop()); - let name = e.target.value; - this.parent.renameType(type, name); - }, - handleUnassignParticles: function (e) { - let type = Number(e.target.dataset.type); - this.parent.unassignAllParticles(type); - this.parent.renderDomainTypes(); - }, - renderNameView: function () { - if(!this.viewMode) { - let nameInput = new InputView({ - parent: this, - required: true, - name: 'name', - valueType: 'string', - value: this.model.name - }); - app.registerRenderSubview(this, nameInput, "type-" + this.model.typeID) - } - }, - update: function () {}, - updateValid: function () {}, -}); \ No newline at end of file diff --git a/client/views/edit-project.js b/client/views/edit-project.js index 045fe6bea3..70fa82c789 100644 --- a/client/views/edit-project.js +++ b/client/views/edit-project.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/views/input.js b/client/views/input.js index 3e2ba786cc..e510b2f0a4 100644 --- a/client/views/input.js +++ b/client/views/input.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/views/job-listing.js b/client/views/job-listing.js index d5c263f4a4..c561d46dd4 100644 --- a/client/views/job-listing.js +++ b/client/views/job-listing.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/views/jstree-view.js b/client/views/jstree-view.js index ad1fab90e4..a110201253 100644 --- a/client/views/jstree-view.js +++ b/client/views/jstree-view.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/views/main.js b/client/views/main.js index bd8ae35a9e..f81ef9fa64 100644 --- a/client/views/main.js +++ b/client/views/main.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -16,21 +16,22 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -var _ = require('underscore'); -var $ = require('jquery'); -//var setFavicon = require('favicon-setter'); -var App = require('ampersand-app'); -var localLinks = require('local-links'); -var domify = require('domify'); -var path = require('path'); +let _ = require('underscore'); +let $ = require('jquery'); +let App = require('ampersand-app'); +let localLinks = require('local-links'); +let domify = require('domify'); +let path = require('path'); // support files let app = require("../app"); +// models +let Settings = require('../models/user-settings'); //views -var View = require('ampersand-view'); -var ViewSwitcher = require('ampersand-view-switcher'); +let View = require('ampersand-view'); +let ViewSwitcher = require('ampersand-view-switcher'); //templates -var headTemplate = require('!pug-loader!../templates/head.pug'); -var bodyTemplate = require('!pug-loader!../templates/body.pug'); +let headTemplate = require('!pug-loader!../templates/head.pug'); +let bodyTemplate = require('!pug-loader!../templates/body.pug'); String.prototype.toHtmlEntities = function() { return this.replace(/./gm, function(s) { @@ -41,31 +42,26 @@ String.prototype.toHtmlEntities = function() { module.exports = View.extend({ template: bodyTemplate, autoRender: true, - initialize: function () { - this.listenTo(App, 'page', this.handleNewPage); - this.homePath = window.location.pathname.startsWith("/user") ? "/hub/spawn" : "stochss/home" - this.ulClosed = false; - }, events: { 'click [data-hook=registration-link-button]' : 'handleRegistrationLinkClick', 'click [data-hook=user-logs-collapse]' : 'collapseExpandLogs', 'click [data-hook=clear-user-logs]' : 'clearUserLogs', 'click [data-hook=close-user-logs]' : 'closeUserLogs', }, + initialize: function () { + this.listenTo(App, 'page', this.handleNewPage); + this.homePath = window.location.pathname.startsWith("/user") ? "/hub/spawn" : "stochss/home"; + this.ulClosed = false; + }, render: function () { - document.head.appendChild(domify(headTemplate())); - this.renderWithTemplate(this); - this.pageContainer = this.queryByHook('page-container'); - this.pageSwitcher = new ViewSwitcher({ el: this.pageContainer, - show: function (newView, oldView) { + show: (newView, oldView) => { document.title = _.result(newView, 'pageTitle') || 'StochSS'; document.scrollTop = 0; - App.currentPage = newView; } }); @@ -73,32 +69,30 @@ module.exports = View.extend({ $("#presentation-nav-link").css("display", "none"); } this.setupUserLogs(); - this.getExampleLibrary(); return this; }, addNewLogBlock: function () { if(this.logBlock.length > 0) { let logBlock = this.logBlock.join("
"); this.logBlock = []; - $("#user-logs").append("

" + logBlock + "

") - return "" + $("#user-logs").append("

" + logBlock + "

"); + return ""; } - return "
" + return "
"; }, addNewLogs: function (newLogs) { - let self = this; - let logList = newLogs.map(function (log) { + let logList = newLogs.map((log) => { if(log.includes("$ ")){ - let head = self.addNewLogBlock(); - var newLog = self.formatLog(log); + let head = this.addNewLogBlock(); + var newLog = this.formatLog(log); $("#user-logs").append(head + newLog.toHtmlEntities()); }else{ var newLog = log; if(newLog.trim()) { - self.logBlock.push(newLog.toHtmlEntities()); + this.logBlock.push(newLog.toHtmlEntities()); } } - self.logs.push(newLog); + this.logs.push(newLog); }); this.addNewLogBlock(); }, @@ -156,25 +150,24 @@ module.exports = View.extend({ }, getUserLogs: function () { if(this.ulClosed) { return; } - let self = this; - let queryStr = "?logNum=" + this.logs.length; + let queryStr = `?logNum=${this.logs.length}`; let endpoint = path.join(app.getApiPath(), "user-logs") + queryStr; app.getXHR(endpoint, { - success: function (err, response, body) { + success: (err, response, body) => { if(body) { - let scrolled = self.scrolled; - self.addNewLogs(body.logs); - if(!self.scrolled){ + let scrolled = this.scrolled; + this.addNewLogs(body.logs); + if(!this.scrolled){ let element = document.querySelector("#user-logs"); element.scrollTop = element.scrollHeight; - }else if(self.scrollCount < 60) { - self.scrollCount += 1; + }else if(this.scrollCount < 60) { + this.scrollCount += 1; }else{ - self.scrolled = false; - self.scrollCount = 0; + this.scrolled = false; + this.scrollCount = 0; } } - self.updateUserLogs(); + this.updateUserLogs(); } }); }, @@ -183,47 +176,33 @@ module.exports = View.extend({ let date = new Date(time); let months = ['Jan.', 'Feb.', 'Mar.', 'Apr.', 'May', 'Jun.', 'Jul.', 'Aug.', 'Sept.', 'Oct.', 'Nov.', 'Dec.']; var stamp = months[date.getMonth()] + " "; - stamp += date.getDate() + ", "; - stamp += date.getFullYear() + " "; + stamp += `${date.getDate()}, ${date.getFullYear()} `; let hours = date.getHours(); - stamp += (hours < 10 ? "0" + hours : hours) + ":"; + stamp += `${(hours < 10 ? `0${hours}` : hours)}:`; let minutes = date.getMinutes(); - stamp += (minutes < 10 ? '0' + minutes : minutes) + ":"; + stamp += (minutes < 10 ? `0${minutes}` : minutes) + ":"; return log.replace(time, stamp); }, - getExampleLibrary: function () { - let endpoint = path.join(app.getApiPath(), "example-library"); - app.getXHR(endpoint, { - success: (err, response, body) => { - $("#exampleLibraryDropdown").html(body); - } - }); - }, handleNewPage: function (view) { this.pageSwitcher.set(view); - //this.updateActiveNav(); }, - handleLinkClick: function (e) { - var localPath = localLinks.pathname(e); - + let localPath = localLinks.pathname(e); if (localPath) { e.preventDefault(); this.navigate(localPath); } }, - handleRegistrationLinkClick: function () { $(this.queryByHook("registration-form")).collapse('show'); $(this.queryByHook("registration-link")).collapse(); }, - navigate: function (page) { window.location = url; }, setupUserLogs: function ({getLogs = true}={}) { let message = app.getBasePath() === "/" ? "Welcome to StochSS!" : "Welcome to StochSS Live!"; - $("#user-logs").html(message) + $("#user-logs").html(message); this.logBlock = []; this.logs = []; this.scrolled = false; diff --git a/client/views/meta-data.js b/client/views/meta-data.js index f2c26ec83d..b683d404fe 100644 --- a/client/views/meta-data.js +++ b/client/views/meta-data.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/views/model-listing.js b/client/views/model-listing.js index 100749d3c3..c4b9756b59 100644 --- a/client/views/model-listing.js +++ b/client/views/model-listing.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/views/presentation-view.js b/client/views/presentation-view.js index 19032585f4..ef81f7affa 100644 --- a/client/views/presentation-view.js +++ b/client/views/presentation-view.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/views/tests.js b/client/views/tests.js index 7773d5c350..539c997826 100644 --- a/client/views/tests.js +++ b/client/views/tests.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -16,14 +16,38 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -let startChar = (text) => { - if (/^[0-9]+/.test(text)) { - return "Field cannot start with a number. Start with letter or underscore." +let optionalStartChar = (text) => { + return startChar(text, {optional: true}); +} + +let startChar = (text, {optional=false}={}) => { + var failed = false; + if(optional) { + if(text !== "" && /^[0-9]+/.test(text)) { + failed = true; + } + }else if(/^[0-9]+/.test(text)) { + failed = true; + } + if(failed) { + return "Field cannot start with a number. Start with letter or underscore." } } -let invalidChar = (text) => { - if (!/^[a-zA-Z0-9_]+$/.test(text)) { +let optionalInvalidChar = (text) => { + return invalidChar(text, {optional: true}); +} + +let invalidChar = (text, {optional=false}={}) => { + var failed = false; + if(optional) { + if(text !== "" && !/^[a-zA-Z0-9_]+$/.test(text)) { + failed = true; + } + }else if(!/^[a-zA-Z0-9_]+$/.test(text)) { + failed = true; + } + if(failed) { return "Invalid characters. Please only use letters, numbers, or underscores." } } @@ -52,9 +76,13 @@ let negValue = (value) => { module.exports = { invalidChar: invalidChar, + optionalInvalidChar: optionalInvalidChar, + optionalNameTests: [optionalStartChar, optionalInvalidChar], + optionalStartChar: optionalStartChar, nameTests: [startChar, invalidChar], nanValue: nanValue, negValue: negValue, startChar: startChar, + intTest: nanValue, valueTests: [nanValue, negValue] } \ No newline at end of file diff --git a/client/views/view-sweep-parameter.js b/client/views/view-sweep-parameter.js index 46df3985da..dee9357a40 100644 --- a/client/views/view-sweep-parameter.js +++ b/client/views/view-sweep-parameter.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/views/workflow-group-listing.js b/client/views/workflow-group-listing.js index 4dcb3d6c26..820bf4ba01 100644 --- a/client/views/workflow-group-listing.js +++ b/client/views/workflow-group-listing.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/views/workflow-listing.js b/client/views/workflow-listing.js index 0ec73261c0..a7aa775da6 100644 --- a/client/views/workflow-listing.js +++ b/client/views/workflow-listing.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/client/views/workflow-status.js b/client/views/workflow-status.js index 49043b9f51..1eeedd9f92 100644 --- a/client/views/workflow-status.js +++ b/client/views/workflow-status.js @@ -1,6 +1,6 @@ /* StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/jupyterhub/Dockerfile.jupyterhub b/jupyterhub/Dockerfile.jupyterhub index e281edb163..be8fd417b6 100644 --- a/jupyterhub/Dockerfile.jupyterhub +++ b/jupyterhub/Dockerfile.jupyterhub @@ -46,8 +46,8 @@ RUN python3 -m pip install --no-cache-dir \ psycopg2-binary==2.9.* \ nbviewer==1.0.1 \ notebook \ - gillespy2==1.7.0 \ - spatialpy==1.1.0 \ + gillespy2==1.7.2 \ + spatialpy==1.1.2 \ plotly # build the PUG pages diff --git a/jupyterhub/handlers.py b/jupyterhub/handlers.py index ba2a765c3a..547457d44e 100644 --- a/jupyterhub/handlers.py +++ b/jupyterhub/handlers.py @@ -1,6 +1,6 @@ ''' StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/jupyterhub/job_presentation.py b/jupyterhub/job_presentation.py index b67ec88f9d..09e4c3c14b 100644 --- a/jupyterhub/job_presentation.py +++ b/jupyterhub/job_presentation.py @@ -1,6 +1,6 @@ ''' StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/jupyterhub/model_presentation.py b/jupyterhub/model_presentation.py index 34dd9a0fd2..825be8b89e 100644 --- a/jupyterhub/model_presentation.py +++ b/jupyterhub/model_presentation.py @@ -1,6 +1,6 @@ ''' StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -15,15 +15,20 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' - +import os import ast import json import logging +import traceback +import numpy import plotly +import spatialpy + from presentation_base import StochSSBase, get_presentation_from_user -from presentation_error import StochSSAPIError, report_error +from presentation_error import StochSSAPIError, DomainUpdateError, StochSSModelFormatError, \ + report_error from jupyterhub.handlers.base import BaseHandler @@ -50,8 +55,13 @@ async def get(self): log.debug(f"Name to the file: {file}") self.set_header('Content-Type', 'application/json') try: - model = get_presentation_from_user(owner=owner, file=file, kwargs={"file": file}, - process_func=process_model_presentation) + process_funcs = { + "mdl": process_wmmodel_presentation, "smdl": process_smodel_presentation + } + ext = file.split(".").pop() + model = get_presentation_from_user( + owner=owner, file=file, kwargs={"file": file}, process_func=process_funcs[ext] + ) log.debug(f"Contents of the json file: {model['model']}") self.write(model) except StochSSAPIError as load_err: @@ -76,19 +86,27 @@ async def get(self, owner, file): log.debug(f"Name to the file: {file}") self.set_header('Content-Type', 'application/json') try: - model = get_presentation_from_user(owner=owner, file=file, - kwargs={"for_download": True}, - process_func=process_model_presentation) + process_funcs = { + "mdl": process_wmmodel_presentation, "smdl": process_smodel_presentation + } ext = file.split(".").pop() - self.set_header('Content-Disposition', f'attachment; filename="{model["name"]}.{ext}"') - log.debug(f"Contents of the json file: {model}") - self.write(model) + data = get_presentation_from_user( + owner=owner, file=file, kwargs={"for_download": True}, + process_func=process_funcs[ext] + ) + if "name" in data: + filename = f"{data['name']}.{ext}" + else: + filename = f"{data['model']['name']}.{ext}" + self.set_header('Content-Disposition', f'attachment; filename="{filename}"') + log.debug(f"Contents of the json file: {data}") + self.write(data) except StochSSAPIError as load_err: report_error(self, log, load_err) self.finish() -def process_model_presentation(path, file=None, for_download=False): +def process_wmmodel_presentation(path, file=None, for_download=False): ''' Get the model presentation data from the file. @@ -105,13 +123,39 @@ def process_model_presentation(path, file=None, for_download=False): model = json.load(mdl_file) if for_download: return model - file_objs = {"mdl":StochSSModel, "smdl":StochSSSpatialModel} - ext = file.split(".").pop() - file_obj = file_objs[ext](model=model) + file_obj = StochSSModel(model=model) model_pres = file_obj.load() file_obj.print_logs(log) return model_pres +def process_smodel_presentation(path, file=None, for_download=False): + ''' + Get the model presentation data from the file. + + Attributes + ---------- + path : str + Path to the model presentation file. + file : str + Name of the presentation file. + for_download : bool + Whether or not the model presentation is being downloaded. + ''' + with open(path, "r", encoding="utf-8") as mdl_file: + body = json.load(mdl_file) + if "files" not in body: + body = StochSSSpatialModel(model=body).load(v1_domain=True) + if for_download: + return StochSSSpatialModel.get_presentation(**body) + for entry in body['files'].values(): + if not os.path.exists(os.path.dirname(entry['pres_path'])): + os.makedirs(os.path.dirname(entry['pres_path'])) + with open(entry['pres_path'], "w", encoding="utf-8") as entry_fd: + entry_fd.write(entry['body']) + file_obj = StochSSSpatialModel(model=body['model']) + model_pres = file_obj.load() + file_obj.print_logs(log) + return model_pres class StochSSModel(StochSSBase): ''' @@ -132,7 +176,6 @@ def __init__(self, model): super().__init__() self.model = model - @classmethod def __update_event_assignments(cls, event, param_ids): if "eventAssignments" not in event.keys(): @@ -147,14 +190,12 @@ def __update_event_assignments(cls, event, param_ids): except ValueError: pass - def __update_events(self, param_ids): if "eventsCollection" not in self.model.keys() or not param_ids: return for event in self.model['eventsCollection']: self.__update_event_assignments(event=event, param_ids=param_ids) - def __update_parameters(self): if "parameters" not in self.model.keys(): return [] @@ -170,7 +211,6 @@ def __update_parameters(self): pass return param_ids - def __update_reactions(self): if "reactions" not in self.model.keys(): return @@ -184,7 +224,6 @@ def __update_reactions(self): except ValueError: pass - def __update_rules(self, param_ids): if "rules" not in self.model.keys() or not param_ids: return @@ -198,7 +237,6 @@ def __update_rules(self, param_ids): except ValueError: pass - def load(self): ''' Reads the model file, updates the model to the current format, and stores it in self.model @@ -226,7 +264,6 @@ class StochSSSpatialModel(StochSSBase): StochSS spatial model object ################################################################################################ ''' - def __init__(self, model): ''' Intitialize a spatial model object @@ -239,9 +276,318 @@ def __init__(self, model): super().__init__() self.model = model + @classmethod + def __build_geometry(cls, geometry, name=None, formula=None): + if formula is None: + formula = geometry['formula'] + if name is None: + name = geometry['name'] + + class NewGeometry(spatialpy.Geometry): # pylint: disable=too-few-public-methods + ''' + ######################################################################################## + Custom SpatialPy Geometry + ######################################################################################## + ''' + __class__ = f"__main__.{name}" + def __init__(self): + pass + + def inside(self, point, on_boundary): # pylint: disable=no-self-use + ''' + Custom inside function for geometry + ''' + namespace = {'x': point[0], 'y': point[1], 'z': point[2]} + return eval(formula, {}, namespace) + return NewGeometry() + + @classmethod + def __build_stochss_domain_particles(cls, domain): + particles = [] + for i, vertex in enumerate(domain.vertices): + viscosity = domain.nu[i] + fixed = bool(domain.fixed[i]) + type_id = domain.typeNdxMapping[domain.type_id[i]] + particle = { + "particle_id": i + 1, "point": list(vertex), "type": type_id, + "volume": domain.vol[i], "mass": domain.mass[i], "nu": viscosity, + "rho": domain.rho[i], "c": domain.c[i], "fixed": fixed + } + particles.append(particle) + return particles + + def __convert_actions(self, domain, s_domain, type_ids): + geometries = self.__convert_geometries(s_domain) + lattices = self.__convert_lattices(s_domain) + transformations = self.__convert_transformations(s_domain, geometries, lattices) + try: + actions = sorted(s_domain['actions'], key=lambda action: action['priority']) + for i, action in enumerate(actions): + # Get geometry. 'Multi Particle' scope uses geometry from action + if action['geometry'] == "": + geometry = None + elif action['geometry'] in geometries: + geometry = geometries[action['geometry']] + else: + geometry = transformations[action['geometry']] + # Build props arg + if action['type'] in ('Fill Action', 'Set Action'): + kwargs = { + 'type_id': type_ids[action['typeID']], 'mass': action['mass'], + 'vol': action['vol'], 'rho': action['rho'], 'nu': action['nu'], + 'c': action['c'], 'fixed': action['fixed'] + } + else: + kwargs = {} + # Apply actions + if action['type'] == "Fill Action": + if not action['useProps']: + kwargs = {} + if action['scope'] == 'Multi Particle': + if action['lattice'] in lattices: + lattice = lattices[action['lattice']] + else: + lattice = transformations[action['lattice']] + _ = domain.add_fill_action( + lattice=lattice, geometry=geometry, enable=action['enable'], + apply_action=action['enable'], **kwargs + ) + else: + point = [action['point']['x'], action['point']['y'], action['point']['z']] + domain.add_point(point, **kwargs) + else: + # Get proper geometry for scope + # 'Single Particle' scope creates a geometry using actions point. + if action['scope'] == 'Single Particle': + p_x = action['point']['x'] + p_y = action['point']['y'] + p_z = action['point']['z'] + formula = f"x == {p_x} and y == {p_y} and z == {p_z}" + geometry = self.__build_geometry( + None, name=f"SPAGeometry{i + 1}", formula=formula + ) + if action['type'] == "Set Action": + domain.add_set_action( + geometry=geometry, enable=action['enable'], + apply_action=action['enable'], **kwargs + ) + if action['scope'] == "Single Particle": + curr_pnt = numpy.array([ + action['point']['x'], action['point']['y'], action['point']['z'] + ]) + new_pnt = numpy.array([ + action['newPoint']['x'], action['newPoint']['y'], + action['newPoint']['z'] + ]) + if numpy.count_nonzero(curr_pnt - new_pnt) > 0: + for j, vertex in enumerate(domain.vertices): + if numpy.count_nonzero(curr_pnt - vertex) <= 0: + domain.vertices[j] = new_pnt + break + else: + domain.add_remove_action( + geometry=geometry, enable=action['enable'], + apply_action=action['enable'] + ) + except KeyError as err: + message = "Spatial actions are not properly formatted or " + message += f"are referenced incorrectly: {str(err)}" + raise StochSSModelFormatError(message, traceback.format_exc()) from err + + def __convert_domain(self, type_ids, s_domain): + try: + xlim = tuple(s_domain['x_lim']) + ylim = tuple(s_domain['y_lim']) + zlim = tuple(s_domain['z_lim']) + rho0 = s_domain['rho_0'] + c_0 = s_domain['c_0'] + p_0 = s_domain['p_0'] + gravity = s_domain['gravity'] + if gravity == [0, 0, 0]: + gravity = None + domain = spatialpy.Domain( + 0, xlim, ylim, zlim, rho0=rho0, c0=c_0, P0=p_0, gravity=gravity + ) + self.__convert_actions(domain, s_domain, type_ids) + self.__convert_types(domain, type_ids) + return domain + except KeyError as err: + message = "Spatial model domain properties are not properly formatted or " + message += f"are referenced incorrectly: {str(err)}" + raise StochSSModelFormatError(message, traceback.format_exc()) from err + + @classmethod + def __convert_types(cls, domain, type_ids): + domain.typeNdxMapping = {"type_UnAssigned": 0} + domain.typeNameMapping = {0: "type_UnAssigned"} + domain.listOfTypeIDs = [0] + for ndx, name in type_ids.items(): + name = f"type_{name}" + domain.typeNdxMapping[name] = ndx + domain.typeNameMapping[ndx] = name + domain.listOfTypeIDs.append(ndx) + + def __convert_geometries(self, s_domain): + try: + geometries = {} + comb_geoms = [] + for s_geometry in s_domain['geometries']: + if s_geometry['type'] == "Standard Geometry": + geometries[s_geometry['name']] = self.__build_geometry(s_geometry) + else: + name = s_geometry['name'] + comb_geometry = spatialpy.CombinatoryGeometry("", {}) + comb_geometry.formula = s_geometry['formula'] + geometries[name] = comb_geometry + comb_geoms.append(name) + for name in comb_geoms: + geo_namespace = { + key: geometry for key, geometry in geometries.items() if key != name + } + geometries[name].geo_namespace = geo_namespace + return geometries + except KeyError as err: + message = "Spatial geometries are not properly formatted or " + message += f"are referenced incorrectly: {str(err)}" + raise StochSSModelFormatError(message, traceback.format_exc()) from err @classmethod - def __get_trace_data(cls, particles, name=""): + def __convert_lattices(cls, s_domain): + try: + lattices = {} + for s_lattice in s_domain['lattices']: + name = s_lattice['name'] + center = [ + s_lattice['center']['x'], s_lattice['center']['y'], s_lattice['center']['z'] + ] + if s_lattice['type'] == "Cartesian Lattice": + lattice = spatialpy.CartesianLattice( + s_lattice['xmin'], s_lattice['xmax'], s_lattice['deltax'], center=center, + ymin=s_lattice['ymin'], ymax=s_lattice['ymax'], deltay=s_lattice['deltay'], + zmin=s_lattice['zmin'], zmax=s_lattice['zmax'], deltaz=s_lattice['deltaz'] + ) + elif s_lattice['type'] == "Spherical Lattice": + lattice = spatialpy.SphericalLattice( + s_lattice['radius'], s_lattice['deltas'], + center=center, deltar=s_lattice['deltar'] + ) + elif s_lattice['type'] == "Cylindrical Lattice": + lattice = spatialpy.CylindricalLattice( + s_lattice['radius'], s_lattice['length'], s_lattice['deltas'], + center=center, deltar=s_lattice['deltar'] + ) + elif s_lattice['type'] == "XML Mesh Lattice": + lattice = spatialpy.XMLMeshLattice( + s_lattice['filename'], center=center, + subdomain_file=s_lattice['subdomainFile'] + ) + elif s_lattice['type'] == "Mesh IO Lattice": + lattice = spatialpy.MeshIOLattice( + s_lattice['filename'], center=center, + subdomain_file=s_lattice['subdomainFile'] + ) + else: + lattice = spatialpy.StochSSLattice(s_lattice['filename'], center=center) + lattices[name] = lattice + return lattices + except KeyError as err: + message = "Spatial lattices are not properly formatted or " + message += f"are referenced incorrectly: {str(err)}" + raise StochSSModelFormatError(message, traceback.format_exc()) from err + + @classmethod + def __convert_transformations(cls, s_domain, geometries, lattices): + try: + transformations = {} + nested_trans = {} + for s_transformation in s_domain['transformations']: + name = s_transformation['name'] + vector = [ + [ + s_transformation['vector'][0]['x'], + s_transformation['vector'][0]['y'], + s_transformation['vector'][0]['z'] + ], + [ + s_transformation['vector'][1]['x'], + s_transformation['vector'][1]['y'], + s_transformation['vector'][1]['z'] + ] + ] + if s_transformation['geometry'] != "": + geometry = geometries[s_transformation['geometry']] + else: + geometry = None + if s_transformation['lattice'] != "": + lattice = lattices[s_transformation['lattice']] + else: + lattice = None + if s_transformation['transformation'] != "": + nested_trans[name] = s_transformation['transformation'] + if s_transformation['type'] == "Translate Transformation": + transformation = spatialpy.TranslationTransformation( + vector, geometry=geometry, lattice=lattice + ) + elif s_transformation['type'] == "Rotate Transformation": + transformation = spatialpy.RotationTransformation( + vector, s_transformation['angle'], geometry=geometry, lattice=lattice + ) + elif s_transformation['type'] == "Reflect Transformation": + normal = numpy.array([ + s_transformation['normal']['x'], s_transformation['normal']['y'], + s_transformation['normal']['z'] + ]) + point1 = numpy.array([ + s_transformation['point1']['x'], s_transformation['point1']['y'], + s_transformation['point1']['z'] + ]) + point2 = numpy.array([ + s_transformation['point2']['x'], s_transformation['point2']['y'], + s_transformation['point2']['z'] + ]) + point3 = numpy.array([ + s_transformation['point3']['x'], s_transformation['point3']['y'], + s_transformation['point3']['z'] + ]) + if numpy.count_nonzero(point3 - point1) <= 0 or \ + numpy.count_nonzero(point2 - point1) <= 0: + point2 = None + point3 = None + else: + normal = None + transformation = spatialpy.ReflectionTransformation( + point1, normal=normal, point2=point2, point3=point3, + geometry=geometry, lattice=lattice + ) + else: + center = numpy.array([ + s_transformation['center']['x'], s_transformation['center']['y'], + s_transformation['center']['z'] + ]) + transformation = spatialpy.ScalingTransformation( + s_transformation['factor'], center=center, + geometry=geometry, lattice=lattice + ) + transformations[name] = transformation + for trans, nested_tran in nested_trans.items(): + transformations[trans].transformation = transformations[nested_tran] + return transformations + except KeyError as err: + message = "Spatial transformations are not properly formatted or " + message += f"are referenced incorrectly: {str(err)}" + raise StochSSModelFormatError(message, traceback.format_exc()) from err + + @classmethod + def __get_trace_data(cls, particles, name="", index=None): + common_rgb_values = [ + '#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2', '#7f7f7f', + '#bcbd22', '#17becf', '#ff0000', '#00ff00', '#0000ff', '#ffff00', '#00ffff', '#ff00ff', + '#800000', '#808000', '#008000', '#800080', '#008080', '#000080', '#ff9999', '#ffcc99', + '#ccff99', '#cc99ff', '#ffccff', '#62666a', '#8896bb', '#77a096', '#9d5a6c', '#9d5a6c', + '#eabc75', '#ff9600', '#885300', '#9172ad', '#a1b9c4', '#18749b', '#dadecf', '#c5b8a8', + '#000117', '#13a8fe', '#cf0060', '#04354b', '#0297a0', '#037665', '#eed284', '#442244', + '#ffddee', '#702afb' + ] ids = [] x_data = [] y_data = [] @@ -251,92 +597,211 @@ def __get_trace_data(cls, particles, name=""): x_data.append(particle['point'][0]) y_data.append(particle['point'][1]) z_data.append(particle['point'][2]) + marker = {"size":5} + if index is not None: + marker["color"] = common_rgb_values[(index) % len(common_rgb_values)] return plotly.graph_objs.Scatter3d(ids=ids, x=x_data, y=y_data, z=z_data, - name=name, mode="markers", marker={"size":5}) - + name=name, mode="markers", marker=marker) - def __load_domain_plot(self): + def __update_domain_to_current(self, domain=None): domain = self.model['domain'] - trace_list = [] - for i, d_type in enumerate(domain['types']): - if len(domain['types']) > 1: - particles = list(filter(lambda particle, key=i: particle['type'] == key, - domain['particles'])) - else: - particles = domain['particles'] - trace = self.__get_trace_data(particles=particles, name=d_type['name']) - trace_list.append(trace) - layout = {"scene":{"aspectmode":'data'}, "autosize":True} - if len(domain['x_lim']) == 2: - layout["xaxis"] = {"range":domain['x_lim']} - if len(domain['y_lim']) == 2: - layout["yaxis"] = {"range":domain['y_lim']} - return json.dumps({"data":trace_list, "layout":layout, "config":{"responsive":True}}, - cls=plotly.utils.PlotlyJSONEncoder) - - def __update_domain(self, domain=None): - if domain is None: - if "domain" not in self.model.keys() or len(self.model['domain'].keys()) < 6: - raise StochSSAPIError( - 406, "Domain Error", "The model does not contain a domain", None + + if domain['template_version'] == self.DOMAIN_TEMPLATE_VERSION: + return None + + self.__update_domain_to_v1(domain) + # Create version 1 domain directory if needed. + v1_dir = os.path.join('/tmp/presentation_cache', f"{self.model['name']}_domain_files") + if not os.path.exists(v1_dir): + os.mkdir(v1_dir) + # Get the file name for the version 1 domain file + v1_domain = None + file = self.get_file().replace(".smdl", ".domn") + filename = os.path.join(v1_dir, file) + if self.path == filename: + errmsg = f"{self.get_file()} may be a dependency of another doamin (.domn) " + errmsg += "or a spatial model (.smdl) and can't be updated." + raise DomainUpdateError(errmsg) + if os.path.exists(filename): + with open(filename, "r", encoding="utf-8") as v1_domain_fd: + v1_domain = json.dumps(json.load(v1_domain_fd), sort_keys=True, indent=4) + curr_domain = json.dumps(domain, sort_keys=True, indent=4) + if v1_domain != curr_domain: + filename, _ = self.get_unique_path( + self.get_file().replace(".smdl", ".domn"), dirname=v1_dir ) - domain = self.model['domain'] - if "template_version" not in domain or domain['template_version'] != self.TEMPLATE_VERSION: - if "static" not in domain.keys(): - domain['static'] = True - type_changes = {} - for i, d_type in enumerate(domain['types']): - if d_type['typeID'] != i: - type_changes[d_type['typeID']] = i - d_type['typeID'] = i - if "rho" not in d_type.keys(): - d_type['rho'] = d_type['mass'] / d_type['volume'] - if "c" not in d_type.keys(): - d_type['c'] = 10 - if "geometry" not in d_type.keys(): - d_type['geometry'] = "" - if domain['particles']: - for particle in domain['particles']: - if particle['type'] in type_changes: - particle['type'] = type_changes[particle['type']] - if "rho" not in particle.keys(): - particle['rho'] = particle['mass'] / particle['volume'] - if "c" not in particle.keys(): - particle['c'] = 10 - domain['template_version'] = self.TEMPLATE_VERSION + v1_domain = None + entry = { + 'body': json.dumps(domain, sort_keys=True, indent=4), 'name': file, + 'dwn_path': os.path.join(f"{self.model['name']}_domain_files", file), + 'pres_path': filename + } + + geometries = [] + for d_type in domain['types']: + if 'geometry' in d_type and d_type['geometry']: + geometries.append({ + 'name': f"geometry{len(geometries) + 1}", + 'type': 'Standard Geometry', + 'formula': d_type['geometry'] + }) + domain['actions'] = [{ + 'type': 'Fill Action', 'scope': 'Multi Particle', 'priority': 1, 'enable': True, + 'geometry': '', 'lattice': 'lattice1', 'useProps': False, + 'point': {'x': 0, 'y': 0, 'z': 0}, 'newPoint': {'x': 0, 'y': 0, 'z': 0}, + 'c': 10, 'fixed': False, 'mass': 1.0, 'nu': 0.0, 'rho': 1.0, 'typeID': 0, 'vol': 0.0 + }] + domain['geometries'] = geometries + domain['lattices'] = [{ + 'name': 'lattice1', 'type': 'StochSS Lattice', + 'filename': filename.replace(f'{self.user_dir}/', ''), 'subdomainFile': '', + 'center': {'x': 0, 'y': 0, 'z': 0}, 'length': 0, 'radius': 0, + 'deltar': 0,'deltas': 0,'deltax': 0,'deltay': 0,'deltaz': 0, + 'xmax': 0,'xmin': 0,'ymax': 0,'ymin': 0,'zmax': 0,'zmin': 0 + }] + domain['transformations'] = [] + domain['template_version'] = self.DOMAIN_TEMPLATE_VERSION + + return {'lattice1': entry} - def load(self): + @classmethod + def __update_domain_to_v1(cls, domain=None): + if domain['template_version'] == 1: + return + + if "static" not in domain.keys(): + domain['static'] = True + type_changes = {} + for i, d_type in enumerate(domain['types']): + if d_type['typeID'] != i: + type_changes[d_type['typeID']] = i + d_type['typeID'] = i + if "rho" not in d_type.keys(): + d_type['rho'] = d_type['mass'] / d_type['volume'] + if "c" not in d_type.keys(): + d_type['c'] = 10 + if "geometry" not in d_type.keys(): + d_type['geometry'] = "" + if domain['particles']: + for particle in domain['particles']: + if particle['type'] in type_changes: + particle['type'] = type_changes[particle['type']] + if "rho" not in particle.keys(): + particle['rho'] = particle['mass'] / particle['volume'] + if "c" not in particle.keys(): + particle['c'] = 10 + + def __update_model_to_current(self): + if self.model['template_version'] == self.TEMPLATE_VERSION: + return + + if not self.model['defaultMode']: + self.model['defaultMode'] = "discrete" + elif self.model['defaultMode'] == "dynamic": + self.model['defaultMode'] = "discrete-concentration" + if "timestepSize" not in self.model['modelSettings'].keys(): + self.model['modelSettings']['timestepSize'] = 1e-5 + if "boundaryConditions" not in self.model.keys(): + self.model['boundaryConditions'] = [] + for species in self.model['species']: + if "types" not in species.keys(): + species['types'] = list(range(1, len(self.model['domain']['types']))) + if "diffusionConst" not in species.keys(): + if "diffusionCoeff" not in species.keys(): + diff = 0.0 + else: + diff = species['diffusionCoeff'] + species['diffusionConst'] = diff + for reaction in self.model['reactions']: + if "odePropensity" not in reaction.keys(): + reaction['odePropensity'] = reaction['propensity'] + if "types" not in reaction.keys(): + reaction['types'] = list(range(1, len(self.model['domain']['types']))) + + self.model['template_version'] = self.TEMPLATE_VERSION + + @classmethod + def get_presentation(cls, model=None, files=None): + ''' Get the presentation for download. ''' + # Check if the domain has lattices + if len(model['domain']['lattices']) == 0: + return {'model': model, 'files': files} + # Process file based lattices + file_based_types = ('XML Mesh Lattice', 'Mesh IO Lattice', 'StochSS Lattice') + for lattice in model['domain']['lattices']: + if lattice['type'] in file_based_types: + lattice['filename'] = files[lattice['name']]['dwn_path'] + if lattice['subdomainFile'] != "": + entry = files[f"{lattice['name']}_sdf"] + lattice['subdomainFile'] = entry['dwn_path'] + return {'model': model, 'files': files} + + def get_domain_plot(self, domain, s_domain): ''' - Reads the spatial model file, updates it to the current format, and stores it in self.model + Get a plotly plot of the models domain or a prospective domain Attributes ---------- + domain : spatialpy.Domain + SpatialPy domain object used to generate plot data. + s_domain : stochss.Domain + StochSS domain object used to generate plot data. ''' - if "template_version" not in self.model or \ - self.model['template_version'] != self.TEMPLATE_VERSION: - if not self.model['defaultMode']: - self.model['defaultMode'] = "discrete" - elif self.model['defaultMode'] == "dynamic": - self.model['defaultMode'] = "discrete-concentration" - if "timestepSize" not in self.model['modelSettings'].keys(): - self.model['modelSettings']['timestepSize'] = 1e-5 - self.__update_domain() - if "boundaryConditions" not in self.model.keys(): - self.model['boundaryConditions'] = [] - for species in self.model['species']: - if "types" not in species.keys(): - species['types'] = list(range(1, len(self.model['domain']['types']))) - if "diffusionConst" not in species.keys(): - if "diffusionCoeff" not in species.keys(): - diff = 0.0 - else: - diff = species['diffusionCoeff'] - species['diffusionConst'] = diff - for reaction in self.model['reactions']: - if "odePropensity" not in reaction.keys(): - reaction['odePropensity'] = reaction['propensity'] - if "types" not in reaction.keys(): - reaction['types'] = list(range(1, len(self.model['domain']['types']))) - self.model['template_version'] = self.TEMPLATE_VERSION - plot = json.loads(self.__load_domain_plot()) - return {"model": self.model, "domainPlot": plot} + fig = domain.plot_types(return_plotly_figure=True) + # Case #3: 1 or more particles and one type + if len(s_domain['types']) == 1: + fig['data'][0]['name'] = "Un-Assigned" + ids = list(filter(lambda particle: particle['particle_id'], s_domain['particles'])) + fig['data'][0]['ids'] = ids + # Case #4: 1 or more particles and multiple types + else: + for index, d_type in enumerate(s_domain['types']): + if d_type['name'] == "Un-Assigned": + t_test = lambda trace: trace['name'] in ("Un-Assigned", "UnAssigned") + else: + t_test = lambda trace, name=d_type['name']: trace['name'] == name + traces = list(filter(t_test, fig['data'])) + if len(traces) == 0: + fig['data'].insert(index, self.__get_trace_data( + particles=[], name=d_type['name'], index=index + )) + else: + particles = list(filter( + lambda particle, key=d_type['typeID']: particle['type'] == key, + s_domain['particles'] + )) + ids = list(map(lambda particle: particle['particle_id'], particles)) + trace = traces[0] + trace['name'] = d_type['name'] + trace['ids'] = ids + fig['layout']['width'] = None + fig['layout']['height'] = None + fig['layout']['autosize'] = True + fig['config'] = {"responsive":True} + return json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder) + + def load(self, v1_domain=False): + ''' + Reads the spatial model file, updates it to the current format, and stores it in self.model + ''' + if "template_version" not in self.model: + self.model['template_version'] = 0 + if "template_version" not in self.model['domain']: + self.model['domain']['template_version'] = 0 + + self.__update_model_to_current() + files = self.__update_domain_to_current() + if v1_domain: + return {'model': self.model, 'files': files} + plot = self.load_action_preview() + return {"model": self.model, "domainPlot": json.loads(plot)} + + def load_action_preview(self): + ''' Get a domain preview of all enabled actions. ''' + s_domain = self.model['domain'] + types = sorted(s_domain['types'], key=lambda d_type: d_type['typeID']) + type_ids = {d_type['typeID']: d_type['name'] for d_type in types} + domain = self.__convert_domain(type_ids, s_domain) + # xlim, ylim, zlim = domain.get_bounding_box() + s_domain['particles'] = self.__build_stochss_domain_particles(domain) + return self.get_domain_plot(domain, s_domain)#, [list(xlim), list(ylim), list(zlim)] diff --git a/jupyterhub/notebook_presentation.py b/jupyterhub/notebook_presentation.py index e86fa47faa..c148af278b 100644 --- a/jupyterhub/notebook_presentation.py +++ b/jupyterhub/notebook_presentation.py @@ -1,6 +1,6 @@ ''' StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/jupyterhub/presentation_base.py b/jupyterhub/presentation_base.py index 7c4f7b04a3..778d533c16 100644 --- a/jupyterhub/presentation_base.py +++ b/jupyterhub/presentation_base.py @@ -1,6 +1,6 @@ ''' StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -96,6 +96,7 @@ class StochSSBase(): ''' user_dir = os.path.expanduser("~") # returns the path to the users home directory TEMPLATE_VERSION = 1 + DOMAIN_TEMPLATE_VERSION = 2 def __init__(self, path=None): ''' @@ -109,6 +110,17 @@ def __init__(self, path=None): self.path = path self.logs = [] + def get_file(self, path=None): + ''' + Get the file from the path + + Attributes + ---------- + path : str + Path to a file object + ''' + file = self.path if path is None else path + return file.split('/').pop() def log(self, level, message): ''' @@ -123,7 +135,6 @@ def log(self, level, message): ''' self.logs.append({"level":level, "message":message}) - def print_logs(self, log): ''' Display all internal logs to the console diff --git a/jupyterhub/presentation_error.py b/jupyterhub/presentation_error.py index 58da43af51..72185a90d0 100644 --- a/jupyterhub/presentation_error.py +++ b/jupyterhub/presentation_error.py @@ -1,6 +1,6 @@ ''' StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -117,6 +117,47 @@ def __init__(self, msg, trace=None): ''' super().__init__(406, "File Data Not JSON Format", msg, trace) +class StochSSModelFormatError(StochSSAPIError): + ''' + ################################################################################################ + StochSS Model Not In Proper Format + ################################################################################################ + ''' + + def __init__(self, msg, trace=None): + ''' + Indicates that the model does not meet the current format requirements + + Attributes + ---------- + msg : str + Details on what caused the error + trace : str + Error traceback for the error + ''' + super().__init__(406, "StochSS Model Not In Proper Format", msg, trace) + +class DomainUpdateError(StochSSAPIError): + ''' + ################################################################################################ + Domain File Can't Be Updated + ################################################################################################ + ''' + + def __init__(self, msg, trace=None): + ''' + Indicates that the domain file can't be updated as it may be a + dependency of another doamin or a spatial model. + + Attributes + ---------- + msg : str + Details on what caused the error + trace : str + Error traceback for the error + ''' + super().__init__(405, "Domain File Can't Be Updated.", msg, trace) + #################################################################################################### # Job Errors #################################################################################################### diff --git a/requirements.txt b/requirements.txt index bd89812cc4..2488d89577 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,7 @@ python-libsedml==2.0.9 python-libcombine==0.2.7 pyabc==0.12.6 escapism==1.0.1 -gillespy2==1.7.1 -spatialpy==1.1.1 +gillespy2==1.8.0 +spatialpy==1.2.1 +stochss-compute[AWS]==1.0.1 git+https://github.com/StochSS/sciope.git@master diff --git a/stochss/handlers/__init__.py b/stochss/handlers/__init__.py index 6907767ae5..ba5eadd179 100644 --- a/stochss/handlers/__init__.py +++ b/stochss/handlers/__init__.py @@ -1,6 +1,6 @@ ''' StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -36,80 +36,95 @@ def get_page_handlers(route_start): The base url for the web app ''' handlers = [ - # - ## Page Handlers - # + ############################################################################## + # Page Handlers & API Handlers # + ############################################################################## (r'/stochss/home\/?', UserHomeHandler), + (r'/stochss/quickstart\/?', QuickstartHandler), + (r'/stochss/example-library\/?', ExampleLibraryHandler), (r'/stochss/files\/?$', ModelBrowserHandler), (r'/stochss/models/edit\/?', ModelEditorHandler), (r'/stochss/domain/edit\/?', DomainEditorHandler), (r'/stochss/workflow/selection\/?', WorkflowSelectionHandler), (r'/stochss/workflow/edit\/?', WorkflowEditorHandler), - (r'/stochss/quickstart\/?', QuickstartHandler), (r'/stochss/project/manager\/?', ProjectManagerHandler), (r'/stochss/loading-page\/?', LoadingPageHandler), (r'/stochss/multiple-plots\/?', MultiplePlotsHandler), - (r'/stochss/example-library\/?', ExampleLibraryHandler), - # - ## API Handlers - # + (r'/stochss/settings\/?', UserSettingsHandler), (r'/stochss/api/user-logs\/?', UserLogsAPIHandler), - (r'/stochss/api/example-library\/?', ImportFromLibrary), (r'/stochss/api/clear-user-logs\/?', ClearUserLogsAPIHandler), + (r'/stochss/api/user-settings\/?', LoadUserSettings), + (r'/stochss/api/aws/job-config-check\/?', ConfirmAWSConfigHandler), + (r'/stochss/api/aws/launch-cluster\/?', LaunchAWSClusterHandler), + (r'/stochss/api/aws/cluster-status\/?', AWSClusterStatusHandler), + (r'/stochss/api/aws/terminate-cluster\/?', TerminateAWSClusterHandler), + ############################################################################## + # File Browser API Handlers # + ############################################################################## (r"/stochss/api/file/browser-list\/?", ModelBrowserFileList), - (r"/stochss/api/file/upload\/?", UploadFileAPIHandler), - (r"/stochss/api/file/upload-from-link\/?", UploadFileFromLinkAPIHandler), - (r"/stochss/api/file/move\/?", MoveFileAPIHandler), (r"/stochss/api/file/empty-trash\/?", EmptyTrashAPIHandler), (r"/stochss/api/file/delete\/?", DeleteFileAPIHandler), + (r"/stochss/api/file/move\/?", MoveFileAPIHandler), + (r"/stochss/api/file/duplicate\/?", DuplicateModelHandler), + (r"/stochss/api/directory/duplicate\/?", DuplicateDirectoryHandler), (r"/stochss/api/file/rename\/?", RenameAPIHandler), + (r"/stochss/api/model/to-spatial\/?", ConvertToSpatialAPIHandler), + (r"/stochss/api/spatial/to-model\/?", ConvertToModelAPIHandler), + (r"/stochss/api/model/to-sbml\/?", ModelToSBMLAPIHandler), + (r"/stochss/api/sbml/to-model\/?", SBMLToModelAPIHandler), (r"/stochss/api/file/download\/?", DownloadAPIHandler), (r"/stochss/api/file/download-zip\/?", DownloadZipFileAPIHandler), - (r"/stochss/api/file/json-data\/?", JsonFileAPIHandler), - (r"/stochss/api/file/duplicate\/?", DuplicateModelHandler), + (r"/stochss/api/directory/create\/?", CreateDirectoryHandler), + (r"/stochss/api/file/upload\/?", UploadFileAPIHandler), + (r"/stochss/api/workflow/duplicate\/?", DuplicateWorkflowAsNewHandler), + (r"/stochss/api/workflow/edit-model\/?", GetWorkflowModelPathAPIHandler), + (r"/stochss/api/file/upload-from-link\/?", UploadFileFromLinkAPIHandler), (r"/stochss/api/file/unzip\/?", UnzipFileAPIHandler), - (r"/stochss/api/file/presentations\/?", PresentationListAPIHandler), (r"/stochss/api/notebook/presentation\/?", NotebookPresentationAPIHandler), - (r"/stochss/api/directory/duplicate\/?", DuplicateDirectoryHandler), - (r"/stochss/api/directory/create\/?", CreateDirectoryHandler), - (r"/stochss/api/spatial/to-model\/?", ConvertToModelAPIHandler), - (r"/stochss/api/sbml/to-model\/?", SBMLToModelAPIHandler), - (r"/stochss/api/model/to-notebook\/?", ModelToNotebookHandler), - (r"/stochss/api/model/to-spatial\/?", ConvertToSpatialAPIHandler), - (r"/stochss/api/model/to-sbml\/?", ModelToSBMLAPIHandler), + (r"/stochss/api/file/presentations\/?", PresentationListAPIHandler), + (r'/stochss/api/example-library\/?', ImportFromLibrary), + ############################################################################## + # Model API Handlers # + ############################################################################## + (r"/stochss/api/file/json-data\/?", JsonFileAPIHandler), + (r"/stochss/api/spatial-model/load-domain\/?", LoadDomainEditorAPIHandler), + (r"/stochss/api/spatial-model/domain-plot\/?", LoadDomainAPIHandler), (r"/stochss/api/model/run\/?", RunModelAPIHandler), (r"/stochss/api/model/exists\/?", ModelExistsAPIHandler), - (r"/stochss/api/model/presentation\/?", ModelPresentationAPIHandler), - (r"/stochss/api/model/new-bc\/?", CreateNewBoundCondAPIHandler), + (r"/stochss/api/spatial-model/import-mesh\/?", ImportMeshAPIHandler), (r"/stochss/api/spatial-model/domain-list\/?", LoadExternalDomains), - (r"/stochss/api/spatial-model/types-list\/?", LoadParticleTypesDescriptions), + (r"/stochss/api/model/new-bc\/?", CreateNewBoundCondAPIHandler), + (r"/stochss/api/model/presentation\/?", ModelPresentationAPIHandler), + (r"/stochss/api/spatial-model/lattice-files\/?", LoadLatticeFiles), (r"/stochss/api/spatial-model/domain-plot\/?", LoadDomainAPIHandler), (r"/stochss/api/spatial-model/load-domain\/?", LoadDomainEditorAPIHandler), - (r"/stochss/api/spatial-model/particle-types\/?", GetParticlesTypesAPIHandler), (r"/stochss/api/spatial-model/import-mesh\/?", ImportMeshAPIHandler), - (r"/stochss/api/spatial-model/3d-domain\/?", Create3DDomainAPIHandler), - (r"/stochss/api/spatial-model/apply-geometry\/?", ApplyGeometryAPIHandler), - (r"/stochss/api/spatial-model/fill-geometry\/?", FillGeometryAPIHandler), + ############################################################################## + # Project API Handlers # + ############################################################################## (r"/stochss/api/project/new-project\/?", NewProjectAPIHandler), (r"/stochss/api/project/load-project\/?", LoadProjectAPIHandler), (r"/stochss/api/project/load-browser\/?", LoadProjectBrowserAPIHandler), + (r"/stochss/api/project/load-project\/?", LoadProjectAPIHandler), + (r"/stochss/api/project/new-project\/?", NewProjectAPIHandler), (r"/stochss/api/project/new-model\/?", NewModelAPIHandler), (r"/stochss/api/project/add-existing-model\/?", AddExistingModelAPIHandler), (r"/stochss/api/project/extract-model\/?", ExtractModelAPIHandler), (r"/stochss/api/project/extract-workflow\/?", ExtractWorkflowAPIHandler), - (r"/stochss/api/project/export-combine\/?", ExportAsCombineAPIHandler), (r"/stochss/api/project/meta-data\/?", ProjectMetaDataAPIHandler), + (r"/stochss/api/project/export-combine\/?", ExportAsCombineAPIHandler), (r"/stochss/api/project/save-annotation\/?", UpdateAnnotationAPIHandler), (r"/stochss/api/project/update-format\/?", UpadteProjectAPIHandler), - (r"/stochss/api/workflow/notebook\/?", WorkflowNotebookHandler), + ############################################################################## + # Workflow API Handlers # + ############################################################################## (r"/stochss/api/workflow/new\/?", NewWorkflowAPIHandler), (r"/stochss/api/workflow/load-workflow\/?", LoadWorkflowAPIHandler), (r"/stochss/api/workflow/init-job\/?", InitializeJobAPIHandler), (r"/stochss/api/workflow/run-job\/?", RunWorkflowAPIHandler), (r"/stochss/api/workflow/workflow-status\/?", WorkflowStatusAPIHandler), (r"/stochss/api/workflow/plot-results\/?", PlotWorkflowResultsAPIHandler), - (r"/stochss/api/workflow/duplicate\/?", DuplicateWorkflowAsNewHandler), - (r"/stochss/api/workflow/edit-model\/?", GetWorkflowModelPathAPIHandler), + (r"/stochss/api/workflow/notebook\/?", WorkflowNotebookHandler), (r"/stochss/api/workflow/save-plot\/?", SavePlotAPIHandler), (r"/stochss/api/workflow/save-annotation\/?", SaveAnnotationAPIHandler), (r"/stochss/api/workflow/update-format\/?", UpadteWorkflowAPIHandler), diff --git a/stochss/handlers/file_browser.py b/stochss/handlers/file_browser.py index db3b011afa..03eb4c7693 100644 --- a/stochss/handlers/file_browser.py +++ b/stochss/handlers/file_browser.py @@ -1,6 +1,6 @@ ''' StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -30,7 +30,7 @@ from .util import StochSSBase, StochSSFolder, StochSSFile, StochSSModel, StochSSSpatialModel, \ StochSSSBMLModel, StochSSNotebook, StochSSWorkflow, StochSSJob, StochSSProject, \ - StochSSAPIError, report_error + StochSSAPIError, report_error, report_critical_error log = logging.getLogger('stochss') @@ -62,42 +62,8 @@ async def get(self): self.write(node) except StochSSAPIError as err: report_error(self, log, err) - self.finish() - - -class ModelToNotebookHandler(APIHandler): - ''' - ################################################################################################ - Handler for handling conversions from model (.mdl) file to Jupyter Notebook - (.ipynb) file. - ################################################################################################ - ''' - @web.authenticated - async def get(self): - ''' - Runs the convert_to_notebook function from the convert_to_notebook script - to build a Jupyter Notebook version of the model and write it to a file. - - Attributes - ---------- - ''' - path = self.get_query_argument(name="path") - log.debug(f"Path to the model file: {path}") - self.set_header('Content-Type', 'application/json') - try: - is_spatial = path.endswith(".smdl") - log.info(f"Getting data from {path.split('/').pop()}") - model = StochSSSpatialModel(path=path) if is_spatial else StochSSModel(path=path) - data = model.get_notebook_data() - log.debug(f"Notebook data: {data}") - log.info(f"Converting {path.split('/').pop()} to notebook") - notebook = StochSSNotebook(**data) - resp = notebook.create_ses_notebook() if is_spatial else notebook.create_es_notebook() - log.info(f"Successfully created the notebook for {path.split('/').pop()}") - log.debug(f"Notebook file path: {resp}") - self.write(resp) - except StochSSAPIError as err: - report_error(self, log, err) + except Exception as err: + report_critical_error(self, log, err) self.finish() @@ -127,6 +93,8 @@ def get(self): self.write(resp) except StochSSAPIError as err: report_error(self, log, err) + except Exception as err: + report_critical_error(self, log, err) self.finish() @@ -155,6 +123,8 @@ async def get(self): self.write(resp) except StochSSAPIError as err: report_error(self, log, err) + except Exception as err: + report_critical_error(self, log, err) self.finish() @@ -189,6 +159,8 @@ async def get(self): self.write(resp) except StochSSAPIError as err: report_error(self, log, err) + except Exception as err: + report_critical_error(self, log, err) self.finish() @@ -220,6 +192,8 @@ async def get(self): self.write(resp) except StochSSAPIError as err: report_error(self, log, err) + except Exception as err: + report_critical_error(self, log, err) self.finish() @@ -251,6 +225,8 @@ async def get(self): self.write(resp) except StochSSAPIError as err: report_error(self, log, err) + except Exception as err: + report_critical_error(self, log, err) self.finish() @@ -293,6 +269,8 @@ async def get(self): self.write(resp) except StochSSAPIError as err: report_error(self, log, err) + except Exception as err: + report_critical_error(self, log, err) self.finish() @@ -325,6 +303,8 @@ async def get(self): self.write(resp) except StochSSAPIError as err: report_error(self, log, err) + except Exception as err: + report_critical_error(self, log, err) self.finish() @@ -357,6 +337,8 @@ async def get(self): self.write(resp) except StochSSAPIError as err: report_error(self, log, err) + except Exception as err: + report_critical_error(self, log, err) self.finish() @@ -391,6 +373,8 @@ async def get(self): self.write(resp) except StochSSAPIError as err: report_error(self, log, err) + except Exception as err: + report_critical_error(self, log, err) self.finish() @@ -434,6 +418,8 @@ async def get(self): self.write(resp) except StochSSAPIError as err: report_error(self, log, err) + except Exception as err: + report_critical_error(self, log, err) self.finish() @@ -461,6 +447,8 @@ async def get(self): self.write(data) except StochSSAPIError as err: report_error(self, log, err) + except Exception as err: + report_critical_error(self, log, err) self.finish() @@ -498,6 +486,8 @@ async def get(self): self.write(resp) except StochSSAPIError as err: report_error(self, log, err) + except Exception as err: + report_critical_error(self, log, err) self.finish() @@ -526,6 +516,8 @@ async def get(self): self.write(f"{directories} was successfully created!") except StochSSAPIError as err: report_error(self, log, err) + except Exception as err: + report_critical_error(self, log, err) self.finish() @@ -574,6 +566,8 @@ async def post(self): self.write(json.dumps(resp)) except StochSSAPIError as err: report_error(self, log, err) + except Exception as err: + report_critical_error(self, log, err) self.finish() @@ -632,6 +626,8 @@ async def get(self): self.write(resp) except StochSSAPIError as err: report_error(self, log, err) + except Exception as err: + report_critical_error(self, log, err) self.finish() @@ -661,6 +657,8 @@ async def get(self): self.write(resp) except StochSSAPIError as err: report_error(self, log, err) + except Exception as err: + report_critical_error(self, log, err) self.finish() @@ -692,28 +690,36 @@ async def get(self): self.write(resp) except StochSSAPIError as err: report_error(self, log, err) + except Exception as err: + report_critical_error(self, log, err) elif cmd is None: - overwrite = self.get_query_argument(name='overwrite', default=False) - outfile = f"{str(uuid.uuid4()).replace('-', '_')}.tmp" - log.debug(f"Response file name: {outfile}") - exec_cmd = [script, f'{path}', f'{outfile}'] # Script commands for read run_cmd - if overwrite: - exec_cmd.append('-o') - log.debug(f"Exec command: {exec_cmd}") - pipe = subprocess.Popen(exec_cmd) - resp = {"responsePath": outfile} - log.debug(f"Response: {resp}") - self.write(resp) + try: + overwrite = self.get_query_argument(name='overwrite', default=False) + outfile = f"{str(uuid.uuid4()).replace('-', '_')}.tmp" + log.debug(f"Response file name: {outfile}") + exec_cmd = [script, f'{path}', f'{outfile}'] # Script commands for read run_cmd + if overwrite: + exec_cmd.append('-o') + log.debug(f"Exec command: {exec_cmd}") + pipe = subprocess.Popen(exec_cmd) + resp = {"responsePath": outfile} + log.debug(f"Response: {resp}") + self.write(resp) + except Exception as err: + report_critical_error(self, log, err) else: - exec_cmd = [script, 'None', f'{path}'] # Script commands for read run_cmd - log.debug(f"Exec command: {exec_cmd}") - pipe = subprocess.Popen(exec_cmd, stdout=subprocess.PIPE, text=True) - results, error = pipe.communicate() - if error is not None: - log.error(f"Errors thrown by the subprocess: {error}") - resp = json.loads(results) - log.debug(f"Response: {resp}") - self.write(resp) + try: + exec_cmd = [script, 'None', f'{path}'] # Script commands for read run_cmd + log.debug(f"Exec command: {exec_cmd}") + pipe = subprocess.Popen(exec_cmd, stdout=subprocess.PIPE, text=True) + results, error = pipe.communicate() + if error is not None: + log.error(f"Errors thrown by the subprocess: {error}") + resp = json.loads(results) + log.debug(f"Response: {resp}") + self.write(resp) + except Exception as err: + report_critical_error(self, log, err) self.finish() @@ -741,6 +747,8 @@ async def get(self): self.write(resp) except StochSSAPIError as err: report_error(self, log, err) + except Exception as err: + report_critical_error(self, log, err) self.finish() @@ -775,6 +783,8 @@ async def get(self): self.write(resp) except StochSSAPIError as err: report_error(self, log, err) + except Exception as err: + report_critical_error(self, log, err) self.finish() @@ -801,6 +811,8 @@ async def get(self): self.write({"presentations": presentations}) except StochSSAPIError as err: report_error(self, log, err) + except Exception as err: + report_critical_error(self, log, err) self.finish() class ImportFromLibrary(APIHandler): @@ -829,4 +841,6 @@ async def get(self): self.write(examples) except StochSSAPIError as err: report_error(self, log, err) + except Exception as err: + report_critical_error(self, log, err) self.finish() diff --git a/stochss/handlers/log.py b/stochss/handlers/log.py index 0a366b86ed..98f06d7b95 100644 --- a/stochss/handlers/log.py +++ b/stochss/handlers/log.py @@ -1,6 +1,6 @@ ''' StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -22,6 +22,7 @@ from tornado.log import LogFormatter log = logging.getLogger('stochss') +spy_log = logging.getLogger('SpatialPy') def init_log(): ''' @@ -33,6 +34,7 @@ def init_log(): relocate_old_logs() setup_stream_handler() setup_file_handler() + setup_spy_file_handler() log.setLevel(logging.DEBUG) log.propagate = False @@ -119,12 +121,33 @@ def rotator(src, dst): os.rename(src, dst) os.remove(src) + fmt = '%(asctime)s$ %(message)s' + formatter = LogFormatter(fmt=fmt, datefmt="%b %d, %Y %I:%M %p UTC") + path = os.path.join(os.path.expanduser("~"), ".user-logs.txt") handler = logging.handlers.RotatingFileHandler(path, maxBytes=500000, backupCount=1) handler.namer = namer handler.rotator = rotator + handler.setFormatter(formatter) + handler.setLevel(logging.INFO) + + log.addHandler(handler) + + +def setup_spy_file_handler(): + ''' + Initialize the SpatialPy file handler + + Attributes + ---------- + ''' + print(spy_log.handlers) fmt = '%(asctime)s$ %(message)s' formatter = LogFormatter(fmt=fmt, datefmt="%b %d, %Y %I:%M %p UTC") + + path = os.path.join(os.path.expanduser("~"), ".user-logs.txt") + handler = logging.FileHandler(path) handler.setFormatter(formatter) handler.setLevel(logging.INFO) - log.addHandler(handler) + + spy_log.addHandler(handler) diff --git a/stochss/handlers/models.py b/stochss/handlers/models.py index 90a5b9c6ab..16401b1bd6 100644 --- a/stochss/handlers/models.py +++ b/stochss/handlers/models.py @@ -1,6 +1,6 @@ ''' StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -28,7 +28,7 @@ # Use finish() for json, write() for text from .util import StochSSFolder, StochSSModel, StochSSSpatialModel, StochSSNotebook, \ - StochSSAPIError, report_error + StochSSAPIError, report_error, report_critical_error log = logging.getLogger('stochss') @@ -83,6 +83,8 @@ async def get(self): report_error(self, log, new_model_err) else: report_error(self, log, load_err) + except Exception as err: + report_critical_error(self, log, err) self.finish() @@ -111,6 +113,8 @@ async def post(self): log.info(f"Successfully saved {model.get_file(path=path)}") except StochSSAPIError as err: report_error(self, log, err) + except Exception as err: + report_critical_error(self, log, err) self.finish() @@ -146,6 +150,8 @@ async def get(self): self.write(resp) except StochSSAPIError as err: report_error(self, log, err) + except Exception as err: + report_critical_error(self, log, err) self.finish() @@ -156,7 +162,7 @@ class LoadDomainAPIHandler(APIHandler): ################################################################################################ ''' @web.authenticated - async def get(self): + async def post(self): ''' Load and return the domain plot. @@ -164,25 +170,21 @@ async def get(self): ---------- ''' self.set_header('Content-Type', 'application/json') - path = self.get_query_argument(name="path", default=None) - log.debug(f"Path to the spatial model: {path}") - d_path = self.get_query_argument(name="domain_path", default=None) - if d_path is not None: - log.debug(f"Path to the domain file: {d_path}") - new = self.get_query_argument(name="new", default=False) - log.debug(f"The domain is new: {new}") + domain = json.loads(self.request.body.decode()) log.info("Generating the domain plot") try: - model = StochSSSpatialModel(path=path) - fig, trace_temp = model.get_domain_plot(path=d_path, new=new) + model = StochSSSpatialModel(path="") + fig, limits = model.load_action_preview(domain) log.info("Loading the domain plot") if isinstance(fig, str): fig = json.loads(fig) - resp = {"fig":fig, "trace_temp": trace_temp} + resp = {"fig":fig, "particles": domain['particles'], "limits": limits} log.debug(f"Response: {resp}") self.write(resp) except StochSSAPIError as err: report_error(self, log, err) + except Exception as err: + report_critical_error(self, log, err) self.finish() @@ -214,33 +216,39 @@ async def get(self): target = self.get_query_argument(name="target", default=None) resp = {"Running":False, "Outfile":outfile, "Results":""} if run_cmd == "start": - model = StochSSModel(path=path) - if os.path.exists(f".{model.get_name()}-preview.json"): - os.remove(f".{model.get_name()}-preview.json") - exec_cmd = ['/stochss/stochss/handlers/util/scripts/run_preview.py', - f'{path}', f'{outfile}'] - if target is not None: - exec_cmd.insert(1, "--target") - exec_cmd.insert(2, f"{target}") - log.debug(f"Script commands for running a preview: {exec_cmd}") - subprocess.Popen(exec_cmd) - resp['Running'] = True - log.debug(f"Response to the start command: {resp}") - self.write(resp) - else: - model = StochSSModel(path=path) - log.info("Check for preview results ...") - results = model.get_preview_results(outfile=outfile) - log.debug(f"Results for the model preview: {results}") - if results is None: + try: + model = StochSSModel(path=path) + if os.path.exists(f".{model.get_name()}-preview.json"): + os.remove(f".{model.get_name()}-preview.json") + exec_cmd = ['/stochss/stochss/handlers/util/scripts/run_preview.py', + f'{path}', f'{outfile}'] + if target is not None: + exec_cmd.insert(1, "--target") + exec_cmd.insert(2, f"{target}") + log.debug(f"Script commands for running a preview: {exec_cmd}") + subprocess.Popen(exec_cmd) resp['Running'] = True - resp['Results'] = model.get_live_results() - log.info("The preview is still running") - else: - resp['Results'] = results - log.info("Loading the preview results") - log.debug(f"Response to the read command: {resp}") - self.write(resp) + log.debug(f"Response to the start command: {resp}") + self.write(resp) + except Exception as err: + report_critical_error(self, log, err) + else: + try: + model = StochSSModel(path=path) + log.info("Check for preview results ...") + results = model.get_preview_results(outfile=outfile) + log.debug(f"Results for the model preview: {results}") + if results is None: + resp['Running'] = True + resp['Results'] = model.get_live_results() + log.info("The preview is still running") + else: + resp['Results'] = results + log.info("Loading the preview results") + log.debug(f"Response to the read command: {resp}") + self.write(resp) + except Exception as err: + report_critical_error(self, log, err) self.finish() @@ -261,10 +269,13 @@ async def get(self): self.set_header('Content-Type', 'application/json') path = self.get_query_argument(name="path") log.debug(f"Path to the file: {path}") - model = StochSSModel(path=path) - resp = {"exists":os.path.exists(model.get_path(full=True))} - log.debug(f"Response: {resp}") - self.write(resp) + try: + model = StochSSModel(path=path) + resp = {"exists":os.path.exists(model.get_path(full=True))} + log.debug(f"Response: {resp}") + self.write(resp) + except Exception as err: + report_critical_error(self, log, err) self.finish() @@ -283,25 +294,32 @@ async def post(self): ---------- ''' self.set_header('Content-Type', 'application/json') - log.info(f"Loading the domain from {self.request.files['datafile'][0]['filename']}") - data = self.request.files['datafile'][0]['body'].decode() + dirname = os.path.dirname(self.request.body_arguments['path'][0].decode()) + if dirname == '.': + dirname = "" + elif ".wkgp" in dirname: + dirname = os.path.dirname(dirname) + mesh_file = self.request.files['datafile'][0] + log.info(f"Importing mesh: {mesh_file['filename']}") if "typefile" in self.request.files.keys(): - log.info(f"Loading the particle types from \ - {self.request.files['typefile'][0]['filename']}") - types = self.request.files['typefile'][0]['body'].decode().strip().split("\n") + type_file = self.request.files['typefile'][0] + log.info(f"Importing type descriptions: {type_file['filename']}") else: - types = None - log.info("Loading particle data") - particle_data = json.loads(self.request.body_arguments['particleData'][0].decode()) + type_file = None try: - log.info("Generating new particles") - resp = StochSSSpatialModel.get_particles_from_remote(domain=data, data=particle_data, - types=types) - log.debug(f"Number of Particles: {len(resp['particles'])}") - log.info("Successfully created new particles") + folder = StochSSFolder(path=dirname) + mesh_resp = folder.upload('file', mesh_file['filename'], mesh_file['body']) + resp = {'meshPath': mesh_resp['path'], 'meshFile': mesh_resp['file']} + if type_file is not None: + types_resp = folder.upload('file', type_file['filename'], type_file['body']) + resp['typesPath'] = types_resp['path'] + resp['typesFile'] = types_resp['file'] + log.info("Successfully uploaded files") self.write(resp) except StochSSAPIError as err: report_error(self, log, err) + except Exception as err: + report_critical_error(self, log, err) self.finish() @@ -328,95 +346,47 @@ async def get(self): self.write(resp) except StochSSAPIError as err: report_error(self, log, err) + except Exception as err: + report_critical_error(self, log, err) self.finish() -class LoadParticleTypesDescriptions(APIHandler): +class LoadLatticeFiles(APIHandler): ''' ################################################################################################ - Handler for getting particle type description files. + Handler for getting mesh/domain and type description files. ################################################################################################ ''' @web.authenticated async def get(self): ''' - Get text files on disc for particles type description selection. + Get mesh/domain and type description files on disc for file selections. Attributes ---------- ''' self.set_header('Content-Type', 'application/json') + target_ext = self.get_query_argument(name="ext") + include_types = bool(self.get_query_argument(name="includeTypes", default=False)) try: folder = StochSSFolder(path="") test = lambda ext, root, file: bool( - "trash" in root.split("/") or file.startswith('.') or 'wkfl' in root or root.startswith('.') + "trash" in root.split("/") or file.startswith('.') or \ + 'wkfl' in root or root.startswith('.') ) - resp = folder.get_file_list(ext=".txt", test=test) - log.debug("Response: {resp}") - self.write(resp) - except StochSSAPIError as err: - report_error(self, log, err) - self.finish() - - -class Create3DDomainAPIHandler(APIHandler): - ''' - ################################################################################################ - Handler for creating a 3D domain. - ################################################################################################ - ''' - @web.authenticated - async def post(self): - ''' - Create a 3D domain and return its particles. - - Attributes - ---------- - ''' - self.set_header('Content-Type', 'application/json') - log.info("Loading particle data") - data = json.loads(self.request.body.decode()) - log.debug("Data used to create the domain: {data}") - try: - log.info("Generating new particles") - model = StochSSSpatialModel(path="") - resp = model.get_particles_from_3d_domain(data=data) - log.debug(f"Number of Particles: {len(resp['particles'])}") - log.info("Successfully created new particles") - self.write(resp) - except StochSSAPIError as err: - report_error(self, log, err) - self.finish() - - -class GetParticlesTypesAPIHandler(APIHandler): - ''' - ################################################################################################ - Handler getting particle types. - ################################################################################################ - ''' - @web.authenticated - async def get(self): - ''' - Get particle types from a text file. - - Attributes - ---------- - ''' - self.set_header('Content-Type', 'application/json') - path = self.get_query_argument(name="path") - log.debug("Path to the file: {}", path) - try: - log.info(f"Loading particle types from {path.split('/').pop()}") - model = StochSSSpatialModel(path="") - resp = model.get_types_from_file(path=path) - log.debug(f"Number of Particles: {len(resp['types'])}") + mesh_files = folder.get_file_list(ext=target_ext, test=test) + resp = {'meshFiles': mesh_files} + if include_types: + type_files = folder.get_file_list(ext=".txt", test=test) + resp['typeFiles'] = type_files + log.debug(f"Response: {resp}") self.write(resp) except StochSSAPIError as err: report_error(self, log, err) + except Exception as err: + report_critical_error(self, log, err) self.finish() - class ModelPresentationAPIHandler(APIHandler): ''' ################################################################################################ @@ -444,13 +414,16 @@ async def get(self): message = f"A presentation for {model.get_name()} already exists." else: message = f"Successfully published the {model.get_name()} presentation." - file_objs[ext](**data) + if ext == "mdl": + file_objs[ext](**data) resp = {"message": message, "links": links} log.info(resp['message']) log.debug(f"Response Message: {resp}") self.write(resp) except StochSSAPIError as err: report_error(self, log, err) + except Exception as err: + report_critical_error(self, log, err) self.finish() @@ -482,59 +455,6 @@ async def post(self): self.write(resp) except StochSSAPIError as err: report_error(self, log, err) - self.finish() - -class ApplyGeometryAPIHandler(APIHandler): - ''' - ################################################################################################ - Handler apply type and properties to particles. - ################################################################################################ - ''' - @web.authenticated - async def post(self): - ''' - Apply type and properties to particles. - - Attributes - ---------- - ''' - self.set_header('Content-Type', 'application/json') - data = json.loads(self.request.body.decode()) - log.debug(f"Type used to apply geometry: {data['type']}") - try: - log.info("Applying type and properties to particles.") - resp = StochSSSpatialModel.apply_geometry(data['particles'], data['type'], data['center']) - log.info("Successfully applied type and properties to particles.") - log.debug(f"Response Message: {resp}") - self.write(resp) - except StochSSAPIError as err: - report_error(self, log, err) - self.finish() - -class FillGeometryAPIHandler(APIHandler): - ''' - ################################################################################################ - Handler fill geometry with particles. - ################################################################################################ - ''' - @web.authenticated - async def post(self): - ''' - Fill a geomatry with particles. - - Attributes - ---------- - ''' - self.set_header('Content-Type', 'application/json') - data = json.loads(self.request.body.decode()) - log.debug(f"Agrs passed to Domain.fill_with_particles: {data['kwargs']}") - log.debug(f"Type used to fill geometry: {data['type']}") - try: - log.info("Creating particles within the geometry.") - resp = StochSSSpatialModel.fill_geometry(data['kwargs'], data['type']) - log.info("Successfully filled the geometry particles.") - log.debug(f"Response Message: {resp}") - self.write(resp) - except StochSSAPIError as err: - report_error(self, log, err) + except Exception as err: + report_critical_error(self, log, err) self.finish() diff --git a/stochss/handlers/pages.py b/stochss/handlers/pages.py index 58cf4d8100..5057e9fd24 100644 --- a/stochss/handlers/pages.py +++ b/stochss/handlers/pages.py @@ -1,6 +1,6 @@ ''' StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -17,7 +17,10 @@ ''' import os +import json +import time import logging +import subprocess from tornado import web from notebook.base.handlers import IPythonHandler, APIHandler # APIHandler documentation: @@ -25,6 +28,9 @@ # Note APIHandler.finish() sets Content-Type handler to 'application/json' # Use finish() for json, write() for text +from .util import StochSSBase, report_error +from .util.stochss_errors import AWSConfigurationError + log = logging.getLogger('stochss') # pylint: disable=abstract-method @@ -265,6 +271,23 @@ async def get(self): self.render("multiple-plots-page.html", server_path=self.get_server_path()) +class UserSettingsHandler(PageHandler): + ''' + ################################################################################################ + StochSS User Settings Page Handler + ################################################################################################ + ''' + @web.authenticated + async def get(self): + ''' + Render the StochSS user settings page. + + Attributes + ---------- + ''' + self.render("stochss-user-settings.html", server_path=self.get_server_path()) + + class UserLogsAPIHandler(APIHandler): ''' ################################################################################################ @@ -317,3 +340,171 @@ async def get(self): os.remove(f'{path}.bak') open(path, "w", encoding="utf-8").close() self.finish() + + +class LoadUserSettings(APIHandler): + ''' + ################################################################################################ + StochSS handler for loading user settings + ################################################################################################ + ''' + @web.authenticated + async def get(self): + ''' + Load the existing user settings. + + Attributes + ---------- + ''' + file = StochSSBase(path='.user-settings.json') + settings = file.load_user_settings() + + i_path = "/stochss/stochss_templates/instance_types.txt" + with open(i_path, "r", encoding="utf-8") as itype_fd: + instance_types = itype_fd.read().strip().split('|') + instances = {} + for instance_type in instance_types: + (i_type, size) = instance_type.split('.') + if i_type in instances: + instances[i_type].append(size) + else: + instances[i_type] = [size] + + self.write({"settings": settings, "instances": instances}) + self.finish() + + @web.authenticated + async def post(self): + ''' + Save the user settings. + + Attributes + ---------- + ''' + data = json.loads(self.request.body.decode()) + log.debug(f"Settings data to be saved: {data}") + + def check_env_data(data): + if data['settings']['awsRegion'] == "": + return False + if data['settings']['awsAccessKeyID'] == "": + return False + if data['secret_key'] is None: + return False + return True + + if check_env_data(data): + if not os.path.exists(".aws"): + os.mkdir(".aws") + with open(".aws/awsec2.env", "w", encoding="utf-8") as env_fd: + contents = "\n".join([ + f"AWS_DEFAULT_REGION={data['settings']['awsRegion']}", + f"AWS_ACCESS_KEY_ID={data['settings']['awsAccessKeyID']}", + f"AWS_SECRET_ACCESS_KEY={data['secret_key']}" + ]) + env_fd.write(contents) + + with open(".user-settings.json", "w", encoding="utf-8") as usrs_fd: + json.dump(data['settings'], usrs_fd, indent=4, sort_keys=True) + self.finish() + +class ConfirmAWSConfigHandler(APIHandler): + ''' + ################################################################################################ + StochSS handler for comfirming AWS configuration + ################################################################################################ + ''' + @web.authenticated + async def get(self): + ''' + Confirm that AWS is configured for running jobs.. + + Attributes + ---------- + ''' + file = StochSSBase(path='.user-settings.json') + settings = file.load_user_settings() + if settings['awsHeadNodeStatus'] != "running": + file.update_aws_status(settings['headNode']) + err = AWSConfigurationError("AWS is not properly configured for running jobs.") + report_error(self, log, err) + + self.write({"configured": True}) + self.finish() + +class LaunchAWSClusterHandler(APIHandler): + ''' + ################################################################################################ + StochSS handler for launching the AWS cluster + ################################################################################################ + ''' + @web.authenticated + async def get(self): + ''' + Launch the AWS cluster. + + Attributes + ---------- + ''' + file = StochSSBase(path='.user-settings.json') + + script = "/stochss/stochss/handlers/util/scripts/aws_compute.py" + exec_cmd = [f"{script}", "-lv"] + with subprocess.Popen(exec_cmd): + print("Launching AWS") + + time.sleep(1) + settings = file.load_user_settings() + + self.write({"settings": settings}) + self.finish() + +class AWSClusterStatusHandler(APIHandler): + ''' + ################################################################################################ + StochSS handler for updating the AWS cluster status + ################################################################################################ + ''' + @web.authenticated + async def get(self): + ''' + Update the AWS cluster status. + + Attributes + ---------- + ''' + file = StochSSBase(path='.user-settings.json') + + settings = file.load_user_settings() + + file.update_aws_status(settings['headNode']) + + self.write({"settings": settings}) + self.finish() + +class TerminateAWSClusterHandler(APIHandler): + ''' + ################################################################################################ + StochSS handler for terminating the AWS cluster + ################################################################################################ + ''' + @web.authenticated + async def get(self): + ''' + Terminate the AWS cluster. + + Attributes + ---------- + ''' + file = StochSSBase(path='.user-settings.json') + + script = "/stochss/stochss/handlers/util/scripts/aws_compute.py" + exec_cmd = [f"{script}", "-tv"] + with subprocess.Popen(exec_cmd): + print("Terminating AWS") + + time.sleep(1) + settings = file.load_user_settings() + + self.write({"settings": settings}) + self.finish() diff --git a/stochss/handlers/project.py b/stochss/handlers/project.py index a41c68adbb..1b5b63b6a5 100644 --- a/stochss/handlers/project.py +++ b/stochss/handlers/project.py @@ -1,6 +1,6 @@ ''' StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -28,7 +28,7 @@ # Use finish() for json, write() for text from .util import StochSSFolder, StochSSProject, StochSSModel, StochSSSpatialModel, \ - StochSSAPIError, report_error + StochSSAPIError, report_error, report_critical_error log = logging.getLogger('stochss') @@ -57,6 +57,8 @@ async def get(self): self.write(data) except StochSSAPIError as err: report_error(self, log, err) + except Exception as err: + report_critical_error(self, log, err) self.finish() @@ -85,6 +87,8 @@ def get(self): self.write(s_project) except StochSSAPIError as err: report_error(self, log, err) + except Exception as err: + report_critical_error(self, log, err) self.finish() @@ -115,6 +119,8 @@ def get(self): self.write(resp) except StochSSAPIError as err: report_error(self, log, err) + except Exception as err: + report_critical_error(self, log, err) self.finish() @@ -132,7 +138,6 @@ def get(self): Attributes ---------- ''' - log.setLevel(logging.DEBUG) self.set_header('Content-Type', 'application/json') path = self.get_query_argument(name="path") log.debug(f"Path to the project: {path}") @@ -146,7 +151,8 @@ def get(self): self.write(resp) except StochSSAPIError as err: report_error(self, log, err) - log.setLevel(logging.WARNING) + except Exception as err: + report_critical_error(self, log, err) self.finish() @@ -178,6 +184,8 @@ def get(self): self.write(data) except StochSSAPIError as err: report_error(self, log, err) + except Exception as err: + report_critical_error(self, log, err) self.finish() @@ -207,6 +215,8 @@ def post(self): self.write(resp) except StochSSAPIError as err: report_error(self, log, err) + except Exception as err: + report_critical_error(self, log, err) self.finish() @@ -243,6 +253,8 @@ def get(self): self.write(message) except StochSSAPIError as err: report_error(self, log, err) + except Exception as err: + report_critical_error(self, log, err) self.finish() @@ -274,6 +286,8 @@ def get(self): self.write(resp) except StochSSAPIError as err: report_error(self, log, err) + except Exception as err: + report_critical_error(self, log, err) self.finish() @@ -303,6 +317,8 @@ def post(self): log.info("Successfully saved the metadata") except StochSSAPIError as err: report_error(self, log, err) + except Exception as err: + report_critical_error(self, log, err) self.finish() @@ -361,6 +377,8 @@ def post(self): log.info("Successfully saved the annotation") except StochSSAPIError as err: report_error(self, log, err) + except Exception as err: + report_critical_error(self, log, err) self.finish() @@ -385,4 +403,6 @@ async def get(self): proj.update_project_format() except StochSSAPIError as err: report_error(self, log, err) + except Exception as err: + report_critical_error(self, log, err) self.finish() diff --git a/stochss/handlers/util/__init__.py b/stochss/handlers/util/__init__.py index cbc45fad0a..c0b67a89ef 100644 --- a/stochss/handlers/util/__init__.py +++ b/stochss/handlers/util/__init__.py @@ -1,6 +1,6 @@ ''' StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -30,4 +30,4 @@ from .stochss_project import StochSSProject from .ensemble_simulation import EnsembleSimulation from .parameter_sweep import ParameterSweep -from .stochss_errors import StochSSAPIError, report_error +from .stochss_errors import StochSSAPIError, report_error, report_critical_error diff --git a/stochss/handlers/util/ensemble_simulation.py b/stochss/handlers/util/ensemble_simulation.py index bc6bd61f74..b47d0c1464 100644 --- a/stochss/handlers/util/ensemble_simulation.py +++ b/stochss/handlers/util/ensemble_simulation.py @@ -1,6 +1,6 @@ ''' StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -18,12 +18,13 @@ import os -import numpy +import copy import pickle import logging import traceback -from gillespy2 import TimeSpan +import gillespy2 +from stochss_compute import RemoteSimulation from .stochss_job import StochSSJob from .stochss_errors import StochSSAPIError, StochSSJobResultsError @@ -89,21 +90,10 @@ def __update_timespan(self): end = self.settings['timespanSettings']['endSim'] step_size = self.settings['timespanSettings']['timeStep'] self.g_model.timespan( - TimeSpan.arange(step_size, t=end + step_size) + gillespy2.TimeSpan.arange(step_size, t=end + step_size) ) - - def run(self, preview=False, verbose=True): - ''' - Run a GillesPy2 ensemble simulation job - - Attributes - ---------- - preview : bool - Indicates whether or not to run a 5 sec preivew - verbose : bool - Indicates whether or not to print debug statements - ''' + def __run(self, run_func, preview=False, verbose=True): if preview: if verbose: log.info(f"Running {self.g_model.name} preview simulation") @@ -117,16 +107,14 @@ def run(self, preview=False, verbose=True): plot["layout"]["autosize"] = True plot["config"] = {"responsive": True, "displayModeBar": True} return plot - if verbose: - log.info("Running the ensemble simulation") if self.settings['simulationSettings']['isAutomatic']: self.__update_timespan() is_ode = self.g_model.get_best_solver().name in ["ODESolver", "ODECSolver"] - results = self.g_model.run(number_of_trajectories=1 if is_ode else 100) + results = run_func(verbose=verbose, number_of_trajectories=1 if is_ode else 100) else: kwargs = self.__get_run_settings() self.__update_timespan() - results = self.g_model.run(**kwargs) + results = run_func(verbose=verbose, **kwargs) if verbose: log.info("The ensemble simulation has completed") log.info("Storing the results as pickle") @@ -140,3 +128,38 @@ def run(self, preview=False, verbose=True): trace = str(pkl_err) raise StochSSJobResultsError(message, trace) return None + + def __run_in_aws(self, verbose=False, **kwargs): + aws_kwargs = copy.deepcopy(kwargs) + if 'solver' in kwargs: + aws_kwargs['solver'] = kwargs['solver'].__class__ + if verbose: + log.info("Running the ensemble simulation in AWS") + + cluster = self.get_aws_cluster() + # Run the simulation + simulation = RemoteSimulation(self.g_model, server=cluster) + aws_results = simulation.run(**aws_kwargs) + return aws_results.get_gillespy2_results() + + def __run_local(self, verbose=False, **kwargs): + if verbose: + log.info("Running the ensemble simulation locally") + return self.g_model.run(**kwargs) + + def run(self, preview=False, verbose=True): + ''' + Run a GillesPy2 ensemble simulation job + + Attributes + ---------- + preview : bool + Indicates whether or not to run a 5 sec preivew + verbose : bool + Indicates whether or not to print debug statements + ''' + compute_env = self.load_info()['compute_env'] + if compute_env == 'AWS': + results = self.__run(self.__run_in_aws, preview=preview, verbose=verbose) + return results + return self.__run(self.__run_local, preview=preview, verbose=verbose) diff --git a/stochss/handlers/util/parameter_scan.py b/stochss/handlers/util/parameter_scan.py index b0a4f54f44..1d306d1fd2 100644 --- a/stochss/handlers/util/parameter_scan.py +++ b/stochss/handlers/util/parameter_scan.py @@ -1,6 +1,6 @@ ''' StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/stochss/handlers/util/parameter_sweep.py b/stochss/handlers/util/parameter_sweep.py index e6b733cce6..a83957e0ee 100644 --- a/stochss/handlers/util/parameter_sweep.py +++ b/stochss/handlers/util/parameter_sweep.py @@ -1,6 +1,6 @@ ''' StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/stochss/handlers/util/parameter_sweep_1d.py b/stochss/handlers/util/parameter_sweep_1d.py index 96cf7aed89..796f97210b 100644 --- a/stochss/handlers/util/parameter_sweep_1d.py +++ b/stochss/handlers/util/parameter_sweep_1d.py @@ -1,6 +1,6 @@ ''' StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/stochss/handlers/util/parameter_sweep_2d.py b/stochss/handlers/util/parameter_sweep_2d.py index 41fd65f84e..82fecc8918 100644 --- a/stochss/handlers/util/parameter_sweep_2d.py +++ b/stochss/handlers/util/parameter_sweep_2d.py @@ -1,6 +1,6 @@ ''' StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/stochss/handlers/util/parameter_sweep_notebook.py b/stochss/handlers/util/parameter_sweep_notebook.py index f213f7d33e..18513e1b2c 100644 --- a/stochss/handlers/util/parameter_sweep_notebook.py +++ b/stochss/handlers/util/parameter_sweep_notebook.py @@ -1,6 +1,6 @@ ''' StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -16,9 +16,11 @@ along with this program. If not, see . ''' +import traceback from nbformat import v4 as nbf from .stochss_notebook import StochSSNotebook +from .stochss_errors import StochSSModelFormatError class StochSSParamSweepNotebook(StochSSNotebook): ''' @@ -36,307 +38,323 @@ def __init__(self, path, new=False, models=None, settings=None): Path to the notebook''' super().__init__(path=path, new=new, models=models, settings=settings) - - def __create_1d_class_cell(self): - pad = " " - run_str = self.__create_1d_run_str() - plt_strs = [f"{pad}def plot(c):", f"{pad*2}from matplotlib import pyplot as plt", - f"{pad*2}from mpl_toolkits.axes_grid1 import make_axes_locatable", - f"{pad*2}fig, ax = plt.subplots(figsize=(8, 8))", - pad * 2 + "plt.title(f'Parameter Sweep - Variable:{c.variable_of_interest}')", - f"{pad*2}plt.errorbar(c.p1_range, c.data[:, 0], c.data[:, 1])", - f"{pad*2}plt.xlabel(c.p1, fontsize=16, fontweight='bold')", - f"{pad*2}plt.ylabel('Population', fontsize=16, fontweight='bold')"] - pltly_str = self.__create_1d_plotly_str() - class_cell = ["class ParameterSweep1D():", "", run_str, "", "", - "\n".join(plt_strs), "", "", pltly_str] - return nbf.new_code_cell("\n".join(class_cell)) - - - def __create_1d_config_cell(self): - pad = " " - if "CSolver" in self.settings['solver']: - model_str = f"{pad}model = {self.get_class_name()}()" - else: - model_str = f"{pad}ps_class = {self.get_class_name()}" - config_cell = ["class ParameterSweepConfig(ParameterSweep1D):", - f"{pad}# What class defines the GillesPy2 model", model_str] - settings = self.settings['parameterSweepSettings'] - eval_str = "float(eval(model.get_parameter(p1).expression))" - number_of_trajectories = self.settings['simulationSettings']['realizations'] - if not settings['parameters']: - param = self.s_model['parameters'][0] - p_min = f"0.5 * {eval_str}" - p_max = f"1.5 * {eval_str}" - p_steps = "11" - spec_of_interest = self.s_model['species'][0] - else: - param = settings['parameters'][0] - p_min = param['min'] - p_max = param['max'] - p_steps = param['steps'] - spec_of_interest = settings['speciesOfInterest'] - config_cell.extend([f"{pad}# ENTER PARAMETER HERE", f"{pad}p1 = '{param['name']}'", - f"{pad}# ENTER START VALUE FOR P1 RANGE HERE", f"{pad}p1_min = {p_min}", - f"{pad}# ENTER END VALUE FOR P1 RANGE HERE", f"{pad}p1_max = {p_max}", - f"{pad}# ENTER THE NUMBER OF STEPS FOR P1 HERE", - f"{pad}p1_steps = {p_steps}", - f"{pad}p1_range = np.linspace(p1_min, p1_max, p1_steps)", - f"{pad}# ENTER VARIABLE OF INTEREST HERE", - f"{pad}variable_of_interest = '{spec_of_interest['name']}'", - f"{pad}number_of_trajectories = {number_of_trajectories}", - f"{pad}# What feature of the simulation are we examining", - f"{pad}feature_extraction = population_at_last_timepoint", - f"{pad}# for ensemble resutls: how do we aggreggate the values", - f"{pad}ensemble_aggragator = mean_std_of_ensemble"]) - return nbf.new_code_cell("\n".join(config_cell)) - - - @classmethod - def __create_1d_plotly_str(cls): - pad = " " - trace_str = f"{pad*2}trace_list = [go.Scatter(x=c.p1_range, y=c.data[:, 0]" - trace_str += ", error_y=error_y)]" - title_str = f"{pad*2}title = dict(text=f'Parameter Sweep - Variable: " - title_str += "{c.variable_of_interest}', x=0.5)" - lyout_str = f"{pad*2}layout = go.Layout(title=title, xaxis=xaxis_label, yaxis=yaxis_label)" - pltly_strs = [f"{pad}def plotplotly(c, return_plotly_figure=False):", - f"{pad*2}from plotly.offline import iplot", - f"{pad*2}import plotly.graph_objs as go", "", - f"{pad*2}visible = c.number_of_trajectories > 1", - f"{pad*2}error_y = dict(type='data', array=c.data[:, 1], visible=visible)", - "", trace_str, "", title_str, - f"{pad*2}yaxis_label = dict(title='Population')", - pad * 2 + "xaxis_label = dict(title=f'{c.p1}')", "", - lyout_str, "", f"{pad*2}fig = dict(data=trace_list, layout=layout)", "", - f"{pad*2}if return_plotly_figure:", - f"{pad*3}return fig", f"{pad*2}iplot(fig)"] - return "\n".join(pltly_strs) - - - def __create_1d_run_str(self): - pad = " " - run_strs = [f"{pad}def run(c, kwargs, verbose=False):", - f"{pad*2}c.verbose = verbose", - f"{pad*2}fn = c.feature_extraction", - f"{pad*2}ag = c.ensemble_aggragator", - f"{pad*2}data = np.zeros((len(c.p1_range), 2)) # mean and std", - f"{pad*2}for i, v1 in enumerate(c.p1_range):"] - res_str = f"{pad*4}tmp_results = " - if self.settings['solver'] == "SSACSolver": - res_str += "model.run(**kwargs, variables={c.p1:v1})" - else: - res_str += "tmp_model.run(**kwargs)" - run_strs.extend([f"{pad*3}tmp_model = c.ps_class()", - f"{pad*3}tmp_model.listOfParameters[c.p1].expression = v1"]) - run_strs.extend([f"{pad*3}if c.verbose:", - pad * 4 + "print(f'running {c.p1}={v1}')", - f"{pad*3}if(c.number_of_trajectories > 1):", - res_str, - f"{pad*4}(m, s) = ag([fn(x) for x in tmp_results])", - f"{pad*4}data[i, 0] = m", - f"{pad*4}data[i, 1] = s", - f"{pad*3}else:", - res_str.replace("results", "result"), - f"{pad*4}data[i, 0] = c.feature_extraction(tmp_result)", - f"{pad*2}c.data = data"]) - return "\n".join(run_strs) - - - def __create_2d_class_cell(self): - pad = " " - run_str = self.__create_2d_run_str() - plt_strs = [f"{pad}def plot(c):", f"{pad*2}from matplotlib import pyplot as plt", - f"{pad*2}from mpl_toolkits.axes_grid1 import make_axes_locatable", - f"{pad*2}fig, ax = plt.subplots(figsize=(8, 8))", - f"{pad*2}plt.imshow(c.data)", - f"{pad*2}ax.set_xticks(np.arange(c.data.shape[1]) + 0.5, minor=False)", - f"{pad*2}ax.set_yticks(np.arange(c.data.shape[0]) + 0.5, minor=False)", - pad * 2 + "plt.title(f'Parameter Sweep - Variable: {c.variable_of_interest}')", - f"{pad*2}ax.set_xticklabels(c.p1_range, minor=False, rotation=90)", - f"{pad*2}ax.set_yticklabels(c.p2_range, minor=False)", - f"{pad*2}ax.set_xlabel(c.p1, fontsize=16, fontweight='bold')", - f"{pad*2}ax.set_ylabel(c.p2, fontsize=16, fontweight='bold')", - f"{pad*2}divider = make_axes_locatable(ax)", - f"{pad*2}cax = divider.append_axes('right', size='5%', pad=0.2)", - f"{pad*2}_ = plt.colorbar(ax=ax, cax=cax)"] - pltly_str = self.__create_2d_plotly_str() - class_cell = ["class ParameterSweep2D():", "", run_str, "", "", - "\n".join(plt_strs), "", "", pltly_str] - return nbf.new_code_cell("\n".join(class_cell)) - - - def __create_2d_config_cell(self): - pad = " " - if "CSolver" in self.settings['solver']: - model_str = f"{pad}model = {self.get_class_name()}()" + def __create_1d_aggregator(self): + pad = ' ' + nb_agg = [ + f"{pad}def ensemble_aggregator(self, results, key='avg', verbose=False):", + f"{pad*2}self.visible = len(results[0]) > 1", + f"{pad*2}data = [[self.func_map[key](result), numpy.std(result)] for result in results]", + f"{pad*2}if verbose:", + f"{pad*3}print(f'" + r"{key} and std of the ensembles: {data}')", + f"{pad*2}return numpy.array(data)", "" + ] + return '\n'.join(nb_agg) + + def __create_1d_plot(self): + pad = ' ' + nb_plot = [ + f"{pad}def plot(self, species, fe_key='final', ea_key='avg', verbose=False):", + f"{pad*2}x_data = self.parameters[0]['range']", + f"{pad*2}fe_data = self.feature_extractor(species, key=fe_key, verbose=verbose)", + f"{pad*2}data = self.ensemble_aggregator(fe_data, key=ea_key, verbose=verbose)", "", + f"{pad*2}fig, ax = plt.subplots(figsize=(15, 6))", + f"{pad*2}plt.title(f'Parameter Sweep - Variable: " + r"{species}', fontsize=18, fontweight='bold')", + f"{pad*2}plt.errorbar(x_data, data[:, 0], data[:, 1])", + f"{pad*2}plt.xlabel(self.parameters[0]['param'], fontsize=16, fontweight='bold')", + f"{pad*2}plt.ylabel('Population', fontsize=16, fontweight='bold')", "" + ] + return '\n'.join(nb_plot) + + def __create_1d_plotplotly(self): + pad = ' ' + nb_plotplotly = [ + f"{pad}def plotplotly(self, species, fe_key='final', ea_key='avg',", + f"{pad*6}return_plotly_figure=False, verbose=False):", + f"{pad*2}x_data = self.parameters[0]['range']", + f"{pad*2}fe_data = self.feature_extractor(species, key=fe_key, verbose=verbose)", + f"{pad*2}data = self.ensemble_aggregator(fe_data, key=ea_key, verbose=verbose)", "", + f"{pad*2}error_y = dict(type='data', array=data[:, 1], visible=self.visible)", + f"{pad*2}trace_list = [go.Scatter(x=x_data, y=data[:, 0], error_y=error_y)]", "", + f"{pad*2}title = f'Parameter Sweep - Variable: " + r"{species}'", + f"{pad*2}layout = go.Layout(", f"{pad*3}title=dict(text=title, x=0.5),", + f"{pad*3}xaxis=dict(title=f'" + r'{self.parameters[0]["param"]}' + "'),", + f"{pad*3}yaxis=dict(title='Population')", f"{pad*2})", "", + f"{pad*2}fig = dict(data=trace_list, layout=layout)", + f"{pad*2}if return_plotly_figure:", f"{pad*3}return fig", f"{pad*2}iplot(fig)" + ] + return '\n'.join(nb_plotplotly) + + def __create_2d_aggregator(self): + pad = ' ' + nb_agg = [ + f"{pad}def ensemble_aggregator(self, results, key='avg', verbose=False):", + f"{pad*2}data = [self.func_map[key](result) for result in results]", + f"{pad*2}if verbose:", f"{pad*3}print(f'" + r"{key} of the ensembles: {data}')", + f"{pad*2}return numpy.array(data)", "" + ] + return '\n'.join(nb_agg) + + def __create_2d_plot(self): + pad = ' ' + nb_plot = [ + f"{pad}def plot(self, species, fe_key='final', ea_key='avg', verbose=False):", + f"{pad*2}x_data = self.parameters[0]['range']", + f"{pad*2}y_data = self.parameters[1]['range']", + f"{pad*2}fe_data = self.feature_extractor(species, key=fe_key, verbose=verbose)", + f"{pad*2}ea_data = self.ensemble_aggregator(fe_data, key=ea_key, verbose=verbose)", + f"{pad*2}shape = (len(self.parameters[1]['range']), len(self.parameters[0]['range']))", + f"{pad*2}data = numpy.flip(numpy.reshape(ea_data, shape))", "", + f"{pad*2}fig, ax = plt.subplots(figsize=(16, 2 * len(data)))", + f"{pad*2}plt.imshow(data)", + f"{pad*2}ax.set_xticks(numpy.arange(data.shape[1]) + 0.5, minor=False)", + f"{pad*2}ax.set_yticks(numpy.arange(data.shape[0]) + 0.5, minor=False)", + f"{pad*2}plt.title(f'Parameter Sweep - Variable: " + r"{species}', fontsize=18, fontweight='bold')", + f"{pad*2}ax.set_xticklabels(x_data)", f"{pad*2}ax.set_yticklabels(y_data)", + f"{pad*2}ax.set_xlabel(self.parameters[1]['param'], fontsize=16, fontweight='bold')", + f"{pad*2}ax.set_ylabel(self.parameters[0]['param'], fontsize=16, fontweight='bold')", + f"{pad*2}ax.tick_params(axis='x', labelsize=14, labelrotation=90)", + f"{pad*2}ax.tick_params(axis='y', labelsize=14)", + f"{pad*2}divider = make_axes_locatable(ax)", + f"{pad*2}cax = divider.append_axes('right', size='5%', pad=0.2)", + f"{pad*2}_ = plt.colorbar(ax=ax, cax=cax)", "" + ] + return '\n'.join(nb_plot) + + def __create_2d_plotplotly(self): + pad = ' ' + nb_plotplotly = [ + f"{pad}def plotplotly(self, species, fe_key='final', ea_key='avg',", + f"{pad*6}return_plotly_figure=False, verbose=False):", + f"{pad*2}x_data = self.parameters[1]['range']", + f"{pad*2}y_data = self.parameters[0]['range']", + f"{pad*2}fe_data = self.feature_extractor(species, key=fe_key, verbose=verbose)", + f"{pad*2}ea_data = self.ensemble_aggregator(fe_data, key=ea_key, verbose=verbose)", + f"{pad*2}shape = (len(self.parameters[1]['range']), len(self.parameters[0]['range']))", + f"{pad*2}data = numpy.reshape(ea_data, shape)", "", + f"{pad*2}trace_list = [go.Heatmap(z=data, x=x_data, y=y_data)]", "", + f"{pad*2}title = f'Parameter Sweep - Variable: " + r"{species}'", + f"{pad*2}layout = go.Layout(", f"{pad*3}title=dict(text=title, x=0.5),", + f"{pad*3}xaxis=dict(title=f'" + '{self.parameters[1]["param"]}' + "'),", + f"{pad*3}yaxis=dict(title=f'" + '{self.parameters[0]["param"]}' + "')", + f"{pad*2})", "", f"{pad*2}fig = dict(data=trace_list, layout=layout)", + f"{pad*2}if return_plotly_figure:", f"{pad*3}return fig", f"{pad*2}iplot(fig)" + ] + return '\n'.join(nb_plotplotly) + + def __create_header_cells(self, cells): + cells.extend([ + nbf.new_markdown_cell("***\n## Parameter Sweep\n***"), + nbf.new_markdown_cell("### Instantiate the Parameter Sweep"), + nbf.new_markdown_cell("***\n## Configure the Parameter Sweep\n***"), + nbf.new_markdown_cell("***\n## Run the Parameter Sweep\n***"), + nbf.new_markdown_cell("***\n## Visualization\n***") + ]) + + def __create_import_cells(self, cells, results=None): + base_imports = ["import copy", "import hashlib"] + if results is not None: + base_imports.insert(0, "import os") + base_imports.insert(2, "import pickle") + cells.insert(1, nbf.new_code_cell("\n".join(base_imports))) + cells.insert(2, nbf.new_code_cell("import numpy")) + cells.insert(3, nbf.new_markdown_cell( + "MatPlotLib and Plotly are used for creating custom visualizations" + )) + mpl_imports = ["from matplotlib import pyplot as plt"] + if self.nb_type == self.PARAMETER_SWEEP_2D: + mpl_imports.append("from mpl_toolkits.axes_grid1 import make_axes_locatable") + cells.insert(4, nbf.new_code_cell("\n".join(mpl_imports))) + cells.insert(5, nbf.new_code_cell( + "from plotly.offline import iplot\nimport plotly.graph_objs as go" + )) + + def __create_parameter_sweep_class(self): + pad = ' ' + nb_psweep_class = [ + "class ParameterSweep:", f"{pad}def __init__(self, model):", + f"{pad*2}self.model = model", f"{pad*2}self.settings = " + r"{}", + f"{pad*2}self.parameters = []", f"{pad*2}self.__parameters = " + r"{}", + "", f"{pad*2}self.results = " + r"{}", f"{pad*2}self.__results = " + r"{}", + "", f"{pad}def __reset(self):", f"{pad*2}self.results = " + r"{}", + f"{pad*2}self.__results = " + r"{}", "", + f"{pad}def __run(self, variables, index, verbose=False):", + f"{pad*2}if index < len(self.parameters):", + f"{pad*3}parameter = self.parameters[index]", f"{pad*3}index += 1", + f"{pad*3}for value in parameter['range']:", + f"{pad*4}variables[parameter['param']] = value", + f"{pad*4}self.__run(variables, index, verbose=verbose)", f"{pad*2}else:", + f"{pad*3}sim_key = hashlib.md5(str(variables).encode('utf-8')).hexdigest()", + f"{pad*3}if sim_key not in self.results:", + f"{pad*4}model = self.__setup_model(variables=variables)", + f"{pad*4}if verbose:", f"{pad*5}print(f'--> running: " + "{str(variables)}" + "')", + f"{pad*4}self.results[sim_key] = model.run(**self.settings)", "", + f"{pad}def __resume(self, resume):", f"{pad*2}orig_params = " + r"{}", + f"{pad*2}self.results = resume.data", f"{pad*2}for parameter in resume.parameters:", + f"{pad*3}orig_params[parameter['param']] = parameter['range']", + f"{pad*3}self.add_parameter(parameter['param'], values= parameter['range'])", + f"{pad*2}new_params = [param for param in self.__parameters if param not in orig_params]", + f"{pad*2}if len(new_params) > 0:", f"{pad*3}variables = " + \ + "{new_param: self.model.listOfParameters[new_param].value for new_param in new_params}", + f"{pad*3}self.__update_results(resume.parameters, variables, " + r"{})", + f"{pad*3}self.results = self.__results", "", f"{pad}def __setup_model(self, variables):", + f"{pad*2}if 'solver' in self.settings and 'CSolver' in self.settings['solver'].name:", + f"{pad*3}self.settings['variables'] = copy.deepcopy(variables)", + f"{pad*3}return self.model", f"{pad*2}model = copy.deepcopy(self.model)", + f"{pad*2}for name, value in variables.items():", + f"{pad*3}model.listOfParameters[name].expression = str(value)",f"{pad*2}return model", + "", f"{pad}def __update_results(self, parameters, variables, orig_vars, index=0):", + f"{pad*2}if index < len(parameters):", f"{pad*3}parameter = parameters[index]", + f"{pad*3}index += 1", f"{pad*3}for value in parameter['range']:", + f"{pad*4}orig_vars[parameter['param']] = value", + f"{pad*4}variables[parameter['param']] = value", + f"{pad*4}self.__update_results(parameters, variables, orig_vars, index=index)", + f"{pad*2}else:", + f"{pad*3}o_sim_key = hashlib.md5(str(orig_vars).encode('utf-8')).hexdigest()", + f"{pad*3}n_sim_key = hashlib.md5(str(variables).encode('utf-8')).hexdigest()", + f"{pad*3}self.__results[n_sim_key] = self.results[o_sim_key]", "", + f"{pad}def add_parameter(self, param, bounds=None, steps=11, values=None):", + f"{pad*2}if values is None:", + f"{pad*3}values = numpy.linspace(bounds[0], bounds[1], steps)", f"{pad*2}else:", + f"{pad*3}values = numpy.array(values)", + f"{pad*2}if param in self.__parameters:", f"{pad*3}for value in values:", + f"{pad*4}if value not in self.__parameters[param]:", + f"{pad*5}self.__parameters[param] = numpy.append(self.__parameters[param], value)", + f"{pad*3}self.__parameters[param] = numpy.sort(self.__parameters[param])", + f"{pad*2}else:", f"{pad*3}self.__parameters[param] = values", + f"{pad*2}self.parameters = [" + \ + "{'param': param, 'range': ps_range} for param, ps_range in self.__parameters.items()]", + "", f"{pad}def run(self, settings=None, resume=None, verbose=False):", + f"{pad*2}if settings is not None:", f"{pad*3}self.settings = settings", "", + f"{pad*2}if resume is not None:", f"{pad*3}self.__resume(resume)", + f"{pad*2}index = 0", f"{pad*2}variables = " + r"{}", + f"{pad*2}self.__run(variables, index, verbose=verbose)", "", + f"{pad*2}results = Results(self.results, self.parameters)", + f"{pad*2}self.__reset()", f"{pad*2}return results" + ] + return nbf.new_code_cell('\n'.join(nb_psweep_class)) + + def __create_parameter_sweep_results_class(self): + pad = ' ' + if self.nb_type == self.PARAMETER_SWEEP_1D: + aggregator = self.__create_1d_aggregator() + plot = self.__create_1d_plot() + plotplotly = self.__create_1d_plotplotly() else: - model_str = f"{pad}ps_class = {self.get_class_name()}" - config_cell = ["class ParameterSweepConfig(ParameterSweep2D):", - f"{pad}# What class defines the GillesPy2 model", model_str] - settings = self.settings['parameterSweepSettings'] - p1_eval_str = "float(eval(model.get_parameter(p1).expression))" - p2_eval_str = "float(eval(model.get_parameter(p2).expression))" - number_of_trajectories = self.settings['simulationSettings']['realizations'] - if not settings['parameters']: - param1 = self.s_model['parameters'][0] - p1_min = f"0.5 * {p1_eval_str}" - p1_max = f"1.5 * {p1_eval_str}" - param2 = self.s_model['parameters'][1] - p2_min = f"0.5 * {p2_eval_str}" - p2_max = f"1.5 * {p2_eval_str}" - spec_of_interest = self.s_model['species'][0] + aggregator = self.__create_2d_aggregator() + plot = self.__create_2d_plot() + plotplotly = self.__create_2d_plotplotly() + nb_results = [ + "class Results:", f"{pad}func_map = " + "{", + f"{pad*2}'min': numpy.min, 'max': numpy.max, 'avg': numpy.mean,", + f"{pad*2}'var': numpy.var, 'final': lambda res: res[-1]", f"{pad}" + "}", "", + f"{pad}def __init__(self, data, parameters):", f"{pad*2}self.data = data", + f"{pad*2}self.visible = False", f"{pad*2}self.parameters = parameters", "", + f"{pad}def feature_extractor(self, species, results=None, key='final', verbose=False):", + f"{pad*2}if results is None:", f"{pad*3}results = self.data.values()", + f"{pad*2}data = [[self.func_map[key](traj[species]) for traj in result] for result in results]", + f"{pad*2}if verbose:", + f"{pad*3}print(f'" + r"{key} populations for {species}: {data}')", + f"{pad*2}return numpy.array(data)", "", aggregator, plot, plotplotly + ] + return nbf.new_code_cell('\n'.join(nb_results)) + + def __create_parameter_sweep_config(self, cells, index, count, results): + pad = ' ' + if results is None: + tmp_val = "float(eval(model.get_parameter('__NAME__').expression))" + tmp_bounds = f"p__I___bounds = (\n{pad}0.5 * {tmp_val},\n{pad}1.5 * {tmp_val}\n)" + tmp = f"{tmp_bounds}\npsweep.add_parameter('__NAME__', bounds=p1_bounds, steps=11)" + try: + for i in range(count): + parameter = self.s_model['parameters'][i] + psweep_config = tmp.replace("__I__", str(i + 1)) + psweep_config = psweep_config.replace("__NAME__", parameter['name']) + cells.insert(index, nbf.new_code_cell(psweep_config)) + index += 1 + except KeyError as err: + message = "Parameters are not properly formatted or " + message += f"are referenced incorrectly for notebooks: {str(err)}" + raise StochSSModelFormatError(message, traceback.format_exc()) from err else: - param1 = settings['parameters'][0] - p1_min = param1['min'] - p1_max = param1['max'] - param2 = settings['parameters'][1] - p2_min = param2['min'] - p2_max = param2['max'] - spec_of_interest = settings['speciesOfInterest'] - config_cell.extend([f"{pad}# ENTER PARAMETER 1 HERE", f"{pad}p1 = '{param1['name']}'", - f"{pad}# ENTER PARAMETER 2 HERE", f"{pad}p2 = '{param2['name']}'", - f"{pad}# ENTER START VALUE FOR P1 RANGE HERE", - f"{pad}p1_min = {p1_min}", - f"{pad}# ENTER END VALUE FOR P1 RANGE HERE", f"{pad}p1_max = {p1_max}", - f"{pad}# ENTER THE NUMBER OF STEPS FOR P1 HERE", - f"{pad}p1_steps = {param1['steps'] if settings['parameters'] else 11}", - f"{pad}p1_range = np.linspace(p1_min, p1_max, p1_steps)", - f"{pad}# ENTER START VALUE FOR P2 RANGE HERE", - f"{pad}p2_min = {p2_min}", - f"{pad}# ENTER END VALUE FOR P2 RANGE HERE", f"{pad}p2_max = {p2_max}", - f"{pad}# ENTER THE NUMBER OF STEPS FOR P2 HERE", - f"{pad}p2_steps = {param2['steps'] if settings['parameters'] else 11}", - f"{pad}p2_range = np.linspace(p2_min, p2_max, p2_steps)", - f"{pad}# ENTER VARIABLE OF INTEREST HERE", - f"{pad}variable_of_interest = '{spec_of_interest['name']}'", - f"{pad}number_of_trajectories = {number_of_trajectories}", - f"{pad}# What feature of the simulation are we examining", - f"{pad}feature_extraction = population_at_last_timepoint", - f"{pad}# for ensemble resutls: how do we aggreggate the values", - f"{pad}ensemble_aggragator = average_of_ensemble"]) - return nbf.new_code_cell("\n".join(config_cell)) - - - @classmethod - def __create_2d_plotly_str(cls): - pad = " " - title_str = f"{pad*2}title = dict(text=f'Parameter Sweep - Variable: " - title_str += "{c.variable_of_interest}', x=0.5)" - lyout_str = f"{pad*2}layout = go.Layout(title=title, xaxis=xaxis_label, yaxis=yaxis_label)" - pltly_strs = [f"{pad}def plotplotly(c, return_plotly_figure=False):", - f"{pad*2}from plotly.offline import init_notebook_mode, iplot", - f"{pad*2}import plotly.graph_objs as go", "", - f"{pad*2}xaxis_ticks = c.p1_range", f"{pad*2}yaxis_ticks = c.p2_range", "", - f"{pad*2}trace_list = [go.Heatmap(z=c.data, x=xaxis_ticks, y=yaxis_ticks)]", - title_str, pad * 2 + "xaxis_label = dict(title=f'{c.p1}')", - pad * 2 + "yaxis_label = dict(title=f'{c.p2}')", "", - lyout_str, "", f"{pad*2}fig = dict(data=trace_list, layout=layout)", "", - f"{pad*2}if return_plotly_figure:", - f"{pad*3}return fig", f"{pad*2}iplot(fig)"] - return "\n".join(pltly_strs) - - - def __create_2d_run_str(self): - pad = " " - run_strs = [f"{pad}def run(c, kwargs, verbose=False):", - f"{pad*2}c.verbose = verbose", - f"{pad*2}fn = c.feature_extraction", - f"{pad*2}ag = c.ensemble_aggragator", - f"{pad*2}data = np.zeros((len(c.p1_range), len(c.p2_range)))", - f"{pad*2}for i, v1 in enumerate(c.p1_range):", - f"{pad*3}for j, v2 in enumerate(c.p2_range):"] - res_str = f"{pad*5}tmp_results = " - if self.settings['solver'] == "SSACSolver": - res_str += "model.run(**kwargs, variables={c.p1:v1, c.p2:v2})" + tmp = "psweep.add_parameter('__NAME__', values=__VALUE__)" + try: + for parameter in self.settings['parameterSweepSettings']['parameters']: + psweep_config = tmp.replace("__NAME__", parameter['name']) + values = [str(value) for value in parameter['range']] + psweep_config = psweep_config.replace( + "__VALUE__", f"[\n{pad}{', '.join(values)}\n]" + ) + cells.insert(index, nbf.new_code_cell(psweep_config)) + index += 1 + except KeyError as err: + message = "Parameter sweep settings are not properly formatted or " + message += f"are referenced incorrectly for notebooks: {str(err)}" + raise StochSSModelFormatError(message, traceback.format_exc()) from err + return index + 1 + + def __create_visualization(self, results): + if results is None: + spec_of_interest = self.s_model['species'][0]['name'] else: - res_str += "tmp_model.run(**kwargs)" - run_strs.extend([f"{pad*4}tmp_model = c.ps_class()", - f"{pad*4}tmp_model.listOfParameters[c.p1].expression = v1", - f"{pad*4}tmp_model.listOfParameters[c.p2].expression = v2"]) - run_strs.extend([f"{pad*4}if c.verbose:", - pad * 5 + "print(f'running {c.p1}={v1}, {c.p2}={v2}')", - f"{pad*4}if(c.number_of_trajectories > 1):", - res_str, - f"{pad*5}data[i, j] = ag([fn(x) for x in tmp_results])", - f"{pad*4}else:", - res_str.replace("results", "result"), - f"{pad*5}data[i, j] = c.feature_extraction(tmp_result)", - f"{pad*2}c.data = data"]) - return "\n".join(run_strs) - + spec_of_interest = self.settings['parameterSweepSettings']['speciesOfInterest']['name'] + return nbf.new_code_cell(f"results.plotplotly('{spec_of_interest}')") - def __create_post_process_cells(self): - pad = " " - fe_vbs_pnt = f"{pad*2}print(f'population_at_last_timepoint" - fe_vbs_pnt += " {c.variable_of_interest}={res[c.variable_of_interest][-1]}')" - # feature extraction cell - fe_cell = ["# What value(s) do you want to extract from the simulation trajectory", - "def population_at_last_timepoint(c, res):", f"{pad}if c.verbose:", - fe_vbs_pnt, f"{pad}return res[c.variable_of_interest][-1]"] - # mean std aggragator cell - msa_cell = ["# How do we combine the values from multiple trajectores", - "def mean_std_of_ensemble(c, data):", f"{pad}a = np.average(data)", - f"{pad}s = np.std(data)", f"{pad}if c.verbose:", - pad * 2 + "print(f'mean_std_of_ensemble m:{a} s:{s}')", - f"{pad}return (a, s)"] - # average aggragator cell - aa_cell = [msa_cell[0], "def average_of_ensemble(c, data):", - f"{pad}a = np.average(data)", f"{pad}if c.verbose:", - pad * 2 + "print(f'average_of_ensemble = {a}')", - f"{pad}return a"] - cells = [nbf.new_markdown_cell("# Post Processing"), - nbf.new_markdown_cell("## Feature extraction function"), - nbf.new_code_cell('\n'.join(fe_cell)), - nbf.new_markdown_cell("## Aggregation function")] - if self.nb_type == self.PARAMETER_SWEEP_1D: - cells.append(nbf.new_code_cell('\n'.join(msa_cell))) + def __create_run(self, results): + nb_run = ["kwargs = configure_simulation()"] + if results is None: + nb_run.append("results = psweep.run(settings=kwargs)") else: - cells.append(nbf.new_code_cell('\n'.join(aa_cell))) - return cells - - - def create_1d_notebook(self): - '''Create a 1D parameter sweep jupiter notebook for a StochSS model/workflow - - Attributes - ----------''' + nb_load_res = [ + "# results = psweep.run(settings=kwargs)", f"path = '{results}'", + "with open(os.path.join(os.path.expanduser('~'), path), 'rb') as results_file:", + " results = Results(pickle.load(results_file), psweep.parameters)" + ] + nb_run.extend(nb_load_res) + nb_run = "\n".join(nb_run) + return nbf.new_code_cell(nb_run) + + def create_1d_notebook(self, results=None, compute="StochSS"): + '''Create a 1D parameter sweep jupiter notebook for a StochSS model/workflow''' self.nb_type = self.PARAMETER_SWEEP_1D - self.settings['solver'] = self.get_gillespy2_solver_name() - run_strs = ["kwargs = configure_simulation()", "ps = ParameterSweepConfig()", - "%time ps.run(kwargs)"] cells = self.create_common_cells() - cells.extend(self.__create_post_process_cells()) - cells.extend([nbf.new_markdown_cell("# Parameter Sweep"), - self.__create_1d_class_cell(), - self.__create_1d_config_cell(), - nbf.new_code_cell("\n".join(run_strs)), - nbf.new_markdown_cell("# Visualization"), - nbf.new_code_cell("ps.plot()"), - nbf.new_code_cell("ps.plotplotly()")]) + self.__create_import_cells(cells, results) + self.__create_header_cells(cells) + + self.settings['solver'] = self.get_gillespy2_solver_name() + cells.insert(14, self.__create_parameter_sweep_class()) + cells.insert(15, self.__create_parameter_sweep_results_class()) + cells.insert(17, nbf.new_code_cell("psweep = ParameterSweep(model=model)")) + index = self.__create_parameter_sweep_config(cells, 19, 1, results) + cells.insert(index, self.__create_run(results)) + cells.append(self.__create_visualization(results)) + if compute != "StochSS": + self.log( + "warning", + "AWS Cloud compute environment is not supported by 1D parameter sweep workflows." + ) message = self.write_notebook_file(cells=cells) return {"Message":message, "FilePath":self.get_path(), "File":self.get_file()} - - def create_2d_notebook(self): - '''Create a 2D parameter sweep jupiter notebook for a StochSS model/workflow - - Attributes - ----------''' + def create_2d_notebook(self, results=None, compute="StochSS"): + '''Create a 2D parameter sweep jupiter notebook for a StochSS model/workflow''' self.nb_type = self.PARAMETER_SWEEP_2D - self.settings['solver'] = self.get_gillespy2_solver_name() - run_strs = ["kwargs = configure_simulation()", "ps = ParameterSweepConfig()", - "%time ps.run(kwargs)"] cells = self.create_common_cells() - cells.extend(self.__create_post_process_cells()) - cells.extend([nbf.new_markdown_cell("# Parameter Sweep"), - self.__create_2d_class_cell(), - self.__create_2d_config_cell(), - nbf.new_code_cell("\n".join(run_strs)), - nbf.new_markdown_cell("# Visualization"), - nbf.new_code_cell("ps.plot()"), - nbf.new_code_cell("ps.plotplotly()")]) + self.__create_import_cells(cells, results) + self.__create_header_cells(cells) + + self.settings['solver'] = self.get_gillespy2_solver_name() + cells.insert(14, self.__create_parameter_sweep_class()) + cells.insert(15, self.__create_parameter_sweep_results_class()) + cells.insert(17, nbf.new_code_cell("psweep = ParameterSweep(model=model)")) + index = self.__create_parameter_sweep_config(cells, 19, 2, results) + cells.insert(index, self.__create_run(results)) + cells.append(self.__create_visualization(results)) + if compute != "StochSS": + self.log( + "warning", + "AWS Cloud compute environment is not supported by 2D parameter sweep workflows." + ) message = self.write_notebook_file(cells=cells) return {"Message":message, "FilePath":self.get_path(), "File":self.get_file()} diff --git a/stochss/handlers/util/sciope_notebook.py b/stochss/handlers/util/sciope_notebook.py index acaee8471c..63ec8d093a 100644 --- a/stochss/handlers/util/sciope_notebook.py +++ b/stochss/handlers/util/sciope_notebook.py @@ -1,6 +1,6 @@ ''' StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -36,184 +36,216 @@ def __init__(self, path, new=False, models=None, settings=None): Path to the notebook''' super().__init__(path=path, new=new, models=models, settings=settings) - - @classmethod - def __create_me_expres_cells(cls): - # res conf cell - param_conf = "met.data.configurations['listOfParameters'] = " - param_conf += "list(model.listOfParameters.keys())" - rconf_strs = ["# First lets add some appropiate information about the model and features", - param_conf, - "met.data.configurations['listOfSpecies'] = list(model.listOfSpecies.keys())", - "met.data.configurations['listOfSummaries'] = met.summaries.features", - "met.data.configurations['timepoints'] = model.tspan"] - # met explore cell - mtexp_strs = ["# Here we use UMAP for dimension reduction", "met.explore(dr_method='umap')"] - # supervised train cell - sptrn_strs = ["from sciope.models.label_propagation import LPModel", - "# here lets use the dimension reduction embedding as input data", - "data = met.dr_model.embedding_", "", - "model_lp = LPModel()", "# train using basinhopping", - "model_lp.train(data, met.data.user_labels, min_=0.01, max_=10, niter=50)"] - # map label cell - cmt_str = "# just to vislualize the result we will map the label distribution " - cmt_str += "to the user_labels\n# (will enable us to see the LP model output " - cmt_str += "when using method 'explore')" - mplbl_strs = [cmt_str, "user_labels = np.copy(met.data.user_labels)", - "# takes the label corresponding to index 0", - "met.data.user_labels = model_lp.model.label_distributions_[:, 0]"] - cells = [nbf.new_markdown_cell("## Explore the result"), - nbf.new_code_cell("\n".join(rconf_strs)), - nbf.new_code_cell("\n".join(mtexp_strs)), - nbf.new_code_cell("\n".join(sptrn_strs)), - nbf.new_code_cell("\n".join(mplbl_strs)), - nbf.new_code_cell("met.explore(dr_method='umap')"), - nbf.new_code_cell("met.data.user_labels = user_labels")] - return cells - - - def __create_me_setup_cells(self): - spec_of_interest = list(self.model.get_all_species().keys()) - # Wrapper cell - sim_str = "simulator = wrapper.get_simulator(gillespy_model=model, " - sim_str += f"run_settings=settings, species_of_interest={spec_of_interest})" - sim_strs = ["from sciope.utilities.gillespy2 import wrapper", - "settings = configure_simulation()", sim_str, - "expression_array = wrapper.get_parameter_expression_array(model)"] - # Dask cell - dask_strs = ["from dask.distributed import Client", "", "c = Client()"] - # lhc cell - lhc_str = "lhc = latin_hypercube_sampling.LatinHypercube(" - lhc_str += "xmin=expression_array, xmax=expression_array*3)" - lhc_strs = ["from sciope.designs import latin_hypercube_sampling", - "from sciope.utilities.summarystats.auto_tsfresh import SummariesTSFRESH", "", - lhc_str, "lhc.generate_array(1000) # creates a LHD of size 1000", "", - "# will use default minimal set of features", - "summary_stats = SummariesTSFRESH()"] - # stochmet cell - ism_strs = ["from sciope.stochmet.stochmet import StochMET", "", - "met = StochMET(simulator, lhc, summary_stats)"] - cells = [nbf.new_markdown_cell("## Define simulator function (using gillespy2 wrapper)"), - nbf.new_code_cell("\n".join(sim_strs)), - nbf.new_markdown_cell("## Start local cluster using dask client"), - nbf.new_code_cell("\n".join(dask_strs)), - nbf.new_markdown_cell("## Define parameter sampler/design and summary statistics"), - nbf.new_code_cell("\n".join(lhc_strs)), - nbf.new_markdown_cell("## Initiate StochMET"), - nbf.new_code_cell("\n".join(ism_strs))] - return cells - - - def __create_mi_setup_cells(self): - pad = " " - priors = ["from sciope.utilities.priors import uniform_prior", "", - "# take default from mode 1 as reference", - "default_param = np.array(list(model.listOfParameters.items()))[:, 1]", - "", "bound = []", "for exp in default_param:", - f"{pad}bound.append(float(exp.expression))", "", "# Set the bounds", - "bound = np.array(bound)", "dmin = bound * 0.1", "dmax = bound * 2.0", - "", "# Here we use uniform prior", - "uni_prior = uniform_prior.UniformPrior(dmin, dmax)"] - stat_dist = ["from sciope.utilities.summarystats import auto_tsfresh", - "from sciope.utilities.distancefunctions import naive_squared", "", - "# Function to generate summary statistics", - "summ_func = auto_tsfresh.SummariesTSFRESH()", "", - "# Distance", "ns = naive_squared.NaiveSquaredDistance()"] - cells = [nbf.new_markdown_cell("## Define prior distribution"), - nbf.new_code_cell("\n".join(priors)), - nbf.new_markdown_cell("## Define simulator"), - self.__create_mi_simulator_cell(), - nbf.new_markdown_cell("## Define summary statistics and distance function"), - nbf.new_code_cell("\n".join(stat_dist))] - return cells - - - def __create_mi_simulator_cell(self): - pad = " " - comment = f"{pad}# params - array, need to have the same order as model.listOfParameters" - loop = f"{pad}for e, pname in enumerate(model.listOfParameters.keys()):" - if self.settings['solver'] == "SSACSolver": - comment += "\n"+ pad +"variables = {}" - func_def = "def get_variables(params, model):" - body = f"{pad*2}variables[pname] = params[e]" - return_str = f"{pad}return variables" - call = f"{pad}variables = get_variables(params, model)" - run = f"{pad}res = model.run(**kwargs, variables=variables)" + def __create_abc_cells(self, cells): + pad = ' ' + cell1 = [ + "abc = ABC(", f"{pad}fixed_data, sim=simulator2, prior_function=uni_prior,", + f"{pad}summaries_function=summ_func.compute, distance_function=ns", ")" + ] + cells.insert(27, nbf.new_code_cell("\n".join(cell1))) + cells.append(nbf.new_code_cell("abc.compute_fixed_mean(chunk_size=2)")) + cells.append(nbf.new_code_cell( + "res = abc.infer(num_samples=100, batch_size=10, chunk_size=2)" + )) + cells.append(nbf.new_code_cell( + "mae_inference = mean_absolute_error(bound, abc.results['inferred_parameters'])" + )) + + def __create_explore_cells(self, cells): + cell1 = [ + "met.data.configurations['listOfParameters'] = list(model.listOfParameters.keys())", + "met.data.configurations['listOfSpecies'] = list(model.listOfSpecies.keys())", + "met.data.configurations['listOfSummaries'] = met.summaries.features", + "met.data.configurations['timepoints'] = model.tspan", + ] + cell3 = [ + "data = met.dr_model.embedding_", "", + "model_lp = LPModel()", "# train using basinhopping", + "model_lp.train(data, met.data.user_labels, min_=0.01, max_=10, niter=50)" + ] + cell4 = [ + "user_labels = numpy.copy(met.data.user_labels)", + "# takes the label corresponding to index 0", + "met.data.user_labels = model_lp.model.label_distributions_[:, 0]" + ] + cells.insert(21, nbf.new_code_cell("\n".join(cell1))) + cells.insert(23, nbf.new_code_cell("met.explore(dr_method='umap')")) + cells.insert(25, nbf.new_code_cell("\n".join(cell3))) + cells.insert(27, nbf.new_code_cell("\n".join(cell4))) + cells.insert(28, nbf.new_code_cell("met.explore(dr_method='umap')")) + cells.insert(29, nbf.new_code_cell("met.data.user_labels = user_labels")) + + def __create_import_cells(self, cells): + base_imports = ["import numpy", "from dask.distributed import Client"] + if self.nb_type == self.MODEL_INFERENCE: + base_imports.insert( + 1, "from tsfresh.feature_extraction.settings import MinimalFCParameters" + ) + base_imports.append("from sklearn.metrics import mean_absolute_error") + sciope_imports = [ + "from sciope.utilities.priors import uniform_prior", + "from sciope.utilities.summarystats import auto_tsfresh", + "from sciope.utilities.distancefunctions import naive_squared", + "from sciope.inference.abc_inference import ABC" + ] else: - func_def = "def set_model_parameters(params, model):" - body = f"{pad*2}model.get_parameter(pname).expression = params[e]" - return_str = f"{pad}return model" - call = f"{pad}model_update = set_model_parameters(params, model)" - run = f"{pad}res = model_update.run(**kwargs)" - sim_strs = [func_def, comment, loop, body, return_str, ""] - simulator = ["# Here we use the GillesPy2 Solver", "def simulator(params, model):", - call, "", run, f"{pad}res = res.to_array()", - f"{pad}tot_res = np.asarray([x.T for x in res]) # reshape to (N, S, T)", - f"{pad}# should not contain timepoints", f"{pad}tot_res = tot_res[:, 1:, :]", - "", f"{pad}return tot_res", ""] - sim_strs.extend(simulator) - sim2_com = "# Wrapper, simulator function to abc should should only take one argument " - sim2_com += "(the parameter point)" - simulator2 = [sim2_com, "def simulator2(x):", f"{pad}return simulator(x, model=model)"] - sim_strs.extend(simulator2) - return nbf.new_code_cell("\n".join(sim_strs)) - - - def create_me_notebook(self): - '''Create a model exploration jupiter notebook for a StochSS model/workflow - - Attributes - ----------''' + sciope_imports = [ + "from sciope.utilities.gillespy2 import wrapper", + "from sciope.designs import latin_hypercube_sampling", + "from sciope.utilities.summarystats.auto_tsfresh import SummariesTSFRESH", + "from sciope.stochmet.stochmet import StochMET", + "from sciope.models.label_propagation import LPModel" + ] + cells.insert(1, nbf.new_code_cell("\n".join(base_imports))) + cells.insert(3, nbf.new_code_cell("\n".join(sciope_imports))) + + def __create_me_header_cells(self, cells): + header1 = "***\n## Model Exploration\n***\n" + \ + "### Define simulator function (using gillespy2 wrapper)" + header6 = "***\n## Explore the result\n***\n" + \ + "First lets add some appropiate information about the model and features" + header9 = "Just to vislualize the result we will map the " + \ + "label distribution to the user_labels " + \ + "(will enable us to see the LP model output when using method 'explore')" + cells.extend([ + nbf.new_markdown_cell(header1), + nbf.new_markdown_cell("### Start local cluster using dask client"), + nbf.new_markdown_cell("### Define parameter sampler/design and summary statistics"), + nbf.new_markdown_cell("### Initiate StochMET"), + nbf.new_markdown_cell("***\n## Run parameter sweep\n***"), + nbf.new_markdown_cell(header6), + nbf.new_markdown_cell("Here we use UMAP for dimension reduction"), + nbf.new_markdown_cell("Here lets use the dimension reduction embedding as input data"), + nbf.new_markdown_cell(header9) + ]) + + def __create_mi_header_cells(self, cells): + header1 = "***\n## Model Inference\n***\n### Generate some fixed" + \ + "(observed) data based on default parameters of the model" + header6 = "Wrapper, simulator function to abc should " + \ + "should only take one argument (the parameter point)" + header7 = "### Define summary statistics and distance function" + \ + "\nFunction to generate summary statistics" + cells.extend([ + nbf.new_markdown_cell(header1), + nbf.new_markdown_cell("Reshape the data and remove timepoints array"), + nbf.new_markdown_cell( + "### Define prior distribution\nTake default from mode 1 as reference" + ), + nbf.new_markdown_cell("### Define simulator"), + nbf.new_markdown_cell("Here we use the GillesPy2 Solver"), + nbf.new_markdown_cell(header6), + nbf.new_markdown_cell(header7), + nbf.new_markdown_cell("### Start local cluster using dask client"), + nbf.new_markdown_cell("***\n## Run the abc instance\n***"), + nbf.new_markdown_cell("First compute the fixed(observed) mean") + ]) + + def __create_observed_data(self, cells): + cell2 = [ + "fixed_data = fixed_data.to_array()", + "fixed_data = numpy.asarray([x.T for x in fixed_data])", + "fixed_data = fixed_data[:, 1:, :]" + ] + cells.insert(11, nbf.new_code_cell( + "kwargs = configure_simulation()\nfixed_data = model.run(**kwargs)" + )) + cells.insert(13, nbf.new_code_cell("\n".join(cell2))) + + def __create_prior(self): + nb_prior = [ + "default_param = numpy.array(list(model.listOfParameters.items()))[:, 1]", "", + "bound = []", "for exp in default_param:", " bound.append(float(exp.expression))", + "", "# Set the bounds", "bound = numpy.array(bound)", "dmin = bound * 0.1", + "dmax = bound * 2.0", "", "# Here we use uniform prior", + "uni_prior = uniform_prior.UniformPrior(dmin, dmax)" + ] + return nbf.new_code_cell("\n".join(nb_prior)) + + def __create_simulator_cells(self, cells): + pad = ' ' + cell1 = [ + "def get_variables(params, model):", + f"{pad}# params - array, need to have the same order as model.listOfParameters", + f"{pad}variables = " + r"{}", + f"{pad}for e, pname in enumerate(model.listOfParameters.keys()):", + f"{pad*2}variables[pname] = params[e]", f"{pad}return variables" + ] + cell2 = [ + "def simulator(params, model):", f"{pad}variables = get_variables(params, model)", "", + f"{pad}res = model.run(**kwargs, variables=variables)", f"{pad}res = res.to_array()", + f"{pad}tot_res = numpy.asarray([x.T for x in res]) # reshape to (N, S, T)", + f"{pad}# should not contain timepoints", f"{pad}tot_res = tot_res[:, 1:, :]", "", + f"{pad}return tot_res" + ] + cells.insert(16, nbf.new_code_cell("\n".join(cell1))) + cells.insert(18, nbf.new_code_cell("\n".join(cell2))) + cells.insert(20, nbf.new_code_cell( + f"def simulator2(x):\n{pad}return simulator(x, model=model)" + )) + + def __create_simulator_func(self): + nb_sim_func = [ + "settings = configure_simulation()", "simulator = wrapper.get_simulator(", + " gillespy_model=model, run_settings=settings, species_of_interest=['U', 'V']", + ")", "expression_array = wrapper.get_parameter_expression_array(model)" + ] + return nbf.new_code_cell("\n".join(nb_sim_func)) + + def __create_summary_stats(self): + nb_sum_stats = [ + "lhc = latin_hypercube_sampling.LatinHypercube(", + " xmin=expression_array, xmax=expression_array*3", ")", + "lhc.generate_array(1000) # creates a LHD of size 1000", "", + "# will use default minimal set of features", "summary_stats = SummariesTSFRESH()" + ] + return nbf.new_code_cell("\n".join(nb_sum_stats)) + + def create_me_notebook(self, results=None, compute="StochSS"): + '''Create a model exploration jupiter notebook for a StochSS model/workflow.''' self.nb_type = self.MODEL_EXPLORATION + cells = self.create_common_cells() + self.__create_import_cells(cells) + self.__create_me_header_cells(cells) + self.settings['solver'] = self.get_gillespy2_solver_name() - cells = [nbf.new_code_cell("%matplotlib notebook")] - cells.extend(self.create_common_cells()) - cells.append(nbf.new_markdown_cell("# Model Exploration")) - cells.extend(self.__create_me_setup_cells()) - cells.extend([nbf.new_markdown_cell("## Run parameter sweep"), - nbf.new_code_cell("met.compute(n_points=500, chunk_size=10)")]) - cells.extend(self.__create_me_expres_cells()) + cells.insert(11, self.__create_simulator_func()) + cells.insert(13, nbf.new_code_cell("c = Client()\nc")) + cells.insert(15, self.__create_summary_stats()) + cells.insert(17, nbf.new_code_cell("met = StochMET(simulator, lhc, summary_stats)")) + cells.insert(19, nbf.new_code_cell("met.compute(n_points=500, chunk_size=10)")) + self.__create_explore_cells(cells) + if compute != "StochSS": + self.log( + "warning", + "AWS Cloud compute environment is not supported by SCIOPE model exploration workflows." + ) message = self.write_notebook_file(cells=cells) return {"Message":message, "FilePath":self.get_path(), "File":self.get_file()} - - def create_mi_notebook(self): - '''Create a model inference jupiter notebook for a StochSS model/workflow - - Attributes - ----------''' + def create_mi_notebook(self, results=None, compute="StochSS"): + '''Create a model inference jupiter notebook for a StochSS model/workflow.''' self.nb_type = self.MODEL_INFERENCE + cells = self.create_common_cells() + self.__create_import_cells(cells) + self.__create_mi_header_cells(cells) + self.settings['solver'] = self.get_gillespy2_solver_name() - cells = [nbf.new_code_cell("%load_ext autoreload\n%autoreload 2")] - cells.extend(self.create_common_cells()) - imports = "from tsfresh.feature_extraction.settings import MinimalFCParameters" - fd_header = "## Generate some fixed(observed) data based on default parameters of the model" - fd_str = "kwargs = configure_simulation()\nfixed_data = model.run(**kwargs)" - rshp_strs = ["# Reshape the data and remove timepoints array", - "fixed_data = fixed_data.to_array()", - "fixed_data = np.asarray([x.T for x in fixed_data])", - "fixed_data = fixed_data[:, 1:, :]"] - cells.extend([nbf.new_markdown_cell("# Model Inference"), - nbf.new_code_cell(imports), nbf.new_markdown_cell(fd_header), - nbf.new_code_cell(fd_str), nbf.new_code_cell("\n".join(rshp_strs))]) - cells.extend(self.__create_mi_setup_cells()) - # abc cell - abc_str = "from sciope.inference.abc_inference import ABC\n\n" - abc_str += "abc = ABC(fixed_data, sim=simulator2, prior_function=uni_prior, " - abc_str += "summaries_function=summ_func.compute, distance_function=ns)" - # compute fixed mean cell - fm_str = "# First compute the fixed(observed) mean\nabc.compute_fixed_mean(chunk_size=2)" - # run model inference cell - rmi_str = "res = abc.infer(num_samples=100, batch_size=10, chunk_size=2)" - # absolute error cell - abse_str = "from sklearn.metrics import mean_absolute_error\n\n" - abse_str += "mae_inference = mean_absolute_error(bound, abc.results['inferred_parameters'])" - cells.extend([nbf.new_markdown_cell("## Start local cluster using dask client"), - nbf.new_code_cell("from dask.distributed import Client\n\nc = Client()"), - nbf.new_markdown_cell("## Start abc instance"), - nbf.new_code_cell(abc_str), nbf.new_code_cell(fm_str), - nbf.new_code_cell(rmi_str), nbf.new_code_cell(abse_str)]) + self.__create_observed_data(cells) + self.__create_simulator_cells(cells) + cells.insert(15, self.__create_prior()) + cells.insert(23, nbf.new_code_cell( + "summ_func = auto_tsfresh.SummariesTSFRESH()\n\n" + \ + "# Distance\nns = naive_squared.NaiveSquaredDistance()" + )) + cells.insert(25, nbf.new_code_cell("c = Client()\nc")) + self.__create_abc_cells(cells) + if compute != "StochSS": + self.log( + "warning", + "AWS Cloud compute environment is not supported by SCIOPE model inference workflows." + ) message = self.write_notebook_file(cells=cells) return {"Message":message, "FilePath":self.get_path(), "File":self.get_file()} diff --git a/stochss/handlers/util/scripts/aws_compute.py b/stochss/handlers/util/scripts/aws_compute.py new file mode 100755 index 0000000000..063958dde5 --- /dev/null +++ b/stochss/handlers/util/scripts/aws_compute.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 + +''' +StochSS is a platform for simulating biochemical systems +Copyright (C) 2019-2022 StochSS developers. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +''' +import os +import sys +import logging +import argparse + +import fcntl + +sys.path.append("/stochss/stochss/") # pylint: disable=wrong-import-position +sys.path.append("/stochss/stochss/handlers/") # pylint: disable=wrong-import-position +from util.stochss_base import StochSSBase +from handlers.log import init_log + +init_log() +log = logging.getLogger("stochss") + +class OpenAndLock: + ''' + Wrapper for open that controls the file lock. + ''' + def __init__(self, path, *pargs, **kwargs): + self.file = open(path,*pargs, **kwargs) + if self.file.writable(): + fcntl.lockf(self.file, fcntl.LOCK_EX | fcntl.LOCK_NB) + print("File is locked") + self.file.write(str(os.getpid())) + + def __enter__(self, *pargs, **kwargs): + return self.file + + def __exit__(self, exc_type=None, exc_value=None, traceback=None): + self.file.flush() + os.fsync(self.file.fileno()) + if self.file.writable(): + fcntl.lockf(self.file, fcntl.LOCK_UN) + print("File is un-locked") + self.file.close() + return exc_type is None + + def close(self, **kwargs): + ''' + Un-lock and close the file. + ''' + self.__exit__(**kwargs) + +def get_parsed_args(): + ''' + Initializes an argpaser to document this script and returns a dict of + the arguments that were passed to the script from the command line. + ''' + description = "Launch, terminate, or get the status of an AWS instance." + parser = argparse.ArgumentParser(description=description) + parser.add_argument('-l', '--launch', action="store_true", help="Launch an AWS instance.") + parser.add_argument( + '-s', '--status', action="store_true", help="Get the status of an AWS instance." + ) + parser.add_argument( + '-v', '--verbose', action="store_true", help="Print results as they are stored." + ) + parser.add_argument('-t', '--terminate', action="store_true", help="Terminate an AWS instance.") + return parser.parse_args() + +def interact(launch=False, status=False, terminate=False): + ''' + Interact with the aws instance. + + Attributes + ---------- + launch : bool + Indicates a launch interaction. + status : bool + Indicates a status update interaction. + termimate : bool + Indicates a terminate interaction. + ''' + base = StochSSBase(path="") + l_path = os.path.join(base.user_dir, '.aws/awsec2.lock') + + try: + lock_fd = OpenAndLock(l_path, 'w', encoding="utf-8") + except BlockingIOError: + print("The file is already locked") + return + + if launch: + base.launch_aws_cluster() + elif status: + _ = base.get_aws_cluster() + elif terminate: + base.terminate_aws_cluster() + else: + raise Exception("No operation provided, please set -l, -s, or -t flags.") + lock_fd.close() + +if __name__ == "__main__": + pid = os.fork() + if pid > 0: + sys.exit() + + args = get_parsed_args() + interact(launch=args.launch, status=args.status, terminate=args.terminate) diff --git a/stochss/handlers/util/scripts/run_preview.py b/stochss/handlers/util/scripts/run_preview.py index a5d4d97237..f6eb1d6560 100755 --- a/stochss/handlers/util/scripts/run_preview.py +++ b/stochss/handlers/util/scripts/run_preview.py @@ -2,7 +2,7 @@ ''' StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -90,7 +90,7 @@ def run_spatialpy_preview(args): ''' model = StochSSSpatialModel(path=args.path) job = SpatialSimulation(path="", preview=True, target=args.target) - job.s_py_model = model.convert_to_spatialpy() + job.s_py_model = model.convert_to_spatialpy(include_model_settings=True) job.s_model = model.model return job.run(preview=True) diff --git a/stochss/handlers/util/scripts/start_job.py b/stochss/handlers/util/scripts/start_job.py index 1c11f9ff26..008f942f09 100755 --- a/stochss/handlers/util/scripts/start_job.py +++ b/stochss/handlers/util/scripts/start_job.py @@ -2,7 +2,7 @@ ''' StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/stochss/handlers/util/scripts/upload_remote_file.py b/stochss/handlers/util/scripts/upload_remote_file.py index c7c3e2aa5a..ce8d33c6c4 100755 --- a/stochss/handlers/util/scripts/upload_remote_file.py +++ b/stochss/handlers/util/scripts/upload_remote_file.py @@ -2,7 +2,7 @@ ''' StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/stochss/handlers/util/spatial_simulation.py b/stochss/handlers/util/spatial_simulation.py index 299b76a960..cff26bb072 100644 --- a/stochss/handlers/util/spatial_simulation.py +++ b/stochss/handlers/util/spatial_simulation.py @@ -1,6 +1,6 @@ ''' StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/stochss/handlers/util/stochss_base.py b/stochss/handlers/util/stochss_base.py index 6b09a3adee..eea23a2103 100644 --- a/stochss/handlers/util/stochss_base.py +++ b/stochss/handlers/util/stochss_base.py @@ -1,6 +1,6 @@ ''' StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -22,9 +22,14 @@ import shutil import datetime import traceback +import subprocess +import dotenv import requests +from stochss_compute.cloud import EC2Cluster, EC2LocalConfig +from stochss_compute.cloud.exceptions import EC2Exception + from .stochss_errors import StochSSFileNotFoundError, StochSSPermissionsError, \ FileNotJSONFormatError @@ -36,6 +41,7 @@ class StochSSBase(): ''' user_dir = os.path.expanduser("~") # returns the path to the users home directory TEMPLATE_VERSION = 1 + DOMAIN_TEMPLATE_VERSION = 2 def __init__(self, path): ''' @@ -50,37 +56,63 @@ def __init__(self, path): self.logs = [] def __build_example_html(self, exm_data, home): - row = "
__CONTENTS__
" - entry = "__ALERT__' href='__OPEN_LINK__' role='button' style='width: 100%'>__NAME__" - entry_a = f"
__COLUMNS__
" # Well Mixed Examples wm_list = [] for entry in exm_data['Well-Mixed']: - exm_a = entry_a.replace("__ALERT__", entry['alert']) - exm_a = exm_a.replace("__OPEN_LINK__", f"{home}?open={entry['open_link']}") - exm_a = exm_a.replace("__NAME__", entry['name']) - wm_list.append(f"
{exm_a}
") - well_mixed = row.replace("__CONTENTS__", ''.join(wm_list)) + columns = self.__build_html_entry(entry, home) + wm_list.append(row.replace("__COLUMNS__", columns)) + well_mixed = section.replace("__ROWS__", "
".join(wm_list)) # Spatial Examples s_list = [] for entry in exm_data['Spatial']: - exm_a = entry_a.replace("__ALERT__", entry['alert']) - exm_a = exm_a.replace("__OPEN_LINK__", f"{home}?open={entry['open_link']}") - exm_a = exm_a.replace("__NAME__", entry['name']) - s_list.append(f"
{exm_a}
") - spatial = row.replace("__CONTENTS__", ''.join(s_list)) + columns = self.__build_html_entry(entry, home) + s_list.append(row.replace("__COLUMNS__", columns)) + spatial = section.replace("__ROWS__", "
".join(s_list)) return {"wellMixed": well_mixed, "spatial": spatial} - def __get_entry(self, entries, name): + @classmethod + def __build_html_entry(cls, entry, home): + desc = entry['description'] if 'description' in entry else "TODO" + + nav_alert = "secondary" if entry['alert'] == "success" else entry['alert'] + nav_status = "" if entry['alert'] == "primary" else " disabled" + nav_link = f"/stochss/project/manager?path=Examples/{entry['name']}.proj" + nav_title = "Error!" if entry['alert'] == "danger" else "Open" + nav_classes = f"class='btn full-btn btn-outline-{nav_alert} box-shadow{nav_status}'" + nav_a = f"
{nav_title}" + + imp_alert = "success" if entry['alert'] == "danger" else entry['alert'] + imp_link = f"{home}?open={entry['open_link']}" + imp_title = "Update" if entry['alert'] == "primary" else "Import" + imp_classes = f"class='btn full-btn btn-outline-{imp_alert} box-shadow'" + imp_a = f"{imp_title}" + + mod_label = "Added" if entry['alert'] == "success" else "Last Updated" + mod_date = entry['mod_time'] if entry['mod_time'] is not None else "TODO" + + return "".join([ + f"
{entry['name']}
" + f"

{desc}

", + "
", + f"
{nav_a}
", + f"
{imp_a}
", + f"
{mod_label}: {mod_date}
" + ]) + + @classmethod + def __get_entry(cls, entries, name): for entry in entries: if entry['name'] == name: return entry return None - def __get_from_remote(self): + @classmethod + def __get_from_remote(cls): p_path = "/stochss/.proxies.txt" rel_path = "https://raw.githubusercontent.com/StochSS/StochSS_Example_Library/main/example_data.json" if os.path.exists(p_path): @@ -103,12 +135,16 @@ def __update_exm_data(self, exm_data, old_exm_data=None): entry['alert'] = "success" if old_exm_data is not None: old_entry = self.__get_entry(old_exm_data['Well-Mixed'], entry['name']) + if old_entry is None: + continue entry['mod_time'] = old_entry['mod_time'] entry['umd5_sum'] = old_entry['umd5_sum'] for entry in exm_data['Spatial']: entry['alert'] = "success" if old_exm_data is not None: old_entry = self.__get_entry(old_exm_data['Spatial'], entry['name']) + if old_entry is None: + continue entry['mod_time'] = old_entry['mod_time'] entry['umd5_sum'] = old_entry['umd5_sum'] @@ -137,7 +173,12 @@ def __update_umd5_sums(self, exm_data, files=None): entry = self.__get_entry( exm_data['Spatial'], exm_data['Name-Mappings'][example] ) - entry['alert'] = "primary" + if entry is None: + continue + if example == "Example SIR Epidemic Project.proj": + entry['alert'] = "danger" + else: + entry['alert'] = "primary" def add_presentation_name(self, file, name): ''' @@ -219,6 +260,29 @@ def delete_presentation_name(self, file): json.dump(names, names_file) + def get_aws_cluster(self, instance=None): + ''' + Get the AWS cluster. + + Attributes + ---------- + instance : str + AWS EC2 instance. + ''' + key_dir = os.path.join(self.user_dir, ".aws") + if instance is None: + path = os.path.join(self.user_dir, ".user-settings.json") + settings = self.load_user_settings(path=path) + instance = settings['headNode'] + s_path = os.path.join(key_dir, f"{instance.replace('.', '-')}-status.txt") + # Setup the AWS environment + env_path = os.path.join(key_dir, "awsec2.env") + dotenv.load_dotenv(dotenv_path=env_path) + # Configure the AWS cluster + local_config = EC2LocalConfig(key_dir=key_dir, status_file=s_path) + cluster = EC2Cluster(local_config=local_config) + return cluster + @classmethod def get_new_path(cls, dst_path): ''' @@ -262,7 +326,7 @@ def get_model_template(self, as_string=False): as_string : bool Indicates whether or not to return the template in string format ''' - path = '/stochss/stochss_templates/nonSpatialModelTemplate.json' + path = '/stochss/stochss_templates/modelTemplate.json' self.log("debug", f"Using model template at: {path}") try: with open(path, 'r', encoding="utf-8") as template: @@ -440,6 +504,21 @@ def get_unique_copy_path(self, path=None): return os.path.join(dirname, cp_file) + def launch_aws_cluster(self): + ''' + Launch an AWS instance. + ''' + settings = self.load_user_settings(path='.user-settings.json') + instance = settings['headNode'] + + try: + cluster = self.get_aws_cluster(instance=instance) + cluster.launch_single_node_instance(instance) + except EC2Exception: + pass + except Exception: + cluster.clean_up() + def load_example_library(self, home): ''' Load the example library dropdown list. @@ -463,6 +542,33 @@ def load_example_library(self, home): return self.__build_example_html(exm_data, home) + def load_user_settings(self, path=None): + ''' + Load the user settings from file. + + Attributes + ---------- + path : str + Absolute path to the user settings file. + ''' + if path is None and os.path.exists(self.path): + path = self.path + elif path is None or not os.path.exists(path): + path = "/stochss/stochss_templates/userSettingTemplate.json" + with open(path, "r", encoding="utf-8") as usrs_fd: + settings = json.load(usrs_fd) + settings['awsHeadNodeStatus'] = "not launched" + if os.path.exists(os.path.join(self.user_dir, ".aws/awsec2.env")): + settings['awsSecretKey'] = "*"*20 + i_id = settings['headNode'].replace('.', '-') + s_path = os.path.join(self.user_dir, f".aws/{i_id}-status.txt") + if os.path.exists(s_path): + with open(s_path, 'r', encoding="utf-8") as aws_s_fd: + settings['awsHeadNodeStatus'] = aws_s_fd.read().strip() + else: + settings['awsSecretKey'] = None + return settings + def log(self, level, message): ''' Add a log to the objects internal logs @@ -532,5 +638,30 @@ def rename(self, name): message = f"Could not find the file or directory: {str(err)}" raise StochSSFileNotFoundError(message, traceback.format_exc()) from err except PermissionError as err: - message = f"You don not have permission to rename this file or directory: {str(err)}" + message = f"You do not have permission to rename this file or directory: {str(err)}" raise StochSSPermissionsError(message, traceback.format_exc()) from err + + def terminate_aws_cluster(self): + ''' + Terminate an AWS instance. + ''' + cluster = self.get_aws_cluster() + cluster.clean_up() + + def update_aws_status(self, instance): + ''' + Updated the status of the aws instance. + + Attributes + ---------- + instance : str + The AWS instance. + ''' + s_path = os.path.join(self.user_dir, f".aws/{instance.replace('.', '-')}-status.txt") + if not os.path.exists(s_path): + return + + script = "/stochss/stochss/handlers/util/scripts/aws_compute.py" + exec_cmd = [f"{script}", "-sv"] + print("Updating the status of AWS") + process = subprocess.Popen(exec_cmd) diff --git a/stochss/handlers/util/stochss_errors.py b/stochss/handlers/util/stochss_errors.py index cd2c1fb36c..48679844d8 100644 --- a/stochss/handlers/util/stochss_errors.py +++ b/stochss/handlers/util/stochss_errors.py @@ -1,6 +1,6 @@ ''' StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -39,6 +39,23 @@ def report_error(handler, log, err): error['Traceback'] = trace handler.write(error) +def report_critical_error(handler, log, err): + ''' + Report a critical stochss error to the front end. + + Attributes + ---------- + handler : obj + Jupyter Notebook API Handler + log : obj + StochSS log + ''' + handler.set_status(500) + error = {"Reason":"Uncaught Critical Error", "Message":str(err)} + trace = traceback.format_exc() + log.error("Exception information: %s\n%s", error, trace) + error['Traceback'] = trace + handler.write(error) class StochSSAPIError(Exception): ''' @@ -228,16 +245,38 @@ def __init__(self, msg, trace=None): super().__init__(406, "Domain File Not In Proper Format", msg, trace) -class DomainGeometryError(StochSSAPIError): +class DomainUpdateError(StochSSAPIError): + ''' + ################################################################################################ + Domain File Can't Be Updated + ################################################################################################ + ''' + + def __init__(self, msg, trace=None): + ''' + Indicates that the domain file can't be updated as it may be a + dependency of another doamin or a spatial model. + + Attributes + ---------- + msg : str + Details on what caused the error + trace : str + Error traceback for the error + ''' + super().__init__(405, "Domain File Can't Be Updated.", msg, trace) + + +class DomainActionError(StochSSAPIError): ''' ################################################################################################ - Domain Geometry Action Failed + Domain Action Failed to Initialize ################################################################################################ ''' def __init__(self, msg, trace=None): ''' - Indicates that a geometry action for the a domain failed + Indicates that an action for the a domain failed. Attributes ---------- @@ -246,7 +285,49 @@ def __init__(self, msg, trace=None): trace : str Error traceback for the error ''' - super().__init__(406, "Domain Geometry Action Failed", msg, trace) + super().__init__(406, "Domain Action Failed", msg, trace) + + +class DomainShapeError(StochSSAPIError): + ''' + ################################################################################################ + Domain Shape Failed to Initialize + ################################################################################################ + ''' + + def __init__(self, msg, trace=None): + ''' + Indicates that a shape failed to initialize. + + Attributes + ---------- + msg : str + Details on what caused the error + trace : str + Error traceback for the error + ''' + super().__init__(406, "Domain Shape Failed", msg, trace) + + +class DomainTransformationError(StochSSAPIError): + ''' + ################################################################################################ + Domain Transformation Failed to Initialize + ################################################################################################ + ''' + + def __init__(self, msg, trace=None): + ''' + Indicates that a transformation failed to initialize. + + Attributes + ---------- + msg : str + Details on what caused the error + trace : str + Error traceback for the error + ''' + super().__init__(406, "Domain Transformations Failed", msg, trace) #################################################################################################### # Job Errors @@ -334,3 +415,68 @@ def __init__(self, msg, trace=None): Error traceback for the error ''' super().__init__(500, "Job Results Error", msg, trace) + + +#################################################################################################### +# AWS Errors +#################################################################################################### + +class AWSConfigurationError(StochSSAPIError): + ''' + ################################################################################################ + AWS Configuration Error + ################################################################################################ + ''' + + def __init__(self, msg, trace=None): + ''' + Indicates that StochSS Compute configured to run jobs. + + Attributes + ---------- + msg : str + Details on what caused the error + trace : str + Error traceback for the error + ''' + super().__init__(403, "AWS Configuration Error", msg, trace) + +class AWSLauncherError(StochSSAPIError): + ''' + ################################################################################################ + AWS Launcher Error + ################################################################################################ + ''' + + def __init__(self, msg, trace=None): + ''' + Indicates that the StochSS Compute launcher single node instance failed. + + Attributes + ---------- + msg : str + Details on what caused the error + trace : str + Error traceback for the error + ''' + super().__init__(403, "AWS Launch Error", msg, trace) + +class AWSTerminatorError(StochSSAPIError): + ''' + ################################################################################################ + AWS Terminator Error + ################################################################################################ + ''' + + def __init__(self, msg, trace=None): + ''' + Indicates that the StochSS Compute clean up failed. + + Attributes + ---------- + msg : str + Details on what caused the error + trace : str + Error traceback for the error + ''' + super().__init__(403, "AWS Terminate Error", msg, trace) diff --git a/stochss/handlers/util/stochss_file.py b/stochss/handlers/util/stochss_file.py index f96fe377b9..b57594c8f3 100644 --- a/stochss/handlers/util/stochss_file.py +++ b/stochss/handlers/util/stochss_file.py @@ -1,6 +1,6 @@ ''' StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/stochss/handlers/util/stochss_folder.py b/stochss/handlers/util/stochss_folder.py index 65275c76f3..c56d4bee52 100644 --- a/stochss/handlers/util/stochss_folder.py +++ b/stochss/handlers/util/stochss_folder.py @@ -1,6 +1,6 @@ ''' StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -81,8 +81,10 @@ def __get_file_from_link(self, remote_path): response = requests.get(remote_path, allow_redirects=True, proxies=proxies) body = response.content if "download_presentation" in remote_path: - if ext in ("mdl", "smdl"): + if ext =="mdl": file = f"{json.loads(body)['name']}.{ext}" + elif ext == "smdl": + file = f"{json.loads(body)['model']['name']}.{ext}" elif ext == "ipynb": file = json.loads(body)['file'] body = json.dumps(json.loads(body)['notebook']) @@ -199,7 +201,7 @@ def __overwrite(self, path, ext, body): zip_file.write(body) try: with zipfile.ZipFile(ext_path, 'r') as zip_file: - members = set([name.split('/')[0] for name in zip_file.namelist()]) + members = {name.split('/')[0] for name in zip_file.namelist()} for name in members: if "Examples" in path: m_path = self.get_new_path(dst_path=os.path.join("Examples", name)) @@ -228,9 +230,24 @@ def __upload_file(self, file, body, new_name=None): def __upload_model(self, file, body, new_name=None): - is_valid, error = self.__validate_model(body, file) + try: + model = json.loads(body) + if "files" in model: + for entry in model['files'].values(): + if not entry['dwn_path'].startswith(self.user_dir): + entry['dwn_path'] = os.path.join(self.user_dir, entry['dwn_path']) + if not os.path.exists(os.path.dirname(entry['dwn_path'])): + os.makedirs(os.path.dirname(entry['dwn_path'])) + with open(entry['dwn_path'], "w", encoding="utf-8") as entry_fd: + entry_fd.write(entry['body']) + model = model['model'] + is_valid, error = self.__validate_model(model, file) + except json.decoder.JSONDecodeError: + error = [f"The file {file} is not in JSON format."] + is_valid = False + if is_valid: - ext = "smdl" if json.loads(body)['is_spatial'] else "mdl" + ext = "smdl" if model['is_spatial'] else "mdl" else: ext = "json" if new_name is not None: @@ -247,7 +264,9 @@ def __upload_model(self, file, body, new_name=None): path = os.path.join(wkgp_path, file) else: path = os.path.join(self.path, file) - new_file = StochSSFile(path=path, new=True, body=body) + if not isinstance(model, str): + model = json.dumps(model, sort_keys=True, indent=4) + new_file = StochSSFile(path=path, new=True, body=model) file = new_file.get_file() dirname = new_file.get_dir_name() if is_valid: @@ -286,12 +305,6 @@ def __upload_sbml(self, file, body, new_name=None): @classmethod def __validate_model(cls, body, file): - try: - body = json.loads(body) - except json.decoder.JSONDecodeError: - message = [f"The file {file} is not in JSON format."] - return False, message - keys = ["species", "parameters", "reactions", "eventsCollection", "rules", "functionDefinitions"] for key in body.keys(): @@ -633,7 +646,7 @@ def validate_upload_link(self, remote_path): zip_file.write(body) try: with zipfile.ZipFile(ext_path, 'r') as zip_file: - members = set([name.split('/')[0] for name in zip_file.namelist()]) + members = {name.split('/')[0] for name in zip_file.namelist()} for name in members: if "github.com/StochSS/StochSS_Example_Library/raw/" in remote_path: mem_path = self.get_new_path(dst_path=os.path.join("Examples", name)) diff --git a/stochss/handlers/util/stochss_job.py b/stochss/handlers/util/stochss_job.py index 0792138c0d..de96be2b05 100644 --- a/stochss/handlers/util/stochss_job.py +++ b/stochss/handlers/util/stochss_job.py @@ -1,6 +1,6 @@ ''' StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -76,15 +76,20 @@ def __init__(self, path, new=False, data=None): self.time_stamp = None if new: self.type = data['type'] - self.__create_new_job(mdl_path=data['mdl_path'], settings=data['settings']) + self.__create_new_job( + mdl_path=data['mdl_path'], settings=data['settings'], compute=data['compute_env'] + ) - def __create_new_job(self, mdl_path, settings=None): + def __create_new_job(self, mdl_path, settings=None, compute="Local"): path = self.get_path(full=True) try: os.mkdir(path) os.mkdir(self.__get_results_path(full=True)) - info = {"source_model":mdl_path, "wkfl_model":None, "type":self.type, "start_time":None} + info = { + "source_model": mdl_path, "wkfl_model": None, "type": self.type, + "start_time": None, "compute_env": compute + } self.update_info(new_info=info, new=True) with open(os.path.join(path, "logs.txt"), "w", encoding="utf-8") as log_file: log_file.close() @@ -461,7 +466,10 @@ def get_notebook_data(self): wkfl_type = "2d_parameter_sweep" kwargs = {"path":path, "new":True, "settings":settings, "models":{"s_model":s_model, "model":g_model}} - return {"kwargs":kwargs, "type":wkfl_type} + results_path = os.path.join(self.__get_results_path(), "results.p") + return { + "kwargs":kwargs, "type":wkfl_type, "results": results_path, "compute_env": info['compute_env'] + } def get_plot_from_results(self, data_keys, plt_key, add_config=False): @@ -517,19 +525,19 @@ def get_plot_from_spatial_results(self, data_keys, add_config=False): result = self.__get_filtered_ensemble_results(None) self.log("info", "Generating the plot...") if data_keys['target'] in ("type", "nu", "rho", "mass"): - fig = result.plot_property( + fig = result[data_keys['trajectory']].plot_property( data_keys['target'], width="auto", height="auto", animated=True, return_plotly_figure=True ) elif data_keys['target'] == "v": - fig = result.plot_property( + fig = result[data_keys['trajectory']].plot_property( data_keys['target'], p_ndx=data_keys['index'], width="auto", height="auto", animated=True, return_plotly_figure=True ) else: concentration = data_keys['mode'] == "discrete-concentration" deterministic = data_keys['mode'] == "continuous" - fig = result.plot_species( + fig = result[data_keys['trajectory']].plot_species( data_keys['target'], concentration=concentration, deterministic=deterministic, width="auto", height="auto", animated=True, return_plotly_figure=True ) @@ -697,10 +705,10 @@ def load(self, new=False): self.log("error", f"Exception information: {error}") finally: settings = self.load_settings(model=model) + info = self.load_info() + logs = f"Compute Environment: {info['compute_env']}" if os.path.exists(os.path.join(self.path, "logs.txt")): - logs = self.__get_run_logs() - else: - logs = "" + logs = f"{logs}\n{self.__get_run_logs()}" self.job = {"mdlPath":mdl_path, "model":model, "settings":settings, "startTime":info['start_time'], "status":status, "timeStamp":self.time_stamp, "titleType":self.TITLES[info['type']], @@ -725,6 +733,8 @@ def load_info(self): info = json.load(info_file) if "annotation" not in info: info['annotation'] = "" + if "compute_env" not in info: + info['compute_env'] = "Local" return info except FileNotFoundError as err: message = f"Could not find the info file: {str(err)}" @@ -908,16 +918,18 @@ def update_info(self, new_info, new=False): info = new_info else: info = self.load_info() - if "source_model" in new_info.keys(): + if "source_model" in new_info: info['source_model'] = new_info['source_model'] - if "wkfl_model" in new_info.keys(): + if "wkfl_model" in new_info: info['wkfl_model'] = new_info['wkfl_model'] - if "type" in new_info.keys(): + if "type" in new_info: info['type'] = new_info['type'] - if "start_time" in new_info.keys(): + if "start_time" in new_info: info['start_time'] = new_info['start_time'] - if "annotation" in new_info.keys(): + if "annotation" in new_info: info['annotation'] = new_info['annotation'] + if "compute_env" in new_info: + info['compute_env'] = new_info['compute_env'] self.log("debug", f"New info: {info}") with open(self.__get_info_path(full=True), "w", encoding="utf-8") as file: json.dump(info, file, indent=4, sort_keys=True) diff --git a/stochss/handlers/util/stochss_model.py b/stochss/handlers/util/stochss_model.py index 237992ee38..001f5ba58d 100644 --- a/stochss/handlers/util/stochss_model.py +++ b/stochss/handlers/util/stochss_model.py @@ -1,6 +1,6 @@ ''' StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/stochss/handlers/util/stochss_notebook.py b/stochss/handlers/util/stochss_notebook.py index 3bc1486976..7bd96dda32 100644 --- a/stochss/handlers/util/stochss_notebook.py +++ b/stochss/handlers/util/stochss_notebook.py @@ -1,6 +1,6 @@ ''' StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -15,7 +15,6 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' - import os import json import string @@ -25,17 +24,18 @@ from escapism import escape from nbformat import v4 as nbf +import numpy + from gillespy2.solvers.utilities.cpp_support_test import check_cpp_support from .stochss_base import StochSSBase -from .stochss_errors import StochSSFileNotFoundError, StochSSModelFormatError, \ - StochSSPermissionsError +from .stochss_errors import StochSSFileNotFoundError, StochSSModelFormatError, StochSSPermissionsError class StochSSNotebook(StochSSBase): ''' - ################################################################################################ + #################################################################################################################### StochSS notebook object - ################################################################################################ + #################################################################################################################### ''' ENSEMBLE_SIMULATION = 1 SPATIAL_SIMULATION = 2 @@ -44,9 +44,8 @@ class StochSSNotebook(StochSSBase): MODEL_EXPLORATION = 5 MODEL_INFERENCE = 6 SOLVER_MAP = {"SSACSolver":"SSA", "NumPySSASolver":"SSA", "ODESolver":"ODE", "Solver":"SSA", - "TauLeapingSolver":"Tau-Leaping", "TauHybridSolver":"Hybrid-Tau-Leaping", - "ODECSolver":"ODE", "TauLeapingCSolver":"Tau-Leaping", - "TauHybridCSolver":"Hybrid-Tau-Leaping"} + "TauLeapingSolver":"Tau-Leaping", "TauHybridSolver":"Hybrid-Tau-Leaping", "ODECSolver":"ODE", + "TauLeapingCSolver":"Tau-Leaping", "TauHybridCSolver":"Hybrid-Tau-Leaping"} def __init__(self, path, new=False, models=None, settings=None): '''Intitialize a notebook object and if its new create it on the users file system @@ -63,364 +62,642 @@ def __init__(self, path, new=False, models=None, settings=None): self.model = models["model"] if settings is None: self.settings = self.get_settings_template() + if self.s_model['is_spatial']: + self.settings['simulationSettings']['realizations'] = 1 else: self.settings = settings if "timespanSettings" in settings.keys(): keys = settings['timespanSettings'].keys() if "endSim" in keys and "timeStep" in keys: - end = settings['timespanSettings']['endSim'] - step_size = settings['timespanSettings']['timeStep'] - self.s_model['modelSettings']['endSim'] = end - self.s_model['modelSettings']['timeStep'] = step_size + self.s_model['modelSettings']['endSim'] = settings['timespanSettings']['endSim'] + self.s_model['modelSettings']['timeStep'] = settings['timespanSettings']['timeStep'] self.make_parent_dirs() n_path, changed = self.get_unique_path(name=self.get_file()) if changed: self.path = n_path.replace(self.user_dir + '/', "") + @classmethod + def __add_name(cls, names, names_length, name, max_len=100): + if names_length + len(name) > max_len: + names = [f"{', '.join(names)},\n{' ' * (8 + 100 - max_len)}{name}"] + return 8 + len(name), names + names.append(name) + return names_length + 2 + len(name), None - def __create_boundary_condition_cells(self): - pad = " " - bc_cells = [] - try: - for boundary_condition in self.s_model['boundaryConditions']: - bc_cell = [f'class {boundary_condition["name"]}(BoundaryCondition):', - f'{pad}def expression(self):', - f'{pad*2}return """{boundary_condition["expression"]}"""'] - bc_cells.append(nbf.new_code_cell("\n".join(bc_cell))) - return bc_cells - except KeyError as err: - message = "Boundary conditions are not properly formatted or " - message += f"are referenced incorrectly for notebooks: {str(err)}" - raise StochSSModelFormatError(message, traceback.format_exc()) from err - - - def __create_boundary_condition_string(self, model, pad): - if self.s_model['boundaryConditions']: - bound_conds = ["", f"{pad}# Boundary Conditions"] - try: - for bound_cond in self.s_model['boundaryConditions']: - bc_str = f"{pad}self.add_boundary_condition({bound_cond['name']}())" - bound_conds.append(bc_str) - model.extend(bound_conds) - except KeyError as err: - message = "Boundary conditions are not properly formatted or " - message += f"are referenced incorrectly for notebooks: {str(err)}" - raise StochSSModelFormatError(message, traceback.format_exc()) from err + @classmethod + def __build_geometry(cls, formula, name=None, c_name=None): + pad = ' ' + if c_name is None: + c_name = name.title().replace("_", "") + cell_str = "\n".join([ + f"class {c_name}(spatialpy.Geometry):", f"{pad}def __init__(self):", + f"{pad * 2}pass", "", f"{pad}def inside(self, point, on_boundary):", + f"{pad * 2}namespace = " + "{'x': point[0], 'y': point[1], 'z': point[2]}", + f"{pad * 2}formula = '{formula}'", f"{pad * 2}return eval(formula, " + "{}, namespace)" + ]) + return nbf.new_code_cell(cell_str) + @classmethod + def __check_reflect_mathod(cls, transformation): + point1 = numpy.array([ + transformation['point1']['x'], transformation['point1']['y'], transformation['point1']['z'] + ]) + point2 = numpy.array([ + transformation['point2']['x'], transformation['point2']['y'], transformation['point2']['z'] + ]) + point3 = numpy.array([ + transformation['point3']['x'], transformation['point3']['y'], transformation['point3']['z'] + ]) + if numpy.count_nonzero(point3 - point1) == 0: + return "Point-Normal" + if numpy.count_nonzero(point2 - point1) == 0: + return "Point-Normal" + return "3-Point" + + def __create_common_header_cells(self): + name = self.s_model['name'].replace('_', ' ').replace('-', ' ').title() + nb_title = f"# {name}\n***" + if 'annotation' in self.s_model and self.s_model['annotation'] != '': + nb_title = f"{nb_title}\n{self.s_model['annotation']}\n***" + nb_title = f"{nb_title}\n## Setup the Environment\n***" + return [ + nbf.new_markdown_cell(nb_title), nbf.new_markdown_cell(f"***\n## Create the {name} Model\n***"), + nbf.new_markdown_cell("### Instantiate the model"), + nbf.new_markdown_cell("***\n## Simulation Parameters\n***") + ] + + def __create_compute_cells(self, cells, compute): + self.__create_compute_headers(cells, compute) + self.__create_compute_imports(cells, compute) + self.__create_compute_credentials(cells, compute) + self.__create_compute_launch(cells, compute) + self.__create_compute_cleanup(cells, compute) + + def __create_compute_cleanup(self, cells, compute): + cells.append(nbf.new_code_cell("# cluster.clean_up()")) + + def __create_compute_credentials(self, cells, compute): + nb_creds = [ + "key_dir = os.path.join(os.environ.get('HOME'), '.aws')", + "_ = dotenv.load_dotenv(dotenv_path=os.path.join(key_dir, 'awsec2.env'))" + ] + cells.insert(5, nbf.new_code_cell("\n".join(nb_creds))) + + def __create_compute_headers(self, cells, compute): + cells.insert(2, nbf.new_markdown_cell( + "***\n## AWS Credentials\n***\nAWS Credentials for StochSS can be set [here](/stochss/settings)" + )) + cells.insert(3, nbf.new_markdown_cell("***\n## Launch AWS Cluster\n***")) + cells.append(nbf.new_markdown_cell('***\n## Clean Up AWS Cluster\n***')) + + def __create_compute_imports(self, cells, compute): + cells.insert(1, nbf.new_code_cell("import os\nimport dotenv")) + cells.insert(3, nbf.new_code_cell( + "import stochss_compute\nfrom stochss_compute.cloud import EC2Cluster, EC2LocalConfig" + )) + + def __create_compute_launch(self, cells, compute): + instance = self.load_user_settings(path='.user-settings.json')['headNode'] + path = f'{instance.replace(".", "-")}-status.txt' + nb_cluster = [ + "local_config = EC2LocalConfig(", + f" key_dir=key_dir, status_file=os.path.join(key_dir, '{path}')", ")", + "cluster = EC2Cluster(local_config=local_config)", + f"# cluster = EC2Cluster(status_file=os.path.join(key_dir, '{path}'))", + "if cluster._server is None:", + f" cluster.launch_single_node_instance('{instance}')", "cluster.status" + ] + cells.insert(7, nbf.new_code_cell("\n".join(nb_cluster))) def __create_configuration_cell(self): - pad = " " - config = ["def configure_simulation():"] - # Add solver instantiation line if the c solver are available + use_solver = self.nb_type not in (self.ENSEMBLE_SIMULATION, self.SPATIAL_SIMULATION) + algorithm = self.settings['simulationSettings']['algorithm'] is_automatic = self.settings['simulationSettings']['isAutomatic'] - if "CSolver" in self.settings['solver']: - if is_automatic and self.nb_type <= self.ENSEMBLE_SIMULATION: - commented = True - elif is_automatic and "CSolver" not in self.settings['solver']: - commented = True - else: - commented = False - start = f"{pad}# " if commented else pad - if self.nb_type in (self.MODEL_EXPLORATION, self.MODEL_INFERENCE): - del_dir = ", delete_directory=False" - else: - del_dir = "" - config.append(f"{start}solver = {self.settings['solver']}(model=model{del_dir})") - config.append(pad + "kwargs = {") if self.s_model['is_spatial']: settings = self.__get_spatialpy_run_setting() + settings_lists = {"SSA": ["number_of_trajectories", "seed"]} else: - settings = self.__get_gillespy2_run_settings() - settings_lists = {"ODE":['"solver"', '"integrator_options"'], - "SSA":['"solver"', '"seed"', '"number_of_trajectories"'], - "CLE":['"solver"', '"seed"', '"number_of_trajectories"', '"tau_tol"'], - "Tau-Leaping":['"solver"', '"seed"', '"number_of_trajectories"', - '"tau_tol"'], - "Hybrid-Tau-Leaping":['"solver"', '"seed"', '"number_of_trajectories"', - '"tau_tol"', '"integrator_options"']} - algorithm = self.settings['simulationSettings']['algorithm'] - is_spatial = self.s_model['is_spatial'] - for setting in settings: - key = setting.split(':')[0] - if self.nb_type > self.ENSEMBLE_SIMULATION and key == '"solver"' and \ - "CSolver" in self.settings['solver']: - start = pad*2 - elif key not in settings_lists[algorithm]: - start = f"{pad*2}# " - elif is_automatic and not is_spatial and key == '"number_of_trajectories"': - start = pad*2 - elif is_automatic: - start = f"{pad*2}# " + settings = self.__get_gillespy2_run_settings(use_solver=use_solver) + settings_lists = { + "ODE": self.model.get_best_solver_algo("ODE").get_solver_settings(), + "SSA": self.model.get_best_solver_algo("SSA").get_solver_settings(), + "CLE": self.model.get_best_solver_algo("CLE").get_solver_settings(), + "Tau-Leaping": self.model.get_best_solver_algo("Tau-Leaping").get_solver_settings(), + "Hybrid-Tau-Leaping": self.model.get_best_solver_algo("Tau-Hybrid").get_solver_settings() + } + pad = " " + config = ["def configure_simulation():"] + if use_solver: + if self.s_model['is_spatial']: + nb_solver = "spatialpy.solver(model=model)" else: - start = pad*2 - config.append(f"{start}{setting},") + solver = self.get_gillespy2_solver_name() + del_dir = ", delete_directory=False" if "CSolver" in solver else "" + nb_solver = f"gillespy2.{solver}(model=model{del_dir})" + config.append(f"{pad}solver = {nb_solver}") + config.append(pad + "kwargs = {") + for name, val in settings.items(): + if name == "solver" and \ + self.nb_type not in (self.ENSEMBLE_SIMULATION, self.SPATIAL_SIMULATION): + config.append(f"{pad*2}'{name}': {val},") + elif is_automatic or name not in settings_lists[algorithm]: + config.append(f"{pad*2}# '{name}': {val},") + else: + config.append(f"{pad*2}'{name}': {val},") config.extend([pad + "}", f"{pad}return kwargs"]) return nbf.new_code_cell("\n".join(config)) - - def __create_event_strings(self, model, pad): - if self.s_model['eventsCollection']: - triggers = ["", f"{pad}# Event Triggers"] - assignments = ["", f"{pad}# Event Assignments"] - events = ["", f"{pad}# Events"] + def __create_parameters(self, nb_model, index): + if len(self.s_model['parameters']) > 0: + pad = ' ' + package = "spatialpy" if self.s_model['is_spatial'] else "gillespy2" + args_tmp = "name='__NAME__', expression='__EXPRESSION__'" + tmp = f"{pad}__NAME__ = {package}.Parameter({args_tmp})" + names = [] + n_len = 8 + parameters = ["", f"{pad}# Parameters"] try: - for event in self.s_model['eventsCollection']: - t_name = self.__create_event_trigger_string(triggers=triggers, event=event, - pad=pad) - a_names = self.__create_event_assignment_strings(assignments=assignments, - event=event, pad=pad) - delay = f'"{event["delay"]}"' if event['delay'] else None - ev_str = f'{pad}self.add_event(Event(name="{event["name"]}", ' - ev_str += f'trigger={t_name}, assignments=[{a_names}], ' - ev_str += f'delay={delay}, priority="{event["priority"]}", ' - ev_str += f'use_values_from_trigger_time={event["useValuesFromTriggerTime"]}))' - events.append(ev_str) - model.extend(triggers) - model.extend(assignments) - model.extend(events) + for s_parameter in self.s_model['parameters']: + n_len, n_names = self.__add_name(names, n_len, s_parameter['name']) + if n_names is not None: + names = n_names + nb_param = tmp.replace("__NAME__", s_parameter['name']) + nb_param = nb_param.replace("__EXPRESSION__", str(s_parameter['expression'])) + parameters.append(nb_param) + parameters.append(f"{pad}model.add_parameter([\n{pad*2}{', '.join(names)}\n{pad}])") + nb_model.insert(index, '\n'.join(parameters)) + index += 1 except KeyError as err: - message = "Events are not properly formatted or " + message = "Parameters are not properly formatted or " message += f"are referenced incorrectly for notebooks: {str(err)}" raise StochSSModelFormatError(message, traceback.format_exc()) from err - - - @classmethod - def __create_event_assignment_strings(cls, assignments, event, pad): - names = [] - for i, assignment in enumerate(event['eventAssignments']): - name = f'{event["name"]}_assign_{i+1}' - names.append(name) - assign_str = f'{pad}{name} = EventAssignment(variable="{assignment["variable"]["name"]}' - assign_str += f'", expression="{assignment["expression"]}")' - assignments.append(assign_str) - return ', '.join(names) - - - @classmethod - def __create_event_trigger_string(cls, triggers, event, pad): - name = f'{event["name"]}_trig' - trig_str = f'{pad}{name} = EventTrigger(expression="{event["triggerExpression"]}", ' - trig_str += f'initial_value={event["initialValue"]}, persistent={event["persistent"]})' - triggers.append(trig_str) - return name - - - def __create_function_definition_strings(self, model, pad): - if self.s_model['functionDefinitions']: - func_defs = ["", f"{pad}# Function Definitions"] + return index + + def __create_reactions(self, nb_model, index, type_refs=None): + if len(self.s_model['reactions']) > 0: + pad = ' ' + package = "spatialpy" if self.s_model['is_spatial'] else "gillespy2" + l1_tmp = f"{pad*2}name='__NAME__',__RATE____RESTRICT_TO__" + l2_tmp = f"{pad*2}reactants=__REACTANTS__, products=__PRODUCTS__" + tmp = f"{pad}__NAME__ = {package}.Reaction(\n{l1_tmp}\n{l2_tmp}__L3____L4__\n{pad})" + names = [] + n_len = 8 + reactions = ["", f"{pad}# Reactions"] try: - for func_def in self.s_model['functionDefinitions']: - fd_str = f'{pad}self.add_function_definition(FunctionDefinition(' - fd_str += f'name="{func_def["name"]}", function="{func_def["expression"]}", ' - fd_str += f'args={func_def["variables"].split(", ")}))' - func_defs.append(fd_str) - model.extend(func_defs) + for s_reaction in self.s_model['reactions']: + n_len, n_names = self.__add_name(names, n_len, s_reaction['name']) + if n_names is not None: + names = n_names + reactants = self.__create_stoich_species(stoich_species=s_reaction['reactants']) + products = self.__create_stoich_species(stoich_species=s_reaction['products']) + if s_reaction['massaction']: + nb_reac = tmp.replace("__L3____L4__", "") + nb_reac = nb_reac.replace("__RATE__", f" rate='{s_reaction['rate']['name']}',") + else: + ssa = f",\n{pad*2}propensity_function='{s_reaction['propensity']}'," + ode = f"\n{pad*2}ode_propensity_function='{s_reaction['odePropensity']}'" + nb_reac = tmp.replace("__RATE__", "").replace("__L3__", ssa).replace("__L4__", ode) + if type_refs is None or len(s_reaction['types']) == len(type_refs): + nb_reac = nb_reac.replace("__RESTRICT_TO__", "") + else: + types = [type_refs[d_type] for d_type in s_reaction['types']] + restrict_to = f" restrict_to=[{', '.join(types)}]," + nb_reac = nb_reac.replace("__RESTRICT_TO__", restrict_to) + nb_reac = nb_reac.replace("__NAME__", s_reaction['name']) + nb_reac = nb_reac.replace("__REACTANTS__", reactants) + reactions.append(nb_reac.replace("__PRODUCTS__", products)) + reactions.append(f"{pad}model.add_reaction([\n{pad*2}{', '.join(names)}\n{pad}])") + nb_model.insert(index, '\n'.join(reactions)) + index += 1 except KeyError as err: - message = "Function definitions are not properly formatted or " + message = "Reactions are not properly formatted or " message += f"are referenced incorrectly for notebooks: {str(err)}" raise StochSSModelFormatError(message, traceback.format_exc()) from err - - - def __create_import_cell(self): - try: - is_automatic = self.settings['simulationSettings']['isAutomatic'] - imports = ["import numpy as np"] - if self.s_model['is_spatial']: - imports.append("import spatialpy") - imports.append("from spatialpy import Model, Species, Parameter, Reaction,\\") - imports.append(" Domain, PlaceInitialCondition, \\") - imports.append(" UniformInitialCondition, \\") - imports.append(" ScatterInitialCondition") - return nbf.new_code_cell("\n".join(imports)) - imports.append("import gillespy2") - imports.append("from gillespy2 import Model, Species, Parameter, Reaction, Event, \\") - imports.append(" EventTrigger, EventAssignment, RateRule, \\") - imports.append(" AssignmentRule, FunctionDefinition, TimeSpan") - tau_leaping_solver = self.model.get_best_solver_algo("Tau-Leaping").name - tau_hybrid_solver = self.model.get_best_solver_algo("Tau-Hybrid").name - algorithm_map = { - 'ODE': f'from gillespy2 import {self.model.get_best_solver_algo("ODE").name}', - 'SSA': f'from gillespy2 import {self.model.get_best_solver_algo("SSA").name}', - 'CLE': f'from gillespy2 import {self.model.get_best_solver_algo("CLE").name}', - 'Tau-Leaping': f'from gillespy2 import {tau_leaping_solver}', - 'Hybrid-Tau-Leaping': f'from gillespy2 import {tau_hybrid_solver}' - } - algorithm = self.settings['simulationSettings']['algorithm'] - for name, alg_import in algorithm_map.items(): - if not is_automatic and name == algorithm: - imports.append(alg_import) - elif name == algorithm and self.nb_type > self.ENSEMBLE_SIMULATION and \ - "CSolver" in alg_import: - imports.append(alg_import) - else: - imports.append(f"# {alg_import}") - return nbf.new_code_cell("\n".join(imports)) - except KeyError as err: - message = "Workflow settings are not properly formatted or " - message += f"are referenced incorrectly for notebooks: {str(err)}" - raise StochSSModelFormatError(message, traceback.format_exc()) from err - - - def __create_initial_condition_strings(self, model, pad, type_refs=None): - if self.s_model['initialConditions']: - ic_types = {"Place":"PlaceInitialCondition", "Scatter":"ScatterInitialCondition", - "Distribute Uniformly per Voxel":"UniformInitialCondition"} - initial_conditions = ["", f"{pad}# Initial Conditions"] + return index + + def __create_run(self, results, compute="StochSS"): + nb_run_header = "***\n## Run the Simulation\n***" + nb_run = ["kwargs = configure_simulation()"] + if compute in ("AWS Cloud", "AWS"): + nb_run.append("simulation = stochss_compute.RemoteSimulation(model, server=cluster)") + nb_res = "results = simulation.run(**kwargs)" + else: + nb_res = "results = model.run(**kwargs)" + if results is None: + nb_run.append(nb_res) + else: + nb_load_res = [ + f"# {nb_res}", f"path = '{results}'", + "with open(os.path.join(os.path.expanduser('~'), path), 'rb') as results_file:", + " results = pickle.load(results_file)" + ] + nb_run.extend(nb_load_res) + return nbf.new_markdown_cell(nb_run_header), nbf.new_code_cell("\n".join(nb_run)) + + def __create_spatial_actions(self, nb_domain, index, type_refs): + if len(self.s_model['domain']['actions']) > 0: + func_map = {"Remove Action": "add_remove_action", "Set Action": "add_set_action"} + pad = ' ' + c_ndx = 1 + l_ndx = 1 + args_tmp = "__GEOMETRY____ENABLE____PROPS__" + tmp = f"{pad}domain.__FUNCTION__(\n{pad*2}__ARGS__\n{pad})" + actions = ["", f"{pad}# Domain Actions"] try: - for init_cond in self.s_model['initialConditions']: - ic_str = f'{pad}self.add_initial_condition({ic_types[init_cond["icType"]]}(' - ic_str += f'species="{init_cond["specie"]["name"]}", count={init_cond["count"]}' - if init_cond["icType"] == "Place": - place = f'{init_cond["x"]}, {init_cond["y"]}, {init_cond["z"]}' - ic_str += f', location=[{place}]))' + s_actions = sorted(self.s_model['domain']['actions'], key=lambda action: action['priority']) + for s_act in s_actions: + # Build props arg + if s_act['type'] in ('Fill Action', 'Set Action', 'XML Mesh', 'Mesh IO'): + props = [ + f"mass={s_act['mass']}, vol={s_act['vol']}, rho={s_act['rho']}, ", + f"nu={s_act['nu']}, c={s_act['c']}, fixed={s_act['fixed']}", + ] + if s_act['type'] in ('Fill Action', 'Set Action') and s_act['typeID'] > 0: + props.insert(0, f"type_id={type_refs[s_act['typeID']]}, ") + args = args_tmp.replace("__PROPS__", f",\n{pad*2}{''.join(props)}") else: - types = [type_refs[d_type] for d_type in init_cond["types"]] - ic_str += f', types={types}))' - initial_conditions.append(ic_str) - model.extend(initial_conditions) + args = args_tmp.replace("__PROPS__", "") + if s_act['scope'] == 'Multi Particle' or s_act['type'] != "Fill Action": + args = args.replace("__ENABLE__", f"enable={s_act['enable']}, apply_action={s_act['enable']}") + # Apply actions + if s_act['type'] == "Fill Action": + if s_act['scope'] == 'Multi Particle': + if s_act['transformation'] == "": + lattice = f"{s_act['shape']}_latt" + else: + lattice = s_act['transformation'] + actions.append(f"{pad}{lattice}.lattice = {s_act['shape']}_latt") + args = args.replace("__GEOMETRY__", f"lattice={lattice}, geometry={s_act['shape']}_geom, ") + nb_act = tmp.replace("__FUNCTION__", "add_fill_action").replace("__ARGS__", args) + else: + p_x = s_act['point']['x'] + p_y = s_act['point']['y'] + p_z = s_act['point']['z'] + args = args.replace("__GEOMETRY____ENABLE__", f"[{p_x}, {p_y}, {p_z}]") + nb_act = tmp.replace("__FUNCTION__", "add_point").replace("__ARGS__", args) + elif s_act['type'] in ('XML Mesh', 'Mesh IO', 'StochSS Domain'): + args = args.replace("__GEOMETRY__", f"lattice={f'ipa_lattice{l_ndx}'}, ") + nb_act = tmp.replace("__FUNCTION__", "add_fill_action").replace("__ARGS__", args) + l_ndx += 1 + else: + if s_act['scope'] == "Single Particle": + geometry = f"geometry=SPAGeometry{c_ndx}(), " + c_ndx += 1 + elif s_act['transformation'] == "": + geometry = f"geometry={s_act['shape']}_geom, " + else: + geometry = f"geometry={s_act['transformation']}, " + actions.append(f"{pad}{s_act['transformation']}.geometry = {s_act['shape']}_geom") + args = args.replace("__GEOMETRY__", geometry) + nb_act = tmp.replace("__FUNCTION__", func_map[s_act['type']]).replace("__ARGS__", args) + actions.append(nb_act) + nb_domain.insert(index, '\n'.join(actions)) + index += 1 except KeyError as err: - message = "Initial conditions are not properly formatted or " - message += f"are referenced incorrectly for notebooks: {str(err)}" + message = "The domain actions is not properly formatted or " + message += f"is referenced incorrectly for notebooks: {str(err)}" raise StochSSModelFormatError(message, traceback.format_exc()) from err + return index - - def __create_domain_string(self, model, pad): - domain = ["", f"{pad}# Domain", - f"{pad}domain = Domain.read_stochss_domain('{self.s_model['path']}')", - f"{pad}self.add_domain(domain)", - "", f"{pad}self.staticDomain = {self.s_model['domain']['static']}"] - model.extend(domain) - - - def __create_model_cell(self): - pad = ' ' - if self.s_model['is_spatial']: - types = sorted(self.s_model['domain']['types'], key=lambda d_type: d_type['typeID']) - type_ids = {d_type['typeID']:d_type['name'] for d_type in types if d_type['typeID'] > 0} - model = [f"class {self.get_class_name()}(Model):"] - model.extend([f" {type_id.upper()} = '{type_id}'" for type_id in type_ids.values()]) - model.extend(["", " def __init__(self):", - f'{pad}Model.__init__(self, name="{self.get_name()}")']) - type_refs = {index: type_id for index, type_id in type_ids.items()} - self.__create_domain_string(model=model, pad=pad) - self.__create_boundary_condition_string(model=model, pad=pad) - self.__create_species_strings(model=model, pad=pad, type_refs=type_refs) - self.__create_initial_condition_strings(model=model, pad=pad, type_refs=type_refs) - self.__create_parameter_strings(model=model, pad=pad) - self.__create_reaction_strings(model=model, pad=pad, type_refs=type_refs) - else: - model = [f"class {self.get_class_name()}(Model):", - " def __init__(self, parameter_values=None):", - f'{pad}Model.__init__(self, name="{self.get_name()}")', - f"{pad}self.volume = {self.s_model['volume']}"] - self.__create_parameter_strings(model=model, pad=pad) - self.__create_species_strings(model=model, pad=pad) - self.__create_reaction_strings(model=model, pad=pad) - self.__create_event_strings(model=model, pad=pad) - self.__create_rules_strings(model=model, pad=pad) - self.__create_function_definition_strings(model=model, pad=pad) - self.__create_tspan_string(model=model, pad=pad) - return nbf.new_code_cell("\n".join(model)) - - - def __create_parameter_strings(self, model, pad): - if self.s_model['parameters']: - parameters = ["", f"{pad}# Parameters"] + def __create_spatial_boundary_conditions(self, nb_model, index): + if len(self.s_model['boundaryConditions']) > 0: + pad = ' ' + tmp = f"{pad}__NAME__ = __OBJECT__()" names = [] + n_len = 8 + bound_conds = ["", f"{pad}# Boundary Conditions"] try: - for param in self.s_model['parameters']: - names.append(param['name']) - param_str = f'{pad}{param["name"]} = Parameter(name="{param["name"]}", ' - param_str += f'expression="{param["expression"]}")' - parameters.append(param_str) - parameters.append(f"{pad}self.add_parameter([{', '.join(names)}])") - model.extend(parameters) + for s_bound_cond in self.s_model['boundaryConditions']: + name = f"{s_bound_cond['name'].lower()}_bc" + n_len, n_names = self.__add_name(names, n_len, name) + if n_names is not None: + names = n_names + nb_bound_cond = tmp.replace("__NAME__", name) + nb_bound_cond = nb_bound_cond.replace("__OBJECT__", s_bound_cond['name']) + bound_conds.append(nb_bound_cond) + bound_conds.append(f"{pad}model.add_boundary_condition([\n{pad*2}{', '.join(names)}\n{pad}])") + nb_model.insert(index, '\n'.join(bound_conds)) + index += 1 except KeyError as err: - message = "Parameters are not properly formatted or " + message = "Boundary conditions are not properly formatted or " message += f"are referenced incorrectly for notebooks: {str(err)}" raise StochSSModelFormatError(message, traceback.format_exc()) from err + return index + + def __create_spatial_boundary_condition_cells(self, cells): + index = 2 + if len(self.s_model['boundaryConditions']) > 0: + pad = " " + tmp_body = f"{pad}def expression(self):\n{pad*2}return '''__EXPRESSION__'''" + tmp = f"class __NAME__(spatialpy.BoundaryCondition):\n{tmp_body}" + try: + bc_header = "***\n## Creating the Boundary Conditions for the System\n***" + cells.insert(index, nbf.new_markdown_cell(bc_header)) + index += 1 + for s_bound_cond in self.s_model['boundaryConditions']: + bc_cell = tmp.replace("__NAME__", s_bound_cond['name']) + bc_cell = bc_cell.replace("__EXPRESSION__", s_bound_cond['expression']) + cells.insert(index, nbf.new_code_cell(bc_cell)) + index += 1 + return index + 1 + except KeyError as err: + message = "Boundary conditions are not properly formatted or " + message += f"are referenced incorrectly for notebooks: {str(err)}" + raise StochSSModelFormatError(message, traceback.format_exc()) from err + return index + def __create_spatial_domain(self, nb_model, index, type_refs): + try: + pad = ' ' + tmp = f"{pad}model.__TYPE__ = '__ID__'" + xlim = tuple(self.s_model['domain']['x_lim']) + ylim = tuple(self.s_model['domain']['y_lim']) + zlim = tuple(self.s_model['domain']['z_lim']) + rho0 = self.s_model['domain']['rho_0'] + c_0 = self.s_model['domain']['c_0'] + p_0 = self.s_model['domain']['p_0'] + gravity = None if self.s_model['domain']['gravity'] == [0, 0, 0] else self.s_model['domain']['gravity'] + nb_domain = [ + "", f"{pad}# Define Domain Type IDs as constants of the Model", "", f"{pad}# Domain", + f"{pad}domain = spatialpy.Domain(", f"{pad*2}0, {xlim}, {ylim},", + f"{pad*2}{zlim}, rho0={rho0}, c0={c_0}, P0={p_0}, gravity={gravity}", f"{pad})", "", + f"{pad}model.add_domain(domain, allow_all_types=True)", "", + f"{pad}model.staticDomain = {self.s_model['domain']['static']}" + ] + d_index = self.__create_spatial_shapes(nb_domain, 8) + d_index = self.__create_spatial_transformations(nb_domain, d_index) + d_index = self.__create_spatial_actions(nb_domain, d_index, type_refs) + d_index = 2 + for d_type in self.s_model['domain']['types']: + if d_type['typeID'] > 0: + name = f"N_{d_type['name']}" if d_type['name'].isdigit() else d_type['name'].upper() + nb_type = tmp.replace("__TYPE__", name) + nb_domain.insert(d_index, nb_type.replace("__ID__", d_type['name'])) + d_index += 1 + nb_model.insert(index, '\n'.join(nb_domain)) + except KeyError as err: + message = f"The domain is not properly formatted or is referenced incorrectly for notebooks: {str(err)}" + raise StochSSModelFormatError(message, traceback.format_exc()) from err + return index + 1 + + def __create_spatial_geometry_cells(self, cells, index): + exempt_formula = ("", "True", "on_boundary", "not on_boundary") + shapes = list(filter( + lambda shape, exempt=exempt_formula: shape['type'] == "Standard" and shape['formula'] not in exempt, + self.s_model['domain']['shapes'] + )) + actions = list(filter( + lambda action: action['scope'] == "Single Particle" and action['type'] in ("Set Action", "Remove Action"), + self.s_model['domain']['actions'] + )) + if len(shapes) > 0 or len(actions) > 0: + try: + cells.insert(index, nbf.new_markdown_cell("***\n## Geometries\n***")) + index += 1 + for s_shape in shapes: + cells.insert(index, self.__build_geometry(s_shape['formula'], name=s_shape['name'])) + index += 1 + for i, s_act in enumerate(actions): + p_x = s_act['point']['x'] + p_y = s_act['point']['y'] + p_z = s_act['point']['z'] + formula = f"x == {p_x} and y == {p_y} and z == {p_z}" + cells.insert(index, self.__build_geometry(formula, c_name=f"SPAGeometry{i + 1}")) + index += 1 + except KeyError as err: + message = "Shapes or actions are not properly formatted or " + message += f"are referenced incorrectly for notebooks: {str(err)}" + raise StochSSModelFormatError(message, traceback.format_exc()) from err + return index - def __create_reaction_strings(self, model, pad, type_refs=None): - if self.s_model['reactions']: - reactions = ["", f"{pad}# Reactions"] + def __create_spatial_initial_condition(self, nb_model, index, type_refs): + if len(self.s_model['initialConditions']) > 0: + pad = ' ' + args_tmp = "species='__SPECIES__', count=__COUNT__, __DATA__" + tmp = f"{pad}__NAME__ = spatialpy.__OBJECT__({args_tmp})" names = [] + n_len = 8 + ic_types = { + "Place":"PlaceInitialCondition", + "Scatter":"ScatterInitialCondition", + "Distribute Uniformly per Voxel":"UniformInitialCondition" + } + init_cond = ["", f"{pad}# Initial Conditions"] try: - for reac in self.s_model['reactions']: - names.append(reac['name']) - react_str = self.__create_stoich_spec_string(stoich_species=reac['reactants']) - prod_str = self.__create_stoich_spec_string(stoich_species=reac['products']) - - reac_str = f'{pad}{reac["name"]} = Reaction(name="{reac["name"]}", ' - reac_str += f'reactants={react_str}, products={prod_str}, ' - if not reac['massaction']: - reac_str += f'propensity_function="{reac["propensity"]}", ' - reac_str += f'ode_propensity_function="{reac["odePropensity"]}"' + for s_init_cond in self.s_model['initialConditions']: + name = f"{s_init_cond['specie']['name']}_ic" + n_len, n_names = self.__add_name(names, n_len, name) + if n_names is not None: + names = n_names + nb_init_cond = tmp.replace("__OBJECT__", ic_types[s_init_cond['icType']]) + nb_init_cond = nb_init_cond.replace("__NAME__", name) + nb_init_cond = nb_init_cond.replace("__SPECIES__", s_init_cond['specie']['name']) + nb_init_cond = nb_init_cond.replace("__COUNT__", str(s_init_cond['count'])) + if s_init_cond['icType'] == "Place": + location = f"location=[{s_init_cond['x']}, {s_init_cond['y']}, {s_init_cond['z']}]" + nb_init_cond = nb_init_cond.replace("__DATA__", location) else: - reac_str += f'rate="{reac["rate"]["name"]}"' - if self.s_model['is_spatial']: - types = [f"self.{type_refs[d_type].upper()}" for d_type in reac["types"]] - if len(types) < len(type_refs): - reac_str += f', restrict_to=[{", ".join(types)}]' - reac_str += ")" - reactions.append(reac_str) - reactions.append(f"{pad}self.add_reaction([{', '.join(names)}])") - model.extend(reactions) + types = [type_refs[d_type] for d_type in s_init_cond['types']] + nb_init_cond = nb_init_cond.replace("__DATA__", f"types=[{', '.join(types)}]") + init_cond.append(nb_init_cond) + init_cond.append(f"{pad}model.add_initial_condition([\n{pad*2}{', '.join(names)}\n{pad}])") + nb_model.insert(index, '\n'.join(init_cond)) + index += 1 except KeyError as err: - message = "Reactions are not properly formatted or " + message = "Initial Conditions are not properly formatted or " message += f"are referenced incorrectly for notebooks: {str(err)}" raise StochSSModelFormatError(message, traceback.format_exc()) from err + return index - - def __create_rules_strings(self, model, pad): - if self.s_model['rules']: - rate_rules = ["", f"{pad}# Rate Rules"] - assignment_rules = ["", f"{pad}# Assignment Rules"] - rr_start = "self.add_rate_rule(RateRule" - ar_start = "self.add_assignment_rule(AssignmentRule" + def __create_spatial_model_cell(self, func_name): + pad = ' ' + frequency = self.s_model['modelSettings']['timeStep'] + end = self.s_model['modelSettings']['endSim'] + timestep_size = self.s_model['modelSettings']['timestepSize'] + nb_model = [ + f"def {func_name}(parameter_values=None):", + f"{pad}model = spatialpy.Model(name='{self.s_model['name']}')", "", f"{pad}# Timespan", + f"{pad}tspan = spatialpy.TimeSpan.arange({frequency}, t={end}, timestep_size={timestep_size})", + f"{pad}model.timespan(tspan)", f"{pad}return model" + ] + type_refs = self.__get_object_types() + index = self.__create_spatial_domain(nb_model, 2, type_refs) + index = self.__create_spatial_species(nb_model, index, type_refs) + index = self.__create_spatial_initial_condition(nb_model, index, type_refs) + index = self.__create_parameters(nb_model, index) + index = self.__create_reactions(nb_model, index, type_refs=type_refs) + index = self.__create_spatial_boundary_conditions(nb_model, index) + return nbf.new_code_cell("\n".join(nb_model)) + + def __create_spatial_shapes(self, nb_domain, index): + actions = list(filter( + lambda act: act['type'] in ('XML Mesh', 'Mesh IO', 'StochSS Domain'), + self.s_model['domain']['actions'] + )) + if len(self.s_model['domain']['shapes']) > 0 or len(actions) > 0: + class_map = { + 'Cartesian Lattice': 'CartesianLattice', 'Spherical Lattice': 'SphericalLattice', + 'Cylindrical Lattice': 'CylindricalLattice', 'XML Mesh': 'XMLMeshLattice', + 'Mesh IO': 'MeshIOLattice', 'StochSS Domain': 'StochSSLattice' + } + pad = ' ' + comb_geoms = {} + geo_tmp = f"{pad}__NAME__ = __CLASS_NAME__(__ARGS__)" + lat_tmp = f"{pad}__NAME__ = spatialpy.__CLASS__(\n{pad*2}__ARGS__\n{pad})" + geometries = ["", f"{pad}# Domain Geometries"] + lattices = ["", f"{pad}# Domain Lattices"] + if len(actions) > 0: + lattices.insert(1, f"{pad}import os\n{pad}home = os.path.expanduser('~')") try: - for rule in self.s_model['rules']: - start = rr_start if rule['type'] == "Rate Rule" else ar_start - rule_str = f'{pad}{start}(name="{rule["name"]}", formula="{rule["expression"]}"' - rule_str += f', variable="{rule["variable"]["name"]}"))' - if rule['type'] == "Rate Rule": - rate_rules.append(rule_str) + for s_shape in self.s_model['domain']['shapes']: + # Create geometry from shape + geo_name = f"{s_shape['name']}_geom" + if s_shape['type'] == "Standard": + if s_shape['formula'] in ("", "True"): + geometries.append(f"{pad}{geo_name} = spatialpy.GeometryAll()") + elif s_shape['formula'] == "on_boundary": + geometries.append(f"{pad}{geo_name} = spatialpy.GeometryExterior()") + elif s_shape['formula'] == "not on_boundary": + geometries.append(f"{pad}{geo_name} = spatialpy.GeometryInterior()") + else: + c_name = s_shape['name'].title().replace("_", "") + nb_geom = geo_tmp.replace("__ARGS__", "").replace("__NAME__", geo_name) + geometries.append(nb_geom.replace("__CLASS_NAME__", c_name)) else: - assignment_rules.append(rule_str) - if len(rate_rules) > 2: - model.extend(rate_rules) - if len(assignment_rules) > 2: - model.extend(assignment_rules) + comb_geoms[geo_name] = s_shape['formula'] + nb_geom = geo_tmp.replace("__ARGS__", "'', {}").replace("__NAME__", geo_name) + geometries.append(nb_geom.replace("__CLASS_NAME__", "spatialpy.CombinatoryGeometry")) + geometries.append(f"{pad}{geo_name}.formula = '{s_shape['formula']}'") + # Create lattice from shape if fillable + if s_shape['fillable']: + l_class = class_map[s_shape['lattice']] + lat_name = f"{s_shape['name']}_latt" + nb_latt = lat_tmp.replace("__NAME__", lat_name).replace("__CLASS__", l_class) + if l_class == "CartesianLattice": + half_length = s_shape['length'] / 2 + half_height = s_shape['height'] / 2 + half_depth = s_shape['depth'] / 2 + args = ''.join([ + f"-{half_length}, {half_length}, {s_shape['deltax']},\n{pad*2}", + f"ymin=-{half_height}, ymax={half_height}, deltay={s_shape['deltay']}, ", + f"zmin=-{half_depth}, zmax={half_depth}, deltaz={s_shape['deltaz']}" + ]) + else: + slen = "" if l_class == "SphericalLattice" else f", {s_shape['length']}" + args = f"{s_shape['radius']}{slen}, {s_shape['deltas']}, deltar={s_shape['deltar']}" + lattices.append(nb_latt.replace("__ARGS__", args)) + if len(comb_geoms) > 0: + geometries.append("") + items = [' and ', ' or ', ' not ', '(', ')'] + for name, formula in comb_geoms.items(): + if formula.startswith("not "): + formula = formula.replace("not ", "") + for item in items: + formula = formula.replace(item, " ") + deps = formula.split(" ") + geo_namespace = [f"'{dep}': {dep}_geom" for dep in deps if dep != ''] + nb_geo_ns = f"{pad}{name}.geo_namespace = " + "{" + f"{', '.join(geo_namespace)}" + "}" + geometries.append(nb_geo_ns) + if len(geometries) > 2: + nb_domain.insert(index, '\n'.join(geometries)) + index += 1 + for i, s_act in enumerate(actions): + args = f"os.path.join(home, '{s_act['filename']}')" + if s_act['type'] != "StochSS Domain" and s_act['subdomainFile'] != "": + args = f"{args},\n{pad*2}subdomain_file=os.path.join(home, '{s_act['subdomainFile']}')" + nb_latt = lat_tmp.replace("__NAME__", f"ipa_lattice{i+1}") + nb_latt = nb_latt.replace("__CLASS__", class_map[s_act['type']]).replace("__ARGS__", args) + lattices.append(nb_latt) + if len(lattices) > 2: + nb_domain.insert(index, '\n'.join(lattices)) + index += 1 except KeyError as err: - message = "Rules are not properly formatted or " - message += f"are referenced incorrectly for notebooks: {str(err)}" + message = "The domain shape is not properly formatted or " + message += f"is referenced incorrectly for notebooks: {str(err)}" raise StochSSModelFormatError(message, traceback.format_exc()) from err + return index - - def __create_species_strings(self, model, pad, type_refs=None): - if self.s_model['species']: - species = ["", f"{pad}# Variables"] + def __create_spatial_species(self, nb_model, index, type_refs): + if len(self.s_model['species']) > 0: + pad = ' ' + args_tmp = "name='__NAME__', diffusion_coefficient=__VALUE____RESTRICT_TO__" + tmp = f"{pad}__NAME__ = spatialpy.Species({args_tmp})" names = [] + n_len = 8 + species = ["", f"{pad}# Variables"] try: - for spec in self.s_model['species']: - names.append(spec["name"]) - spec_str = f'{pad}{spec["name"]} = Species(name="{spec["name"]}", ' - if self.s_model['is_spatial']: - types = [f"self.{type_refs[d_type].upper()}" for d_type in spec['types']] - spec_str += f"diffusion_coefficient={spec['diffusionConst']}, " - spec_str += f"restrict_to=[{', '.join(types)}])" + for s_species in self.s_model['species']: + n_len, n_names = self.__add_name(names, n_len, s_species['name']) + if n_names is not None: + names = n_names + nb_spec = tmp.replace("__NAME__", s_species['name']) + nb_spec = nb_spec.replace("__VALUE__", str(s_species['diffusionConst'])) + if len(s_species['types']) == len(type_refs): + nb_spec = nb_spec.replace("__RESTRICT_TO__", "") else: - spec_str += f'initial_value={spec["value"]}, mode="{spec["mode"]}")' - species.append(spec_str) - species.append(f"{pad}self.add_species([{', '.join(names)}])") - model.extend(species) + types = [type_refs[d_type] for d_type in s_species['types']] + restrict_to = f", restrict_to=[{', '.join(types)}]" + nb_spec = nb_spec.replace("__RESTRICT_TO__", restrict_to) + species.append(nb_spec) + species.append(f"{pad}model.add_species([\n{pad*2}{', '.join(names)}\n{pad}])") + nb_model.insert(index, '\n'.join(species)) + index += 1 except KeyError as err: - message = "Species are not properly formatted or " - message += f"are referenced incorrectly for notebooks: {str(err)}" + message = f"Species are not properly formatted or are referenced incorrectly for notebooks: {str(err)}" raise StochSSModelFormatError(message, traceback.format_exc()) from err + return index + + def __create_spatial_transformations(self, nb_domain, index): + if len(self.s_model['domain']['transformations']) > 0: + class_map = { + 'Translate Transformation': 'TranslationTransformation', + 'Rotate Transformation': 'RotationTransformation', + 'Reflect Transformation': 'ReflectionTransformation', + 'Scale Transformation': 'ScalingTransformation' + } + pad = ' ' + tmp = f"{pad}__NAME__ = spatialpy.__CLASS__(\n{pad*2}__ARGS__\n{pad})" + nested_trans = {} + transformations = ["", f"{pad}# Domain Transformations"] + try: + for s_tran in self.s_model['domain']['transformations']: + t_class = class_map[s_tran['type']] + nb_tran = tmp.replace("__NAME__", s_tran['name']).replace("__CLASS__", t_class) + if s_tran['transformation'] != "": + nested_trans[s_tran['name']] = s_tran['transformation'] + if t_class in ("TranslationTransformation", "RotationTransformation"): + point1 = f"[{s_tran['vector'][0]['x']}, {s_tran['vector'][0]['y']}, {s_tran['vector'][0]['z']}]" + point2 = f"[{s_tran['vector'][1]['x']}, {s_tran['vector'][1]['y']}, {s_tran['vector'][1]['z']}]" + angle = "" if t_class == "TranslationTransformation" else f", {s_tran['angle']}" + args = f"({point1}, {point2}){angle}" + elif t_class == "ScalingTransformation": + center = f"[{s_tran['center']['x']}, {s_tran['center']['y']}, {s_tran['center']['z']}]" + args = f"{s_tran['factor']}, center={center}" + else: + point1 = f"[{s_tran['point1']['x']}, {s_tran['point1']['y']}, {s_tran['point1']['z']}]" + if self.__check_reflect_mathod(s_tran) == "Point-Normal": + normal = f"[{s_tran['normal']['x']}, {s_tran['normal']['y']}, {s_tran['normal']['z']}]" + args = f"{point1}, normal={normal}" + else: + point2 = f"[{s_tran['point2']['x']}, {s_tran['point2']['y']}, {s_tran['point2']['z']}]" + point3 = f"[{s_tran['point3']['x']}, {s_tran['point3']['y']}, {s_tran['point3']['z']}]" + args = f"{point1}, point2={point2}, point3={point3}" + nb_tran = nb_tran.replace("__ARGS__", args) + transformations.append(nb_tran) + if len(nested_trans) > 0: + transformations.append("") + for tran, nested_tran in nested_trans.items(): + transformations.append(f"{pad}{tran}.transformation = {nested_tran}") + nb_domain.insert(index, '\n'.join(transformations)) + index += 1 + except KeyError as err: + message = "The domain transformation is not properly formatted or " + message += f"is referenced incorrectly for notebooks: {str(err)}" + raise StochSSModelFormatError(message, traceback.format_exc()) from err + return index - - def __create_stoich_spec_string(self, stoich_species): + @classmethod + def __create_stoich_species(cls, stoich_species): species = {} for stoich_spec in stoich_species: name = stoich_spec['specie']['name'] @@ -428,64 +705,213 @@ def __create_stoich_spec_string(self, stoich_species): species[name] += stoich_spec['ratio'] else: species[name] = stoich_spec['ratio'] - spec_list = [] - for name, ratio in species.items(): - spec_list.append(f"'{name}': {ratio}") - return "{" + ", ".join(spec_list) + "}" + return str(species) + def __create_well_mixed_events(self, nb_model, index): + if self.s_model['eventsCollection']: + pad = ' ' + l1_args = f"{pad*2}name='__NAME__', trigger=__TRIGGER__," + l2_args = f"{pad*2}assignments=__ASSIGNMENTS__," + l3_args = f"{pad*2}delay=__DELAY__, priority='__PRIORITY__'," + l4_args = f"{pad*2}use_values_from_trigger_time=__UVFTT__" + tmp = f"{pad}__NAME__ = gillespy2.Event(\n{l1_args}\n{l2_args}\n{l3_args}\n{l4_args}\n{pad})" + names = [] + n_len = 8 + triggers = ["", f"{pad}# Event Triggers"] + assignments = ["", f"{pad}# Event Assignments"] + events = ["", f"{pad}# Events"] + try: + for s_event in self.s_model['eventsCollection']: + t_name, nb_trigger = self.__create_well_mixed_event_trigger(s_event) + triggers.append(nb_trigger) + a_names, nb_assignments = self.__create_well_mixed_event_assignments(s_event) + assignments.extend(nb_assignments) + delay = "None" if s_event['delay'] in (None, "") else f"'{s_event['delay']}'" + n_len, n_names = self.__add_name(names, n_len, s_event['name']) + if n_names is not None: + names = n_names + nb_event = tmp.replace("__NAME__", s_event['name']).replace("__TRIGGER__", t_name) + nb_event = nb_event.replace("__ASSIGNMENTS__", a_names).replace("__DELAY__", delay) + nb_event = nb_event.replace("__PRIORITY__", s_event['priority']) + nb_event = nb_event.replace("__UVFTT__", str(s_event['useValuesFromTriggerTime'])) + events.append(nb_event) + events.append(f"{pad}model.add_event([\n{pad*2}{', '.join(names)}\n{pad}])") + nb_model.insert(index, '\n'.join(triggers)) + index += 1 + nb_model.insert(index, '\n'.join(assignments)) + index += 1 + nb_model.insert(index, '\n'.join(events)) + index += 1 + except KeyError as err: + message = f"Events are not properly formatted or are referenced incorrectly for notebooks: {str(err)}" + raise StochSSModelFormatError(message, traceback.format_exc()) from err + return index - def __create_tspan_string(self, model, pad): - end = self.s_model['modelSettings']['endSim'] - output_freq = self.s_model['modelSettings']['timeStep'] - tspan = ["", f"{pad}# Timespan"] - if self.s_model['is_spatial']: - step_size = self.s_model['modelSettings']['timestepSize'] - ts_str = f'{pad}self.timespan(np.arange(0, {end + step_size}, {output_freq})' - ts_str += f", timestep_size={step_size})" - tspan.append(ts_str) - else: - tspan.append(f'{pad}tspan = TimeSpan(np.arange(0, {end + output_freq}, {output_freq}))') - tspan.append(f'{pad}self.timespan(tspan)') - model.extend(tspan) + def __create_well_mixed_event_assignments(self, s_event): + pad = ' ' + args = f"{pad*2}variable='__VAR__',\n{pad*2}expression='__EXPRESSION__'" + tmp = f"{pad}__NAME__ = gillespy2.EventAssignment(\n{args}\n{pad})" + names = [] + n_len = 8 + assignments = [] + for i, s_assignment in enumerate(s_event['eventAssignments']): + name = f"{s_event['name']}_assign_{i + 1}" + n_len, n_names = self.__add_name(names, n_len, name, max_len=87) + if n_names is not None: + names = n_names + nb_assignment = tmp.replace("__NAME__", name) + nb_assignment = nb_assignment.replace("__VAR__", s_assignment['variable']['name']) + nb_assignment = nb_assignment.replace("__EXPRESSION__", s_assignment['expression']) + assignments.append(nb_assignment) + return f"[{', '.join(names)}]", assignments + @classmethod + def __create_well_mixed_event_trigger(cls, s_event): + pad = ' ' + name = f"{s_event['name']}_trig" + persistent = s_event['persistent'] + l1_args = f"{pad*2}expression='{s_event['triggerExpression']}'," + l2_args = f"{pad*2}initial_value={s_event['initialValue']}, persistent={persistent}" + nb_trig = f"{pad}{name} = gillespy2.EventTrigger(\n{l1_args}\n{l2_args}\n{pad})" + return name, nb_trig + + def __create_well_mixed_function_definition(self, nb_model, index): + if self.s_model['functionDefinitions']: + pad = ' ' + args_tmp = f"{pad*2}name='__NAME__', args=__ARGS__,\n{pad*2}function='__FUNCTION__'" + tmp = f"{pad}__NAME__ = gillespy2.FunctionDefinition(\n{args_tmp}\n{pad})" + names = [] + n_len = 8 + func_defs = ["", f"{pad}# Function Definitions"] + try: + for s_func_def in self.s_model['functionDefinitions']: + args = str(s_func_def['variables'].split(',')) + n_len, n_names = self.__add_name(names, n_len, s_func_def['name']) + if n_names is not None: + names = n_names + nb_func_def = tmp.replace("__NAME__", s_func_def['name']).replace("__ARGS__", args) + nb_func_def = nb_func_def.replace("__FUNCTION__", s_func_def['expression']) + func_defs.append(nb_func_def) + func_defs.append(f"{pad}model.add_function_definition([\n{pad*2}{', '.join(names)}\n{pad}])") + nb_model.insert(index, '\n'.join(func_defs)) + index += 1 + except KeyError as err: + message = "Function definitions are not properly formatted or " + message += f"are referenced incorrectly for notebooks: {str(err)}" + raise StochSSModelFormatError(message, traceback.format_exc()) from err + return index - def __get_gillespy2_run_settings(self): - is_automatic = self.settings['simulationSettings']['isAutomatic'] - if self.nb_type in (self.PARAMETER_SWEEP_1D, self.PARAMETER_SWEEP_2D) and is_automatic: - self.settings['simulationSettings']['realizations'] = 20 - settings = self.settings['simulationSettings'] - if settings['algorithm'] == "ODE": - self.settings['simulationSettings']['realizations'] = 1 - settings['realizations'] = 1 - # Map algorithm for GillesPy2 - tau_hybrid_solver = self.model.get_best_solver_algo("Tau-Hybrid").name - solver_map = { - "ODE":f'"solver":{"solver" if self.is_ssa_c else "ODESolver"}', - "SSA":f'"solver":{"solver" if self.is_ssa_c else "NumPySSASolver"}', - "CLE":'"solver": CLESolver', - "Tau-Leaping":f'"solver":{"solver" if self.is_ssa_c else "TauLeapingSolver"}', - "Hybrid-Tau-Leaping":f'"solver":{"solver" if "CSolver" in tau_hybrid_solver else "TauHybridSolver"}' - } - # Map algorithm settings for GillesPy2. GillesPy2 requires snake case, remap camelCase - settings_map = {"number_of_trajectories":settings['realizations'], - "seed":settings['seed'] if settings['seed'] != -1 else None, - "tau_tol":settings['tauTol'], - "integrator_options":str({"rtol":settings['relativeTol'], - "atol":settings['absoluteTol']})} - #Parse settings for algorithm - run_settings = [solver_map[settings['algorithm']]] - algorithm_settings = [f'"{key}":{val}' for key, val in settings_map.items()] - run_settings.extend(algorithm_settings) - return run_settings + def __create_well_mixed_model_cell(self, func_name): + pad = ' ' + frequency = self.s_model['modelSettings']['timeStep'] + end = self.s_model['modelSettings']['endSim'] + nb_model = [ + f"def {func_name}(parameter_values=None):", + f"{pad}model = gillespy2.Model(name='{self.s_model['name']}')", + f"{pad}model.volume = {self.s_model['volume']}", "", f"{pad}# Timespan", + f"{pad}tspan = gillespy2.TimeSpan.arange({frequency}, t={end})", + f"{pad}model.timespan(tspan)", f"{pad}return model" + ] + index = self.__create_well_mixed_species(nb_model, 3) + index = self.__create_parameters(nb_model, index) + index = self.__create_reactions(nb_model, index) + index = self.__create_well_mixed_rules(nb_model, index) + index = self.__create_well_mixed_events(nb_model, index) + index = self.__create_well_mixed_function_definition(nb_model, index) + return nbf.new_code_cell("\n".join(nb_model)) + + def __create_well_mixed_species(self, nb_model, index): + if len(self.s_model['species']) > 0: + pad = ' ' + args_tmp = "name='__NAME__', initial_value=__VALUE__, mode='__MODE__'" + tmp = f"{pad}__NAME__ = gillespy2.Species({args_tmp})" + names = [] + n_len = 8 + species = ["", f"{pad}# Variables"] + try: + for s_species in self.s_model['species']: + n_len, n_names = self.__add_name(names, n_len, s_species['name']) + if n_names is not None: + names = n_names + nb_spec = tmp.replace("__NAME__", s_species['name']) + nb_spec = nb_spec.replace("__VALUE__", str(s_species['value'])) + nb_spec = nb_spec.replace("__MODE__", s_species['mode']) + species.append(nb_spec) + species.append(f"{pad}model.add_species([\n{pad*2}{', '.join(names)}\n{pad}])") + nb_model.insert(index, '\n'.join(species)) + index += 1 + except KeyError as err: + message = f"Species are not properly formatted or are referenced incorrectly for notebooks: {str(err)}" + raise StochSSModelFormatError(message, traceback.format_exc()) from err + return index + def __create_well_mixed_rules(self, nb_model, index): + if self.s_model['rules']: + pad = ' ' + args_tmp = f"{pad*2}name='__NAME__', variable='__VAR__',\n{pad*2}formula='__FORMULA__'" + tmp = f"{pad}__NAME__ = gillespy2.__TYPE__(\n{args_tmp}\n{pad})" + rr_names = [] + ar_names = [] + rr_n_len = 8 + ar_n_len = 8 + rate_rules = ["", f"{pad}# Rate Rules"] + assignment_rules = ["", f"{pad}# Assignment Rules"] + try: + for s_rule in self.s_model['rules']: + nb_rule = tmp.replace("__NAME__", s_rule["name"]) + nb_rule = nb_rule.replace("__FORMULA__", s_rule["expression"]) + nb_rule = nb_rule.replace("__VAR__", s_rule["variable"]["name"]) + if s_rule['type'] == "Rate Rule": + rr_n_len, n_names = self.__add_name(rr_names, rr_n_len, s_rule['name']) + if n_names is not None: + rr_names = n_names + nb_rule = nb_rule.replace("__TYPE__", "RateRule") + rate_rules.append(nb_rule) + else: + ar_n_len, n_names = self.__add_name(ar_names, ar_n_len, s_rule['name']) + if n_names is not None: + ar_names = n_names + nb_rule = nb_rule.replace("__TYPE__", "AssignmentRule") + assignment_rules.append(nb_rule) + if len(rate_rules) > 2: + rate_rules.append(f"{pad}model.add_rate_rule([\n{pad*2}{', '.join(rr_names)}\n{pad}])") + nb_model.insert(index, '\n'.join(rate_rules)) + index += 1 + if len(assignment_rules) > 2: + assignment_rules.append( + f"{pad}model.add_assignment_rule([\n{pad*2}{', '.join(ar_names)}\n{pad}])" + ) + nb_model.insert(index, '\n'.join(assignment_rules)) + index += 1 + except KeyError as err: + message = f"Rules are not properly formatted or are referenced incorrectly for notebooks: {str(err)}" + raise StochSSModelFormatError(message, traceback.format_exc()) from err + return index - def __get_spatialpy_run_setting(self): - self.settings['simulationSettings']['realizations'] = 1 + def __get_gillespy2_run_settings(self, use_solver=False): settings = self.settings['simulationSettings'] - settings_map = {"number_of_trajectories":settings['realizations'], - "seed":settings['seed'] if settings['seed'] != -1 else None} - return [f'"{key}":{val}' for key, val in settings_map.items()] + # Map algorithm settings for GillesPy2. GillesPy2 requires snake case, remap camelCase + settings_map = { + "number_of_trajectories": settings['realizations'], + "seed": settings['seed'] if settings['seed'] != -1 else None, + "tau_tol": settings['tauTol'], + "integrator_options": str({"rtol":settings['relativeTol'], "atol":settings['absoluteTol']}) + } + if use_solver: + settings_map['solver'] = "solver" + elif settings['algorithm'] == "Hybrid-Tau-Leaping": + settings_map['algorithm'] = "'Tau-Hybrid'" + else: + settings_map['algorithm'] = f"'{settings['algorithm']}'" + return settings_map + def __get_object_types(self): + types = sorted(self.s_model['domain']['types'], key=lambda d_type: d_type['typeID']) + type_map = {} + for d_type in types: + name = f"N_{d_type['name']}" if d_type['name'].isdigit() else d_type['name'].upper() + type_map[d_type['typeID']] = f"model.{name}" + return type_map @classmethod def __get_presentation_links(cls, hostname, file): @@ -496,74 +922,80 @@ def __get_presentation_links(cls, hostname, file): open_link = f"https://open.stochss.org?open={download_link}" return {"presentation": present_link, "download": download_link, "open": open_link} + def __get_spatialpy_run_setting(self): + settings = self.settings['simulationSettings'] + return { + "number_of_trajectories":settings['realizations'], + "seed":settings['seed'] if settings['seed'] != -1 else None + } def create_common_cells(self): ''' Create the cells common to all notebook types. ''' - cells = [self.__create_import_cell(), - nbf.new_markdown_cell(f"# {self.get_name()}"), - self.__create_model_cell(), - nbf.new_code_cell(f'model = {self.get_class_name()}()'), - nbf.new_markdown_cell("# Simulation Parameters"), - self.__create_configuration_cell()] + func_name = self.get_function_name() + cells = self.__create_common_header_cells() + cells.insert(3, nbf.new_code_cell(f"model = {func_name}()")) + if self.nb_type == self.SPATIAL_SIMULATION: + cells.insert(1, nbf.new_code_cell("import spatialpy")) + index = self.__create_spatial_boundary_condition_cells(cells) + index = self.__create_spatial_geometry_cells(cells, index) + cells.insert(index + 1, self.__create_spatial_model_cell(func_name)) + else: + cells.insert(1, nbf.new_code_cell("import gillespy2")) + cells.insert(3, self.__create_well_mixed_model_cell(func_name)) + cells.append(self.__create_configuration_cell()) return cells - - def create_es_notebook(self): - '''Create an ensemble simulation jupiter notebook for a StochSS model/workflow - - Attributes - ----------''' + def create_es_notebook(self, results=None, compute="StochSS"): + '''Create an ensemble simulation jupiter notebook for a StochSS model/workflow ''' self.nb_type = self.ENSEMBLE_SIMULATION - self.settings['solver'] = self.get_gillespy2_solver_name() - run_str = "kwargs = configure_simulation()\nresults = model.run(**kwargs)" cells = self.create_common_cells() - cells.extend([nbf.new_code_cell(run_str), - nbf.new_markdown_cell("# Visualization"), - nbf.new_code_cell("results.plotplotly()")]) + + self.settings['solver'] = self.get_gillespy2_solver_name() + run_header, run_code = self.__create_run(results, compute=compute) + vis_header = nbf.new_markdown_cell("***\n## Visualization\n***") + vis_code = nbf.new_code_cell("results.plotplotly()") + cells.extend([run_header, run_code, vis_header, vis_code]) + if compute != "StochSS": + self.__create_compute_cells(cells, compute) + if results is not None: + cells.insert(1, nbf.new_code_cell("import os\nimport pickle")) message = self.write_notebook_file(cells=cells) return {"Message":message, "FilePath":self.get_path(), "File":self.get_file()} - - def create_ses_notebook(self): - '''Create a spetial ensemble simulation jupiter notebook for a StochSS model/workflow - - Attributes - ----------''' + def create_ses_notebook(self, results=None, compute="StochSS"): + '''Create a spetial ensemble simulation jupiter notebook for a StochSS model/workflow ''' self.nb_type = self.SPATIAL_SIMULATION + cells = self.create_common_cells() + self.settings['solver'] = "Solver" - run_str = "kwargs = configure_simulation()\nresults = model.run(**kwargs)" - if self.s_model['species']: - species = self.s_model['species'][0]['name'] - plot_str = f"results.plot_species('{species}', animated=True, width=None, height=None)" + if results is not None: + cells.insert(1, nbf.new_code_cell("import os\nimport pickle")) + run_header, run_code = self.__create_run(results) + vis_header = nbf.new_markdown_cell("***\n## Visualization\n***") + plt_args = "animated=True, width='auto', height='auto'" + if len(self.s_model['species']) > 0: + plot_str = f"results.plot_species('{self.s_model['species'][0]['name']}', {plt_args})" else: - plot_str = "results.plot_property('type', animated=True, width=None, height=None)" - cells = [nbf.new_code_cell("%load_ext autoreload\n%autoreload 2")] - cells.extend(self.create_common_cells()) - if 'boundaryConditions' in self.s_model.keys(): - bc_cells = self.__create_boundary_condition_cells() - for i, bc_cell in enumerate(bc_cells): - cells.insert(2 + i, bc_cell) - cells.extend([nbf.new_code_cell(run_str), - nbf.new_markdown_cell("# Visualization"), - nbf.new_code_cell(plot_str)]) + plot_str = f"results.plot_property('type', {plt_args})" + vis_code = nbf.new_code_cell(plot_str) + cells.extend([run_header, run_code, vis_header, vis_code]) + if compute != "StochSS": + self.log( + "warning", + "AWS Cloud compute environment is not supported by spatial ensemble simulation workflows." + ) message = self.write_notebook_file(cells=cells) return {"Message":message, "FilePath":self.get_path(), "File":self.get_file()} - - def get_class_name(self): + def get_function_name(self): ''' Get the python style class name, ''' - name = self.get_name() + name = self.s_model['name'].lower() for char in string.punctuation: - name = name.replace(char, "") - l_char = name[0] - if l_char in string.digits: - return f"M{name}" - if l_char in string.ascii_lowercase: - return name.replace(l_char, l_char.upper(), 1) - return name.replace(" ", "") - + if char != "_": + name = name.replace(char, "") + return f"create_{name.replace(' ', '_')}" def get_gillespy2_solver_name(self): ''' Get the name of the gillespy2 solver. ''' @@ -578,10 +1010,8 @@ def get_gillespy2_solver_name(self): 'Tau-Leaping': self.model.get_best_solver_algo("Tau-Leaping").name, 'Hybrid-Tau-Leaping': self.model.get_best_solver_algo("Tau-Hybrid").name } - return algorithm_map[self.settings['simulationSettings']['algorithm']] - def load(self): '''Read the notebook file and return as a dict''' try: @@ -610,8 +1040,7 @@ def publish_presentation(self): exists = False with open(dst, "w", encoding="utf-8") as presentation_file: json.dump(notebook_pres, presentation_file) - links = self.__get_presentation_links(hostname, file) - return links, exists + return self.__get_presentation_links(hostname, file), exists except PermissionError as err: message = f"You do not have permission to publish this file: {str(err)}" raise StochSSPermissionsError(message, traceback.format_exc()) from err diff --git a/stochss/handlers/util/stochss_project.py b/stochss/handlers/util/stochss_project.py index 9c11dd4c2b..67d2e7fb29 100644 --- a/stochss/handlers/util/stochss_project.py +++ b/stochss/handlers/util/stochss_project.py @@ -1,6 +1,6 @@ ''' StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/stochss/handlers/util/stochss_sbml.py b/stochss/handlers/util/stochss_sbml.py index abe62155f4..0331d0ea08 100644 --- a/stochss/handlers/util/stochss_sbml.py +++ b/stochss/handlers/util/stochss_sbml.py @@ -1,6 +1,6 @@ ''' StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/stochss/handlers/util/stochss_spatial_model.py b/stochss/handlers/util/stochss_spatial_model.py index 8594597f7d..dc0bb35e2c 100644 --- a/stochss/handlers/util/stochss_spatial_model.py +++ b/stochss/handlers/util/stochss_spatial_model.py @@ -1,6 +1,6 @@ ''' StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -15,24 +15,28 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' - import os import json import copy import string import hashlib -import tempfile import traceback +import numpy import plotly from escapism import escape from spatialpy import Model, Species, Parameter, Reaction, Domain, DomainError, BoundaryCondition, \ PlaceInitialCondition, UniformInitialCondition, ScatterInitialCondition, \ - Geometry, ModelError, TimeSpan + TimeSpan, Geometry, GeometryAll, GeometryExterior, GeometryInterior, \ + CombinatoryGeometry, CartesianLattice, SphericalLattice, CylindricalLattice, \ + XMLMeshLattice, MeshIOLattice, StochSSLattice, TranslationTransformation, \ + RotationTransformation, ReflectionTransformation, ScalingTransformation, \ + ModelError from .stochss_base import StochSSBase from .stochss_errors import StochSSFileNotFoundError, FileNotJSONFormatError, DomainFormatError, \ - StochSSModelFormatError, StochSSPermissionsError, DomainGeometryError + StochSSModelFormatError, StochSSPermissionsError, DomainUpdateError, \ + DomainActionError, DomainShapeError, DomainTransformationError class StochSSSpatialModel(StochSSBase): ''' @@ -40,7 +44,6 @@ class StochSSSpatialModel(StochSSBase): StochSS spatial model object ################################################################################################ ''' - def __init__(self, path, new=False, model=None): ''' Intitialize a spatial model object and if its new create it on the users file system @@ -73,7 +76,6 @@ def __init__(self, path, new=False, model=None): else: self.model = None - @classmethod def __build_boundary_condition(cls, boundary_condition): class NewBC(BoundaryCondition): # pylint: disable=too-few-public-methods @@ -87,37 +89,32 @@ def __init__(self): pass def expression(self): # pylint: disable=no-self-use - ''' - Custom expression for boundary condition - ''' + ''' Custom expression for boundary condition. ''' return boundary_condition['expression'] return NewBC() - @classmethod - def __build_geometry(cls, d_type, center): - name = d_type['name'].title() - class NewG(Geometry): # pylint: disable=too-few-public-methods + def __build_geometry(cls, geometry, name=None, formula=None): + if formula is None: + formula = geometry['formula'] + if name is None: + name = geometry['name'] + + class NewGeometry(Geometry): # pylint: disable=too-few-public-methods ''' ######################################################################################## Custom SpatialPy Geometry ######################################################################################## ''' __class__ = f"__main__.{name}" - def __init__(self, center): - self.center = center + def __init__(self): + pass def inside(self, point, on_boundary): # pylint: disable=no-self-use - ''' - Custom inside for geometry - ''' - namespace = { - 'cx': self.center[0], 'cy': self.center[1], 'cz': self.center[2], - 'x': point[0], 'y': point[1], 'z': point[2] - } - return eval(d_type['geometry'], {}, namespace) - return NewG(center) - + ''' Custom inside function for geometry. ''' + namespace = {'x': point[0], 'y': point[1], 'z': point[2], 'on_boundary': on_boundary} + return eval(formula, {}, namespace) + return NewGeometry() @classmethod def __build_stochss_domain(cls, s_domain, data=None): @@ -131,20 +128,16 @@ def __build_stochss_domain(cls, s_domain, data=None): "x_lim":s_domain.xlim, "y_lim":s_domain.ylim, "z_lim":s_domain.zlim, - "types":[{"fixed":False, - "mass":1.0, - "name":"Un-Assigned", - "nu":0.0, - "typeID":0, - "volume":1.0}], + "types":[{ + "fixed":False, "mass":1.0, "name":"Un-Assigned", + "nu":0.0, "typeID":0, "volume":1.0 + }], "boundary_condition": { - "reflect_x": True, - "reflect_y": True, - "reflect_z": True}, + "reflect_x": True, "reflect_y": True, "reflect_z": True + }, "particles":particles} return domain - @classmethod def __build_stochss_domain_particles(cls, s_domain, data=None): particles = [] @@ -172,6 +165,96 @@ def __build_stochss_domain_particles(cls, s_domain, data=None): particles.append(particle) return particles + def __convert_actions(self, domain, s_domain, type_ids): + geometries, lattices = self.__convert_shapes(s_domain) + transformations = self.__convert_transformations(s_domain) + try: + actions = list(filter(lambda action: action['enable'], s_domain['actions'])) + actions = sorted(actions, key=lambda action: action['priority']) + for i, action in enumerate(actions): + # Build props arg + if action['type'] in ('Fill Action', 'Set Action', 'XML Mesh', 'Mesh IO'): + kwargs = { + 'mass': action['mass'], 'vol': action['vol'], 'rho': action['rho'], + 'nu': action['nu'], 'c': action['c'], 'fixed': action['fixed'] + } + if action['type'] in ('Fill Action', 'Set Action'): + kwargs['type_id'] = type_ids[action['typeID']].replace("-", "") + else: + kwargs = {} + # Apply actions + if action['type'] == "Fill Action": + if action['scope'] == 'Multi Particle': + geometry = geometries[f"{action['shape']}_geom"] + if action['transformation'] == "": + lattice = lattices[f"{action['shape']}_latt"] + else: + lattice = transformations[action['transformation']] + lattice.lattice = lattices[f"{action['shape']}_latt"] + _ = domain.add_fill_action( + lattice=lattice, geometry=geometry, enable=action['enable'], + apply_action=action['enable'], **kwargs + ) + else: + point = [action['point']['x'], action['point']['y'], action['point']['z']] + domain.add_point(point, **kwargs) + elif action['type'] in ('XML Mesh', 'Mesh IO', 'StochSS Domain'): + lattices = {'XML Mesh': XMLMeshLattice, 'Mesh IO': MeshIOLattice, 'StochSS Domain': StochSSLattice} + filename = os.path.join(self.user_dir, action['filename']) + if action['type'] == "StochSS Domain" or action['subdomainFile'] == "": + lattice = lattices[action['type']](filename) + else: + subdomain_file = os.path.join(self.user_dir, action['subdomainFile']) + lattice = lattices[action['type']](filename, subdomain_file=subdomain_file) + _ = domain.add_fill_action( + lattice=lattice, enable=action['enable'], apply_action=action['enable'], **kwargs + ) + else: + # Get proper geometry for scope + # 'Single Particle' scope creates a geometry using actions point. + if action['scope'] == 'Single Particle': + p_x = action['point']['x'] + p_y = action['point']['y'] + p_z = action['point']['z'] + formula = f"x == {p_x} and y == {p_y} and z == {p_z}" + geometry = self.__build_geometry( + None, name=f"SPAGeometry{i + 1}", formula=formula + ) + elif action['transformation'] == "": + geometry = geometries[f"{action['shape']}_geom"] + else: + geometry = transformations[action['transformation']] + geometry.geometry = geometries[f"{action['shape']}_geom"] + if action['type'] == "Set Action": + domain.add_set_action( + geometry=geometry, enable=action['enable'], + apply_action=action['enable'], **kwargs + ) + if action['scope'] == "Single Particle": + curr_pnt = numpy.array( + [action['point']['x'], action['point']['y'], action['point']['z']] + ) + new_pnt = numpy.array([ + action['newPoint']['x'], action['newPoint']['y'], + action['newPoint']['z'] + ]) + if numpy.count_nonzero(curr_pnt - new_pnt) > 0: + for j, vertex in enumerate(domain.vertices): + if numpy.count_nonzero(curr_pnt - vertex) <= 0: + domain.vertices[j] = new_pnt + break + else: + domain.add_remove_action( + geometry=geometry, enable=action['enable'], + apply_action=action['enable'] + ) + except KeyError as err: + message = "Spatial actions are not properly formatted or " + message += f"are referenced incorrectly: {str(err)}" + raise StochSSModelFormatError(message, traceback.format_exc()) from err + except ModelError as err: + message = f"One or more domain actions are invalid: {err}" + raise DomainActionError(message, traceback.format_exc()) from err def __convert_boundary_conditions(self, model): try: @@ -183,28 +266,33 @@ def __convert_boundary_conditions(self, model): message += f"are referenced incorrectly: {str(err)}" raise StochSSModelFormatError(message, traceback.format_exc()) from err - - def __convert_domain(self, model, type_ids): + def __convert_domain(self, type_ids, model=None, s_domain=None): try: - xlim = tuple(self.model['domain']['x_lim']) - ylim = tuple(self.model['domain']['y_lim']) - zlim = tuple(self.model['domain']['z_lim']) - rho0 = self.model['domain']['rho_0'] - c_0 = self.model['domain']['c_0'] - p_0 = self.model['domain']['p_0'] - gravity = self.model['domain']['gravity'] + if s_domain is None: + s_domain = self.model['domain'] + xlim = tuple(s_domain['x_lim']) + ylim = tuple(s_domain['y_lim']) + zlim = tuple(s_domain['z_lim']) + rho0 = s_domain['rho_0'] + c_0 = s_domain['c_0'] + p_0 = s_domain['p_0'] + gravity = s_domain['gravity'] if gravity == [0, 0, 0]: gravity = None domain = Domain(0, xlim, ylim, zlim, rho0=rho0, c0=c_0, P0=p_0, gravity=gravity) - self.__convert_particles(domain=domain, type_ids=type_ids) - model.add_domain(domain) + self.__convert_actions(domain, s_domain, type_ids) + if model is None: + self.__convert_types(domain, type_ids) + return domain + model.add_domain(domain, allow_all_types=True) + self.__convert_types(model.domain, type_ids) model.staticDomain = self.model['domain']['static'] + return None except KeyError as err: message = "Spatial model domain properties are not properly formatted or " message += f"are referenced incorrectly: {str(err)}" raise StochSSModelFormatError(message, traceback.format_exc()) from err - def __convert_initial_conditions(self, model, type_ids): try: s_species = model.get_all_species() @@ -215,9 +303,9 @@ def __convert_initial_conditions(self, model, type_ids): types = [type_ids[d_type] for d_type in initial_condition['types']] s_ic = ScatterInitialCondition(species, count, types=types) elif initial_condition['icType'] == "Place": - location = [initial_condition['x'], - initial_condition['y'], - initial_condition['z']] + location = [ + initial_condition['x'], initial_condition['y'], initial_condition['z'] + ] s_ic = PlaceInitialCondition(species, count, location) else: types = [type_ids[d_type] for d_type in initial_condition['types']] @@ -228,7 +316,6 @@ def __convert_initial_conditions(self, model, type_ids): message += f"are referenced incorrectly: {str(err)}" raise StochSSModelFormatError(message, traceback.format_exc()) from err - def __convert_model_settings(self, model): try: end = self.model['modelSettings']['endSim'] @@ -241,7 +328,6 @@ def __convert_model_settings(self, model): message += f"are referenced incorrectly: {str(err)}" raise StochSSModelFormatError(message, traceback.format_exc()) from err - def __convert_parameters(self, model): try: for parameter in self.model['parameters']: @@ -252,21 +338,6 @@ def __convert_parameters(self, model): message += f"are referenced incorrectly: {str(err)}" raise StochSSModelFormatError(message, traceback.format_exc()) from err - - def __convert_particles(self, domain, type_ids): - try: - for particle in self.model['domain']['particles']: - domain.add_point( - particle['point'], particle['volume'], particle['mass'], - type_ids[particle['type']], particle['nu'], particle['fixed'], - particle['rho'], particle['c'] - ) - except KeyError as err: - message = "Spatial model domain particle properties are not properly formatted or " - message += f"are referenced incorrectly: {str(err)}" - raise StochSSModelFormatError(message, traceback.format_exc()) from err - - def __convert_reactions(self, model, type_ids): try: s_params = model.get_all_parameters() @@ -284,19 +355,81 @@ def __convert_reactions(self, model, type_ids): types = [type_ids[d_type] for d_type in reaction['types']] if len(types) == len(type_ids): types = None - s_reaction = Reaction(name=reaction['name'], - reactants=reactants, - products=products, - rate=rate, - propensity_function=propensity, - ode_propensity_function=ode_propensity, - restrict_to=types) + s_reaction = Reaction( + name=reaction['name'], reactants=reactants, products=products, + rate=rate, propensity_function=propensity, + ode_propensity_function=ode_propensity, restrict_to=types + ) model.add_reaction(s_reaction) except KeyError as err: message = "Spatial model reactions are not properly formatted or " message += f"are referenced incorrectly: {str(err)}" raise StochSSModelFormatError(message, traceback.format_exc()) from err + def __convert_shapes(self, s_domain): + try: + geometries = {} + comb_geoms = [] + lattices = {} + for s_shape in s_domain['shapes']: + # Create geometry from shape + geo_name = f"{s_shape['name']}_geom" + if s_shape['type'] == "Standard": + if s_shape['formula'] in ("", "True"): + geometries[geo_name] = GeometryAll() + elif s_shape['formula'] == "on_boundary": + geometries[geo_name] = GeometryExterior() + elif s_shape['formula'] == "not on_boundary": + geometries[geo_name] = GeometryInterior() + else: + geometries[geo_name] = self.__build_geometry(None, name=geo_name, formula=s_shape['formula']) + else: + geometry = CombinatoryGeometry("", {}) + geometry.formula = s_shape['formula'] + geometries[geo_name] = geometry + comb_geoms.append(geo_name) + # Create lattice from shape if fillable + if s_shape['fillable']: + lat_name = f"{s_shape['name']}_latt" + if s_shape['lattice'] == "Cartesian Lattice": + half_length = s_shape['length'] / 2 + half_height = s_shape['height'] / 2 + half_depth = s_shape['depth'] / 2 + lattice = CartesianLattice( + -half_length, half_length, s_shape['deltax'], + ymin=-half_height, ymax=half_height, deltay=s_shape['deltay'], + zmin=-half_depth, zmax=half_depth, deltaz=s_shape['deltaz'] + ) + elif s_shape['lattice'] == "Spherical Lattice": + lattice = SphericalLattice( + s_shape['radius'], s_shape['deltas'], deltar=s_shape['deltar'] + ) + elif s_shape['lattice'] == "Cylindrical Lattice": + lattice = CylindricalLattice( + s_shape['radius'], s_shape['length'], s_shape['deltas'], deltar=s_shape['deltar'] + ) + lattices[lat_name] = lattice + items = [' and ', ' or ', ' not ', '(', ')'] + for name in comb_geoms: + formula = geometries[name].formula + if formula.startswith("not "): + formula = formula.replace("not ", "") + for item in items: + formula = formula.replace(item, " ") + formula = formula.split(" ") + geo_namespace = {} + for key, geometry in geometries.items(): + if key != name and key[:-5] in formula: + geo_namespace[key[:-5]] = geometry + geometries[name].geo_namespace = geo_namespace + return geometries, lattices + except KeyError as err: + message = "Spatial domain shapes are not properly formatted or " + message += f"are referenced incorrectly: {str(err)}" + raise StochSSModelFormatError(message, traceback.format_exc()) from err + except ModelError as err: + message = f"One or more domain shapes are invalid: {err}" + raise DomainShapeError(message, traceback.format_exc()) from err def __convert_species(self, model, type_ids): try: @@ -315,7 +448,6 @@ def __convert_species(self, model, type_ids): message += f"are referenced incorrectly: {str(err)}" raise StochSSModelFormatError(message, traceback.format_exc()) from err - @classmethod def __convert_stoich_species(cls, model, reaction): species = model.get_all_species() @@ -342,9 +474,126 @@ def __convert_stoich_species(cls, model, reaction): message += f"are referenced incorrectly: {str(err)}" raise StochSSModelFormatError(message, traceback.format_exc()) from err + @classmethod + def __convert_transformations(cls, s_domain): + try: + transformations = {} + nested_trans = {} + for s_transformation in s_domain['transformations']: + name = s_transformation['name'] + if s_transformation['transformation'] != "": + nested_trans[name] = s_transformation['transformation'] + if s_transformation['type'] in ("Translate Transformation", "Rotate Transformation"): + vector = [ + [ + s_transformation['vector'][0]['x'], + s_transformation['vector'][0]['y'], + s_transformation['vector'][0]['z'] + ], + [ + s_transformation['vector'][1]['x'], + s_transformation['vector'][1]['y'], + s_transformation['vector'][1]['z'] + ] + ] + + if s_transformation['type'] == "Translate Transformation": + transformation = TranslationTransformation(vector) + else: + transformation = RotationTransformation(vector, s_transformation['angle']) + elif s_transformation['type'] == "Reflect Transformation": + normal = numpy.array([ + s_transformation['normal']['x'], s_transformation['normal']['y'], + s_transformation['normal']['z'] + ]) + point1 = numpy.array([ + s_transformation['point1']['x'], s_transformation['point1']['y'], + s_transformation['point1']['z'] + ]) + point2 = numpy.array([ + s_transformation['point2']['x'], s_transformation['point2']['y'], + s_transformation['point2']['z'] + ]) + point3 = numpy.array([ + s_transformation['point3']['x'], s_transformation['point3']['y'], + s_transformation['point3']['z'] + ]) + if numpy.count_nonzero(point3 - point1) <= 0 or \ + numpy.count_nonzero(point2 - point1) <= 0: + point2 = None + point3 = None + else: + normal = None + transformation = ReflectionTransformation(point1, normal=normal, point2=point2, point3=point3) + else: + center = numpy.array([ + s_transformation['center']['x'], s_transformation['center']['y'], + s_transformation['center']['z'] + ]) + transformation = ScalingTransformation(s_transformation['factor'], center=center) + transformations[name] = transformation + for trans, nested_tran in nested_trans.items(): + transformations[trans].transformation = transformations[nested_tran] + return transformations + except KeyError as err: + message = "Spatial transformations are not properly formatted or " + message += f"are referenced incorrectly: {str(err)}" + raise StochSSModelFormatError(message, traceback.format_exc()) from err + except ModelError as err: + message = f"One or more domain transformations are invalid: {err}" + raise DomainTransformationError(message, traceback.format_exc()) from err + + @classmethod + def __convert_types(cls, domain, type_ids): + domain.typeNdxMapping = {"type_UnAssigned": 0} + domain.typeNameMapping = {0: "type_UnAssigned"} + domain.listOfTypeIDs = [0] + for ndx, name in type_ids.items(): + if ndx not in domain.typeNameMapping: + name = f"type_{name}" + domain.typeNdxMapping[name] = ndx + domain.typeNameMapping[ndx] = name + domain.listOfTypeIDs.append(ndx) + types = list(set(domain.type_id)) + for name in types: + if name not in domain.typeNdxMapping: + ndx = len(domain.typeNdxMapping) + domain.typeNdxMapping[name] = ndx + domain.typeNameMapping[ndx] = name + domain.listOfTypeIDs.append(ndx) + + def __create_presentation(self, file, dst): + presentation = {'model': self.model, 'files': {}} + # Check if the domain has lattices + if len(self.model['domain']['lattices']) == 0: + return self.__write_presentation_file(presentation, dst) + # Process file based lattices + file_based_types = ('XML Mesh Lattice', 'Mesh IO Lattice', 'StochSS Lattice') + for lattice in self.model['domain']['lattices']: + if lattice['type'] in file_based_types: + entry = self.__get_presentation_file_entry(lattice['filename'], file) + lattice['filename'] = entry['pres_path'] + presentation['files'][lattice['name']] = entry + if lattice['subdomainFile'] != "": + sdf_entry = self.__get_presentation_file_entry(lattice['subdomainFile'], file) + lattice['subdomainFile'] = sdf_entry['pres_path'] + presentation['files'][f"{lattice['name']}_sdf"] = sdf_entry + return self.__write_presentation_file(presentation, dst) + + def __get_presentation_file_entry(self, path, presentation_file): + entry = {'name': self.get_file(path=path)} + entry['dwn_path'] = os.path.join( + self.user_dir, f"{self.model['name']}_doamin_files", entry['name'] + ) + entry['pres_path'] = os.path.join( + "/tmp/presentation_cache", f"{presentation_file}_doamin_files", entry['name'] + ) + with open(path, "r", encoding="utf-8") as entry_fd: + entry['body'] = entry_fd.read().strip() + return entry @classmethod - def __get_trace_data(cls, particles, name="", index=None): + def __get_trace_data(cls, particles, name="", index=None, dimensions=3): common_rgb_values = [ '#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf', '#ff0000', '#00ff00', '#0000ff', '#ffff00', '#00ffff', '#ff00ff', @@ -366,9 +615,13 @@ def __get_trace_data(cls, particles, name="", index=None): marker = {"size":5} if index is not None: marker["color"] = common_rgb_values[(index) % len(common_rgb_values)] - return plotly.graph_objs.Scatter3d(ids=ids, x=x_data, y=y_data, z=z_data, - name=name, mode="markers", marker=marker) - + if dimensions == 2: + return plotly.graph_objs.Scatter( + ids=ids, x=x_data, y=y_data, name=name, mode="markers", marker=marker + ) + return plotly.graph_objs.Scatter3d( + ids=ids, x=x_data, y=y_data, z=z_data, name=name, mode="markers", marker=marker + ) def __load_domain_from_file(self, path): try: @@ -376,7 +629,8 @@ def __load_domain_from_file(self, path): if path.endswith(".domn"): with open(path, "r", encoding="utf-8") as domain_file: s_domain = json.load(domain_file) - self.__update_domain(domain=s_domain) + self.path = path + self.__update_domain_to_current(domain=s_domain) return s_domain s_domain = Domain.read_xml_mesh(filename=path) return self.__build_stochss_domain(s_domain=s_domain) @@ -390,7 +644,6 @@ def __load_domain_from_file(self, path): message = f"The domain file is not in proper format: {str(err)}" raise DomainFormatError(message, traceback.format_exc()) from err - def __read_model_file(self): try: with open(self.get_path(full=True), "r", encoding="utf-8") as smdl_file: @@ -402,77 +655,123 @@ def __read_model_file(self): message = f"The spatial model is not JSON decobable: {str(err)}" raise FileNotJSONFormatError(message, traceback.format_exc()) from err - - def __update_domain(self, domain=None): + @classmethod + def __update_domain_to_v1(cls, domain=None): + if domain['template_version'] == 1: + return + + if "static" not in domain.keys(): + domain['static'] = True + type_changes = {} + for i, d_type in enumerate(domain['types']): + if d_type['typeID'] != i: + type_changes[d_type['typeID']] = i + d_type['typeID'] = i + if "rho" not in d_type.keys(): + d_type['rho'] = d_type['mass'] / d_type['volume'] + if "c" not in d_type.keys(): + d_type['c'] = 10 + if "geometry" not in d_type.keys(): + d_type['geometry'] = "" + if domain['particles']: + for particle in domain['particles']: + if particle['type'] in type_changes: + particle['type'] = type_changes[particle['type']] + if "rho" not in particle.keys(): + particle['rho'] = particle['mass'] / particle['volume'] + if "c" not in particle.keys(): + particle['c'] = 10 + + def __update_domain_to_current(self, domain=None): if domain is None: if "domain" not in self.model.keys() or len(self.model['domain'].keys()) < 6: self.model['domain'] = self.get_model_template()['domain'] domain = self.model['domain'] - if "template_version" not in domain or domain['template_version'] != self.TEMPLATE_VERSION: - if "static" not in domain.keys(): - domain['static'] = True - type_changes = {} - for i, d_type in enumerate(domain['types']): - if d_type['typeID'] != i: - type_changes[d_type['typeID']] = i - d_type['typeID'] = i - if "rho" not in d_type.keys(): - d_type['rho'] = d_type['mass'] / d_type['volume'] - if "c" not in d_type.keys(): - d_type['c'] = 10 - if "geometry" not in d_type.keys(): - d_type['geometry'] = "" - if domain['particles']: - for particle in domain['particles']: - if particle['type'] in type_changes: - particle['type'] = type_changes[particle['type']] - if "rho" not in particle.keys(): - particle['rho'] = particle['mass'] / particle['volume'] - if "c" not in particle.keys(): - particle['c'] = 10 - domain['template_version'] = self.TEMPLATE_VERSION + if domain['template_version'] == self.DOMAIN_TEMPLATE_VERSION: + return + + self.__update_domain_to_v1(domain) + # Create version 1 domain directory if needed. + v1_dir = os.path.join(self.user_dir, "Version1-Domains") + if not os.path.exists(v1_dir): + os.mkdir(v1_dir) + # Get the file name for the version 1 domain file + v1_domain = None + filename = os.path.join(v1_dir, self.get_file().replace(".smdl", ".domn")) + if self.path == filename: + errmsg = f"{self.get_file()} may be a dependency of another doamin (.domn) " + errmsg += "or a spatial model (.smdl) and can't be updated." + raise DomainUpdateError(errmsg) + if os.path.exists(filename): + with open(filename, "r", encoding="utf-8") as v1_domain_fd: + v1_domain = json.dumps(json.load(v1_domain_fd), sort_keys=True, indent=4) + curr_domain = json.dumps(domain, sort_keys=True, indent=4) + if v1_domain != curr_domain: + filename, _ = self.get_unique_path( + self.get_file().replace(".smdl", ".domn"), dirname=v1_dir + ) + v1_domain = None + # Create the version 1 doman file + if v1_domain is None: + with open(filename, "w", encoding="utf-8") as v1_domain_fd: + json.dump(domain, v1_domain_fd, sort_keys=True, indent=4) + + shapes = [] + for d_type in domain['types']: + if 'geometry' in d_type and d_type['geometry']: + shapes.append({ + 'deltar': 0, 'deltas': 0, 'deltax': 0, 'deltay': 0, 'deltaz': 0, 'depth': 0, 'fillable': False, + 'formula': d_type['geometry'], 'height': 0, 'lattice': 'Cartesian Lattice', + 'length': 0, 'name': f"shape{len(shapes) + 1}", 'radius': 0, 'type': 'Standard' + }) + domain['actions'] = [{ + 'type': 'StochSS Domain', 'scope': 'Multi Particle', 'priority': 1, 'enable': True, 'shape': '', + 'transformation': '', 'filename': filename.replace(f'{self.user_dir}/', ''), 'subdomainFile': '', + 'point': {'x': 0, 'y': 0, 'z': 0}, 'newPoint': {'x': 0, 'y': 0, 'z': 0}, + 'c': 10, 'fixed': False, 'mass': 1.0, 'nu': 0.0, 'rho': 1.0, 'typeID': 0, 'vol': 0.0 + }] + domain['shapes'] = shapes + domain['transformations'] = [] + domain['template_version'] = self.DOMAIN_TEMPLATE_VERSION + + def __update_model_to_current(self): + if self.model['template_version'] == self.TEMPLATE_VERSION: + return + + if not self.model['defaultMode']: + self.model['defaultMode'] = "discrete" + elif self.model['defaultMode'] == "dynamic": + self.model['defaultMode'] = "discrete-concentration" + if "timestepSize" not in self.model['modelSettings'].keys(): + self.model['modelSettings']['timestepSize'] = 1e-5 + if "boundaryConditions" not in self.model.keys(): + self.model['boundaryConditions'] = [] + for species in self.model['species']: + if "types" not in species.keys(): + species['types'] = list(range(1, len(self.model['domain']['types']))) + if "diffusionConst" not in species.keys(): + if "diffusionCoeff" not in species.keys(): + diff = 0.0 + else: + diff = species['diffusionCoeff'] + species['diffusionConst'] = diff + for reaction in self.model['reactions']: + if "odePropensity" not in reaction.keys(): + reaction['odePropensity'] = reaction['propensity'] + if "types" not in reaction.keys(): + reaction['types'] = list(range(1, len(self.model['domain']['types']))) - @classmethod - def apply_geometry(cls, particles, d_type, center): - ''' - Set the properties of the particles found within the geometry. + self.model['template_version'] = self.TEMPLATE_VERSION - Attributes - ---------- - particles : list - List of existing particles. - d_type : str - StochSS domain type. - center : list - Vector representing the center of the geometry. - ''' - def inside(point): - namespace = { - 'cx': center[0], 'cy': center[1], 'cz': center[2], - 'x': point[0], 'y': point[1], 'z': point[2] - } - return eval(d_type['geometry'], {}, namespace) - ids = [] - try: - for particle in particles: - if inside(particle['point']): - ids.append(particle['particle_id']) - return {'particles': ids} - except SyntaxError as err: - message = f"Failed to apply geometry. Reason given: {err}" - raise DomainGeometryError(message, traceback.format_exc()) from err - except NameError as err: - message = f"Failed to apply geometry. Reason given: {err}" - raise DomainGeometryError(message, traceback.format_exc()) from err + @classmethod + def __write_presentation_file(cls, body, path): + with open(path, "w", encoding="utf-8") as presentation_fd: + json.dump(body, presentation_fd, sort_keys=True, indent=4) + return True def convert_to_model(self): - ''' - Convert a spatial model to a non_spatial model - - Attributes - ---------- - ''' + ''' Convert a spatial model to a non_spatial model. ''' if self.model is None: s_model = self.load() s_model['is_spatial'] = False @@ -490,21 +789,16 @@ def convert_to_model(self): message = f"{self.get_file()} was successfully convert to {m_file}!" return {"Message":message, "File":m_file}, {"model":s_model, "path":m_path} - - def convert_to_spatialpy(self): - ''' - Convert a spatial model to a spatialpy model - - Attributes - ---------- - ''' + def convert_to_spatialpy(self, include_model_settings=False): + '''Convert a spatial model to a spatialpy model. ''' if self.model is None: _ = self.load() name = self.get_name() s_model = Model(name=name) - self.__convert_model_settings(model=s_model) + if include_model_settings: + self.__convert_model_settings(model=s_model) types = sorted(self.model['domain']['types'], key=lambda d_type: d_type['typeID']) - type_ids = {d_type['typeID']: d_type['name'] for d_type in types if d_type['typeID'] > 0} + type_ids = {d_type['typeID']: d_type['name'] for d_type in types} self.__convert_domain(model=s_model, type_ids=type_ids) if "boundaryConditions" in self.model.keys(): self.__convert_boundary_conditions(model=s_model) @@ -515,7 +809,6 @@ def convert_to_spatialpy(self): # self.log("debug", str(s_model)) return s_model - def create_boundary_condition(self, kwargs): ''' Create a new boundary condition using spatialpy.BoundaryCondition @@ -530,53 +823,6 @@ def create_boundary_condition(self, kwargs): expression = new_bc.expression() return {"expression": expression} - - @classmethod - def fill_geometry(cls, kwargs, d_type): - ''' - Create particles of the given type with the given properties within the geometry. - - Attributes - ---------- - kwargs : dict - Arguments passed to Domain.fill_with_particles. - d_type : dict - StochSS type definition. - ''' - center = [ - (kwargs['xmax'] + kwargs['xmin']) / 2, - (kwargs['ymax'] + kwargs['ymin']) / 2, - (kwargs['zmax'] + kwargs['zmin']) / 2 - ] - kwargs['geometry_ivar'] = cls.__build_geometry(d_type, center) - kwargs['type_id'] = d_type['name'] - kwargs['mass'] = d_type['mass'] - kwargs['vol'] = d_type['volume'] - kwargs['rho'] = d_type['rho'] - kwargs['nu'] = d_type['nu'] - kwargs['c'] = d_type['c'] - kwargs['fixed'] = d_type['fixed'] - try: - domain = Domain( - 0, (kwargs['xmin'],kwargs['xmax']), - (kwargs['ymin'],kwargs['ymax']), (kwargs['zmin'],kwargs['zmax']) - ) - domain.fill_with_particles(**kwargs) - data = {'type': d_type, 'transformation': None} - particles = cls.__build_stochss_domain_particles(domain, data=data) - limits = {'x_lim': domain.xlim, 'y_lim': domain.ylim, 'z_lim': domain.zlim} - return {'particles': particles, 'limits': limits} - except SyntaxError as err: - message = f"Failed to fill geometry. Reason given: {err}" - raise DomainGeometryError(message, traceback.format_exc()) from err - except NameError as err: - message = f"Failed to fill geometry. Reason given: {err}" - raise DomainGeometryError(message, traceback.format_exc()) from err - except ModelError as err: - message = f"Failed to fill geometry. Reason given: {err}" - raise DomainGeometryError(message, traceback.format_exc()) from err - - def get_domain(self, path=None, new=False): ''' Get a prospective domain @@ -594,7 +840,6 @@ def get_domain(self, path=None, new=False): return self.load()['domain'] return self.__load_domain_from_file(path=path) - def get_domain_plot(self, path=None, new=False, domains=None): ''' Get a plotly plot of the models domain or a prospective domain @@ -610,7 +855,7 @@ def get_domain_plot(self, path=None, new=False, domains=None): ''' if domains is None: if new: - path = '/stochss/stochss_templates/nonSpatialModelTemplate.json' + path = '/stochss/stochss_templates/modelTemplate.json' s_domain = StochSSSpatialModel(path).load()['domain'] elif path is None: path = self.path @@ -643,7 +888,7 @@ def get_domain_plot(self, path=None, new=False, domains=None): # Case #3: 1 or more particles and one type if len(s_domain['types']) == 1: fig['data'][0]['name'] = "Un-Assigned" - ids = list(filter(lambda particle: particle['particle_id'], s_domain['particles'])) + ids = list(map(lambda particle: particle['particle_id'], s_domain['particles'])) fig['data'][0]['ids'] = ids # Case #4: 1 or more particles and multiple types else: @@ -655,7 +900,7 @@ def get_domain_plot(self, path=None, new=False, domains=None): traces = list(filter(t_test, fig['data'])) if len(traces) == 0: fig['data'].insert(index, self.__get_trace_data( - particles=[], name=d_type['name'], index=index + particles=[], name=d_type['name'], index=index, dimensions=domain.dimensions )) else: particles = list(filter( @@ -670,168 +915,52 @@ def get_domain_plot(self, path=None, new=False, domains=None): fig['layout']['height'] = None fig['layout']['autosize'] = True fig['config'] = {"responsive":True} - return json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder), trace_temp - + return json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder) def get_notebook_data(self): - ''' - Get the needed data for converting to notebook - - Attributes - ---------- - ''' + ''' Get the needed data for converting to notebook. ''' file = f"{self.get_name()}.ipynb" path = os.path.join(self.get_dir_name(), file) - s_model = self.convert_to_spatialpy() + s_model = self.convert_to_spatialpy(include_model_settings=True) self.model['path'] = self.get_file() return {"path":path, "new":True, "models":{"s_model":self.model, "model":s_model}} - - def get_particles_from_3d_domain(self, data): - ''' - Create a new 3D domain and return the particles - - Attributes - ---------- - data : dict - Data used to create the 3D domain - ''' - if data['type']['type_id'] == "Un-Assigned": - data['type']['type_id'] = "UnAssigned" - if data['transformation'] is None: - xlim = data['xLim'] - ylim = data['yLim'] - zlim = data['zLim'] - else: - xlim = [coord + data['transformation'][0] for coord in data['xLim']] - ylim = [coord + data['transformation'][1] for coord in data['yLim']] - zlim = [coord + data['transformation'][2] for coord in data['zLim']] - dimensions = bool(xlim[1] - xlim[0]) - dimensions += bool(ylim[1] - ylim[0]) - dimensions += bool(zlim[1] - zlim[0]) - if dimensions > 2: - s_domain = Domain.create_3D_domain(xlim=xlim, ylim=ylim, zlim=zlim, numx=data['nx'], - numy=data['ny'], numz=data['nz'], **data['type']) - else: - s_domain = Domain.create_2D_domain(xlim=xlim, ylim=ylim, - numx=data['nx'], numy=data['ny'], **data['type']) - domain = self.__build_stochss_domain(s_domain=s_domain, data=data) - limits = {"x_lim":domain['x_lim'], "y_lim":domain['y_lim'], "z_lim":domain['z_lim']} - resp = {"particles":domain['particles'], "limits":limits} - return resp - - - @classmethod - def get_particles_from_remote(cls, domain, data, types): - ''' - Get a list of stochss particles from a domain - - Attributes - ---------- - domain : str - Domain containing particle data - data : dict - Property and location data to be applied to each particle - types : list - List of type discriptions (lines from an uploaded file) - ''' - with tempfile.NamedTemporaryFile() as file: - with open(file.name, "w", encoding="utf-8") as domain_file: - domain_file.write(domain) - s_domain = Domain.read_xml_mesh(filename=file.name) - domain = cls.__build_stochss_domain(s_domain=s_domain, data=data) - if types is not None: - type_data = cls.get_types_from_file(lines=types) - for t_data in type_data['types']: - if t_data['particle_id'] < len(domain['particles']): - domain['particles'][t_data['particle_id']]['type'] = t_data['typeID'] - limits = {"x_lim":domain['x_lim'], "y_lim":domain['y_lim'], "z_lim":domain['z_lim']} - resp = {"particles":domain['particles'], "limits":limits} - if types is not None: - resp['types'] = type_data['names'] - return resp - - - @classmethod - def get_types_from_file(cls, path=None, lines=None): - ''' - Get the type descriptions from the .txt file - - Attributes - ---------- - path : str - Path to the types description file - lines : list - Lines from an uploaded file - ''' - if lines is None: - path = os.path.join(cls.user_dir, path) - with open(path, "r", encoding="utf-8") as types_file: - lines = types_file.readlines() - types = [] - names = [] - for line in lines: - if line.strip().replace(".0", "").replace(",", "", 1).isdigit(): - part_id, type_id = line.strip().split(",") - if '.' in type_id: - type_id = float(type_id) - type_id = int(type_id) - if type_id not in names: - names.append(type_id) - types.append({"particle_id":int(part_id), "typeID":type_id}) - else: - message = "The type descriptions are not in the proper format " - message += "(i.e. particle_id (int),type_id (int))" - raise DomainFormatError(message, traceback.format_exc()) - return {"types":types, "names":sorted(names)} - - def load(self): ''' - Reads the spatial model file, updates it to the current format, and stores it in self.model - - Attributes - ---------- + Reads the spatial model file, updates it to the current format, and stores it in self.model. ''' if self.model is None: self.__read_model_file() - self.model['name'] = self.get_name() - if "template_version" not in self.model or \ - self.model['template_version'] != self.TEMPLATE_VERSION: - if not self.model['defaultMode']: - self.model['defaultMode'] = "discrete" - elif self.model['defaultMode'] == "dynamic": - self.model['defaultMode'] = "discrete-concentration" - if "timestepSize" not in self.model['modelSettings'].keys(): - self.model['modelSettings']['timestepSize'] = 1e-5 - self.__update_domain() - if "boundaryConditions" not in self.model.keys(): - self.model['boundaryConditions'] = [] - for species in self.model['species']: - if "types" not in species.keys(): - species['types'] = list(range(1, len(self.model['domain']['types']))) - if "diffusionConst" not in species.keys(): - if "diffusionCoeff" not in species.keys(): - diff = 0.0 - else: - diff = species['diffusionCoeff'] - species['diffusionConst'] = diff - for reaction in self.model['reactions']: - if "odePropensity" not in reaction.keys(): - reaction['odePropensity'] = reaction['propensity'] - if "types" not in reaction.keys(): - reaction['types'] = list(range(1, len(self.model['domain']['types']))) - self.model['template_version'] = self.TEMPLATE_VERSION + + if "domain" in self.model: + self.model['name'] = self.get_name() + if "template_version" not in self.model: + self.model['template_version'] = 0 + self.__update_model_to_current() + + if "template_version" not in self.model['domain']: + self.model['domain']['template_version'] = 0 + self.__update_domain_to_current() + else: + if "template_version" not in self.model: + self.model['template_version'] = 0 + self.__update_domain_to_current(domain=self.model) + return self.model + def load_action_preview(self, s_domain): + ''' Get a domain preview of all enabled actions. ''' + types = sorted(s_domain['types'], key=lambda d_type: d_type['typeID']) + type_ids = {d_type['typeID']: d_type['name'] for d_type in types} + domain = self.__convert_domain(type_ids, s_domain=s_domain) + xlim, ylim, zlim = domain.get_bounding_box() + limits = [list(xlim), list(ylim), list(zlim)] + s_domain['particles'] = self.__build_stochss_domain_particles(domain) + plot = self.get_domain_plot(domains=(domain, s_domain)) + return plot, limits def publish_presentation(self): - ''' - Publish a model or spatial model presentation - - Attributes - ---------- - ''' + ''' Publish a model or spatial model presentation. ''' present_dir = os.path.join(self.user_dir, ".presentations") if not os.path.exists(present_dir): os.mkdir(present_dir) @@ -847,7 +976,7 @@ def publish_presentation(self): data = None else: self.add_presentation_name(file, self.model['name']) - data = {"path": dst, "new":True, "model":self.model} + data = self.__create_presentation(file, dst) query_str = f"?owner={hostname}&file={file}" present_link = f"/stochss/present-model{query_str}" downloadlink = os.path.join("/stochss/download_presentation", @@ -859,7 +988,6 @@ def publish_presentation(self): message = f"You do not have permission to publish this file: {str(err)}" raise StochSSPermissionsError(message, traceback.format_exc()) from err - def save_domain(self, domain): ''' Writes a StochSS Domain to a .domn file diff --git a/stochss/handlers/util/stochss_workflow.py b/stochss/handlers/util/stochss_workflow.py index 488ad299e7..87390c5861 100644 --- a/stochss/handlers/util/stochss_workflow.py +++ b/stochss/handlers/util/stochss_workflow.py @@ -1,6 +1,6 @@ ''' StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -273,7 +273,7 @@ def get_model_path(self): raise StochSSFileNotFoundError(message, traceback.format_exc()) from err - def initialize_job(self, settings, mdl_path, time_stamp, wkfl_type): + def initialize_job(self, settings, mdl_path, time_stamp, wkfl_type, compute): ''' Initialize a new job for this workflow. @@ -287,6 +287,8 @@ def initialize_job(self, settings, mdl_path, time_stamp, wkfl_type): Datetime stamp for the new job wkfl_type : str Type of workflow + compute : str + The compute environment to use. ''' self.log("info", f"Saving {self.get_file()}") self.save(new_settings=settings, mdl_path=mdl_path) @@ -296,7 +298,10 @@ def initialize_job(self, settings, mdl_path, time_stamp, wkfl_type): if self.check_workflow_format(path=self.path): self.log("info", f"Creating job{time_stamp} job") path = os.path.join(self.path, f"job{time_stamp}") - data = {"mdl_path": mdl_path, "settings": settings, "type":wkfl_type} + data = { + "mdl_path": mdl_path, "settings": settings, + "type": wkfl_type, "compute_env": compute + } job = StochSSJob(path=path, new=True, data=data) self.log("info", f"Successfully created {job.get_file()} job") else: @@ -332,7 +337,8 @@ def load(self): self.workflow['type'] = jobdata['titleType'] oldfmtrdy = jobdata['status'] == "ready" self.__update_settings() - if not os.path.exists(self.workflow['model']) and (oldfmtrdy or self.workflow['newFormat']): + if (self.workflow['model'] is None or not os.path.exists(self.workflow['model'])) \ + and (oldfmtrdy or self.workflow['newFormat']): if ".proj" in self.path: if "WorkflowGroup1.wkgp" in self.path: proj = StochSSFolder(path=os.path.dirname(self.get_dir_name(full=True))) diff --git a/stochss/handlers/workflows.py b/stochss/handlers/workflows.py index af5ce1200a..1c043dd888 100644 --- a/stochss/handlers/workflows.py +++ b/stochss/handlers/workflows.py @@ -1,6 +1,6 @@ ''' StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -28,7 +28,8 @@ # Use finish() for json, write() for text from .util import StochSSJob, StochSSModel, StochSSSpatialModel, StochSSNotebook, StochSSWorkflow, \ - StochSSParamSweepNotebook, StochSSSciopeNotebook, StochSSAPIError, report_error + StochSSParamSweepNotebook, StochSSSciopeNotebook, StochSSAPIError, report_error, \ + report_critical_error log = logging.getLogger('stochss') @@ -64,6 +65,8 @@ async def get(self): self.write(resp) except StochSSAPIError as err: report_error(self, log, err) + except Exception as err: + report_critical_error(self, log, err) self.finish() @@ -91,6 +94,8 @@ async def get(self): self.write(resp) except StochSSAPIError as err: report_error(self, log, err) + except Exception as err: + report_critical_error(self, log, err) self.finish() @@ -117,6 +122,8 @@ async def post(self): self.write(resp) except StochSSAPIError as err: report_error(self, log, err) + except Exception as err: + report_critical_error(self, log, err) self.finish() @@ -140,13 +147,17 @@ async def get(self): log.debug(f"Handler query string: {data}") try: wkfl = StochSSWorkflow(path=path) - resp = wkfl.initialize_job(settings=data['settings'], mdl_path=data['mdl_path'], - wkfl_type=data['type'], time_stamp=data['time_stamp']) + resp = wkfl.initialize_job( + settings=data['settings'], mdl_path=data['mdl_path'], wkfl_type=data['type'], + time_stamp=data['time_stamp'], compute=data['compute'] + ) wkfl.print_logs(log) log.debug(f"Response message: {resp}") self.write(resp) except StochSSAPIError as err: report_error(self, log, err) + except Exception as err: + report_critical_error(self, log, err) self.finish() @@ -184,6 +195,8 @@ async def get(self): log.debug('The workflow has started') except StochSSAPIError as err: report_error(self, log, err) + except Exception as err: + report_critical_error(self, log, err) self.finish() @@ -211,6 +224,8 @@ async def get(self): self.write(status) except StochSSAPIError as err: report_error(self, log, err) + except Exception as err: + report_critical_error(self, log, err) self.finish() @@ -253,6 +268,8 @@ async def get(self): self.write(fig) except StochSSAPIError as err: report_error(self, log, err) + except Exception as err: + report_critical_error(self, log, err) self.finish() @@ -274,6 +291,9 @@ async def get(self): path = self.get_query_argument(name="path") log.debug(f"Path to the model/workflow: {path}") wkfl_type = self.get_query_argument(name="type") + log.debug(f"Type of workflow: {wkfl_type}") + compute = self.get_query_argument(name="compute", default=None) + log.debug(f"Compute Environment: {compute}") try: if path.endswith(".mdl"): file_obj = StochSSModel(path=path) @@ -285,9 +305,12 @@ async def get(self): kwargs = file_obj.get_notebook_data() if "type" in kwargs: wkfl_type = kwargs['type'] + results = kwargs['results'] + compute = kwargs['compute_env'] kwargs = kwargs['kwargs'] log.info(f"Converting {file_obj.get_file()} to notebook") else: + results = None log.info(f"Creating notebook workflow for {file_obj.get_file()}") log.debug(f"Type of workflow to be run: {wkfl_type}") if wkfl_type in ("1d_parameter_sweep", "2d_parameter_sweep"): @@ -302,13 +325,15 @@ async def get(self): notebook = StochSSNotebook(**kwargs) notebooks = {"gillespy":notebook.create_es_notebook, "spatial":notebook.create_ses_notebook} - resp = notebooks[wkfl_type]() + resp = notebooks[wkfl_type](results=results, compute=compute) notebook.print_logs(log) log.debug(f"Response: {resp}") log.info(f"Successfully created the notebook for {file_obj.get_file()}") self.write(resp) except StochSSAPIError as err: report_error(self, log, err) + except Exception as err: + report_critical_error(self, log, err) self.finish() @@ -340,6 +365,8 @@ async def post(self): self.write(resp) except StochSSAPIError as err: report_error(self, log, err) + except Exception as err: + report_critical_error(self, log, err) self.finish() @@ -377,6 +404,8 @@ async def post(self): self.write(resp) except StochSSAPIError as err: report_error(self, log, err) + except Exception as err: + report_critical_error(self, log, err) self.finish() @@ -403,6 +432,8 @@ async def get(self): self.write(resp) except StochSSAPIError as err: report_error(self, log, err) + except Exception as err: + report_critical_error(self, log, err) self.finish() @@ -439,6 +470,8 @@ async def get(self): self.write(resp) except StochSSAPIError as err: report_error(self, log, err) + except Exception as err: + report_critical_error(self, log, err) self.finish() @@ -475,4 +508,6 @@ async def get(self): self.write(csv_data) except StochSSAPIError as err: report_error(self, log, err) + except Exception as err: + report_critical_error(self, log, err) self.finish() diff --git a/stochss/tests/example_models.py b/stochss/tests/example_models.py index d129665623..267a3c2db7 100644 --- a/stochss/tests/example_models.py +++ b/stochss/tests/example_models.py @@ -1,6 +1,6 @@ ''' StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/stochss/tests/run_tests.py b/stochss/tests/run_tests.py index f1ec19d2ac..b794f7c240 100755 --- a/stochss/tests/run_tests.py +++ b/stochss/tests/run_tests.py @@ -2,7 +2,7 @@ ''' StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/stochss/tests/test_gillespy2.py b/stochss/tests/test_gillespy2.py index 1a47a1ee79..9741595518 100644 --- a/stochss/tests/test_gillespy2.py +++ b/stochss/tests/test_gillespy2.py @@ -1,6 +1,6 @@ ''' StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/stochss/tests/test_model_template.py b/stochss/tests/test_model_template.py index bd86dbd901..e9a5e77ce0 100644 --- a/stochss/tests/test_model_template.py +++ b/stochss/tests/test_model_template.py @@ -1,6 +1,6 @@ ''' StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -32,9 +32,9 @@ def setUp(self): ''' Get the model template prior to each test. ''' - template_path = "stochss_templates/nonSpatialModelTemplate.json" + template_path = "stochss_templates/modelTemplate.json" - with open(template_path, "r") as template_file: + with open(template_path, "r", encoding="utf8") as template_file: self.template = json.load(template_file) @@ -45,7 +45,7 @@ def test_model_elements(self): template_keys = sorted(list(self.template.keys())) model_path = "client/models/model.js" - with open(model_path, "r") as model_file: + with open(model_path, "r", encoding="utf8") as model_file: data = model_file.read() props = data.split("props: {").pop().split('}')[0].split(',') collections = data.split("collections: {").pop().split('}')[0].split(',') @@ -66,7 +66,7 @@ def test_timespan_settings_elements(self): template_keys = sorted(list(self.template['modelSettings'].keys())) mdl_settings_path = "client/models/timespan-settings.js" - with open(mdl_settings_path, "r") as mdl_settings_file: + with open(mdl_settings_path, "r", encoding="utf8") as mdl_settings_file: data = mdl_settings_file.read().split("props: {").pop().split('}')[0].split(',') mdl_settings_keys = sorted(list(map(lambda item: item.strip().split(':')[0], data))) @@ -81,7 +81,7 @@ def test_model_domain_elements(self): template_keys = sorted(list(self.template['domain'].keys())) domain_path = "client/models/domain.js" - with open(domain_path, "r") as domain_file: + with open(domain_path, "r", encoding="utf8") as domain_file: data = domain_file.read() props = data.split("props: {").pop().split('}')[0].split(',') collections = data.split("collections: {").pop().split('}')[0].split(',') diff --git a/stochss/tests/test_settings_template.py b/stochss/tests/test_settings_template.py index fce01d01ce..26028a15de 100644 --- a/stochss/tests/test_settings_template.py +++ b/stochss/tests/test_settings_template.py @@ -1,6 +1,6 @@ ''' StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/stochss/tests/test_stochss_base.py b/stochss/tests/test_stochss_base.py index 1688fe8ecb..e121259364 100644 --- a/stochss/tests/test_stochss_base.py +++ b/stochss/tests/test_stochss_base.py @@ -1,6 +1,6 @@ ''' StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/stochss/tests/test_stochss_file.py b/stochss/tests/test_stochss_file.py index cfc5492cd7..6dd7f0f42f 100644 --- a/stochss/tests/test_stochss_file.py +++ b/stochss/tests/test_stochss_file.py @@ -1,6 +1,6 @@ ''' StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. +Copyright (C) 2019-2023 StochSS developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/stochss_templates/instance_types.txt b/stochss_templates/instance_types.txt new file mode 100644 index 0000000000..0330e84c14 --- /dev/null +++ b/stochss_templates/instance_types.txt @@ -0,0 +1 @@ +a1.medium|a1.large|a1.xlarge|a1.2xlarge|a1.4xlarge|a1.metal|c1.medium|c1.xlarge|c3.large|c3.xlarge|c3.2xlarge|c3.4xlarge|c3.8xlarge|c4.large|c4.xlarge|c4.2xlarge|c4.4xlarge|c4.8xlarge|c5.large|c5.xlarge|c5.2xlarge|c5.4xlarge|c5.9xlarge|c5.12xlarge|c5.18xlarge|c5.24xlarge|c5.metal|c5a.large|c5a.xlarge|c5a.2xlarge|c5a.4xlarge|c5a.8xlarge|c5a.12xlarge|c5a.16xlarge|c5a.24xlarge|c5ad.large|c5ad.xlarge|c5ad.2xlarge|c5ad.4xlarge|c5ad.8xlarge|c5ad.12xlarge|c5ad.16xlarge|c5ad.24xlarge|c5d.large|c5d.xlarge|c5d.2xlarge|c5d.4xlarge|c5d.9xlarge|c5d.12xlarge|c5d.18xlarge|c5d.24xlarge|c5d.metal|c5n.large|c5n.xlarge|c5n.2xlarge|c5n.4xlarge|c5n.9xlarge|c5n.18xlarge|c5n.metal|c6g.medium|c6g.large|c6g.xlarge|c6g.2xlarge|c6g.4xlarge|c6g.8xlarge|c6g.12xlarge|c6g.16xlarge|c6g.metal|c6gd.medium|c6gd.large|c6gd.xlarge|c6gd.2xlarge|c6gd.4xlarge|c6gd.8xlarge|c6gd.12xlarge|c6gd.16xlarge|c6gd.metal|c6gn.medium|c6gn.large|c6gn.xlarge|c6gn.2xlarge|c6gn.4xlarge|c6gn.8xlarge|c6gn.12xlarge|c6gn.16xlarge|c6i.large|c6i.xlarge|c6i.2xlarge|c6i.4xlarge|c6i.8xlarge|c6i.12xlarge|c6i.16xlarge|c6i.24xlarge|c6i.32xlarge|c6i.metal|cc1.4xlarge|cc2.8xlarge|cg1.4xlarge|cr1.8xlarge|d2.xlarge|d2.2xlarge|d2.4xlarge|d2.8xlarge|d3.xlarge|d3.2xlarge|d3.4xlarge|d3.8xlarge|d3en.xlarge|d3en.2xlarge|d3en.4xlarge|d3en.6xlarge|d3en.8xlarge|d3en.12xlarge|dl1.24xlarge|f1.2xlarge|f1.4xlarge|f1.16xlarge|g2.2xlarge|g2.8xlarge|g3.4xlarge|g3.8xlarge|g3.16xlarge|g3s.xlarge|g4ad.xlarge|g4ad.2xlarge|g4ad.4xlarge|g4ad.8xlarge|g4ad.16xlarge|g4dn.xlarge|g4dn.2xlarge|g4dn.4xlarge|g4dn.8xlarge|g4dn.12xlarge|g4dn.16xlarge|g4dn.metal|g5.xlarge|g5.2xlarge|g5.4xlarge|g5.8xlarge|g5.12xlarge|g5.16xlarge|g5.24xlarge|g5.48xlarge|g5g.xlarge|g5g.2xlarge|g5g.4xlarge|g5g.8xlarge|g5g.16xlarge|g5g.metal|hi1.4xlarge|hpc6a.48xlarge|hs1.8xlarge|h1.2xlarge|h1.4xlarge|h1.8xlarge|h1.16xlarge|i2.xlarge|i2.2xlarge|i2.4xlarge|i2.8xlarge|i3.large|i3.xlarge|i3.2xlarge|i3.4xlarge|i3.8xlarge|i3.16xlarge|i3.metal|i3en.large|i3en.xlarge|i3en.2xlarge|i3en.3xlarge|i3en.6xlarge|i3en.12xlarge|i3en.24xlarge|i3en.metal|im4gn.large|im4gn.xlarge|im4gn.2xlarge|im4gn.4xlarge|im4gn.8xlarge|im4gn.16xlarge|inf1.xlarge|inf1.2xlarge|inf1.6xlarge|inf1.24xlarge|is4gen.medium|is4gen.large|is4gen.xlarge|is4gen.2xlarge|is4gen.4xlarge|is4gen.8xlarge|m1.small|m1.medium|m1.large|m1.xlarge|m2.xlarge|m2.2xlarge|m2.4xlarge|m3.medium|m3.large|m3.xlarge|m3.2xlarge|m4.large|m4.xlarge|m4.2xlarge|m4.4xlarge|m4.10xlarge|m4.16xlarge|m5.large|m5.xlarge|m5.2xlarge|m5.4xlarge|m5.8xlarge|m5.12xlarge|m5.16xlarge|m5.24xlarge|m5.metal|m5a.large|m5a.xlarge|m5a.2xlarge|m5a.4xlarge|m5a.8xlarge|m5a.12xlarge|m5a.16xlarge|m5a.24xlarge|m5ad.large|m5ad.xlarge|m5ad.2xlarge|m5ad.4xlarge|m5ad.8xlarge|m5ad.12xlarge|m5ad.16xlarge|m5ad.24xlarge|m5d.large|m5d.xlarge|m5d.2xlarge|m5d.4xlarge|m5d.8xlarge|m5d.12xlarge|m5d.16xlarge|m5d.24xlarge|m5d.metal|m5dn.large|m5dn.xlarge|m5dn.2xlarge|m5dn.4xlarge|m5dn.8xlarge|m5dn.12xlarge|m5dn.16xlarge|m5dn.24xlarge|m5dn.metal|m5n.large|m5n.xlarge|m5n.2xlarge|m5n.4xlarge|m5n.8xlarge|m5n.12xlarge|m5n.16xlarge|m5n.24xlarge|m5n.metal|m5zn.large|m5zn.xlarge|m5zn.2xlarge|m5zn.3xlarge|m5zn.6xlarge|m5zn.12xlarge|m5zn.metal|m6a.large|m6a.xlarge|m6a.2xlarge|m6a.4xlarge|m6a.8xlarge|m6a.12xlarge|m6a.16xlarge|m6a.24xlarge|m6a.32xlarge|m6a.48xlarge|m6g.metal|m6g.medium|m6g.large|m6g.xlarge|m6g.2xlarge|m6g.4xlarge|m6g.8xlarge|m6g.12xlarge|m6g.16xlarge|m6gd.metal|m6gd.medium|m6gd.large|m6gd.xlarge|m6gd.2xlarge|m6gd.4xlarge|m6gd.8xlarge|m6gd.12xlarge|m6gd.16xlarge|m6i.large|m6i.xlarge|m6i.2xlarge|m6i.4xlarge|m6i.8xlarge|m6i.12xlarge|m6i.16xlarge|m6i.24xlarge|m6i.32xlarge|m6i.metal|mac1.metal|p2.xlarge|p2.8xlarge|p2.16xlarge|p3.2xlarge|p3.8xlarge|p3.16xlarge|p3dn.24xlarge|p4d.24xlarge|r3.large|r3.xlarge|r3.2xlarge|r3.4xlarge|r3.8xlarge|r4.large|r4.xlarge|r4.2xlarge|r4.4xlarge|r4.8xlarge|r4.16xlarge|r5.large|r5.xlarge|r5.2xlarge|r5.4xlarge|r5.8xlarge|r5.12xlarge|r5.16xlarge|r5.24xlarge|r5.metal|r5a.large|r5a.xlarge|r5a.2xlarge|r5a.4xlarge|r5a.8xlarge|r5a.12xlarge|r5a.16xlarge|r5a.24xlarge|r5ad.large|r5ad.xlarge|r5ad.2xlarge|r5ad.4xlarge|r5ad.8xlarge|r5ad.12xlarge|r5ad.16xlarge|r5ad.24xlarge|r5b.large|r5b.xlarge|r5b.2xlarge|r5b.4xlarge|r5b.8xlarge|r5b.12xlarge|r5b.16xlarge|r5b.24xlarge|r5b.metal|r5d.large|r5d.xlarge|r5d.2xlarge|r5d.4xlarge|r5d.8xlarge|r5d.12xlarge|r5d.16xlarge|r5d.24xlarge|r5d.metal|r5dn.large|r5dn.xlarge|r5dn.2xlarge|r5dn.4xlarge|r5dn.8xlarge|r5dn.12xlarge|r5dn.16xlarge|r5dn.24xlarge|r5dn.metal|r5n.large|r5n.xlarge|r5n.2xlarge|r5n.4xlarge|r5n.8xlarge|r5n.12xlarge|r5n.16xlarge|r5n.24xlarge|r5n.metal|r6g.medium|r6g.large|r6g.xlarge|r6g.2xlarge|r6g.4xlarge|r6g.8xlarge|r6g.12xlarge|r6g.16xlarge|r6g.metal|r6gd.medium|r6gd.large|r6gd.xlarge|r6gd.2xlarge|r6gd.4xlarge|r6gd.8xlarge|r6gd.12xlarge|r6gd.16xlarge|r6gd.metal|r6i.large|r6i.xlarge|r6i.2xlarge|r6i.4xlarge|r6i.8xlarge|r6i.12xlarge|r6i.16xlarge|r6i.24xlarge|r6i.32xlarge|r6i.metal|t1.micro|t2.nano|t2.micro|t2.small|t2.medium|t2.large|t2.xlarge|t2.2xlarge|t3.nano|t3.micro|t3.small|t3.medium|t3.large|t3.xlarge|t3.2xlarge|t3a.nano|t3a.micro|t3a.small|t3a.medium|t3a.large|t3a.xlarge|t3a.2xlarge|t4g.nano|t4g.micro|t4g.small|t4g.medium|t4g.large|t4g.xlarge|t4g.2xlarge|u-6tb1.56xlarge|u-6tb1.112xlarge|u-9tb1.112xlarge|u-12tb1.112xlarge|u-6tb1.metal|u-9tb1.metal|u-12tb1.metal|u-18tb1.metal|u-24tb1.metal|vt1.3xlarge|vt1.6xlarge|vt1.24xlarge|x1.16xlarge|x1.32xlarge|x1e.xlarge|x1e.2xlarge|x1e.4xlarge|x1e.8xlarge|x1e.16xlarge|x1e.32xlarge|x2iezn.2xlarge|x2iezn.4xlarge|x2iezn.6xlarge|x2iezn.8xlarge|x2iezn.12xlarge|x2iezn.metal|x2gd.medium|x2gd.large|x2gd.xlarge|x2gd.2xlarge|x2gd.4xlarge|x2gd.8xlarge|x2gd.12xlarge|x2gd.16xlarge|x2gd.metal|z1d.large|z1d.xlarge|z1d.2xlarge|z1d.3xlarge|z1d.6xlarge|z1d.12xlarge|z1d.metal|x2idn.16xlarge|x2idn.24xlarge|x2idn.32xlarge|x2iedn.xlarge|x2iedn.2xlarge|x2iedn.4xlarge|x2iedn.8xlarge|x2iedn.16xlarge|x2iedn.24xlarge|x2iedn.32xlarge|c6a.large|c6a.xlarge|c6a.2xlarge|c6a.4xlarge|c6a.8xlarge|c6a.12xlarge|c6a.16xlarge|c6a.24xlarge|c6a.32xlarge|c6a.48xlarge|c6a.metal|m6a.metal|i4i.large|i4i.xlarge|i4i.2xlarge|i4i.4xlarge|i4i.8xlarge|i4i.16xlarge|i4i.32xlarge|i4i.metal|x2idn.metal|x2iedn.metal|c7g.medium|c7g.large|c7g.xlarge|c7g.2xlarge|c7g.4xlarge|c7g.8xlarge|c7g.12xlarge|c7g.16xlarge|mac2.metal|c6id.large|c6id.xlarge|c6id.2xlarge|c6id.4xlarge|c6id.8xlarge|c6id.12xlarge|c6id.16xlarge|c6id.24xlarge|c6id.32xlarge|c6id.metal|m6id.large|m6id.xlarge|m6id.2xlarge|m6id.4xlarge|m6id.8xlarge|m6id.12xlarge|m6id.16xlarge|m6id.24xlarge|m6id.32xlarge|m6id.metal|r6id.large|r6id.xlarge|r6id.2xlarge|r6id.4xlarge|r6id.8xlarge|r6id.12xlarge|r6id.16xlarge|r6id.24xlarge|r6id.32xlarge|r6id.metal|r6a.large|r6a.xlarge|r6a.2xlarge|r6a.4xlarge|r6a.8xlarge|r6a.12xlarge|r6a.16xlarge|r6a.24xlarge|r6a.32xlarge|r6a.48xlarge|r6a.metal|p4de.24xlarge|u-3tb1.56xlarge \ No newline at end of file diff --git a/stochss_templates/nonSpatialModelTemplate.json b/stochss_templates/modelTemplate.json similarity index 83% rename from stochss_templates/nonSpatialModelTemplate.json rename to stochss_templates/modelTemplate.json index d422dfd9d6..579d0acfe0 100644 --- a/stochss_templates/nonSpatialModelTemplate.json +++ b/stochss_templates/modelTemplate.json @@ -10,19 +10,20 @@ "timestepSize": 1e-5 }, "domain": { - "size": null, - "rho_0": 1.0, - "c_0": 10, - "p_0": 100.0, - "gravity": [0, 0, 0], - "x_lim": [0, 0], - "y_lim": [0, 0], - "z_lim": [0, 0], - "static": true, - "boundary_condition": { + "actions": [], + "boundary_condition": { "reflect_x": true, "reflect_y": true, "reflect_z": true}, + "c_0": 10, + "gravity": [0, 0, 0], + "p_0": 100.0, + "rho_0": 1.0, + "shapes": [], + "size": null, + "static": true, + "template_version": 2, + "transformations": [], "types": [ { "c":10, @@ -35,8 +36,9 @@ "volume":1.0 } ], - "particles": [], - "template_version": 1 + "x_lim": [0, 0], + "y_lim": [0, 0], + "z_lim": [0, 0] }, "species": [], "initialConditions": [], diff --git a/stochss_templates/userSettingTemplate.json b/stochss_templates/userSettingTemplate.json new file mode 100644 index 0000000000..0b97ea18d1 --- /dev/null +++ b/stochss_templates/userSettingTemplate.json @@ -0,0 +1,6 @@ +{ + "awsAccessKeyID": "", + "awsRegion": "", + "headNode": "", + "userLogs": true +} diff --git a/webpack.config.js b/webpack.config.js index 22f1f93036..a2ca6255b1 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -5,6 +5,7 @@ module.exports = { mode: 'development', entry: { home: './client/pages/users-home.js', + userSettings: './client/pages/user-settings.js', quickstart: './client/pages/quickstart.js', exampleLibrary: './client/pages/example-library.js', browser: './client/pages/browser.js', @@ -29,6 +30,13 @@ module.exports = { name: 'home', inject: false }), + new HtmlWebpackPlugin({ + title: 'StochSS | User Settings', + filename: 'stochss-user-settings.html', + template: 'page_template.pug', + name: 'userSettings', + inject: false + }), new HtmlWebpackPlugin({ title: 'StochSS | Quickstart', filename: 'stochss-quickstart.html',