From af8277a80036bf72c8c8cef370482dcd5dd0c46b Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Thu, 26 Jan 2023 12:49:55 -0500 Subject: [PATCH 001/109] Added inference settings to the settings template. Added settings template version constant to stochss base. --- stochss/handlers/util/stochss_base.py | 1 + .../workflowSettingsTemplate.json | 25 ++++++++++++------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/stochss/handlers/util/stochss_base.py b/stochss/handlers/util/stochss_base.py index 6b09a3adee..889791e983 100644 --- a/stochss/handlers/util/stochss_base.py +++ b/stochss/handlers/util/stochss_base.py @@ -36,6 +36,7 @@ class StochSSBase(): ''' user_dir = os.path.expanduser("~") # returns the path to the users home directory TEMPLATE_VERSION = 1 + SETTINGS_TEMPLATE_VERSION = 1 def __init__(self, path): ''' diff --git a/stochss_templates/workflowSettingsTemplate.json b/stochss_templates/workflowSettingsTemplate.json index b1a402f656..53d7b6941d 100644 --- a/stochss_templates/workflowSettingsTemplate.json +++ b/stochss_templates/workflowSettingsTemplate.json @@ -1,4 +1,19 @@ { + "inferenceSettings":{ + "obsData": "", + "parameters": [], + "priorMethod": "Uniform Prior", + "summryStats": "" + }, + "parameterSweepSettings":{ + "parameters": [], + "speciesOfInterest": {} + }, + "resultsSettings":{ + "mapper": "final", + "reducer": "avg", + "outputs": [] + }, "simulationSettings": { "isAutomatic": true, "relativeTol": 1e-3, @@ -8,15 +23,7 @@ "seed": -1, "tauTol": 0.03 }, - "parameterSweepSettings":{ - "parameters": [], - "speciesOfInterest": {} - }, - "resultsSettings":{ - "mapper":"final", - "reducer":"avg", - "outputs": [] - }, + "template_version": 1, "timespanSettings": { "endSim": 20, "timeStep": 0.05, From 71f0da0ad69e3f145bf981f4b51a399f0c22fa00 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Thu, 26 Jan 2023 12:51:09 -0500 Subject: [PATCH 002/109] Added functions to update the workflow and job settings to the current version. --- stochss/handlers/util/stochss_job.py | 75 ++++++++++++++--------- stochss/handlers/util/stochss_workflow.py | 59 ++++++++++-------- 2 files changed, 82 insertions(+), 52 deletions(-) diff --git a/stochss/handlers/util/stochss_job.py b/stochss/handlers/util/stochss_job.py index 1e4f70d40e..6b156457bc 100644 --- a/stochss/handlers/util/stochss_job.py +++ b/stochss/handlers/util/stochss_job.py @@ -256,35 +256,52 @@ def __is_result_valid(cls, f_keys, key): return True - def __update_settings(self): - settings = self.job['settings']['parameterSweepSettings'] - if "parameters" not in settings.keys(): + def __update_settings_to_current(self): + settings = self.job['settings'] + if settings['template_version'] == self.SETTINGS_TEMPLATE_VERSION: + return + + if "parameters" not in settings['parameterSweepSettings']: parameters = [] - if "paramID" in settings['parameterOne']: - p1_range = list(numpy.linspace(settings['p1Min'], settings['p1Max'], - settings['p1Steps'])) - param1 = {"paramID": settings['parameterOne']['paramID'], - "min": settings['p1Min'], - "max": settings['p1Max'], - "name": settings['parameterOne']['name'], - "range" : p1_range, - "steps": settings['p1Steps'], - "hasChangedRanged": False} - parameters.append(param1) - if "paramID" in settings['parameterTwo']: - p2_range = list(numpy.linspace(settings['p2Min'], settings['p2Max'], - settings['p2Steps'])) - param2 = {"paramID": settings['parameterTwo']['paramID'], - "min": settings['p2Min'], - "max": settings['p2Max'], - "name": settings['parameterTwo']['name'], - "range" : p2_range, - "steps": settings['p2Steps'], - "hasChangedRanged": False} - parameters.append(param2) + if "paramID" in settings['parameterSweepSettings']['parameterOne']: + p1_range = list(numpy.linspace( + settings['parameterSweepSettings']['p1Min'], + settings['parameterSweepSettings']['p1Max'], + settings['parameterSweepSettings']['p1Steps'] + )) + parameters.append({ + "paramID": settings['parameterSweepSettings']['parameterOne']['paramID'], + "min": settings['parameterSweepSettings']['p1Min'], + "max": settings['parameterSweepSettings']['p1Max'], + "name": settings['parameterSweepSettings']['parameterOne']['name'], + "range" : p1_range, + "steps": settings['parameterSweepSettings']['p1Steps'], + "hasChangedRanged": False + }) + if "paramID" in settings['parameterSweepSettings']['parameterTwo']: + p2_range = list(numpy.linspace( + settings['parameterSweepSettings']['p2Min'], + settings['parameterSweepSettings']['p2Max'], + settings['parameterSweepSettings']['p2Steps'] + )) + parameters.append({ + "paramID": settings['parameterSweepSettings']['parameterTwo']['paramID'], + "min": settings['parameterSweepSettings']['p2Min'], + "max": settings['parameterSweepSettings']['p2Max'], + "name": settings['parameterSweepSettings']['parameterTwo']['name'], + "range" : p2_range, + "steps": settings['parameterSweepSettings']['p2Steps'], + "hasChangedRanged": False + }) soi = settings['speciesOfInterest'] - self.job['settings']['parameterSweepSettings'] = {"speciesOfInterest": soi, - "parameters": parameters} + settings['parameterSweepSettings'] = { + "speciesOfInterest": soi, "parameters": parameters + } + + settings['inferenceSettings'] = { + "obsData": "", "parameters": [], "priorMethod": "Uniform Prior", "summaryStats": "" + } + settings['template_version'] = self.SETTINGS_TEMPLATE_VERSION @classmethod @@ -706,7 +723,9 @@ def load(self, new=False): "startTime":info['start_time'], "status":status, "timeStamp":self.time_stamp, "titleType":self.TITLES[info['type']], "type":self.type, "directory":self.path, "logs":logs} - self.__update_settings() + if "template_version" not in self.job['settings']: + self.job['settings']['template_version'] = 0 + self.__update_settings_to_current() if error is not None: self.job['error'] = error return self.job diff --git a/stochss/handlers/util/stochss_workflow.py b/stochss/handlers/util/stochss_workflow.py index 488ad299e7..04d59a610f 100644 --- a/stochss/handlers/util/stochss_workflow.py +++ b/stochss/handlers/util/stochss_workflow.py @@ -146,31 +146,40 @@ def __update_results_settings(cls, settings): p_range = list(numpy.linspace(param['min'], param['max'], param['steps'])) param['range'] = p_range + def __update_settings_to_current(self): + settings = self.workflow['settings'] + if settings['template_version'] == self.SETTINGS_TEMPLATE_VERSION: + return - def __update_settings(self): - settings = self.workflow['settings']['parameterSweepSettings'] parameters = [] - if "parameters" not in settings.keys(): - if "paramID" in settings['parameterOne']: - param1 = {"paramID": settings['parameterOne']['paramID'], - "min": settings['p1Min'], - "max": settings['p1Max'], - "name": settings['parameterOne']['name'], - "steps": settings['p1Steps'], - "hasChangedRanged": False} - parameters.append(param1) - if "paramID" in settings['parameterTwo']: - param2 = {"paramID": settings['parameterTwo']['paramID'], - "min": settings['p2Min'], - "max": settings['p2Max'], - "name": settings['parameterTwo']['name'], - "steps": settings['p2Steps'], - "hasChangedRanged": False} - parameters.append(param2) - soi = settings['speciesOfInterest'] - self.workflow['settings']['parameterSweepSettings'] = {"speciesOfInterest": soi, - "parameters": parameters} - + if "parameters" not in settings['parameterSweepSettings']: + if "paramID" in settings['parameterSweepSettings']['parameterOne']: + parameters.append({ + "paramID": settings['parameterOne']['paramID'], + "min": settings['parameterSweepSettings']['p1Min'], + "max": settings['parameterSweepSettings']['p1Max'], + "name": settings['parameterSweepSettings']['parameterOne']['name'], + "steps": settings['parameterSweepSettings']['p1Steps'], + "hasChangedRanged": False + }) + if "paramID" in settings['parameterSweepSettings']['parameterTwo']: + parameters.append({ + "paramID": settings['parameterSweepSettings']['parameterTwo']['paramID'], + "min": settings['parameterSweepSettings']['p2Min'], + "max": settings['parameterSweepSettings']['p2Max'], + "name": settings['parameterSweepSettings']['parameterTwo']['name'], + "steps": settings['parameterSweepSettings']['p2Steps'], + "hasChangedRanged": False + }) + soi = settings['parameterSweepSettings']['speciesOfInterest'] + settings['parameterSweepSettings'] = { + "speciesOfInterest": soi, "parameters": parameters + } + + settings['inferenceSettings'] = { + "obsData": "", "parameters": [], "priorMethod": "Uniform Prior", "summaryStats": "" + } + settings['template_version'] = self.SETTINGS_TEMPLATE_VERSION @classmethod def __write_new_files(cls, settings, annotation): @@ -331,7 +340,9 @@ def load(self): self.workflow['settings'] = jobdata['settings'] self.workflow['type'] = jobdata['titleType'] oldfmtrdy = jobdata['status'] == "ready" - self.__update_settings() + if "template_version" not in self.workflow['settings']: + self.workflow['settings']['template_version'] = 0 + self.__update_settings_to_current() if not os.path.exists(self.workflow['model']) and (oldfmtrdy or self.workflow['newFormat']): if ".proj" in self.path: if "WorkflowGroup1.wkgp" in self.path: From 1b7ba9b9d3c52d0ff579c445c92fb59ee2b0c19d Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Thu, 26 Jan 2023 14:11:55 -0500 Subject: [PATCH 003/109] Added inference settings and parameter model files. --- client/models/inference-parameter.js | 57 ++++++++++++++++++++++++++ client/models/inference-parameters.js | 42 +++++++++++++++++++ client/models/inference-settings.js | 58 +++++++++++++++++++++++++++ client/models/settings.js | 15 ++++--- 4 files changed, 166 insertions(+), 6 deletions(-) create mode 100644 client/models/inference-parameter.js create mode 100644 client/models/inference-parameters.js create mode 100644 client/models/inference-settings.js diff --git a/client/models/inference-parameter.js b/client/models/inference-parameter.js new file mode 100644 index 0000000000..01a0d48fb2 --- /dev/null +++ b/client/models/inference-parameter.js @@ -0,0 +1,57 @@ +/* +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: { + hasChangedRange: 'boolean', + m: 'any', + min: 'any', + max: 'any', + name: 'string', + p: 'any', + paramID: 'number', + pm: 'any', + s: 'any', + u: 'any' + }, + derived: { + elementID: { + deps: ["collection"], + fn: function () { + if(this.collection) { + return "IT" + (this.collection.indexOf(this) + 1); + } + return "IT-" + } + } + }, + initialize: function(attrs, options) { + State.prototype.initialize.apply(this, arguments); + }, + updateVariable: function (parameter) { + let value = parameter.expression; + if(this.min <= 0 || !this.hasChangedRange) { + this.min = value * 0.5; + } + if(this.max <= 0 || !this.hasChangedRange) { + this.max = value * 1.5; + } + } +}); diff --git a/client/models/inference-parameters.js b/client/models/inference-parameters.js new file mode 100644 index 0000000000..218082c316 --- /dev/null +++ b/client/models/inference-parameters.js @@ -0,0 +1,42 @@ +/* +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 InferenceParameter = require('./inference-parameter'); +//collections +let Collection = require('ampersand-collection'); + +module.exports = Collection.extend({ + model: InferenceParameter, + addInferenceParameter: function (paramID, name) { + let variable = this.add({ + hasChangedRange: false, + m: null, + min: 0, + max: 0, + name: name, + p: null, + paramID: paramID, + pm: null, + s: null, + u: null + }); + }, + removeInferenceParameter: function (variable) { + this.remove(variable); + } +}); diff --git a/client/models/inference-settings.js b/client/models/inference-settings.js new file mode 100644 index 0000000000..04d07c8554 --- /dev/null +++ b/client/models/inference-settings.js @@ -0,0 +1,58 @@ +/* +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 . +*/ +//collections +let InferenceParameters = require('./inference-parameters'); +//models +let State = require('ampersand-state'); + +module.exports = State.extend({ + Props: { + obsData: 'string', + priorMethod: 'string', + summaryStats: 'string' + }, + collections: { + parameters: InferenceParameters + }, + derived: { + elementID: { + deps: ["parent"], + fn: function () { + if(this.parent) { + return this.parent.elementID + "IS-"; + } + return "IS-" + } + } + }, + initialize: function(attrs, options) { + State.prototype.initialize.apply(this, arguments); + }, + updateVariables: function (parameters) { + this.parameters.forEach((variable) => { + let parameter = parameters.filter((parameter) => { + return parameter.compID === variable.paramID; + })[0]; + if(parameter === undefined) { + this.removeVariable(variable); + }else{ + variable.updateVariable(variable); + } + }); + } +}); diff --git a/client/models/settings.js b/client/models/settings.js index 85f9cfd0ee..75f9eebd81 100644 --- a/client/models/settings.js +++ b/client/models/settings.js @@ -15,15 +15,18 @@ 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 -var State = require('ampersand-state'); +let State = require('ampersand-state'); +let ResultsSettings = require('./results-settings'); let TimespanSettings = require('./timespan-settings'); -var SimulationSettings = require('./simulation-settings'); -var ParameterSweepSettings = require('./parameter-sweep-settings'); -var ResultsSettings = require('./results-settings'); +let SimulationSettings = require('./simulation-settings'); +let ParameterSweepSettings = require('./parameter-sweep-settings'); +let InferenceSettings = require('./inference-settings'); module.exports = State.extend({ + props: { + template_veersion: 'number' + }, children: { timespanSettings: TimespanSettings, simulationSettings: SimulationSettings, @@ -44,4 +47,4 @@ module.exports = State.extend({ } } } -}); \ No newline at end of file +}); From aab3d2c41cf3b90adee4d9c6920f45e436eacc87 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Thu, 26 Jan 2023 15:21:56 -0500 Subject: [PATCH 004/109] Added functionality for creating model inference workflows. --- client/app.js | 3 ++- client/templates/includes/modelListing.pug | 1 + client/templates/includes/workflowGroupListing.pug | 1 + client/views/jstree-view.js | 6 ++++++ client/views/model-listing.js | 6 +++++- client/views/workflow-group-listing.js | 6 +++++- stochss/handlers/util/stochss_workflow.py | 2 +- 7 files changed, 21 insertions(+), 4 deletions(-) diff --git a/client/app.js b/client/app.js index 65275f7076..cb819d7647 100644 --- a/client/app.js +++ b/client/app.js @@ -170,7 +170,8 @@ let newWorkflow = (parent, mdlPath, isSpatial, type) => { let typeCodes = { "Ensemble Simulation": "_ES", "Spatial Ensemble Simulation": "_SES", - "Parameter Sweep": "_PS" + "Parameter Sweep": "_PS", + "Model Inference": "_MI" } let self = parent; let ext = isSpatial ? /.smdl/g : /.mdl/g diff --git a/client/templates/includes/modelListing.pug b/client/templates/includes/modelListing.pug index c369e20929..453704fc74 100644 --- a/client/templates/includes/modelListing.pug +++ b/client/templates/includes/modelListing.pug @@ -22,6 +22,7 @@ div if(!this.model.is_spatial) li.dropdown-item(id=this.model.elementID + "-parameter-sweep") Parameter Sweep + li.dropdown-item(id=this.model.elementID + "-model-inference") Model Inference li.dropdown-item diff --git a/client/templates/includes/workflowGroupListing.pug b/client/templates/includes/workflowGroupListing.pug index eb5f735f4f..6512af8954 100644 --- a/client/templates/includes/workflowGroupListing.pug +++ b/client/templates/includes/workflowGroupListing.pug @@ -26,6 +26,7 @@ div if(!this.model.model.is_spatial) li.dropdown-item(id=this.model.elementID + "-parameter-sweep") Parameter Sweep + li.dropdown-item(id=this.model.elementID + "-model-inference") Model Inference li.dropdown-item diff --git a/client/views/jstree-view.js b/client/views/jstree-view.js index ad1fab90e4..cc68b08663 100644 --- a/client/views/jstree-view.js +++ b/client/views/jstree-view.js @@ -495,6 +495,12 @@ module.exports = View.extend({ this.newWorkflow(node, "Parameter Sweep"); } }), + modelInference: this.buildContextBase({ + label: "Model Inference", + action: (data) => { + this.newWorkflow(node, "Model Inference"); + } + }), jupyterNotebook: this.getNotebookNewWorkflowContext(node) } }); diff --git a/client/views/model-listing.js b/client/views/model-listing.js index 100749d3c3..8e0aa97e65 100644 --- a/client/views/model-listing.js +++ b/client/views/model-listing.js @@ -33,6 +33,7 @@ module.exports = View.extend({ events['change [data-hook=' + this.model.elementID + '-annotation]'] = 'updateAnnotation'; events['click #' + this.model.elementID + "-ensemble-simulation"] = 'handleEnsembleSimulationClick'; events['click #' + this.model.elementID + "-parameter-sweep"] = 'handleParameterSweepClick'; + events['click #' + this.model.elementID + "-model-inference"] = 'handleModelInferenceClick'; events['click [data-hook=' + this.model.elementID + '-notes-btn]'] = 'handleEditNotesClick'; events['click [data-hook=' + this.model.elementID + '-remove]'] = 'handleTrashModelClick'; events['click [data-hook=' + this.model.elementID + '-annotation-btn'] = 'changeCollapseButtonText'; @@ -73,8 +74,11 @@ module.exports = View.extend({ let type = this.model.is_spatial ? "Spatial Ensemble Simulation" : "Ensemble Simulation"; this.newWorkflow(type); }, + handleModelInferenceClick: function (e) { + this.newWorkflow("Model Inference"); + }, handleParameterSweepClick: function (e) { - this.newWorkflow("Parameter Sweep") + this.newWorkflow("Parameter Sweep"); }, handleTrashModelClick: function (e) { if(document.querySelector('#moveToTrashConfirmModal')) { diff --git a/client/views/workflow-group-listing.js b/client/views/workflow-group-listing.js index 4dcb3d6c26..ba9e5b14e4 100644 --- a/client/views/workflow-group-listing.js +++ b/client/views/workflow-group-listing.js @@ -36,6 +36,7 @@ module.exports = View.extend({ events['change [data-hook=' + this.model.elementID + '-annotation'] = 'updateAnnotation'; events['click #' + this.model.elementID + "-ensemble-simulation"] = 'handleEnsembleSimulationClick'; events['click #' + this.model.elementID + "-parameter-sweep"] = 'handleParameterSweepClick'; + events['click #' + this.model.elementID + "-model-inference"] = 'handleModelInferenceClick'; events['click [data-hook=' + this.model.elementID + '-remove'] = 'handleTrashModelClick'; events['click [data-hook=' + this.model.elementID + '-tab-btn'] = 'changeCollapseButtonText'; return events; @@ -58,8 +59,11 @@ module.exports = View.extend({ let type = this.model.model.is_spatial ? "Spatial Ensemble Simulation" : "Ensemble Simulation"; this.newWorkflow(type); }, + handleModelInferenceClick: function (e) { + this.newWorkflow("Model Inference"); + }, handleParameterSweepClick: function (e) { - this.newWorkflow("Parameter Sweep") + this.newWorkflow("Parameter Sweep"); }, handleTrashModelClick: function () { if(document.querySelector('#moveToTrashConfirmModal')) { diff --git a/stochss/handlers/util/stochss_workflow.py b/stochss/handlers/util/stochss_workflow.py index 04d59a610f..8c61ccdddf 100644 --- a/stochss/handlers/util/stochss_workflow.py +++ b/stochss/handlers/util/stochss_workflow.py @@ -62,7 +62,7 @@ def __init__(self, path, new=False, mdl_path=None, wkfl_type=None): settings = self.get_settings_template() if wkfl_type == "Parameter Sweep": settings['simulationSettings']['realizations'] = 20 - if wkfl_type == "Spatial Ensemble Simulation": + if wkfl_type in ("Spatial Ensemble Simulation", "Model Inference"): settings['simulationSettings']['realizations'] = 1 if os.path.exists(mdl_path): with open(mdl_path, "r", encoding="utf-8") as mdl_file: From aed872d70ab00a3f65a0076f4270795eeaf5227e Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Thu, 26 Jan 2023 16:24:04 -0500 Subject: [PATCH 005/109] Fixed issue with settings model. --- client/models/settings.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/models/settings.js b/client/models/settings.js index 75f9eebd81..98eb3f36c8 100644 --- a/client/models/settings.js +++ b/client/models/settings.js @@ -19,9 +19,9 @@ along with this program. If not, see . let State = require('ampersand-state'); let ResultsSettings = require('./results-settings'); let TimespanSettings = require('./timespan-settings'); +let InferenceSettings = require('./inference-settings'); let SimulationSettings = require('./simulation-settings'); let ParameterSweepSettings = require('./parameter-sweep-settings'); -let InferenceSettings = require('./inference-settings'); module.exports = State.extend({ props: { @@ -31,6 +31,7 @@ module.exports = State.extend({ timespanSettings: TimespanSettings, simulationSettings: SimulationSettings, parameterSweepSettings: ParameterSweepSettings, + inferenceSettings: InferenceSettings, resultsSettings: ResultsSettings }, initialize: function(attrs, options) { From 2c79c466f887f1322f2332d45f634d0b173ed66e Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Thu, 26 Jan 2023 16:41:54 -0500 Subject: [PATCH 006/109] Added basic view for inference settings in model inference workflows. --- client/pages/workflow-manager.js | 17 ++- client/settings-view/settings-view.js | 18 ++- client/settings-view/settingsView.pug | 2 + .../templates/inferenceSettingsView.pug | 93 +++++++++++++++ .../views/inference-settings-view.js | 110 ++++++++++++++++++ 5 files changed, 234 insertions(+), 6 deletions(-) create mode 100644 client/settings-view/templates/inferenceSettingsView.pug create mode 100644 client/settings-view/views/inference-settings-view.js diff --git a/client/pages/workflow-manager.js b/client/pages/workflow-manager.js index 07acc47fd8..defc41ca75 100644 --- a/client/pages/workflow-manager.js +++ b/client/pages/workflow-manager.js @@ -320,25 +320,32 @@ let WorkflowManager = PageView.extend({ if(this.model.settings.parameterSweepSettings.parameters.length < 1) { $(this.queryByHook("start-job")).prop("disabled", true); } - this.model.settings.parameterSweepSettings.parameters.on("add remove", _.bind(function (e) { + this.model.settings.parameterSweepSettings.parameters.on("add remove", _.bind((e) => { let numParams = this.model.settings.parameterSweepSettings.parameters.length; $(this.queryByHook("start-job")).prop("disabled", numParams < 1); }, this)) + }else if(this.model.type === "Model Inference") { + if(this.model.settings.inferenceSettings.parameters.length < 1) { + $(this.queryByHook("start-job")).prop("disabled", true); + } + this.model.settings.inferenceSettings.parameters.on("add remove", _.bind((e) => { + let numParams = this.model.settings.inferenceSettings.parameters.length; + $(this.queryByHook("start-job")).prop("disabled", numParams < 1); + }, this)) } let options = { model: this.model.settings, newFormat: this.model.newFormat, type: this.model.type } - if(this.model.type === "Parameter Sweep") { - let self = this; + if(["Parameter Sweep", "Model Inference"].includes(this.model.type)) { options['stochssModel'] = new Model({ directory: this.model.model }); app.getXHR(options.stochssModel.url(), { - success: function (err, response, body) { + success: (err, response, body) => { options.stochssModel.set(body); - self.renderSettingsView(options); + this.renderSettingsView(options); } }); }else { diff --git a/client/settings-view/settings-view.js b/client/settings-view/settings-view.js index 00b37ea448..e98b299a6c 100644 --- a/client/settings-view/settings-view.js +++ b/client/settings-view/settings-view.js @@ -20,10 +20,11 @@ along with this program. If not, see . let app = require('../app'); //views let View = require('ampersand-view'); +let SpatialSettingsView = require('./views/spatial-settings-view'); let TimespanSettingsView = require('./views/timespan-settings-view'); let ParameterSettingsView = require('./views/parameter-settings-view'); +let InferenceSettingsView = require('./views/inference-settings-view'); let WellMixedSettingsView = require('./views/well-mixed-settings-view'); -let SpatialSettingsView = require('./views/spatial-settings-view'); //templates let template = require('./settingsView.pug'); @@ -43,9 +44,24 @@ module.exports = View.extend({ } if(this.type === "Parameter Sweep") { this.renderParameterSettingsView(); + }else if(this.type === "Model Inference") { + this.renderInferenceSettingsView(); } this.renderSimulationSettingsView(); }, + renderInferenceSettingsView: function () { + if(this.inferenceSettingsView) { + this.inferenceSettingsView.remove(); + } + console.log(this.stochssModel) + this.inferenceSettingsView = new InferenceSettingsView({ + model: this.model.inferenceSettings, + stochssModel: this.stochssModel, + readOnly: this.readOnly + }); + let hook = "inference-settings-container"; + app.registerRenderSubview(this, this.inferenceSettingsView, hook); + }, renderParameterSettingsView: function () { if(this.parameterSettingsView) { this.parameterSettingsView.remove(); diff --git a/client/settings-view/settingsView.pug b/client/settings-view/settingsView.pug index 9a83214c72..b3588a74f7 100644 --- a/client/settings-view/settingsView.pug +++ b/client/settings-view/settingsView.pug @@ -4,4 +4,6 @@ div#workflow-settings div(data-hook="param-sweep-settings-container") + div(data-hook="inference-settings-container") + div(data-hook="sim-settings-container") diff --git a/client/settings-view/templates/inferenceSettingsView.pug b/client/settings-view/templates/inferenceSettingsView.pug new file mode 100644 index 0000000000..781628e35e --- /dev/null +++ b/client/settings-view/templates/inferenceSettingsView.pug @@ -0,0 +1,93 @@ +div#inference-settings.card + + div.card-header.pb-0 + + h3.inline.mr-3 Inference Settings + + div.inline.mr-3 + + ul.nav.nav-tabs.card-header-tabs(id="inference-settings-tabs") + + li.nav-item + + a.nav-link.tab.active(data-hook=this.model.elementID + "-inference-settings-edit-tab" data-toggle="tab" href="#" + this.model.elementID + "-edit-inference-settings") Edit + + li.nav-item + + a.nav-link.tab(data-hook=this.model.elementID + "-inference-settings-view-tab" data-toggle="tab" href="#" + this.model.elementID + "-view-inference-settings") View + + button.btn.btn-outline-collapse(data-toggle="collapse" data-target="#" + this.model.elementID + "collapse-inference-settings" data-hook="collapse") - + + div.collapse(class="show" id=this.model.elementID + "collapse-inference-settings") + + div.card-body.tab-content + + div.tab-pane.active(id=this.model.elementID + "-edit-inference-settings" data-hook=this.model.elementID + "-edit-inference-settings") + + div + + h5 Configure Parameter Space + + hr + + div.mx-1.row.head.align-items-baseline + + div.col-sm-3 + + h6.inline Sweep Target + + div.tooltip-icon(data-html="true" data-toggle="tooltip" title=this.tooltips.variable) + + div.col-sm-3 + + h6.inline Current Value + + div.tooltip-icon(data-html="true" data-toggle="tooltip" title=this.tooltips.value) + + div.col-sm-2 + + h6.inline Minimum Value + + div.tooltip-icon(data-html="true" data-toggle="tooltip" title=this.tooltips.min) + + div.col-sm-2 + + h6.inline Maximum Value + + div.tooltip-icon(data-html="true" data-toggle="tooltip" title=this.tooltips.max) + + div.col-sm-2 + + h6 Remove + + div.my-3(data-hook="edit-mi-parameter-collection") + + button.btn.btn-outline-secondary.box-shadow(data-hook="add-mi-parameter") Add Parameter + + div.tab-pane(id=this.model.elementID + "-view-inference-settings" data-hook=this.model.elementID + "-view-inference-settings") + + div + + h5.mt-3 Parameter Space Configuration + + hr + + div.mx-1.row.head.align-items-baseline + + div.col-sm-3 + + h6.inline Sweep Target + + div.col-sm-3 + + h6.inline Current Value + + div.col-sm-3 + + h6.inline Minimum Value + + div.col-sm-3 + + h6.inline Maximum Value + + div.my-3(data-hook="view-mi-parameter-collection") diff --git a/client/settings-view/views/inference-settings-view.js b/client/settings-view/views/inference-settings-view.js new file mode 100644 index 0000000000..1be6e65c76 --- /dev/null +++ b/client/settings-view/views/inference-settings-view.js @@ -0,0 +1,110 @@ +/* +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 SelectView = require('ampersand-select-view'); +// let ParameterView = require('./sweep-parameter-view'); +//templates +let template = require('../templates/inferenceSettingsView.pug'); + +module.exports = View.extend({ + template: template, + events: { + 'click [data-hook=collapse]' : 'changeCollapseButtonText', + 'click [data-hook=add-mi-parameter]' : 'handleAddParameterClick' + }, + initialize: function (attrs, options) { + View.prototype.initialize.apply(this, arguments); + this.readOnly = attrs.readOnly ? attrs.readOnly : false; + this.tooltips = Tooltips.parameterSweepSettings; + this.stochssModel = attrs.stochssModel; + if(!this.readOnly) { + this.model.updateVariables(this.stochssModel.parameters); + } + }, + render: function () { + View.prototype.render.apply(this, arguments); + if(this.readOnly) { + $(this.queryByHook(this.model.elementID + '-inference-settings-edit-tab')).addClass("disabled"); + $(".nav .disabled>a").on("click", function(e) { + e.preventDefault(); + return false; + }); + $(this.queryByHook(this.model.elementID + '-inference-settings-view-tab')).tab('show'); + $(this.queryByHook(this.model.elementID + '-edit-inference-settings')).removeClass('active'); + $(this.queryByHook(this.model.elementID + '-view-inference-settings')).addClass('active'); + }else{ + this.model.parameters.on("add remove", () => { + let disable = this.model.parameters.length >= this.stochssModel.parameters.length; + $(this.queryByHook("add-mi-parameter")).prop("disabled", disable); + }, this) + // this.renderEditSweepParameters(); + } + // this.renderViewSweepParameters(); + }, + changeCollapseButtonText: function (e) { + app.changeCollapseButtonText(this, e); + }, + getParameter: function () { + let parameters = this.model.parameters.map((param) => { return param.paramID; }); + let target = this.stochssModel.parameters.filter((param) => { + return !parameters.includes(param.compID); + })[0]; + return target; + }, + handleAddParameterClick: function (e) { + let target = this.getParameter(); + this.model.parameters.addInferenceParameter(target.compID, target.name); + // this.renderViewSweepParameters(); + }, + // renderEditSweepParameters: function () { + // if(this.editSweepParameters) { + // this.editSweepParameters.remove(); + // } + // let options = {"viewOptions": { + // parent: this, + // stochssParams: this.stochssModel.parameters + // }} + // this.editSweepParameters = this.renderCollection( + // this.model.parameters, + // ParameterView, + // this.queryByHook("ps-parameter-collection"), + // options + // ); + // }, + // renderViewSweepParameters: function () { + // if(this.viewSweepParameters) { + // this.viewSweepParameters.remove(); + // } + // let options = {"viewOptions": { + // parent: this, + // stochssParams: this.stochssModel.parameters, + // viewMode: true + // }} + // this.viewSweepParameters = this.renderCollection( + // this.model.parameters, + // ParameterView, + // this.queryByHook("view-sweep-parameters"), + // options + // ); + // } +}); From 9ac08fa8fb13bfd9e25abba2c4fb35a3ec6faaa4 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Fri, 27 Jan 2023 10:18:43 -0500 Subject: [PATCH 007/109] Added and additional view and template file layer. --- client/models/inference-parameters.js | 12 +++ client/models/inference-settings.js | 12 --- .../templates/editUniformParameters.pug | 42 ++++++++ .../templates/inferenceSettingsView.pug | 66 +------------ .../templates/viewUniformParameters.pug | 25 +++++ .../views/inference-parameters-view.js | 96 +++++++++++++++++++ .../views/inference-settings-view.js | 83 +++++----------- 7 files changed, 203 insertions(+), 133 deletions(-) create mode 100644 client/settings-view/templates/editUniformParameters.pug create mode 100644 client/settings-view/templates/viewUniformParameters.pug create mode 100644 client/settings-view/views/inference-parameters-view.js diff --git a/client/models/inference-parameters.js b/client/models/inference-parameters.js index 218082c316..95117cdd1c 100644 --- a/client/models/inference-parameters.js +++ b/client/models/inference-parameters.js @@ -38,5 +38,17 @@ module.exports = Collection.extend({ }, removeInferenceParameter: function (variable) { this.remove(variable); + }, + updateVariables: function (parameters) { + this.forEach((variable) => { + let parameter = parameters.filter((parameter) => { + return parameter.compID === variable.paramID; + })[0]; + if(parameter === undefined) { + this.removeVariable(variable); + }else{ + variable.updateVariable(variable); + } + }); } }); diff --git a/client/models/inference-settings.js b/client/models/inference-settings.js index 04d07c8554..3f5335362a 100644 --- a/client/models/inference-settings.js +++ b/client/models/inference-settings.js @@ -42,17 +42,5 @@ module.exports = State.extend({ }, initialize: function(attrs, options) { State.prototype.initialize.apply(this, arguments); - }, - updateVariables: function (parameters) { - this.parameters.forEach((variable) => { - let parameter = parameters.filter((parameter) => { - return parameter.compID === variable.paramID; - })[0]; - if(parameter === undefined) { - this.removeVariable(variable); - }else{ - variable.updateVariable(variable); - } - }); } }); diff --git a/client/settings-view/templates/editUniformParameters.pug b/client/settings-view/templates/editUniformParameters.pug new file mode 100644 index 0000000000..304105834c --- /dev/null +++ b/client/settings-view/templates/editUniformParameters.pug @@ -0,0 +1,42 @@ +div + + div + + h5 Configure Parameter Space + + hr + + div.mx-1.row.head.align-items-baseline + + div.col-sm-3 + + h6.inline Sweep Target + + div.tooltip-icon(data-html="true" data-toggle="tooltip" title=this.tooltips.variable) + + div.col-sm-3 + + h6.inline Current Value + + div.tooltip-icon(data-html="true" data-toggle="tooltip" title=this.tooltips.value) + + div.col-sm-2 + + h6.inline Minimum Value + + div.tooltip-icon(data-html="true" data-toggle="tooltip" title=this.tooltips.min) + + div.col-sm-2 + + h6.inline Maximum Value + + div.tooltip-icon(data-html="true" data-toggle="tooltip" title=this.tooltips.max) + + div.col-sm-2 + + h6 Remove + + div.my-3(data-hook="edit-mi-parameter-collection") + + button.btn.btn-outline-secondary.box-shadow(data-hook="add-mi-parameter") Add Parameter + diff --git a/client/settings-view/templates/inferenceSettingsView.pug b/client/settings-view/templates/inferenceSettingsView.pug index 781628e35e..3be982fd48 100644 --- a/client/settings-view/templates/inferenceSettingsView.pug +++ b/client/settings-view/templates/inferenceSettingsView.pug @@ -24,70 +24,8 @@ div#inference-settings.card div.tab-pane.active(id=this.model.elementID + "-edit-inference-settings" data-hook=this.model.elementID + "-edit-inference-settings") - div - - h5 Configure Parameter Space - - hr - - div.mx-1.row.head.align-items-baseline - - div.col-sm-3 - - h6.inline Sweep Target - - div.tooltip-icon(data-html="true" data-toggle="tooltip" title=this.tooltips.variable) - - div.col-sm-3 - - h6.inline Current Value - - div.tooltip-icon(data-html="true" data-toggle="tooltip" title=this.tooltips.value) - - div.col-sm-2 - - h6.inline Minimum Value - - div.tooltip-icon(data-html="true" data-toggle="tooltip" title=this.tooltips.min) - - div.col-sm-2 - - h6.inline Maximum Value - - div.tooltip-icon(data-html="true" data-toggle="tooltip" title=this.tooltips.max) - - div.col-sm-2 - - h6 Remove - - div.my-3(data-hook="edit-mi-parameter-collection") - - button.btn.btn-outline-secondary.box-shadow(data-hook="add-mi-parameter") Add Parameter + div(data-hook="edit-parameter-space-container") div.tab-pane(id=this.model.elementID + "-view-inference-settings" data-hook=this.model.elementID + "-view-inference-settings") - div - - h5.mt-3 Parameter Space Configuration - - hr - - div.mx-1.row.head.align-items-baseline - - div.col-sm-3 - - h6.inline Sweep Target - - div.col-sm-3 - - h6.inline Current Value - - div.col-sm-3 - - h6.inline Minimum Value - - div.col-sm-3 - - h6.inline Maximum Value - - div.my-3(data-hook="view-mi-parameter-collection") + div(data-hook="view-parameter-space-container") diff --git a/client/settings-view/templates/viewUniformParameters.pug b/client/settings-view/templates/viewUniformParameters.pug new file mode 100644 index 0000000000..9c0cd42e42 --- /dev/null +++ b/client/settings-view/templates/viewUniformParameters.pug @@ -0,0 +1,25 @@ +div + + h5.mt-3 Parameter Space Configuration + + hr + + div.mx-1.row.head.align-items-baseline + + div.col-sm-3 + + h6.inline Sweep Target + + div.col-sm-3 + + h6.inline Current Value + + div.col-sm-3 + + h6.inline Minimum Value + + div.col-sm-3 + + h6.inline Maximum Value + + div.my-3(data-hook="view-mi-parameter-collection") \ No newline at end of file diff --git a/client/settings-view/views/inference-parameters-view.js b/client/settings-view/views/inference-parameters-view.js new file mode 100644 index 0000000000..404ea0da1d --- /dev/null +++ b/client/settings-view/views/inference-parameters-view.js @@ -0,0 +1,96 @@ +/* +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 Tooltips = require('../../tooltips'); +//views +let View = require('ampersand-view'); +// let ParameterView = require('./sweep-parameter-view'); +//templates +let editTemplate = require('../templates/editUniformParameters.pug'); +let viewTemplate = require('../templates/viewUniformParameters.pug'); + +module.exports = View.extend({ + events: { + 'click [data-hook=add-mi-parameter]' : 'handleAddParameterClick' + }, + initialize: function (attrs, options) { + View.prototype.initialize.apply(this, arguments); + this.readOnly = attrs.readOnly ? attrs.readOnly : false; + this.tooltips = Tooltips.parameterSweepSettings; + this.stochssModel = attrs.stochssModel; + if(!this.readOnly) { + this.collection.updateVariables(this.stochssModel.parameters); + } + }, + render: function () { + this.template = this.readOnly ? viewTemplate : editTemplate; + View.prototype.render.apply(this, arguments); + if(!this.readOnly) { + this.collection.on("add remove", () => { + let disable = this.collection.length >= this.stochssModel.parameters.length; + $(this.queryByHook("add-mi-parameter")).prop("disabled", disable); + }, this) + // this.renderEditSweepParameters(); + } + // this.renderViewSweepParameters(); + }, + getParameter: function () { + let parameters = this.collection.map((param) => { return param.paramID; }); + let target = this.stochssModel.parameters.filter((param) => { + return !parameters.includes(param.compID); + })[0]; + return target; + }, + handleAddParameterClick: function (e) { + let target = this.getParameter(); + this.collection.addInferenceParameter(target.compID, target.name); + // this.renderViewSweepParameters(); + }, + // renderEditSweepParameters: function () { + // if(this.editSweepParameters) { + // this.editSweepParameters.remove(); + // } + // let options = {"viewOptions": { + // parent: this, + // stochssParams: this.stochssModel.parameters + // }} + // this.editSweepParameters = this.renderCollection( + // this.model.parameters, + // ParameterView, + // this.queryByHook("ps-parameter-collection"), + // options + // ); + // }, + // renderViewSweepParameters: function () { + // if(this.viewSweepParameters) { + // this.viewSweepParameters.remove(); + // } + // let options = {"viewOptions": { + // parent: this, + // stochssParams: this.stochssModel.parameters, + // viewMode: true + // }} + // this.viewSweepParameters = this.renderCollection( + // this.model.parameters, + // ParameterView, + // this.queryByHook("view-sweep-parameters"), + // options + // ); + // } +}); \ No newline at end of file diff --git a/client/settings-view/views/inference-settings-view.js b/client/settings-view/views/inference-settings-view.js index 1be6e65c76..0f85128468 100644 --- a/client/settings-view/views/inference-settings-view.js +++ b/client/settings-view/views/inference-settings-view.js @@ -18,28 +18,21 @@ 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 SelectView = require('ampersand-select-view'); -// let ParameterView = require('./sweep-parameter-view'); +let InferenceParametersView = require('./inference-parameters-view'); //templates let template = require('../templates/inferenceSettingsView.pug'); module.exports = View.extend({ template: template, events: { - 'click [data-hook=collapse]' : 'changeCollapseButtonText', - 'click [data-hook=add-mi-parameter]' : 'handleAddParameterClick' + 'click [data-hook=collapse]' : 'changeCollapseButtonText' }, initialize: function (attrs, options) { View.prototype.initialize.apply(this, arguments); this.readOnly = attrs.readOnly ? attrs.readOnly : false; - this.tooltips = Tooltips.parameterSweepSettings; this.stochssModel = attrs.stochssModel; - if(!this.readOnly) { - this.model.updateVariables(this.stochssModel.parameters); - } }, render: function () { View.prototype.render.apply(this, arguments); @@ -53,58 +46,34 @@ module.exports = View.extend({ $(this.queryByHook(this.model.elementID + '-edit-inference-settings')).removeClass('active'); $(this.queryByHook(this.model.elementID + '-view-inference-settings')).addClass('active'); }else{ - this.model.parameters.on("add remove", () => { - let disable = this.model.parameters.length >= this.stochssModel.parameters.length; - $(this.queryByHook("add-mi-parameter")).prop("disabled", disable); - }, this) - // this.renderEditSweepParameters(); + this.renderEditParameterSpace(); } - // this.renderViewSweepParameters(); + this.renderViewParameterSpace(); }, changeCollapseButtonText: function (e) { app.changeCollapseButtonText(this, e); }, - getParameter: function () { - let parameters = this.model.parameters.map((param) => { return param.paramID; }); - let target = this.stochssModel.parameters.filter((param) => { - return !parameters.includes(param.compID); - })[0]; - return target; - }, - handleAddParameterClick: function (e) { - let target = this.getParameter(); - this.model.parameters.addInferenceParameter(target.compID, target.name); - // this.renderViewSweepParameters(); + renderEditParameterSpace: function () { + if(this.editParameterSpace) { + this.editParameterSpace.remove(); + } + this.editParameterSpace = new InferenceParametersView({ + collection: this.model.parameters, + stochssModel: this.stochssModel + }); + let hook = "edit-parameter-space-container"; + app.registerRenderSubview(this, this.editParameterSpace, hook); }, - // renderEditSweepParameters: function () { - // if(this.editSweepParameters) { - // this.editSweepParameters.remove(); - // } - // let options = {"viewOptions": { - // parent: this, - // stochssParams: this.stochssModel.parameters - // }} - // this.editSweepParameters = this.renderCollection( - // this.model.parameters, - // ParameterView, - // this.queryByHook("ps-parameter-collection"), - // options - // ); - // }, - // renderViewSweepParameters: function () { - // if(this.viewSweepParameters) { - // this.viewSweepParameters.remove(); - // } - // let options = {"viewOptions": { - // parent: this, - // stochssParams: this.stochssModel.parameters, - // viewMode: true - // }} - // this.viewSweepParameters = this.renderCollection( - // this.model.parameters, - // ParameterView, - // this.queryByHook("view-sweep-parameters"), - // options - // ); - // } + renderViewParameterSpace: function () { + if(this.viewParameterSpace) { + this.viewParameterSpace.remove(); + } + this.viewParameterSpace = new InferenceParametersView({ + collection: this.model.parameters, + readOnly: true, + stochssModel: this.stochssModel + }); + let hook = "view-parameter-space-container"; + app.registerRenderSubview(this, this.viewParameterSpace, hook); + } }); From 76a0e4a0ce2f8d20e98445919c8bfe1e778d6f5c Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Fri, 27 Jan 2023 11:24:47 -0500 Subject: [PATCH 008/109] Added view and template files for uniform inference parameters. --- .../templates/editUniformParameterView.pug | 26 ++++ .../templates/viewUniformParameterView.pug | 16 ++ .../views/inference-parameters-view.js | 96 +++++++----- .../views/inference-settings-view.js | 6 +- .../views/uniform-parameter-view.js | 145 ++++++++++++++++++ 5 files changed, 249 insertions(+), 40 deletions(-) create mode 100644 client/settings-view/templates/editUniformParameterView.pug create mode 100644 client/settings-view/templates/viewUniformParameterView.pug create mode 100644 client/settings-view/views/uniform-parameter-view.js diff --git a/client/settings-view/templates/editUniformParameterView.pug b/client/settings-view/templates/editUniformParameterView.pug new file mode 100644 index 0000000000..071d80cc93 --- /dev/null +++ b/client/settings-view/templates/editUniformParameterView.pug @@ -0,0 +1,26 @@ +div.mx-1 + + if(this.model.collection.indexOf(this.model) !== 0) + hr + + div.row + + div.col-sm-3 + + div(data-hook=this.model.elementID + "-sweep-target") + + div.col-sm-3 + + div(data-hook=this.model.elementID + "-target-value")=this.parameter.expression + + div.col-sm-2 + + div(data-hook=this.model.elementID + "-target-min") + + div.col-sm-2 + + div(data-hook=this.model.elementID + "-target-max") + + div.col-sm-2 + + button.btn.btn-outline-secondary.box-shadow(data-hook=this.model.elementID + "-remove") X diff --git a/client/settings-view/templates/viewUniformParameterView.pug b/client/settings-view/templates/viewUniformParameterView.pug new file mode 100644 index 0000000000..316233caf8 --- /dev/null +++ b/client/settings-view/templates/viewUniformParameterView.pug @@ -0,0 +1,16 @@ +div.mx-1 + + if(this.model.collection.indexOf(this.model) !== 0) + hr + + div.row + + div.col-sm-3 + + div.pl-2=this.model.name + + div.col-sm-3=this.parameter.expression + + div.col-sm-3=this.model.min + + div.col-sm-3=this.model.max diff --git a/client/settings-view/views/inference-parameters-view.js b/client/settings-view/views/inference-parameters-view.js index 404ea0da1d..1e17290304 100644 --- a/client/settings-view/views/inference-parameters-view.js +++ b/client/settings-view/views/inference-parameters-view.js @@ -20,10 +20,10 @@ let $ = require('jquery'); let Tooltips = require('../../tooltips'); //views let View = require('ampersand-view'); -// let ParameterView = require('./sweep-parameter-view'); +let UniformParameterView = require('./uniform-parameter-view'); //templates -let editTemplate = require('../templates/editUniformParameters.pug'); -let viewTemplate = require('../templates/viewUniformParameters.pug'); +let editUniformTemplate = require('../templates/editUniformParameters.pug'); +let viewUniformTemplate = require('../templates/viewUniformParameters.pug'); module.exports = View.extend({ events: { @@ -34,21 +34,26 @@ module.exports = View.extend({ this.readOnly = attrs.readOnly ? attrs.readOnly : false; this.tooltips = Tooltips.parameterSweepSettings; this.stochssModel = attrs.stochssModel; + this.priorMethod = attrs.priorMethod; if(!this.readOnly) { this.collection.updateVariables(this.stochssModel.parameters); } }, render: function () { - this.template = this.readOnly ? viewTemplate : editTemplate; + // if(this.priorMethod === "Uniform Prior") { + // this.template = this.readOnly ? viewUniformTemplate : editUniformTemplate; + // } + this.template = this.readOnly ? viewUniformTemplate : editUniformTemplate; View.prototype.render.apply(this, arguments); if(!this.readOnly) { this.collection.on("add remove", () => { let disable = this.collection.length >= this.stochssModel.parameters.length; $(this.queryByHook("add-mi-parameter")).prop("disabled", disable); }, this) - // this.renderEditSweepParameters(); + this.renderEditInferenceParameter(); + }else{ + this.renderViewInferenceParameter(); } - // this.renderViewSweepParameters(); }, getParameter: function () { let parameters = this.collection.map((param) => { return param.paramID; }); @@ -60,37 +65,52 @@ module.exports = View.extend({ handleAddParameterClick: function (e) { let target = this.getParameter(); this.collection.addInferenceParameter(target.compID, target.name); - // this.renderViewSweepParameters(); + this.updateTargetOptions(); }, - // renderEditSweepParameters: function () { - // if(this.editSweepParameters) { - // this.editSweepParameters.remove(); - // } - // let options = {"viewOptions": { - // parent: this, - // stochssParams: this.stochssModel.parameters - // }} - // this.editSweepParameters = this.renderCollection( - // this.model.parameters, - // ParameterView, - // this.queryByHook("ps-parameter-collection"), - // options - // ); - // }, - // renderViewSweepParameters: function () { - // if(this.viewSweepParameters) { - // this.viewSweepParameters.remove(); - // } - // let options = {"viewOptions": { - // parent: this, - // stochssParams: this.stochssModel.parameters, - // viewMode: true - // }} - // this.viewSweepParameters = this.renderCollection( - // this.model.parameters, - // ParameterView, - // this.queryByHook("view-sweep-parameters"), - // options - // ); - // } + renderEditInferenceParameter: function () { + if(this.editInferenceParameter) { + this.editInferenceParameter.remove(); + } + let options = {"viewOptions": { + parent: this, + stochssParams: this.stochssModel.parameters + }} + // if(this.priorMethod === "Uniform Prior") { + // var inferenceParameterView = UniformParameterView; + // } + var inferenceParameterView = UniformParameterView; + this.editInferenceParameter = this.renderCollection( + this.collection, + inferenceParameterView, + this.queryByHook("edit-mi-parameter-collection"), + options + ); + }, + renderViewInferenceParameter: function () { + if(this.viewInferenceParameter) { + this.viewInferenceParameter.remove(); + } + let options = {"viewOptions": { + parent: this, viewMode: true, + stochssParams: this.stochssModel.parameters + }} + // if(this.priorMethod === "Uniform Prior") { + // var inferenceParameterView = UniformParameterView; + // } + var inferenceParameterView = UniformParameterView; + this.viewInferenceParameter = this.renderCollection( + this.collection, + inferenceParameterView, + this.queryByHook("view-mi-parameter-collection"), + options + ); + }, + updateTargetOptions: function () { + this.editInferenceParameter.views.forEach((view) => { + view.renderTargetSelectView(); + }); + }, + updateViewer: function () { + this.parent.renderViewParameterSpace(); + } }); \ No newline at end of file diff --git a/client/settings-view/views/inference-settings-view.js b/client/settings-view/views/inference-settings-view.js index 0f85128468..a4defd0a25 100644 --- a/client/settings-view/views/inference-settings-view.js +++ b/client/settings-view/views/inference-settings-view.js @@ -59,7 +59,8 @@ module.exports = View.extend({ } this.editParameterSpace = new InferenceParametersView({ collection: this.model.parameters, - stochssModel: this.stochssModel + stochssModel: this.stochssModel, + priorMethod: this.model.priorMethod }); let hook = "edit-parameter-space-container"; app.registerRenderSubview(this, this.editParameterSpace, hook); @@ -71,7 +72,8 @@ module.exports = View.extend({ this.viewParameterSpace = new InferenceParametersView({ collection: this.model.parameters, readOnly: true, - stochssModel: this.stochssModel + stochssModel: this.stochssModel, + priorMethod: this.model.priorMethod }); let hook = "view-parameter-space-container"; app.registerRenderSubview(this, this.viewParameterSpace, hook); diff --git a/client/settings-view/views/uniform-parameter-view.js b/client/settings-view/views/uniform-parameter-view.js new file mode 100644 index 0000000000..4ecc7af299 --- /dev/null +++ b/client/settings-view/views/uniform-parameter-view.js @@ -0,0 +1,145 @@ +/* +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 InputView = require('../../views/input'); +let View = require('ampersand-view'); +let SelectView = require('ampersand-select-view'); +//templates +let editTemplate = require('../templates/editUniformParameterView.pug'); +let viewTemplate = require('../templates/viewUniformParameterView.pug'); + +module.exports = View.extend({ + events: function () { + let events = {}; + events['change [data-hook=' + this.model.elementID + '-sweep-target]'] = 'setSelectedTarget'; + events['change [data-hook=' + this.model.elementID + '-target-min]'] = 'setHasChangedRange'; + events['change [data-hook=' + this.model.elementID + '-target-max]'] = 'setHasChangedRange'; + events['click [data-hook=' + this.model.elementID + '-remove'] = 'removeSweepParameter'; + return events; + }, + initialize: function (attrs, options) { + View.prototype.initialize.apply(this, arguments); + this.viewMode = attrs.viewMode ? attrs.viewMode : false; + this.parameters = attrs.stochssParams; + this.parameter = this.parameters.filter((param) => { + return param.compID === this.model.paramID; + })[0]; + if(!this.viewMode) { + this.model.updateVariable(this.parameter); + } + }, + render: function (attrs, options) { + this.template = this.viewMode ? viewTemplate : editTemplate; + View.prototype.render.apply(this, arguments); + if(!this.viewMode){ + this.renderTargetSelectView(); + this.renderMinValInputView(); + this.renderMaxValInputView(); + } + }, + getAvailableParameters: function () { + let variableTargets = this.model.collection.map((variable) => { return variable.paramID; }); + let availableParameters = this.parameters.filter((param) => { + return !variableTargets.includes(param.compID); + }).map((param) => { return param.name; }); + if(!availableParameters.includes(this.parameter.name)) { + availableParameters.push(this.parameter.name); + } + return availableParameters; + }, + removeSweepParameter: function () { + this.model.collection.removeInferenceParameter(this.model); + this.remove(); + this.parent.updateTargetOptions(); + }, + renderMaxValInputView: function () { + if(this.maxValInputView) { + this.maxValInputView.remove(); + } + this.maxValInputView = new InputView({ + parent: this, + required: true, + name: 'max-value', + tests: tests.valueTests, + modelKey: 'max', + valueType: 'number', + value: this.model.max + }); + app.registerRenderSubview(this, this.maxValInputView, this.model.elementID + "-target-max"); + }, + renderMinValInputView: function () { + if(this.minValInputView) { + this.minValInputView.remove(); + } + this.minValInputView = new InputView({ + parent: this, + required: true, + name: 'min-value', + tests: tests.valueTests, + modelKey: 'min', + valueType: 'number', + value: this.model.min + }); + app.registerRenderSubview(this, this.minValInputView, this.model.elementID + "-target-min"); + }, + renderTargetSelectView: function (e) { + if(this.vieWMode) { return } + if(this.targetSelectView) { + this.targetSelectView.remove(); + } + let options = this.getAvailableParameters(); + this.targetSelectView = new SelectView({ + name: 'variable-target', + required: true, + idAttribute: 'cid', + options: options, + value: this.parameter.name + }); + app.registerRenderSubview(this, this.targetSelectView, this.model.elementID + "-sweep-target"); + }, + setHasChangedRange: function () { + this.model.hasChangedRange = true; + this.updateViewer(); + }, + setSelectedTarget: function (e) { + let targetName = e.target.value; + this.parameter = this.parameters.filter(function (param) { + return param.name === targetName; + })[0]; + this.model.paramID = this.parameter.compID; + this.model.name = this.parameter.name; + this.model.hasChangedRange = false; + this.model.updateVariable(this.parameter); + this.parent.updateTargetOptions(); + this.renderMinValInputView(); + this.renderMaxValInputView(); + $(this.queryByHook(this.model.elementID + "-target-value")).text(this.parameter.expression) + this.updateViewer(); + }, + update: function () {}, + updateValid: function () {}, + updateViewer: function () { + this.parent.updateViewer(); + } +}); From 9c5e27d232fe51cc2f39b0aad027f8ab38e5ac82 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Fri, 27 Jan 2023 12:56:21 -0500 Subject: [PATCH 009/109] Updated parameter space header. --- client/settings-view/templates/editUniformParameters.pug | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/settings-view/templates/editUniformParameters.pug b/client/settings-view/templates/editUniformParameters.pug index 304105834c..bbec71a885 100644 --- a/client/settings-view/templates/editUniformParameters.pug +++ b/client/settings-view/templates/editUniformParameters.pug @@ -2,7 +2,7 @@ div div - h5 Configure Parameter Space + h5 Parameter Space Configuration hr From 2b741b56011319a9f9caab233cf5b892027bb4ef Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Fri, 27 Jan 2023 16:59:11 -0500 Subject: [PATCH 010/109] Added UI elements for prior method, summary stats, and obs data file. --- client/models/inference-settings.js | 2 +- client/settings-view/settings-view.js | 1 - .../templates/inferenceSettingsView.pug | 124 ++++++++++++ .../views/inference-settings-view.js | 176 +++++++++++++++++- client/styles/styles.css | 4 + 5 files changed, 304 insertions(+), 3 deletions(-) diff --git a/client/models/inference-settings.js b/client/models/inference-settings.js index 3f5335362a..c20ed4b231 100644 --- a/client/models/inference-settings.js +++ b/client/models/inference-settings.js @@ -21,7 +21,7 @@ let InferenceParameters = require('./inference-parameters'); let State = require('ampersand-state'); module.exports = State.extend({ - Props: { + props: { obsData: 'string', priorMethod: 'string', summaryStats: 'string' diff --git a/client/settings-view/settings-view.js b/client/settings-view/settings-view.js index e98b299a6c..88865e3bcb 100644 --- a/client/settings-view/settings-view.js +++ b/client/settings-view/settings-view.js @@ -53,7 +53,6 @@ module.exports = View.extend({ if(this.inferenceSettingsView) { this.inferenceSettingsView.remove(); } - console.log(this.stochssModel) this.inferenceSettingsView = new InferenceSettingsView({ model: this.model.inferenceSettings, stochssModel: this.stochssModel, diff --git a/client/settings-view/templates/inferenceSettingsView.pug b/client/settings-view/templates/inferenceSettingsView.pug index 3be982fd48..5ed1b78687 100644 --- a/client/settings-view/templates/inferenceSettingsView.pug +++ b/client/settings-view/templates/inferenceSettingsView.pug @@ -24,8 +24,132 @@ div#inference-settings.card div.tab-pane.active(id=this.model.elementID + "-edit-inference-settings" data-hook=this.model.elementID + "-edit-inference-settings") + hr + + div.mx-1.row.head.align-items-baseline + + div.col-sm-2 + + h6 Prior Method + + div.col-sm-10 + + h6.inline Summary Statistics + + div.mx-1.my-3.row + + div.col-sm-2 + + div(data-hook="prior-method") + + div.col-sm-10 + + div(data-hook="summary-statistics") + div(data-hook="edit-parameter-space-container") + div.mt-3.accordion(id="obs-data-section" data-hook="obs-data-section") + + div.card + + div.card-header(id="importObsDataHeader") + + h3.mb-0.inline="Import Observed Data" + + button.btn.btn-outline-collapse.collapsed.inline(type="button" data-toggle="collapse" data-target="#collapseImportObsData" data-hook="collapseImportObsData" aria-expanded="false" aria-controls="collapseImportObsData" 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="collapseImportObsData" aria-labelledby="importObsDataHeader" data-parent="#obs-data-section") + + div.card-body + + div.my-3 + + span.inline(for="obsDataFile")=`Please specify a file to import: ` + + input.ml-2(id="obsDataFile" data-hook="obs-data-file" type="file" name="obs-data-file" size="30" accept='.txt, .csv' required) + + div.inline + + button.btn.btn-outline-primary.box-shadow(data-hook="import-obs-data-file" disabled)=`Import File` + + div.mdl-edit-btn.saving-status.inline(data-hook="iodf-in-progress") + + div.spinner-grow.mr-2 + + span Importing observed data ... + + div.mdl-edit-btn.saved-status.inline(data-hook="iodf-complete") + + span Successfully imported observed data! + + div.mdl-edit-btn.save-error-status(data-hook="iodf-error") + + span(data-hook="iodf-action-error") + + div.card + + div.card-header(id="uploadObsDataHeader") + + h3.mb-0.inline="Upload Observed Data" + + button.btn.btn-outline-collapse.collapsed.inline(type="button" data-toggle="collapse" data-target="#collapseUploadObsData" data-hook="collapseUploadObsData" aria-expanded="false" aria-controls="collapseUploadObsData" style="float: right") + + div(data-hook="upload-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="collapseUploadObsData" aria-labelledby="uploadObsDataHeader" data-parent="#obs-data-section") + + div.card-body + + div + + div.hidden.text-info(data-hook="obs-data-location-message")=`There are multiple observed data files with that name, please select a location` + + div + + div.inline.mr-3 + + span.inline(for="obs-data-file-select")=`Please specify a file to upload: ` + + div.inline(id="obs-data-file-select" data-hook="obs-data-file-select") + + div.hidden.inline(data-hook="obs-data-location-container") + + span.inline.mr-2(for="obs-data-location-select") Location: + + div.inline(id="obs-data-location-select" data-hook="obs-data-location-select") + div.tab-pane(id=this.model.elementID + "-view-inference-settings" data-hook=this.model.elementID + "-view-inference-settings") + hr + + div.mx-1.row.head.align-items-baseline + + div.col-sm-2 + + h6 Prior Method + + div.col-sm-10 + + h6.inline Summary Statistics + + div.mx-1.my-3.row + + div.col-sm-2 + + div(data-hook="view-prior-method")=this.model.priorMethod + + div.col-sm-10 + + div(data-hook="view-summary-stats")=this.model.summaryStats + div(data-hook="view-parameter-space-container") diff --git a/client/settings-view/views/inference-settings-view.js b/client/settings-view/views/inference-settings-view.js index a4defd0a25..56011b83c4 100644 --- a/client/settings-view/views/inference-settings-view.js +++ b/client/settings-view/views/inference-settings-view.js @@ -16,10 +16,13 @@ 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 InputView = require('../../views/input'); +let SelectView = require('ampersand-select-view'); let InferenceParametersView = require('./inference-parameters-view'); //templates let template = require('../templates/inferenceSettingsView.pug'); @@ -27,12 +30,32 @@ let template = require('../templates/inferenceSettingsView.pug'); module.exports = View.extend({ template: template, events: { - 'click [data-hook=collapse]' : 'changeCollapseButtonText' + 'change [data-hook=prior-method]' : 'setPriorMethod', + 'change [data-hook=summary-statistics]' : 'updateSummaryStatsView', + 'change [data-hook=obs-data-file]' : 'setObsDataFile', + 'click [data-hook=collapse]' : 'changeCollapseButtonText', + 'click [data-hook=collapseImportObsData]' : 'toggleImportFiles', + 'click [data-hook=collapseUploadObsData]' : 'toggleUploadFiles', + 'click [data-hook=import-obs-data-file]' : 'handleImportObsData' }, initialize: function (attrs, options) { View.prototype.initialize.apply(this, arguments); this.readOnly = attrs.readOnly ? attrs.readOnly : false; this.stochssModel = attrs.stochssModel; + this.obsDataFiles = null; + this.obsDataFile = null; + this.chevrons = { + hide: ` + + + + `, + show: ` + + + + ` + } }, render: function () { View.prototype.render.apply(this, arguments); @@ -53,6 +76,39 @@ module.exports = View.extend({ changeCollapseButtonText: function (e) { app.changeCollapseButtonText(this, e); }, + completeAction: function () { + $(this.queryByHook("iodf-in-progress")).css("display", "none"); + $(this.queryByHook("iodf-complete")).css("display", "inline-block"); + setTimeout(() => { + $(this.queryByHook("iodf-complete")).css("display", "none"); + }, 5000); + }, + errorAction: function (action) { + $(this.queryByHook("iodf-in-progress")).css("display", "none"); + $(this.queryByHook("iodf-action-error")).text(action); + $(this.queryByHook("iodf-error")).css("display", "block"); + }, + handleImportObsData: function () { + this.startAction(); + let formData = new FormData(); + var filePath = this.model.parent.parent.directory; + formData.append("path", filePath); + formData.append("datafile", this.obsDataFile); + let endpoint = path.join(app.getApiPath(), 'workflow/import-obs-data'); + app.postXHR(endpoint, formData, { + success: (err, response, body) => { + body = JSON.parse(body); + this.model.obsData = path.join(body.obsDataPath, body.obsDataFile); + this.completeAction(); + $(this.queryByHook('collapseUploadObsData')).click(); + this.renderObsDataSelects(); + }, + error: (err, response, body) => { + body = JSON.parse(body); + this.errorAction(body.Message); + } + }, false); + }, renderEditParameterSpace: function () { if(this.editParameterSpace) { this.editParameterSpace.remove(); @@ -65,6 +121,55 @@ module.exports = View.extend({ let hook = "edit-parameter-space-container"; app.registerRenderSubview(this, this.editParameterSpace, hook); }, + renderObsDataSelects: function () { + let queryStr = "?ext=.txt,.csv" + let endpoint = `${path.join(app.getApiPath(), 'workflow/obs-data-files')}${queryStr}`; + app.getXHR(endpoint, {success: (err, response, body) => { + this.obsDataFiles = body.obsDataFiles; + this.renderObsDataSelectView(); + }}); + }, + renderObsDataSelectView: function () { + if(this.obsDataSelectView) { + this.obsDataSelectView.remove(); + } + let files = this.obsDataFiles.files.filter((file) => { + if(file[1] === this.model.obsData.split('/').pop()) { + return file; + } + }); + let value = files.length > 0 ? files[0] : ""; + this.obsDataSelectView = new SelectView({ + name: 'obs-data-files', + required: true, + idAttributes: 'cid', + options: this.obsDataFiles.files, + value: value, + unselectedText: "-- Select Data File --" + }); + let hook = "obs-data-file-select"; + app.registerRenderSubview(this, this.obsDataSelectView, hook); + if(value !== "" && this.obsDataFiles.paths[value[0]].length > 1) { + this.renderObsDataLocationSelectView(value[0]); + $(this.queryByHook("obs-data-location-container")).css("display", "inline-block"); + } + }, + renderObsDataLocationSelectView: function () { + if(this.obsDataLocationSelectView) { + this.obsDataLocationSelectView.remove(); + } + let value = Boolean(this.model.obsData) ? this.model.obsData : ""; + this.obsDataLocationSelectView = new SelectView({ + name: 'obs-data-locations', + required: true, + idAttributes: 'cid', + options: this.obsDataFiles.paths[index], + value: value, + unselectedText: "-- Select Data File Location --" + }); + let hook = "obs-data-location-select"; + app.registerRenderSubview(this, this.obsDataLocationSelectView, hook); + }, renderViewParameterSpace: function () { if(this.viewParameterSpace) { this.viewParameterSpace.remove(); @@ -77,5 +182,74 @@ module.exports = View.extend({ }); let hook = "view-parameter-space-container"; app.registerRenderSubview(this, this.viewParameterSpace, hook); + }, + setObsDataFile: function (e) { + this.obsDataFile = e.target.files[0]; + $(this.queryByHook("import-obs-data-file")).prop('disabled', !this.obsDataFile); + console.log(this.obsDataFile) + }, + setPriorMethod: function (e) { + this.model.priorMethod = e.target.value; + $(this.queryByHook("view-prior-method")).text(this.model.priorMethod); + this.renderEditParameterSpace(); + this.renderViewParameterSpace(); + }, + startAction: function () { + $(this.queryByHook("iodf-complete")).css("display", "none"); + $(this.queryByHook("iodf-error")).css("display", "none"); + $(this.queryByHook("iodf-in-progress")).css("display", "inline-block"); + }, + toggleImportFiles: function (e) { + let classes = $(this.queryByHook('collapseImportObsData')).attr("class").split(/\s+/); + $(this.queryByHook('upload-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); + } + }, + toggleUploadFiles: function (e) { + let classes = $(this.queryByHook('collapseUploadObsData')).attr("class").split(/\s+/); + $(this.queryByHook('import-chevron')).html(this.chevrons.hide); + if(classes.includes('collapsed')) { + $(this.queryByHook('upload-chevron')).html(this.chevrons.show); + }else{ + $(this.queryByHook('upload-chevron')).html(this.chevrons.hide); + } + }, + update: function (e) {}, + updateSummaryStatsView: function (e) { + $(this.queryByHook("view-summary-stats")).text(this.model.summaryStats ? this.model.summaryStats : 'None') + }, + updateValid: function (e) {}, + subviews: { + priorMethodView: { + hook: "prior-method", + prepareView: function (el) { + let options = [ + "Uniform Prior"//, "Gaussian Prior" + ] + return new SelectView({ + name: 'prior-method', + required: true, + eagerValidate: true, + options: options, + value: this.model.priorMethod + }); + } + }, + summaryStatsView: { + hook: "summary-statistics", + prepareView: function (el) { + return new InputView({ + parent: this, + required: false, + name: 'summary-statistics', + modelKey: 'summaryStats', + valueType: 'string', + value: this.model.summaryStats + }); + } + } } }); diff --git a/client/styles/styles.css b/client/styles/styles.css index 3f3e7906a3..2c53e6aa7c 100644 --- a/client/styles/styles.css +++ b/client/styles/styles.css @@ -27,6 +27,10 @@ img.quickstart { margin: 2em auto; } +svg { + pointer-events: none; +} + .page { padding-bottom: 150px; } From a492167d9114bf6504d74bdfe4a2ebf76867d431 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Fri, 27 Jan 2023 17:00:46 -0500 Subject: [PATCH 011/109] Connect back end elements for importing and selecting observed data files. --- stochss/handlers/__init__.py | 2 + stochss/handlers/workflows.py | 74 +++++++++++++++++++++++++++++++++-- 2 files changed, 73 insertions(+), 3 deletions(-) diff --git a/stochss/handlers/__init__.py b/stochss/handlers/__init__.py index 6907767ae5..c0dde21f7e 100644 --- a/stochss/handlers/__init__.py +++ b/stochss/handlers/__init__.py @@ -113,6 +113,8 @@ def get_page_handlers(route_start): (r"/stochss/api/workflow/save-plot\/?", SavePlotAPIHandler), (r"/stochss/api/workflow/save-annotation\/?", SaveAnnotationAPIHandler), (r"/stochss/api/workflow/update-format\/?", UpadteWorkflowAPIHandler), + (r"/stochss/api/workflow/import-obs-data\/?", ImportObsDataAPIHandler), + (r"/stochss/api/workflow/obs-data-files\/?", LoadObsDataFiles), (r"/stochss/api/job/presentation\/?", JobPresentationAPIHandler), (r"/stochss/api/job/csv\/?", DownloadCSVZipAPIHandler) ] diff --git a/stochss/handlers/workflows.py b/stochss/handlers/workflows.py index 89d97ec41a..1283ea735b 100644 --- a/stochss/handlers/workflows.py +++ b/stochss/handlers/workflows.py @@ -27,9 +27,9 @@ # Note APIHandler.finish() sets Content-Type handler to 'application/json' # Use finish() for json, write() for text -from .util import StochSSJob, StochSSModel, StochSSSpatialModel, StochSSNotebook, StochSSWorkflow, \ - StochSSParamSweepNotebook, StochSSSciopeNotebook, StochSSAPIError, report_error, \ - report_critical_error +from .util import StochSSFolder, StochSSJob, StochSSModel, StochSSSpatialModel, StochSSNotebook, \ + StochSSWorkflow, StochSSParamSweepNotebook, StochSSSciopeNotebook, \ + StochSSAPIError, report_error, report_critical_error log = logging.getLogger('stochss') @@ -505,3 +505,71 @@ async def get(self): except Exception as err: report_critical_error(self, log, err) self.finish() + + +class ImportObsDataAPIHandler(APIHandler): + ''' + ################################################################################################ + Handler for importing observed data from remote file. + ################################################################################################ + ''' + @web.authenticated + async def post(self): + ''' + Imports observed data from a file. + + Attributes + ---------- + ''' + self.set_header('Content-Type', 'application/json') + dirname = os.path.dirname(self.request.body_arguments['path'][0].decode()) + if dirname == '.': + dirname = "" + elif ".wkgp" in dirname: + dirname = os.path.dirname(dirname) + data_file = self.request.files['datafile'][0] + log.info(f"Importing observed data: {data_file['filename']}") + try: + folder = StochSSFolder(path=dirname) + data_resp = folder.upload('file', data_file['filename'], data_file['body']) + resp = {'obsDataPath': data_resp['path'], 'obsDataFile': data_resp['file']} + log.info("Successfully uploaded observed data") + 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 LoadObsDataFiles(APIHandler): + ''' + ################################################################################################ + Handler for getting observed data files. + ################################################################################################ + ''' + @web.authenticated + async def get(self): + ''' + Get observed data files on disc for file selections. + + Attributes + ---------- + ''' + self.set_header('Content-Type', 'application/json') + target_ext = self.get_query_argument(name="ext").split(',') + try: + folder = StochSSFolder(path="") + test = lambda ext, root, file: bool( + "trash" in root.split("/") or file.startswith('.') or \ + 'wkfl' in root or root.startswith('.') + ) + data_files = folder.get_file_list(ext=target_ext, test=test) + resp = {'obsDataFiles': data_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() From b75336f4d156a429d7ac836fd14325a981d638a9 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Tue, 31 Jan 2023 14:35:59 -0500 Subject: [PATCH 012/109] Updated the observed data section headers. --- client/settings-view/templates/inferenceSettingsView.pug | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/settings-view/templates/inferenceSettingsView.pug b/client/settings-view/templates/inferenceSettingsView.pug index 5ed1b78687..0642f85f92 100644 --- a/client/settings-view/templates/inferenceSettingsView.pug +++ b/client/settings-view/templates/inferenceSettingsView.pug @@ -54,7 +54,7 @@ div#inference-settings.card div.card-header(id="importObsDataHeader") - h3.mb-0.inline="Import Observed Data" + h3.mb-0.inline="Import Observed Data File" button.btn.btn-outline-collapse.collapsed.inline(type="button" data-toggle="collapse" data-target="#collapseImportObsData" data-hook="collapseImportObsData" aria-expanded="false" aria-controls="collapseImportObsData" style="float: right") @@ -96,7 +96,7 @@ div#inference-settings.card div.card-header(id="uploadObsDataHeader") - h3.mb-0.inline="Upload Observed Data" + h3.mb-0.inline="Select Observed Data File" button.btn.btn-outline-collapse.collapsed.inline(type="button" data-toggle="collapse" data-target="#collapseUploadObsData" data-hook="collapseUploadObsData" aria-expanded="false" aria-controls="collapseUploadObsData" style="float: right") From cc42d9636237e9e38bc5226df5cf5d02682e7d39 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Tue, 31 Jan 2023 14:36:58 -0500 Subject: [PATCH 013/109] Fixed issue with observed data select view rendering. --- client/settings-view/views/inference-settings-view.js | 1 + 1 file changed, 1 insertion(+) diff --git a/client/settings-view/views/inference-settings-view.js b/client/settings-view/views/inference-settings-view.js index 56011b83c4..bee757ee04 100644 --- a/client/settings-view/views/inference-settings-view.js +++ b/client/settings-view/views/inference-settings-view.js @@ -70,6 +70,7 @@ module.exports = View.extend({ $(this.queryByHook(this.model.elementID + '-view-inference-settings')).addClass('active'); }else{ this.renderEditParameterSpace(); + this.renderObsDataSelects(); } this.renderViewParameterSpace(); }, From c4ca85b0bc3334f3d03edfbda7ade8c7afffd8cd Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Tue, 31 Jan 2023 14:37:37 -0500 Subject: [PATCH 014/109] Fixed broken test. Added test for inference settings. --- stochss/tests/test_settings_template.py | 27 +++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/stochss/tests/test_settings_template.py b/stochss/tests/test_settings_template.py index fce01d01ce..f26e5f02ac 100644 --- a/stochss/tests/test_settings_template.py +++ b/stochss/tests/test_settings_template.py @@ -45,8 +45,11 @@ def test_workflow_settings_elements(self): model_path = "client/models/settings.js" with open(model_path, "r") as model_file: - children = model_file.read().split("children: {").pop().split('}')[0].split(',') - model_keys = sorted(list(map(lambda item: item.strip().split(':')[0], children))) + data = model_file.read() + props = data.split("props: {").pop().split("}")[0].split(",") + children = data.split("children: {").pop().split('}')[0].split(',') + model_keys = sorted(list(map(lambda item: item.strip().split(':')[0], props))) + model_keys.extend(sorted(list(map(lambda item: item.strip().split(':')[0], children)))) self.assertEqual(template_keys, model_keys) @@ -114,3 +117,23 @@ def test_workflow_timespan_settings_elements(self): tspan_settings_keys = sorted(list(map(lambda item: item.strip().split(':')[0], data))) self.assertEqual(template_keys, tspan_settings_keys) + + + def test_workflow_inference_settings_elements(self): + ''' + Check if the inference settings in the settings template has + all of the properties currently in the inference settings model. + ''' + template_keys = sorted(list(self.template['inferenceSettings'].keys())) + tspan_settings_path = "client/models/inference-settings.js" + + with open(tspan_settings_path, "r") as tspan_settings_file: + data = tspan_settings_file.read() + props = data.split("props: {").pop().split('}')[0].split(',') + collections = data.split("collections: {").pop().split('}')[0].split(',') + inf_settings_keys = sorted(list(map(lambda item: item.strip().split(':')[0], props))) + inf_settings_keys.extend( + sorted(list(map(lambda item: item.strip().split(':')[0], collections))) + ) + + self.assertEqual(template_keys, inf_settings_keys) From 6337679a17b567b8577aa624c81a65587143e43a Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Wed, 1 Feb 2023 10:40:33 -0500 Subject: [PATCH 015/109] Fixed issue with saving inference workflows. --- client/models/settings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/models/settings.js b/client/models/settings.js index 98eb3f36c8..ea08bc34ac 100644 --- a/client/models/settings.js +++ b/client/models/settings.js @@ -25,7 +25,7 @@ let ParameterSweepSettings = require('./parameter-sweep-settings'); module.exports = State.extend({ props: { - template_veersion: 'number' + template_version: 'number' }, children: { timespanSettings: TimespanSettings, From b79112c8f2bcafd2ef65472cb8723b795552e287 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Wed, 1 Feb 2023 10:46:48 -0500 Subject: [PATCH 016/109] Fixed broken test. --- stochss/tests/test_settings_template.py | 1 + 1 file changed, 1 insertion(+) diff --git a/stochss/tests/test_settings_template.py b/stochss/tests/test_settings_template.py index f26e5f02ac..f259ce40bc 100644 --- a/stochss/tests/test_settings_template.py +++ b/stochss/tests/test_settings_template.py @@ -136,4 +136,5 @@ def test_workflow_inference_settings_elements(self): sorted(list(map(lambda item: item.strip().split(':')[0], collections))) ) + inf_settings_keys.sort() self.assertEqual(template_keys, inf_settings_keys) From b77ea1b535c4a0fcdd6c237c4187049ccc4b7d4e Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Wed, 1 Feb 2023 10:57:15 -0500 Subject: [PATCH 017/109] Fixed broken test take 2. --- stochss_templates/workflowSettingsTemplate.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stochss_templates/workflowSettingsTemplate.json b/stochss_templates/workflowSettingsTemplate.json index 53d7b6941d..d3e7d727cb 100644 --- a/stochss_templates/workflowSettingsTemplate.json +++ b/stochss_templates/workflowSettingsTemplate.json @@ -3,7 +3,7 @@ "obsData": "", "parameters": [], "priorMethod": "Uniform Prior", - "summryStats": "" + "summaryStats": "" }, "parameterSweepSettings":{ "parameters": [], From b624f8c46b07c97be1c5d345e673d5b64a0d4502 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Wed, 1 Feb 2023 11:08:06 -0500 Subject: [PATCH 018/109] Fixed broken test take 3. --- stochss/tests/test_settings_template.py | 1 + 1 file changed, 1 insertion(+) diff --git a/stochss/tests/test_settings_template.py b/stochss/tests/test_settings_template.py index f259ce40bc..6a81ff0849 100644 --- a/stochss/tests/test_settings_template.py +++ b/stochss/tests/test_settings_template.py @@ -51,6 +51,7 @@ def test_workflow_settings_elements(self): model_keys = sorted(list(map(lambda item: item.strip().split(':')[0], props))) model_keys.extend(sorted(list(map(lambda item: item.strip().split(':')[0], children)))) + model_keys.sort() self.assertEqual(template_keys, model_keys) From 292a8577c7b6dff9e1898f6bf304c53e43df401f Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Mon, 13 Feb 2023 15:49:37 -0500 Subject: [PATCH 019/109] Updated the accepted types for observed data files. --- client/settings-view/templates/inferenceSettingsView.pug | 2 +- client/settings-view/views/inference-settings-view.js | 2 +- stochss/handlers/workflows.py | 8 +++++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/client/settings-view/templates/inferenceSettingsView.pug b/client/settings-view/templates/inferenceSettingsView.pug index 0642f85f92..ae00d13092 100644 --- a/client/settings-view/templates/inferenceSettingsView.pug +++ b/client/settings-view/templates/inferenceSettingsView.pug @@ -72,7 +72,7 @@ div#inference-settings.card span.inline(for="obsDataFile")=`Please specify a file to import: ` - input.ml-2(id="obsDataFile" data-hook="obs-data-file" type="file" name="obs-data-file" size="30" accept='.txt, .csv' required) + input.ml-2(id="obsDataFile" data-hook="obs-data-file" type="file" name="obs-data-file" size="30" accept='.zip, .csv' required) div.inline diff --git a/client/settings-view/views/inference-settings-view.js b/client/settings-view/views/inference-settings-view.js index bee757ee04..f24f8c0c8d 100644 --- a/client/settings-view/views/inference-settings-view.js +++ b/client/settings-view/views/inference-settings-view.js @@ -123,7 +123,7 @@ module.exports = View.extend({ app.registerRenderSubview(this, this.editParameterSpace, hook); }, renderObsDataSelects: function () { - let queryStr = "?ext=.txt,.csv" + let queryStr = "?ext=.obsd,.csv" let endpoint = `${path.join(app.getApiPath(), 'workflow/obs-data-files')}${queryStr}`; app.getXHR(endpoint, {success: (err, response, body) => { this.obsDataFiles = body.obsDataFiles; diff --git a/stochss/handlers/workflows.py b/stochss/handlers/workflows.py index 1283ea735b..40b1fed2b9 100644 --- a/stochss/handlers/workflows.py +++ b/stochss/handlers/workflows.py @@ -531,7 +531,13 @@ async def post(self): log.info(f"Importing observed data: {data_file['filename']}") try: folder = StochSSFolder(path=dirname) - data_resp = folder.upload('file', data_file['filename'], data_file['body']) + if data_file['filename'].endswith(".zip"): + new_name = data_file['filename'].replace(".zip", ".obsd") + else: + new_name = None + data_resp = folder.upload( + 'file', data_file['filename'], data_file['body'], new_name=new_name + ) resp = {'obsDataPath': data_resp['path'], 'obsDataFile': data_resp['file']} log.info("Successfully uploaded observed data") self.write(resp) From 02f85368c9a2ce4b8e89dbb4bf63c96da9be8a75 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Tue, 14 Feb 2023 11:58:08 -0500 Subject: [PATCH 020/109] Created a function to allow users to preview observed data for inference workflows. --- stochss/handlers/util/stochss_workflow.py | 72 +++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/stochss/handlers/util/stochss_workflow.py b/stochss/handlers/util/stochss_workflow.py index 8c61ccdddf..5ccfc13128 100644 --- a/stochss/handlers/util/stochss_workflow.py +++ b/stochss/handlers/util/stochss_workflow.py @@ -17,12 +17,15 @@ ''' import os +import csv import json import shutil import datetime import traceback import numpy +import plotly +import plotly.graph_objs as go from .stochss_base import StochSSBase from .stochss_folder import StochSSFolder @@ -75,6 +78,57 @@ def __init__(self, path, new=False, mdl_path=None, wkfl_type=None): else: self.workflow['settings'] = None + @classmethod + def __get_csv_data(cls, path): + with open(path, "r", newline="", encoding="utf-8") as csv_fd: + csv_reader = csv.reader(csv_fd, delimiter=",") + rows = [] + headers = None + for i, row in enumerate(csv_reader): + if i == 0: + if len(row) > 0: + headers = row[1:] + else: + rows.append(row) + data = numpy.array(rows).swapaxes(0, 1) + time = data[0].astype("float") + obs_data = data[1:].astype("float") + if headers is None: + headers = [f"obs{i+1}" for i in range(len(obs_data))] + return headers, time, obs_data + + def __get_obs_trace(self, path, traces, f_ndx=None): + if path.endswith(".obsd"): + for ndx, file in enumerate(os.listdir(path)): + traces = self.__get_obs_trace(os.path.join(path, file), traces, f_ndx=ndx) + return traces + if path.endswith(".csv"): + 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' + ] + headers, time, data = self.__get_csv_data(path) + for i, obs in enumerate(data): + line_dict = {"color": common_rgb_values[(i-1)%len(common_rgb_values)]} + if f_ndx is not None and f_ndx > 0: + traces.append(go.Scatter( + x=time, y=obs, mode="lines", name=headers[i], line=line_dict, + legendgroup=headers[i], showlegend=False + )) + else: + traces.append(go.Scatter( + x=time, y=obs, mode="lines", name=headers[i], line=line_dict, + legendgroup=headers[i] + )) + return traces + self.log("warning", f"Observed data file found that is not a CSV file.\n\t File: {path}") + return traces def __get_old_wkfl_data(self): job = StochSSJob(path=self.path) @@ -367,6 +421,24 @@ def load(self): self.workflow['models'] = models return self.workflow + def preview_obs_data(self, path): + ''' + Preview the observed data for inference workflow. + + Attributes + ---------- + path : str + Path to the observed data. + ''' + traces = self.__get_obs_trace(path, []) + if not traces: + raise Exception("No observed data found.") + + layout = go.Layout( + showlegend=True, title="Observed Data", xaxis_title="Time", yaxis_title="Value" + ) + fig = {"data": traces, "layout": layout, "config": {"responsive": True}} + return json.loads(json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder)) def save(self, new_settings, mdl_path): ''' From 0a12a6a65b7afa60e433fe7b548e78529c60dca4 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Tue, 14 Feb 2023 11:59:50 -0500 Subject: [PATCH 021/109] Added route and handler function to support previews of observed data. --- stochss/handlers/__init__.py | 1 + stochss/handlers/workflows.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/stochss/handlers/__init__.py b/stochss/handlers/__init__.py index c0dde21f7e..98e33afb94 100644 --- a/stochss/handlers/__init__.py +++ b/stochss/handlers/__init__.py @@ -115,6 +115,7 @@ def get_page_handlers(route_start): (r"/stochss/api/workflow/update-format\/?", UpadteWorkflowAPIHandler), (r"/stochss/api/workflow/import-obs-data\/?", ImportObsDataAPIHandler), (r"/stochss/api/workflow/obs-data-files\/?", LoadObsDataFiles), + (r"/stochss/api/workflow/preview-obs-data\/?", PreviewOBSDataAPIHandler), (r"/stochss/api/job/presentation\/?", JobPresentationAPIHandler), (r"/stochss/api/job/csv\/?", DownloadCSVZipAPIHandler) ] diff --git a/stochss/handlers/workflows.py b/stochss/handlers/workflows.py index 40b1fed2b9..6ef7e5c8af 100644 --- a/stochss/handlers/workflows.py +++ b/stochss/handlers/workflows.py @@ -579,3 +579,31 @@ async def get(self): except Exception as err: report_critical_error(self, log, err) self.finish() + +class PreviewOBSDataAPIHandler(APIHandler): + ''' + ################################################################################################ + Handler for previewing observed data. + ################################################################################################ + ''' + @web.authenticated + async def get(self): + ''' + Preview the observed data files. + + Attributes + ---------- + ''' + self.set_header('Content-Type', 'application/json') + path = self.get_query_argument(name="path") + try: + wkfl = StochSSWorkflow(path="") + resp = {"figure": wkfl.preview_obs_data(path)} + wkfl.print_logs(log) + 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() From 8b9104dceb8e313e5ae8752152a10a3e980dba4b Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Tue, 14 Feb 2023 13:06:43 -0500 Subject: [PATCH 022/109] Fixed obs data file select. --- stochss/handlers/util/stochss_folder.py | 17 ++++++++++++----- stochss/handlers/workflows.py | 4 ++-- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/stochss/handlers/util/stochss_folder.py b/stochss/handlers/util/stochss_folder.py index 65275c76f3..fd0376b27b 100644 --- a/stochss/handlers/util/stochss_folder.py +++ b/stochss/handlers/util/stochss_folder.py @@ -395,7 +395,7 @@ def generate_zip_file(self): return {"Message":message, "Path":zip_path} - def get_file_list(self, ext, folder=False, test=None): + def get_file_list(self, ext, inc_folders=False, inc_files=True, test=None): ''' Get the list of files matching the ext in this directory and all sub-directories @@ -403,8 +403,10 @@ def get_file_list(self, ext, folder=False, test=None): ---------- ext : str or list Extension of file object to search for - folder : bool - Indicates whether or not the file object is a folder + inc_folder : bool + Indicates whether or not to include 'folder' file objects + inc_file : bool + Indicates whether or not to include 'file' file objects test : func Function that determines if a file object should be excluded ''' @@ -412,7 +414,12 @@ def get_file_list(self, ext, folder=False, test=None): domain_files = {} for root, folders, files in os.walk(self.get_path(full=True)): root = root.replace(self.user_dir+"/", "") - file_list = folders if folder else files + if inc_folders: + file_list = folders + if inc_files: + file_list.extend(files) + else: + file_list = files for file in file_list: exclude = False if test is None else test(ext, root, file) if not exclude and '.' in file and f".{file.split('.').pop()}" in ext: @@ -506,7 +513,7 @@ def get_project_list(self): ---------- ''' test = lambda ext, root, file: bool("trash" in root.split("/")) - data = self.get_file_list(ext=".proj", folder=True, test=test) + data = self.get_file_list(ext=".proj", inc_folders=True, inc_files=False, test=test) projects = [] for file in data['files']: for path in data['paths'][file[0]]: diff --git a/stochss/handlers/workflows.py b/stochss/handlers/workflows.py index 6ef7e5c8af..72e3e4ca06 100644 --- a/stochss/handlers/workflows.py +++ b/stochss/handlers/workflows.py @@ -568,9 +568,9 @@ async def get(self): folder = StochSSFolder(path="") test = lambda ext, root, file: bool( "trash" in root.split("/") or file.startswith('.') or \ - 'wkfl' in root or root.startswith('.') + 'wkfl' in root or root.startswith('.') or root.endswith("obsd") ) - data_files = folder.get_file_list(ext=target_ext, test=test) + data_files = folder.get_file_list(ext=target_ext, test=test, inc_folders=True) resp = {'obsDataFiles': data_files} log.debug(f"Response: {resp}") self.write(resp) From 3836a2d1feedf89de37c4205c216c1fbc1c13ea0 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Tue, 14 Feb 2023 15:13:32 -0500 Subject: [PATCH 023/109] Added event handlers for selecting obs data files and locations. --- .../views/inference-settings-view.js | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/client/settings-view/views/inference-settings-view.js b/client/settings-view/views/inference-settings-view.js index f24f8c0c8d..f5b613bef2 100644 --- a/client/settings-view/views/inference-settings-view.js +++ b/client/settings-view/views/inference-settings-view.js @@ -33,6 +33,8 @@ module.exports = View.extend({ 'change [data-hook=prior-method]' : 'setPriorMethod', 'change [data-hook=summary-statistics]' : 'updateSummaryStatsView', 'change [data-hook=obs-data-file]' : 'setObsDataFile', + 'change [data-hook=obs-data-file-select]' : 'selectObsDataFile', + 'change [data-hook=obs-data-location-select]' : 'selectObsDataLocation', 'click [data-hook=collapse]' : 'changeCollapseButtonText', 'click [data-hook=collapseImportObsData]' : 'toggleImportFiles', 'click [data-hook=collapseUploadObsData]' : 'toggleUploadFiles', @@ -155,7 +157,7 @@ module.exports = View.extend({ $(this.queryByHook("obs-data-location-container")).css("display", "inline-block"); } }, - renderObsDataLocationSelectView: function () { + renderObsDataLocationSelectView: function (index) { if(this.obsDataLocationSelectView) { this.obsDataLocationSelectView.remove(); } @@ -187,7 +189,6 @@ module.exports = View.extend({ setObsDataFile: function (e) { this.obsDataFile = e.target.files[0]; $(this.queryByHook("import-obs-data-file")).prop('disabled', !this.obsDataFile); - console.log(this.obsDataFile) }, setPriorMethod: function (e) { this.model.priorMethod = e.target.value; @@ -195,6 +196,28 @@ module.exports = View.extend({ this.renderEditParameterSpace(); this.renderViewParameterSpace(); }, + selectObsDataFile: function (e) { + let value = e.target.value; + var msgDisplay = "none"; + var contDisplay = "none"; + if(value) { + if(this.obsDataFiles.paths[value].length > 1) { + msgDisplay = "block"; + contDisplay = "inline-block"; + this.renderObsDataLocationSelectView(value); + this.model.obsData = ""; + }else{ + this.model.obsData = this.obsDataFiles.paths[value][0]; + } + }else{ + this.model.obsData = ""; + } + $(this.queryByHook("obs-data-location-message")).css('display', msgDisplay); + $(this.queryByHook("obs-data-location-container")).css("display", contDisplay); + }, + selectObsDataLocation: function (e) { + this.model.obsData = e.target.value ? e.target.value : ""; + }, startAction: function () { $(this.queryByHook("iodf-complete")).css("display", "none"); $(this.queryByHook("iodf-error")).css("display", "none"); From 975d726c3fafe731274904cafe9fad797b7a71ce Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Tue, 14 Feb 2023 16:57:32 -0500 Subject: [PATCH 024/109] Added preview functionality for previewing observed data. --- client/modals.js | 24 +++++++++++ .../templates/inferenceSettingsView.pug | 6 ++- .../views/inference-settings-view.js | 42 +++++++++++++++++-- client/styles/styles.css | 8 ++++ 4 files changed, 76 insertions(+), 4 deletions(-) diff --git a/client/modals.js b/client/modals.js index 0f711d6436..e395a6131e 100644 --- a/client/modals.js +++ b/client/modals.js @@ -250,6 +250,27 @@ let templates = { ` + }, + previewPlot : (title) => { + return ` + ` } } @@ -511,5 +532,8 @@ module.exports = { let message = `StochSS ${fileType}s have a new format. Would you like to update this ${target} to the new format?`; return templates.confirmation_with_message(modalID, title, message); + }, + obsPreviewHtml: (title) => { + return templates.previewPlot(title); } } \ No newline at end of file diff --git a/client/settings-view/templates/inferenceSettingsView.pug b/client/settings-view/templates/inferenceSettingsView.pug index ae00d13092..0afe84a746 100644 --- a/client/settings-view/templates/inferenceSettingsView.pug +++ b/client/settings-view/templates/inferenceSettingsView.pug @@ -118,7 +118,7 @@ div#inference-settings.card div.inline.mr-3 - span.inline(for="obs-data-file-select")=`Please specify a file to upload: ` + span.inline(for="obs-data-file-select")=`Please specify a file: ` div.inline(id="obs-data-file-select" data-hook="obs-data-file-select") @@ -128,6 +128,10 @@ div#inference-settings.card div.inline(id="obs-data-location-select" data-hook="obs-data-location-select") + div.inline + + button.btn.btn-outline-secondary.box-shadow(data-hook="preview-obs-data") Preview + div.tab-pane(id=this.model.elementID + "-view-inference-settings" data-hook=this.model.elementID + "-view-inference-settings") hr diff --git a/client/settings-view/views/inference-settings-view.js b/client/settings-view/views/inference-settings-view.js index f5b613bef2..841e8b75d7 100644 --- a/client/settings-view/views/inference-settings-view.js +++ b/client/settings-view/views/inference-settings-view.js @@ -19,6 +19,8 @@ let $ = require('jquery'); let path = require('path'); //support files let app = require('../../app'); +let modals = require('../../modals'); +let Plotly = require('plotly.js-dist'); //views let View = require('ampersand-view'); let InputView = require('../../views/input'); @@ -29,6 +31,14 @@ let template = require('../templates/inferenceSettingsView.pug'); module.exports = View.extend({ template: template, + bindings: { + 'model.obsData' : { + type: function (el, value, previousValue) { + el.disabled = value == ""; + }, + hook: 'preview-obs-data' + } + }, events: { 'change [data-hook=prior-method]' : 'setPriorMethod', 'change [data-hook=summary-statistics]' : 'updateSummaryStatsView', @@ -38,7 +48,8 @@ module.exports = View.extend({ 'click [data-hook=collapse]' : 'changeCollapseButtonText', 'click [data-hook=collapseImportObsData]' : 'toggleImportFiles', 'click [data-hook=collapseUploadObsData]' : 'toggleUploadFiles', - 'click [data-hook=import-obs-data-file]' : 'handleImportObsData' + 'click [data-hook=import-obs-data-file]' : 'handleImportObsData', + 'click [data-hook=preview-obs-data]' : 'handlePreviewObsData' }, initialize: function (attrs, options) { View.prototype.initialize.apply(this, arguments); @@ -46,6 +57,7 @@ module.exports = View.extend({ this.stochssModel = attrs.stochssModel; this.obsDataFiles = null; this.obsDataFile = null; + this.obsFig = null; this.chevrons = { hide: ` @@ -101,6 +113,7 @@ module.exports = View.extend({ app.postXHR(endpoint, formData, { success: (err, response, body) => { body = JSON.parse(body); + this.obsFig = null; this.model.obsData = path.join(body.obsDataPath, body.obsDataFile); this.completeAction(); $(this.queryByHook('collapseUploadObsData')).click(); @@ -112,6 +125,27 @@ module.exports = View.extend({ } }, false); }, + handlePreviewObsData: function () { + if(this.obsFig !== null) { + this.previewObsData(); + }else{ + let queryStr = `?path=${this.model.obsData}` + let endpoint = `${path.join(app.getApiPath(), 'workflow/preview-obs-data')}${queryStr}`; + app.getXHR(endpoint, {success: (err, response, body) => { + this.obsFig = body.figure; + this.previewObsData(); + }}); + } + }, + previewObsData: function () { + if(document.querySelector('#modal-preview-plot')) { + document.querySelector('#modal-preview-plot').remove(); + } + let modal = $(modals.obsPreviewHtml(this.model.obsData.split('/').pop())).modal(); + let plotEl = document.querySelector('#modal-preview-plot #modal-plot-container'); + console.log(plotEl) + Plotly.newPlot(plotEl, this.obsFig); + }, renderEditParameterSpace: function () { if(this.editParameterSpace) { this.editParameterSpace.remove(); @@ -161,7 +195,7 @@ module.exports = View.extend({ if(this.obsDataLocationSelectView) { this.obsDataLocationSelectView.remove(); } - let value = Boolean(this.model.obsData) ? this.model.obsData : ""; + let value = this.model.obsData !== "" ? this.model.obsData : ""; this.obsDataLocationSelectView = new SelectView({ name: 'obs-data-locations', required: true, @@ -197,6 +231,7 @@ module.exports = View.extend({ this.renderViewParameterSpace(); }, selectObsDataFile: function (e) { + this.obsFig = null; let value = e.target.value; var msgDisplay = "none"; var contDisplay = "none"; @@ -204,8 +239,8 @@ module.exports = View.extend({ if(this.obsDataFiles.paths[value].length > 1) { msgDisplay = "block"; contDisplay = "inline-block"; - this.renderObsDataLocationSelectView(value); this.model.obsData = ""; + this.renderObsDataLocationSelectView(value); }else{ this.model.obsData = this.obsDataFiles.paths[value][0]; } @@ -216,6 +251,7 @@ module.exports = View.extend({ $(this.queryByHook("obs-data-location-container")).css("display", contDisplay); }, selectObsDataLocation: function (e) { + this.obsFig = null; this.model.obsData = e.target.value ? e.target.value : ""; }, startAction: function () { diff --git a/client/styles/styles.css b/client/styles/styles.css index 2c53e6aa7c..73badd8e7a 100644 --- a/client/styles/styles.css +++ b/client/styles/styles.css @@ -409,6 +409,10 @@ input[type="file"]::-ms-browse { margin: 0.5rem auto; } +.modal-dialog.preview-plot { + max-width: 1000px; +} + .modal-backdrop { width: 100%; height: 100%; @@ -423,6 +427,10 @@ input[type="file"]::-ms-browse { width: 60%; } +.modal-content.preview-plot { + max-width: 1000px; +} + .spinner-border { display: none; position: relative; From d236e9bda763c77ba0574b2e5ffefab10c4d2e7f Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Wed, 22 Feb 2023 10:26:15 -0500 Subject: [PATCH 025/109] Merged changes from develop. --- stochss/handlers/util/stochss_base.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/stochss/handlers/util/stochss_base.py b/stochss/handlers/util/stochss_base.py index 30c8efad44..a50d07608c 100644 --- a/stochss/handlers/util/stochss_base.py +++ b/stochss/handlers/util/stochss_base.py @@ -36,11 +36,8 @@ class StochSSBase(): ''' user_dir = os.path.expanduser("~") # returns the path to the users home directory TEMPLATE_VERSION = 1 -<<<<<<< HEAD SETTINGS_TEMPLATE_VERSION = 1 -======= DOMAIN_TEMPLATE_VERSION = 2 ->>>>>>> 1e296b8bf538a24466440cfc96bb8119f59807f2 def __init__(self, path): ''' From bd3102699e03fd6ec12dcb310bea2a284258f513 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Wed, 22 Feb 2023 12:53:05 -0500 Subject: [PATCH 026/109] Added a summary statistis collection to the inference settings. --- client/models/inference-settings.js | 11 ++++- client/models/summary-stat.js | 62 +++++++++++++++++++++++++++++ client/models/summary-stats.js | 50 +++++++++++++++++++++++ 3 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 client/models/summary-stat.js create mode 100644 client/models/summary-stats.js diff --git a/client/models/inference-settings.js b/client/models/inference-settings.js index c20ed4b231..66fc08f732 100644 --- a/client/models/inference-settings.js +++ b/client/models/inference-settings.js @@ -16,6 +16,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ //collections +let SummaryStats = require('./summary-stats'); let InferenceParameters = require('./inference-parameters'); //models let State = require('ampersand-state'); @@ -24,10 +25,11 @@ module.exports = State.extend({ props: { obsData: 'string', priorMethod: 'string', - summaryStats: 'string' + summaryStatsType: 'string' }, collections: { - parameters: InferenceParameters + parameters: InferenceParameters, + summaryStats: SummaryStats }, derived: { elementID: { @@ -42,5 +44,10 @@ module.exports = State.extend({ }, initialize: function(attrs, options) { State.prototype.initialize.apply(this, arguments); + }, + resetNewSummaryStats: function () { + let summaryStats = this.summaryStats; + this.summaryStats = new SummaryStats(); + return summaryStats; } }); diff --git a/client/models/summary-stat.js b/client/models/summary-stat.js new file mode 100644 index 0000000000..02068d1f77 --- /dev/null +++ b/client/models/summary-stat.js @@ -0,0 +1,62 @@ +/* +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 . +*/ +//models +let State = require('ampersand-state'); + +module.exports = State.extend({ + props: { + args: 'object', + formula: 'string', + name: 'string' + }, + derived: { + elementID: { + deps: ["collection"], + fn: function () { + if(this.collection) { + return "ISS" + (this.collection.indexOf(this) + 1); + } + return "ISS-" + } + }, + argsDisplay: { + deps: ["args"], + fn: function () { + if([undefined, null].includes(this.args)) { return "None"; } + let argStrs = []; + this.args.forEach((arg) => { + argStrs.push(JSON.stringify(arg)); + }); + console.log(argStrs.join()) + return argStrs.join(); + } + } + }, + initialize: function(attrs, options) { + State.prototype.initialize.apply(this, arguments); + }, + setArgs: function (argStr) { + let argStrs = argStr.replace(/ /g, '').split(','); + let args = []; + argStrs.forEach((arg) => { + args.push(JSON.parse(arg)); + }); + this.args = args; + console.log(this.args) + } +}); diff --git a/client/models/summary-stats.js b/client/models/summary-stats.js new file mode 100644 index 0000000000..640eb298db --- /dev/null +++ b/client/models/summary-stats.js @@ -0,0 +1,50 @@ +/* +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('underscore'); +//models +let SummaryStat = require('./summary-stat'); +//collections +let Collection = require('ampersand-collection'); + +module.exports = Collection.extend({ + model: SummaryStat, + addSummaryStat: function ({name=null}={}) { + if(name === null) { + name = getDefaultName(); + } + let summaryStat = new SummaryStat({ + formula: '', + name: name + }); + summaryStat.setArgs(null); + this.add(summaryStat); + }, + getDefaultName: function () { + var i = this.length + 1; + var name = 'obs' + i; + var names = this.map((summaryStat) => { return summaryStat.name; }); + while(_.contains(names, name)) { + i += 1; + name = 'obs' + i; + } + return name; + }, + removeSummaryStat: function (summaryStat) { + this.remove(summaryStat); + } +}); From f857daba04dfdc3655072d9fbce50b220a333535 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Wed, 22 Feb 2023 13:10:40 -0500 Subject: [PATCH 027/109] Added the new summary stats collection to the settings template. --- stochss_templates/workflowSettingsTemplate.json | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/stochss_templates/workflowSettingsTemplate.json b/stochss_templates/workflowSettingsTemplate.json index d3e7d727cb..0a9d80a239 100644 --- a/stochss_templates/workflowSettingsTemplate.json +++ b/stochss_templates/workflowSettingsTemplate.json @@ -3,7 +3,17 @@ "obsData": "", "parameters": [], "priorMethod": "Uniform Prior", - "summaryStats": "" + "summaryStatsType": "minimal", + "summaryStats": [ + {"sum_values": null}, + {"median": null}, + {"mean": null}, + {"length": null}, + {"standard_deviation": null}, + {"variance": null}, + {"maximum": null}, + {"minimum": null} + ], }, "parameterSweepSettings":{ "parameters": [], From 9076f546275dd522f703163fb0d14e8f11588598 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Wed, 22 Feb 2023 13:11:21 -0500 Subject: [PATCH 028/109] Added support for empty args in set args. --- client/models/summary-stat.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/client/models/summary-stat.js b/client/models/summary-stat.js index 02068d1f77..3fa191fc48 100644 --- a/client/models/summary-stat.js +++ b/client/models/summary-stat.js @@ -51,12 +51,16 @@ module.exports = State.extend({ State.prototype.initialize.apply(this, arguments); }, setArgs: function (argStr) { - let argStrs = argStr.replace(/ /g, '').split(','); - let args = []; - argStrs.forEach((arg) => { - args.push(JSON.parse(arg)); - }); - this.args = args; + if(argStr === "") { + this.args = null; + }else{ + let argStrs = argStr.replace(/ /g, '').split(','); + let args = []; + argStrs.forEach((arg) => { + args.push(JSON.parse(arg)); + }); + this.args = args; + } console.log(this.args) } }); From 21723f0376d0925240c7766fa5b43e6377e48462 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Wed, 22 Feb 2023 13:39:42 -0500 Subject: [PATCH 029/109] Fixed settings template file. --- .../workflowSettingsTemplate.json | 52 ++++++++++++------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/stochss_templates/workflowSettingsTemplate.json b/stochss_templates/workflowSettingsTemplate.json index 0a9d80a239..e15d38fe9d 100644 --- a/stochss_templates/workflowSettingsTemplate.json +++ b/stochss_templates/workflowSettingsTemplate.json @@ -1,35 +1,51 @@ { - "inferenceSettings":{ + "inferenceSettings": { "obsData": "", "parameters": [], "priorMethod": "Uniform Prior", - "summaryStatsType": "minimal", "summaryStats": [ - {"sum_values": null}, - {"median": null}, - {"mean": null}, - {"length": null}, - {"standard_deviation": null}, - {"variance": null}, - {"maximum": null}, - {"minimum": null} + { + "sum_values": null + }, + { + "median": null + }, + { + "mean": null + }, + { + "length": null + }, + { + "standard_deviation": null + }, + { + "variance": null + }, + { + "maximum": null + }, + { + "minimum": null + } ], + "summaryStatsType": "minimal" }, - "parameterSweepSettings":{ + "parameterSweepSettings": { "parameters": [], "speciesOfInterest": {} }, - "resultsSettings":{ + "resultsSettings": { "mapper": "final", - "reducer": "avg", - "outputs": [] + "outputs": [], + "reducer": "avg" }, "simulationSettings": { + "absoluteTol": 1e-06, + "algorithm": "SSA", "isAutomatic": true, - "relativeTol": 1e-3, - "absoluteTol": 1e-6, "realizations": 100, - "algorithm": "SSA", + "relativeTol": 0.001, "seed": -1, "tauTol": 0.03 }, @@ -37,6 +53,6 @@ "timespanSettings": { "endSim": 20, "timeStep": 0.05, - "timestepSize": 1e-5 + "timestepSize": 1e-05 } } \ No newline at end of file From 3ea761024b7d5c8484a37bec843c6133f5cdfe89 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Thu, 23 Feb 2023 08:31:40 -0500 Subject: [PATCH 030/109] Added base views and templatesfor identity summary statistics. --- client/models/inference-settings.js | 2 +- .../templates/editIdentitySummaryStatView.pug | 18 ++++ .../templates/editIdentitySummaryStats.pug | 24 ++++++ .../templates/inferenceSettingsView.pug | 16 +++- .../templates/viewIdentitySummaryStatView.pug | 12 +++ .../templates/viewIdentitySummaryStats.pug | 15 ++++ .../views/identity-summary-stat-view.js | 51 ++++++++++++ .../views/inference-settings-view.js | 34 +++++++- .../settings-view/views/summary-stats-view.js | 82 +++++++++++++++++++ 9 files changed, 248 insertions(+), 6 deletions(-) create mode 100644 client/settings-view/templates/editIdentitySummaryStatView.pug create mode 100644 client/settings-view/templates/editIdentitySummaryStats.pug create mode 100644 client/settings-view/templates/viewIdentitySummaryStatView.pug create mode 100644 client/settings-view/templates/viewIdentitySummaryStats.pug create mode 100644 client/settings-view/views/identity-summary-stat-view.js create mode 100644 client/settings-view/views/summary-stats-view.js diff --git a/client/models/inference-settings.js b/client/models/inference-settings.js index 66fc08f732..6759d7f998 100644 --- a/client/models/inference-settings.js +++ b/client/models/inference-settings.js @@ -45,7 +45,7 @@ module.exports = State.extend({ initialize: function(attrs, options) { State.prototype.initialize.apply(this, arguments); }, - resetNewSummaryStats: function () { + resetSummaryStats: function () { let summaryStats = this.summaryStats; this.summaryStats = new SummaryStats(); return summaryStats; diff --git a/client/settings-view/templates/editIdentitySummaryStatView.pug b/client/settings-view/templates/editIdentitySummaryStatView.pug new file mode 100644 index 0000000000..fd55201193 --- /dev/null +++ b/client/settings-view/templates/editIdentitySummaryStatView.pug @@ -0,0 +1,18 @@ +div.mx-1 + + if(this.model.collection.indexOf(this.model) !== 0) + hr + + div.row + + div.col-sm-2 + + div(data-hook="summary-stat-name") + + div.col-sm-8 + + div(data-hook="summary-stat-formula") + + div.col-sm-2 + + button.btn.btn-outline-secondary.box-shadow(data-hook="remove") X diff --git a/client/settings-view/templates/editIdentitySummaryStats.pug b/client/settings-view/templates/editIdentitySummaryStats.pug new file mode 100644 index 0000000000..aad330d877 --- /dev/null +++ b/client/settings-view/templates/editIdentitySummaryStats.pug @@ -0,0 +1,24 @@ +div + + div + + hr + + div.mx-1.row.head.align-items-baseline + + div.col-sm-2 + + h6.inline Name + + div.col-sm-8 + + h6.inline Formula + + div.col-sm-2 + + h6 Remove + + div.my-3(data-hook="edit-summary-stat-collection") + + button.btn.btn-outline-secondary.box-shadow(data-hook="add-identity-summary-stat") Add Summary Statistic + diff --git a/client/settings-view/templates/inferenceSettingsView.pug b/client/settings-view/templates/inferenceSettingsView.pug index 0afe84a746..43c48ce862 100644 --- a/client/settings-view/templates/inferenceSettingsView.pug +++ b/client/settings-view/templates/inferenceSettingsView.pug @@ -46,15 +46,19 @@ div#inference-settings.card div(data-hook="summary-statistics") - div(data-hook="edit-parameter-space-container") + h4 Summary Statistics - div.mt-3.accordion(id="obs-data-section" data-hook="obs-data-section") + div(data-hook="edit-summary-stats-container") + + div.my-3(data-hook="edit-parameter-space-container") + + div.accordion(id="obs-data-section" data-hook="obs-data-section") div.card div.card-header(id="importObsDataHeader") - h3.mb-0.inline="Import Observed Data File" + h4.mb-0.inline="Import Observed Data File" button.btn.btn-outline-collapse.collapsed.inline(type="button" data-toggle="collapse" data-target="#collapseImportObsData" data-hook="collapseImportObsData" aria-expanded="false" aria-controls="collapseImportObsData" style="float: right") @@ -96,7 +100,7 @@ div#inference-settings.card div.card-header(id="uploadObsDataHeader") - h3.mb-0.inline="Select Observed Data File" + h4.mb-0.inline="Select Observed Data File" button.btn.btn-outline-collapse.collapsed.inline(type="button" data-toggle="collapse" data-target="#collapseUploadObsData" data-hook="collapseUploadObsData" aria-expanded="false" aria-controls="collapseUploadObsData" style="float: right") @@ -156,4 +160,8 @@ div#inference-settings.card div(data-hook="view-summary-stats")=this.model.summaryStats + h4 Summary Statistics + + div.mb-3(data-hook="view-summary-stats-container") + div(data-hook="view-parameter-space-container") diff --git a/client/settings-view/templates/viewIdentitySummaryStatView.pug b/client/settings-view/templates/viewIdentitySummaryStatView.pug new file mode 100644 index 0000000000..f025506c69 --- /dev/null +++ b/client/settings-view/templates/viewIdentitySummaryStatView.pug @@ -0,0 +1,12 @@ +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-10=this.model.formula diff --git a/client/settings-view/templates/viewIdentitySummaryStats.pug b/client/settings-view/templates/viewIdentitySummaryStats.pug new file mode 100644 index 0000000000..49b531d58c --- /dev/null +++ b/client/settings-view/templates/viewIdentitySummaryStats.pug @@ -0,0 +1,15 @@ +div + + hr + + div.mx-1.row.head.align-items-baseline + + div.col-sm-2 + + h6.inline Name + + div.col-sm-10 + + h6.inline Formula + + div.my-3(data-hook="view-summary-stat-collection") \ No newline at end of file diff --git a/client/settings-view/views/identity-summary-stat-view.js b/client/settings-view/views/identity-summary-stat-view.js new file mode 100644 index 0000000000..75e2f219e2 --- /dev/null +++ b/client/settings-view/views/identity-summary-stat-view.js @@ -0,0 +1,51 @@ +/* +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 tests = require('../../views/tests'); +//views +// let InputView = require('../../views/input'); +let View = require('ampersand-view'); +// let SelectView = require('ampersand-select-view'); +//templates +let editTemplate = require('../templates/editIdentitySummaryStatView.pug'); +let viewTemplate = require('../templates/viewIdentitySummaryStatView.pug'); + +module.exports = View.extend({ + events: {}, + initialize: function (attrs, options) { + View.prototype.initialize.apply(this, arguments); + this.viewMode = attrs.viewMode ? attrs.viewMode : false; + // if(!this.viewMode) { + // } + }, + render: function (attrs, options) { + this.template = this.viewMode ? viewTemplate : editTemplate; + View.prototype.render.apply(this, arguments); + // if(!this.viewMode){ + // } + }, + update: function () {}, + updateValid: function () {}, + updateViewer: function () { + this.parent.updateViewer(); + } +}); diff --git a/client/settings-view/views/inference-settings-view.js b/client/settings-view/views/inference-settings-view.js index 841e8b75d7..9f3ea12aaf 100644 --- a/client/settings-view/views/inference-settings-view.js +++ b/client/settings-view/views/inference-settings-view.js @@ -25,6 +25,7 @@ let Plotly = require('plotly.js-dist'); let View = require('ampersand-view'); let InputView = require('../../views/input'); let SelectView = require('ampersand-select-view'); +let SummaryStatsView = require('./summary-stats-view'); let InferenceParametersView = require('./inference-parameters-view'); //templates let template = require('../templates/inferenceSettingsView.pug'); @@ -70,6 +71,9 @@ module.exports = View.extend({ ` } + this.summaryStatCollections = { + identity: null, minimal: null, custom: null + } }, render: function () { View.prototype.render.apply(this, arguments); @@ -83,9 +87,11 @@ module.exports = View.extend({ $(this.queryByHook(this.model.elementID + '-edit-inference-settings')).removeClass('active'); $(this.queryByHook(this.model.elementID + '-view-inference-settings')).addClass('active'); }else{ + this.renderEditSummaryStats(); this.renderEditParameterSpace(); this.renderObsDataSelects(); } + this.renderViewSummaryStats(); this.renderViewParameterSpace(); }, changeCollapseButtonText: function (e) { @@ -143,7 +149,6 @@ module.exports = View.extend({ } let modal = $(modals.obsPreviewHtml(this.model.obsData.split('/').pop())).modal(); let plotEl = document.querySelector('#modal-preview-plot #modal-plot-container'); - console.log(plotEl) Plotly.newPlot(plotEl, this.obsFig); }, renderEditParameterSpace: function () { @@ -158,6 +163,21 @@ module.exports = View.extend({ let hook = "edit-parameter-space-container"; app.registerRenderSubview(this, this.editParameterSpace, hook); }, + renderEditSummaryStats: function () { + if(this.editSummaryStats) { + this.editSummaryStats.remove(); + } + // This block is for testing only + this.model.summaryStatsType = "identity"; + this.summaryStatCollections[this.model.summaryStatsType] = this.model.resetSummaryStats(); + // End testing block + this.editSummaryStats = new SummaryStatsView({ + collection: this.model.summaryStats, + summariesType: this.model.summaryStatsType + }); + let hook = "edit-summary-stats-container"; + app.registerRenderSubview(this, this.editSummaryStats, hook); + }, renderObsDataSelects: function () { let queryStr = "?ext=.obsd,.csv" let endpoint = `${path.join(app.getApiPath(), 'workflow/obs-data-files')}${queryStr}`; @@ -220,6 +240,18 @@ module.exports = View.extend({ let hook = "view-parameter-space-container"; app.registerRenderSubview(this, this.viewParameterSpace, hook); }, + renderViewSummaryStats: function () { + if(this.viewSummaryStats) { + this.viewSummaryStats.remove(); + } + this.viewSummaryStats = new SummaryStatsView({ + collection: this.model.summaryStats, + summariesType: this.model.summaryStatsType, + readOnly: true + }); + let hook = "view-summary-stats-container"; + app.registerRenderSubview(this, this.viewSummaryStats, hook); + }, setObsDataFile: function (e) { this.obsDataFile = e.target.files[0]; $(this.queryByHook("import-obs-data-file")).prop('disabled', !this.obsDataFile); diff --git a/client/settings-view/views/summary-stats-view.js b/client/settings-view/views/summary-stats-view.js new file mode 100644 index 0000000000..bd19fdd522 --- /dev/null +++ b/client/settings-view/views/summary-stats-view.js @@ -0,0 +1,82 @@ +/* +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'); +//support files +// let Tooltips = require('../../tooltips'); +//views +let View = require('ampersand-view'); +let IdentitySummaryStatView = require('./identity-summary-stat-view'); +//templates +let editIdentityTemplate = require('../templates/editIdentitySummaryStats.pug'); +let viewIdentityTemplate = require('../templates/viewIdentitySummaryStats.pug'); + +module.exports = View.extend({ + events: {}, + initialize: function (attrs, options) { + View.prototype.initialize.apply(this, arguments); + this.readOnly = attrs.readOnly ? attrs.readOnly : false; + this.summariesType = attrs.summariesType; + // if(!this.readOnly) { + // } + }, + render: function () { + // if(this.summariesType === "identity") { + // this.template = this.readOnly ? viewIdentityTemplate : editIdentityTemplate; + // } + this.template = this.readOnly ? viewIdentityTemplate : editIdentityTemplate; + View.prototype.render.apply(this, arguments); + if(!this.readOnly) { + this.renderEditSummaryStat(); + }else{ + this.renderViewSummaryStat(); + } + }, + renderEditSummaryStat: function () { + if(this.editSummaryStat) { + this.editSummaryStat.remove(); + } + // if(this.summariesType === "identity") { + // var summaryStatView = IdentitySummaryStatView; + // } + var summaryStatView = IdentitySummaryStatView; + this.editSummaryStat = this.renderCollection( + this.collection, + summaryStatView, + this.queryByHook("edit-summary-stat-collection"), + ); + }, + renderViewSummaryStat: function () { + if(this.viewSummaryStat) { + this.viewSummaryStat.remove(); + } + let options = {"viewOptions": { viewMode: true }}; + // if(this.summariesType === "identity") { + // var summaryStatView = IdentitySummaryStatView; + // } + var summaryStatView = IdentitySummaryStatView; + this.viewSummaryStat = this.renderCollection( + this.collection, + summaryStatView, + this.queryByHook("view-summary-stat-collection"), + options + ); + }, + updateViewer: function () { + this.parent.renderViewSummaryStats(); + } +}); From 2d607ee8444fb11f323cfcddca6208a62de9efc8 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Thu, 23 Feb 2023 09:12:59 -0500 Subject: [PATCH 031/109] Added functionality to identity summary stat views. --- client/models/summary-stats.js | 4 +- .../templates/editIdentitySummaryStatView.pug | 4 +- .../templates/viewIdentitySummaryStatView.pug | 2 +- .../views/identity-summary-stat-view.js | 42 +++++++++++++++++-- .../settings-view/views/summary-stats-view.js | 9 +++- 5 files changed, 52 insertions(+), 9 deletions(-) diff --git a/client/models/summary-stats.js b/client/models/summary-stats.js index 640eb298db..794eaa586e 100644 --- a/client/models/summary-stats.js +++ b/client/models/summary-stats.js @@ -25,13 +25,13 @@ module.exports = Collection.extend({ model: SummaryStat, addSummaryStat: function ({name=null}={}) { if(name === null) { - name = getDefaultName(); + name = this.getDefaultName(); } let summaryStat = new SummaryStat({ formula: '', name: name }); - summaryStat.setArgs(null); + summaryStat.setArgs(""); this.add(summaryStat); }, getDefaultName: function () { diff --git a/client/settings-view/templates/editIdentitySummaryStatView.pug b/client/settings-view/templates/editIdentitySummaryStatView.pug index fd55201193..b83a80bec7 100644 --- a/client/settings-view/templates/editIdentitySummaryStatView.pug +++ b/client/settings-view/templates/editIdentitySummaryStatView.pug @@ -7,11 +7,11 @@ div.mx-1 div.col-sm-2 - div(data-hook="summary-stat-name") + div(data-target="identity-property" data-hook="summary-stat-name") div.col-sm-8 - div(data-hook="summary-stat-formula") + div(data-target="identity-property" data-hook="summary-stat-formula") div.col-sm-2 diff --git a/client/settings-view/templates/viewIdentitySummaryStatView.pug b/client/settings-view/templates/viewIdentitySummaryStatView.pug index f025506c69..c99c9e9732 100644 --- a/client/settings-view/templates/viewIdentitySummaryStatView.pug +++ b/client/settings-view/templates/viewIdentitySummaryStatView.pug @@ -9,4 +9,4 @@ div.mx-1 div.pl-2=this.model.name - div.col-sm-10=this.model.formula + div.col-sm-10=this.model.formula ? this.model.formula : "None" diff --git a/client/settings-view/views/identity-summary-stat-view.js b/client/settings-view/views/identity-summary-stat-view.js index 75e2f219e2..e685e1a166 100644 --- a/client/settings-view/views/identity-summary-stat-view.js +++ b/client/settings-view/views/identity-summary-stat-view.js @@ -20,9 +20,9 @@ along with this program. If not, see . // let _ = require('underscore'); //support files // let app = require('../../app'); -// let tests = require('../../views/tests'); +let tests = require('../../views/tests'); //views -// let InputView = require('../../views/input'); +let InputView = require('../../views/input'); let View = require('ampersand-view'); // let SelectView = require('ampersand-select-view'); //templates @@ -30,7 +30,10 @@ let editTemplate = require('../templates/editIdentitySummaryStatView.pug'); let viewTemplate = require('../templates/viewIdentitySummaryStatView.pug'); module.exports = View.extend({ - events: {}, + events: { + 'change [data-target=identity-property]' : 'updateViewer', + 'click [data-hook=remove]' : 'removeSummaryStat' + }, initialize: function (attrs, options) { View.prototype.initialize.apply(this, arguments); this.viewMode = attrs.viewMode ? attrs.viewMode : false; @@ -43,9 +46,42 @@ module.exports = View.extend({ // if(!this.viewMode){ // } }, + removeSummaryStat: function () { + this.model.collection.removeSummaryStat(this.model); + }, update: function () {}, updateValid: function () {}, updateViewer: function () { this.parent.updateViewer(); + }, + subviews: { + nameInputView: { + hook: "summary-stat-name", + prepareView: function (el) { + return new InputView({ + parent: this, + required: true, + name: 'observable-name', + modelKey: 'name', + tests: tests.nameTests, + valueType: 'string', + value: this.model.name + }); + } + }, + formulaInputView: { + hook: "summary-stat-formula", + prepareView: function (el) { + return new InputView({ + parent: this, + required: false, + name: 'observable-calculator', + modelKey: 'formula', + valueType: 'string', + value: this.model.formula, + placeholder: "-- Formula in terms of the model's species --" + }); + } + } } }); diff --git a/client/settings-view/views/summary-stats-view.js b/client/settings-view/views/summary-stats-view.js index bd19fdd522..43bce0ad1a 100644 --- a/client/settings-view/views/summary-stats-view.js +++ b/client/settings-view/views/summary-stats-view.js @@ -26,7 +26,9 @@ let editIdentityTemplate = require('../templates/editIdentitySummaryStats.pug'); let viewIdentityTemplate = require('../templates/viewIdentitySummaryStats.pug'); module.exports = View.extend({ - events: {}, + events: { + 'click [data-hook=add-identity-summary-stat]' : 'addSummaryStatistic' + }, initialize: function (attrs, options) { View.prototype.initialize.apply(this, arguments); this.readOnly = attrs.readOnly ? attrs.readOnly : false; @@ -46,6 +48,11 @@ module.exports = View.extend({ this.renderViewSummaryStat(); } }, + addSummaryStatistic: function () { + if(this.summariesType === "identity") { + this.collection.addSummaryStat(); + } + }, renderEditSummaryStat: function () { if(this.editSummaryStat) { this.editSummaryStat.remove(); From 805f711a4a407f1a1cca996a30ce43b1f1f8f450 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Thu, 23 Feb 2023 09:18:09 -0500 Subject: [PATCH 032/109] Updated the headers for the parameter space configuration section. --- client/settings-view/templates/editUniformParameters.pug | 2 +- client/settings-view/templates/viewUniformParameters.pug | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/settings-view/templates/editUniformParameters.pug b/client/settings-view/templates/editUniformParameters.pug index bbec71a885..eae415f497 100644 --- a/client/settings-view/templates/editUniformParameters.pug +++ b/client/settings-view/templates/editUniformParameters.pug @@ -2,7 +2,7 @@ div div - h5 Parameter Space Configuration + h4 Parameter Space Configuration hr diff --git a/client/settings-view/templates/viewUniformParameters.pug b/client/settings-view/templates/viewUniformParameters.pug index 9c0cd42e42..3062b74601 100644 --- a/client/settings-view/templates/viewUniformParameters.pug +++ b/client/settings-view/templates/viewUniformParameters.pug @@ -1,6 +1,6 @@ div - h5.mt-3 Parameter Space Configuration + h4.mt-3 Parameter Space Configuration hr From 925e3d283e24839881dbbf0880c4ee8876b90cc9 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Thu, 23 Feb 2023 09:20:57 -0500 Subject: [PATCH 033/109] Added an indexes property to the summary stats collection to support get by name. --- client/models/summary-stats.js | 1 + 1 file changed, 1 insertion(+) diff --git a/client/models/summary-stats.js b/client/models/summary-stats.js index 794eaa586e..80a4f558d2 100644 --- a/client/models/summary-stats.js +++ b/client/models/summary-stats.js @@ -23,6 +23,7 @@ let Collection = require('ampersand-collection'); module.exports = Collection.extend({ model: SummaryStat, + indexes: ['name'], addSummaryStat: function ({name=null}={}) { if(name === null) { name = this.getDefaultName(); From 9bc9e31c622c1a8a4193fe5013604729a564a1c2 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Thu, 23 Feb 2023 12:08:32 -0500 Subject: [PATCH 034/109] Added view and template files for custom summary statistics. Added tooltips for summary statistics. --- .../templates/editCustomSummaryStatView.pug | 18 ++++ .../templates/editCustomSummaryStats.pug | 27 ++++++ .../templates/editIdentitySummaryStats.pug | 7 +- .../templates/inferenceSettingsView.pug | 8 +- .../templates/viewCustomSummaryStatView.pug | 12 +++ .../templates/viewCustomSummaryStats.pug | 15 ++++ .../templates/viewIdentitySummaryStats.pug | 2 +- .../views/custom-summary-stat-view.js | 84 +++++++++++++++++++ .../views/identity-summary-stat-view.js | 10 +-- .../views/inference-settings-view.js | 2 +- .../settings-view/views/summary-stats-view.js | 41 +++++---- client/tooltips.js | 11 +++ 12 files changed, 205 insertions(+), 32 deletions(-) create mode 100644 client/settings-view/templates/editCustomSummaryStatView.pug create mode 100644 client/settings-view/templates/editCustomSummaryStats.pug create mode 100644 client/settings-view/templates/viewCustomSummaryStatView.pug create mode 100644 client/settings-view/templates/viewCustomSummaryStats.pug create mode 100644 client/settings-view/views/custom-summary-stat-view.js diff --git a/client/settings-view/templates/editCustomSummaryStatView.pug b/client/settings-view/templates/editCustomSummaryStatView.pug new file mode 100644 index 0000000000..68ff26f09a --- /dev/null +++ b/client/settings-view/templates/editCustomSummaryStatView.pug @@ -0,0 +1,18 @@ +div.mx-1 + + if(this.model.collection.indexOf(this.model) !== 0) + hr + + div.row + + div.col-sm-2 + + div(data-target="identity-property" data-hook="summary-stat-name") + + div.col-sm-8 + + div(data-hook="summary-stat-args") + + div.col-sm-2 + + button.btn.btn-outline-secondary.box-shadow(data-hook="remove") X diff --git a/client/settings-view/templates/editCustomSummaryStats.pug b/client/settings-view/templates/editCustomSummaryStats.pug new file mode 100644 index 0000000000..b044706c4d --- /dev/null +++ b/client/settings-view/templates/editCustomSummaryStats.pug @@ -0,0 +1,27 @@ +div + + div + + hr + + div.mx-1.row.head.align-items-baseline + + div.col-sm-2 + + h6.inline Feature Calculator + + div.tooltip-icon(data-html="true" data-toggle="tooltip" title=this.tooltips.customName) + + div.col-sm-8 + + h6.inline Args + + div.tooltip-icon(data-html="true" data-toggle="tooltip" title=this.tooltips.customArgs) + + div.col-sm-2 + + h6 Remove + + div.my-3(data-hook="edit-summary-stat-collection") + + button.btn.btn-outline-secondary.box-shadow(data-hook="add-summary-stat") Add Summary Statistic diff --git a/client/settings-view/templates/editIdentitySummaryStats.pug b/client/settings-view/templates/editIdentitySummaryStats.pug index aad330d877..8896578253 100644 --- a/client/settings-view/templates/editIdentitySummaryStats.pug +++ b/client/settings-view/templates/editIdentitySummaryStats.pug @@ -10,15 +10,18 @@ div h6.inline Name + div.tooltip-icon(data-html="true" data-toggle="tooltip" title=this.tooltips.identityName) + div.col-sm-8 h6.inline Formula + div.tooltip-icon(data-html="true" data-toggle="tooltip" title=this.tooltips.identityFormula) + div.col-sm-2 h6 Remove div.my-3(data-hook="edit-summary-stat-collection") - button.btn.btn-outline-secondary.box-shadow(data-hook="add-identity-summary-stat") Add Summary Statistic - + button.btn.btn-outline-secondary.box-shadow(data-hook="add-summary-stat") Add Summary Statistic diff --git a/client/settings-view/templates/inferenceSettingsView.pug b/client/settings-view/templates/inferenceSettingsView.pug index 43c48ce862..2e9edd3802 100644 --- a/client/settings-view/templates/inferenceSettingsView.pug +++ b/client/settings-view/templates/inferenceSettingsView.pug @@ -46,9 +46,13 @@ div#inference-settings.card div(data-hook="summary-statistics") - h4 Summary Statistics + div + + h4 Summary Statistics + + p.hidden(data-hook="tsfresh-docs-link") See TSFresh Documentation for a full list of supported feature calculators. - div(data-hook="edit-summary-stats-container") + div(data-hook="edit-summary-stats-container") div.my-3(data-hook="edit-parameter-space-container") diff --git a/client/settings-view/templates/viewCustomSummaryStatView.pug b/client/settings-view/templates/viewCustomSummaryStatView.pug new file mode 100644 index 0000000000..25c8f5c31d --- /dev/null +++ b/client/settings-view/templates/viewCustomSummaryStatView.pug @@ -0,0 +1,12 @@ +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-10=this.model.argsDisplay diff --git a/client/settings-view/templates/viewCustomSummaryStats.pug b/client/settings-view/templates/viewCustomSummaryStats.pug new file mode 100644 index 0000000000..c808548739 --- /dev/null +++ b/client/settings-view/templates/viewCustomSummaryStats.pug @@ -0,0 +1,15 @@ +div + + hr + + div.mx-1.row.head.align-items-baseline + + div.col-sm-2 + + h6.inline Name + + div.col-sm-10 + + h6.inline Args + + div.my-3(data-hook="view-summary-stat-collection") diff --git a/client/settings-view/templates/viewIdentitySummaryStats.pug b/client/settings-view/templates/viewIdentitySummaryStats.pug index 49b531d58c..0831ba2eaf 100644 --- a/client/settings-view/templates/viewIdentitySummaryStats.pug +++ b/client/settings-view/templates/viewIdentitySummaryStats.pug @@ -12,4 +12,4 @@ div h6.inline Formula - div.my-3(data-hook="view-summary-stat-collection") \ No newline at end of file + div.my-3(data-hook="view-summary-stat-collection") diff --git a/client/settings-view/views/custom-summary-stat-view.js b/client/settings-view/views/custom-summary-stat-view.js new file mode 100644 index 0000000000..862705bfe1 --- /dev/null +++ b/client/settings-view/views/custom-summary-stat-view.js @@ -0,0 +1,84 @@ +/* +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 . +*/ + +//support files +let tests = require('../../views/tests'); +//views +let InputView = require('../../views/input'); +let View = require('ampersand-view'); +//templates +let editTemplate = require('../templates/editCustomSummaryStatView.pug'); +let viewTemplate = require('../templates/viewCustomSummaryStatView.pug'); + +module.exports = View.extend({ + events: { + 'change [data-target=identity-property]' : 'updateViewer', + 'change [data-hook=summary-stat-args]' : 'setCalculatorArgs', + 'click [data-hook=remove]' : 'removeSummaryStat' + }, + 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); + }, + removeSummaryStat: function () { + this.model.collection.removeSummaryStat(this.model); + }, + setCalculatorArgs: function (e) { + this.model.setArgs(e.target.value); + this.updateViewer(); + }, + update: function () {}, + updateValid: function () {}, + updateViewer: function () { + this.parent.updateViewer(); + }, + subviews: { + nameInputView: { + hook: "summary-stat-name", + prepareView: function (el) { + return new InputView({ + parent: this, + required: true, + name: 'observable-name', + modelKey: 'name', + tests: tests.nameTests, + valueType: 'string', + value: this.model.name, + placeholder: "-- i.e. autocorrelation --" + }); + } + }, + formulaInputView: { + hook: "summary-stat-args", + prepareView: function (el) { + return new InputView({ + parent: this, + required: false, + name: 'summary-calculator-args', + valueType: 'string', + value: this.model.argsDisplay === "None" ? "" : this.model.argsDisplay, + placeholder: '-- i.e. Requires arguments: {"lag":1} or leave blank if no arguments are required --' + }); + } + } + } +}); diff --git a/client/settings-view/views/identity-summary-stat-view.js b/client/settings-view/views/identity-summary-stat-view.js index e685e1a166..2153e3784d 100644 --- a/client/settings-view/views/identity-summary-stat-view.js +++ b/client/settings-view/views/identity-summary-stat-view.js @@ -16,15 +16,11 @@ 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 InputView = require('../../views/input'); let View = require('ampersand-view'); -// let SelectView = require('ampersand-select-view'); //templates let editTemplate = require('../templates/editIdentitySummaryStatView.pug'); let viewTemplate = require('../templates/viewIdentitySummaryStatView.pug'); @@ -37,14 +33,10 @@ module.exports = View.extend({ initialize: function (attrs, options) { View.prototype.initialize.apply(this, arguments); this.viewMode = attrs.viewMode ? attrs.viewMode : false; - // if(!this.viewMode) { - // } }, render: function (attrs, options) { this.template = this.viewMode ? viewTemplate : editTemplate; View.prototype.render.apply(this, arguments); - // if(!this.viewMode){ - // } }, removeSummaryStat: function () { this.model.collection.removeSummaryStat(this.model); @@ -79,7 +71,7 @@ module.exports = View.extend({ modelKey: 'formula', valueType: 'string', value: this.model.formula, - placeholder: "-- Formula in terms of the model's species --" + placeholder: "-- i.e. Juvenile + Susceptible + Exposed + Infected + Diseased --" }); } } diff --git a/client/settings-view/views/inference-settings-view.js b/client/settings-view/views/inference-settings-view.js index 9f3ea12aaf..a85245a95e 100644 --- a/client/settings-view/views/inference-settings-view.js +++ b/client/settings-view/views/inference-settings-view.js @@ -168,8 +168,8 @@ module.exports = View.extend({ this.editSummaryStats.remove(); } // This block is for testing only - this.model.summaryStatsType = "identity"; this.summaryStatCollections[this.model.summaryStatsType] = this.model.resetSummaryStats(); + this.model.summaryStatsType = "custom"; // End testing block this.editSummaryStats = new SummaryStatsView({ collection: this.model.summaryStats, diff --git a/client/settings-view/views/summary-stats-view.js b/client/settings-view/views/summary-stats-view.js index 43bce0ad1a..4a0b0cf37a 100644 --- a/client/settings-view/views/summary-stats-view.js +++ b/client/settings-view/views/summary-stats-view.js @@ -17,30 +17,33 @@ along with this program. If not, see . */ // let $ = require('jquery'); //support files -// let Tooltips = require('../../tooltips'); +let Tooltips = require('../../tooltips'); //views let View = require('ampersand-view'); +let CustomSummaryStatView = require('./custom-summary-stat-view'); let IdentitySummaryStatView = require('./identity-summary-stat-view'); //templates +let editCustomTemplate = require('../templates/editCustomSummaryStats.pug'); +let viewCustomTemplate = require('../templates/viewCustomSummaryStats.pug'); let editIdentityTemplate = require('../templates/editIdentitySummaryStats.pug'); let viewIdentityTemplate = require('../templates/viewIdentitySummaryStats.pug'); module.exports = View.extend({ events: { - 'click [data-hook=add-identity-summary-stat]' : 'addSummaryStatistic' + 'click [data-hook=add-summary-stat]' : 'addSummaryStatistic' }, initialize: function (attrs, options) { View.prototype.initialize.apply(this, arguments); this.readOnly = attrs.readOnly ? attrs.readOnly : false; + this.tooltips = Tooltips.summaryStats; this.summariesType = attrs.summariesType; - // if(!this.readOnly) { - // } }, render: function () { - // if(this.summariesType === "identity") { - // this.template = this.readOnly ? viewIdentityTemplate : editIdentityTemplate; - // } - this.template = this.readOnly ? viewIdentityTemplate : editIdentityTemplate; + if(this.summariesType === "identity") { + this.template = this.readOnly ? viewIdentityTemplate : editIdentityTemplate; + }else if(this.summariesType === "custom"){ + this.template = this.readOnly ? viewCustomTemplate : editCustomTemplate; + } View.prototype.render.apply(this, arguments); if(!this.readOnly) { this.renderEditSummaryStat(); @@ -48,19 +51,22 @@ module.exports = View.extend({ this.renderViewSummaryStat(); } }, - addSummaryStatistic: function () { + addSummaryStatistic: function ({name=""}={}) { if(this.summariesType === "identity") { this.collection.addSummaryStat(); + }else{ + this.collection.addSummaryStat({name: name}); } }, renderEditSummaryStat: function () { if(this.editSummaryStat) { this.editSummaryStat.remove(); } - // if(this.summariesType === "identity") { - // var summaryStatView = IdentitySummaryStatView; - // } - var summaryStatView = IdentitySummaryStatView; + if(this.summariesType === "identity") { + var summaryStatView = IdentitySummaryStatView; + }else if(this.summariesType === "custom"){ + var summaryStatView = CustomSummaryStatView; + } this.editSummaryStat = this.renderCollection( this.collection, summaryStatView, @@ -72,10 +78,11 @@ module.exports = View.extend({ this.viewSummaryStat.remove(); } let options = {"viewOptions": { viewMode: true }}; - // if(this.summariesType === "identity") { - // var summaryStatView = IdentitySummaryStatView; - // } - var summaryStatView = IdentitySummaryStatView; + if(this.summariesType === "identity") { + var summaryStatView = IdentitySummaryStatView; + }else if(this.summariesType === "custom"){ + var summaryStatView = CustomSummaryStatView; + } this.viewSummaryStat = this.renderCollection( this.collection, summaryStatView, diff --git a/client/tooltips.js b/client/tooltips.js index 8165377942..9ec4bcb133 100644 --- a/client/tooltips.js +++ b/client/tooltips.js @@ -161,6 +161,17 @@ module.exports = { steps: "The number of steps used to determine the sweep values across the sweep range." }, + summaryStats: { + customArgs: "Arguments to the TSFresh feature calculator.
  • Leave the feild empty if no arguments are required.
  • " + + '
  • To run once provide a single dictionary. i.e. {"lag":1}
  • ' + + '
  • To run multiple times provide a list of dictionaries. i.e. {"lag":1},{"lag":2}
  • ', + + customName: "Name of the TSFresh feature calculator.", + + identityFormula: "An expression that is evaluable within a python environment and a namespace comprised of the model's species.", + + identityName: "Name of the observable data.", + }, jobResults: { species: "The variables to view sweep data for", From 722632fa8c0f0f7e2cf2138c1fb0939c6ef504cd Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Thu, 23 Feb 2023 14:15:44 -0500 Subject: [PATCH 035/109] Added template files for minimal summary statistics. --- client/models/summary-stat.js | 2 - .../templates/editMinimalSummaryStats.pug | 155 ++++++++++++++++++ .../templates/viewMinimalSummaryStats.pug | 155 ++++++++++++++++++ .../views/inference-settings-view.js | 4 +- .../settings-view/views/summary-stats-view.js | 72 +++++--- 5 files changed, 362 insertions(+), 26 deletions(-) create mode 100644 client/settings-view/templates/editMinimalSummaryStats.pug create mode 100644 client/settings-view/templates/viewMinimalSummaryStats.pug diff --git a/client/models/summary-stat.js b/client/models/summary-stat.js index 3fa191fc48..d515a6c9f3 100644 --- a/client/models/summary-stat.js +++ b/client/models/summary-stat.js @@ -42,7 +42,6 @@ module.exports = State.extend({ this.args.forEach((arg) => { argStrs.push(JSON.stringify(arg)); }); - console.log(argStrs.join()) return argStrs.join(); } } @@ -61,6 +60,5 @@ module.exports = State.extend({ }); this.args = args; } - console.log(this.args) } }); diff --git a/client/settings-view/templates/editMinimalSummaryStats.pug b/client/settings-view/templates/editMinimalSummaryStats.pug new file mode 100644 index 0000000000..e40d16c674 --- /dev/null +++ b/client/settings-view/templates/editMinimalSummaryStats.pug @@ -0,0 +1,155 @@ +div + + hr + + div.mx-1.row.head.align-items-baseline + + div.col-sm-3 + + div.row + + div.col-sm-4 + + h6.inline Enable + + div.col-sm-8 + + h6.inline Feature Calculator + + div.col-sm-3 + + div.row + + div.col-sm-4 + + h6.inline Enable + + div.col-sm-8 + + h6.inline Feature Calculator + + div.col-sm-3 + + div.row + + div.col-sm-4 + + h6.inline Enable + + div.col-sm-8 + + h6.inline Feature Calculator + + div.col-sm-3 + + div.row + + div.col-sm-4 + + h6.inline Enable + + div.col-sm-8 + + h6.inline Feature Calculator + + div.my-3.ml-3.pl-3 + + div.row + + div.pl-0.col-sm-3 + + div.row + + div.col-sm-4 + + input(type="checkbox" data-hook="minimal-sum-values" data-target="minimal" data-name="sum_values") + + div.col-sm-8 + + div Sum of Values + + div.pl-0.col-sm-3 + + div.row + + div.col-sm-4 + + input(type="checkbox" data-hook="minimal-median" data-target="minimal" data-name="median") + + div.col-sm-8 + + div Median + + div.pl-0.col-sm-3 + + div.row + + div.col-sm-4 + + input(type="checkbox" data-hook="minimal-mean" data-target="minimal" data-name="mean") + + div.col-sm-8 + + div Mean + + div.pl-0.col-sm-3 + + div.row + + div.col-sm-4 + + input(type="checkbox" data-hook="minimal-length" data-target="minimal" data-name="length") + + div.col-sm-8 + + div Length + + div.row + + div.pl-0.col-sm-3 + + div.row + + div.col-sm-4 + + input(type="checkbox" data-hook="minimal-standard-deviation" data-target="minimal" data-name="standard_deviation") + + div.col-sm-8 + + div Standard Deviation + + div.pl-0.col-sm-3 + + div.row + + div.col-sm-4 + + input(type="checkbox" data-hook="minimal-variance" data-target="minimal" data-name="variance") + + div.col-sm-8 + + div Variance + + div.pl-0.col-sm-3 + + div.row + + div.col-sm-4 + + input(type="checkbox" data-hook="minimal-maximum" data-target="minimal" data-name="maximum") + + div.col-sm-8 + + div Maximum + + div.pl-0.col-sm-3 + + div.row + + div.col-sm-4 + + input(type="checkbox" data-hook="minimal-minimum" data-target="minimal" data-name="minimum") + + div.col-sm-8 + + div Minimum diff --git a/client/settings-view/templates/viewMinimalSummaryStats.pug b/client/settings-view/templates/viewMinimalSummaryStats.pug new file mode 100644 index 0000000000..a919236c92 --- /dev/null +++ b/client/settings-view/templates/viewMinimalSummaryStats.pug @@ -0,0 +1,155 @@ +div + + hr + + div.mx-1.row.head.align-items-baseline + + div.col-sm-3 + + div.row + + div.col-sm-4 + + h6.inline Enable + + div.col-sm-8 + + h6.inline Feature Calculator + + div.col-sm-3 + + div.row + + div.col-sm-4 + + h6.inline Enable + + div.col-sm-8 + + h6.inline Feature Calculator + + div.col-sm-3 + + div.row + + div.col-sm-4 + + h6.inline Enable + + div.col-sm-8 + + h6.inline Feature Calculator + + div.col-sm-3 + + div.row + + div.col-sm-4 + + h6.inline Enable + + div.col-sm-8 + + h6.inline Feature Calculator + + div.my-3.ml-3.pl-3 + + div.row + + div.pl-0.col-sm-3 + + div.row + + div.col-sm-4 + + input(type="checkbox" data-hook="minimal-sum-values" data-target="minimal" data-name="sum_values" disabled) + + div.col-sm-8 + + div Sum of Values + + div.pl-0.col-sm-3 + + div.row + + div.col-sm-4 + + input(type="checkbox" data-hook="minimal-median" data-target="minimal" data-name="median" disabled) + + div.col-sm-8 + + div Median + + div.pl-0.col-sm-3 + + div.row + + div.col-sm-4 + + input(type="checkbox" data-hook="minimal-mean" data-target="minimal" data-name="mean" disabled) + + div.col-sm-8 + + div Mean + + div.pl-0.col-sm-3 + + div.row + + div.col-sm-4 + + input(type="checkbox" data-hook="minimal-length" data-target="minimal" data-name="length" disabled) + + div.col-sm-8 + + div Length + + div.row + + div.pl-0.col-sm-3 + + div.row + + div.col-sm-4 + + input(type="checkbox" data-hook="minimal-standard-deviation" data-target="minimal" data-name="standard_deviation" disabled) + + div.col-sm-8 + + div Standard Deviation + + div.pl-0.col-sm-3 + + div.row + + div.col-sm-4 + + input(type="checkbox" data-hook="minimal-variance" data-target="minimal" data-name="variance" disabled) + + div.col-sm-8 + + div Variance + + div.pl-0.col-sm-3 + + div.row + + div.col-sm-4 + + input(type="checkbox" data-hook="minimal-maximum" data-target="minimal" data-name="maximum" disabled) + + div.col-sm-8 + + div Maximum + + div.pl-0.col-sm-3 + + div.row + + div.col-sm-4 + + input(type="checkbox" data-hook="minimal-minimum" data-target="minimal" data-name="minimum" disabled) + + div.col-sm-8 + + div Minimum diff --git a/client/settings-view/views/inference-settings-view.js b/client/settings-view/views/inference-settings-view.js index a85245a95e..9dbf101ce6 100644 --- a/client/settings-view/views/inference-settings-view.js +++ b/client/settings-view/views/inference-settings-view.js @@ -168,8 +168,8 @@ module.exports = View.extend({ this.editSummaryStats.remove(); } // This block is for testing only - this.summaryStatCollections[this.model.summaryStatsType] = this.model.resetSummaryStats(); - this.model.summaryStatsType = "custom"; + // this.summaryStatCollections[this.model.summaryStatsType] = this.model.resetSummaryStats(); + // this.model.summaryStatsType = "custom"; // End testing block this.editSummaryStats = new SummaryStatsView({ collection: this.model.summaryStats, diff --git a/client/settings-view/views/summary-stats-view.js b/client/settings-view/views/summary-stats-view.js index 4a0b0cf37a..9d5fc0c273 100644 --- a/client/settings-view/views/summary-stats-view.js +++ b/client/settings-view/views/summary-stats-view.js @@ -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'); +let $ = require('jquery'); //support files let Tooltips = require('../../tooltips'); //views @@ -25,11 +25,14 @@ let IdentitySummaryStatView = require('./identity-summary-stat-view'); //templates let editCustomTemplate = require('../templates/editCustomSummaryStats.pug'); let viewCustomTemplate = require('../templates/viewCustomSummaryStats.pug'); +let editMinimalTemplate = require('../templates/editMinimalSummaryStats.pug'); +let viewMinimalTemplate = require('../templates/viewMinimalSummaryStats.pug'); let editIdentityTemplate = require('../templates/editIdentitySummaryStats.pug'); let viewIdentityTemplate = require('../templates/viewIdentitySummaryStats.pug'); module.exports = View.extend({ events: { + 'change [data-target=minimal]' : 'updateMinimalFeatures', 'click [data-hook=add-summary-stat]' : 'addSummaryStatistic' }, initialize: function (attrs, options) { @@ -41,8 +44,10 @@ module.exports = View.extend({ render: function () { if(this.summariesType === "identity") { this.template = this.readOnly ? viewIdentityTemplate : editIdentityTemplate; - }else if(this.summariesType === "custom"){ + }else if(this.summariesType === "custom") { this.template = this.readOnly ? viewCustomTemplate : editCustomTemplate; + }else if(this.summariesType === "minimal") { + this.template = this.readOnly ? viewMinimalTemplate : editMinimalTemplate; } View.prototype.render.apply(this, arguments); if(!this.readOnly) { @@ -62,33 +67,56 @@ module.exports = View.extend({ if(this.editSummaryStat) { this.editSummaryStat.remove(); } - if(this.summariesType === "identity") { - var summaryStatView = IdentitySummaryStatView; - }else if(this.summariesType === "custom"){ - var summaryStatView = CustomSummaryStatView; + if(this.summariesType === "minimal") { + this.renderMinimalSummaryStats(); + }else{ + if(this.summariesType === "identity") { + var summaryStatView = IdentitySummaryStatView; + }else if(this.summariesType === "custom"){ + var summaryStatView = CustomSummaryStatView; + } + this.editSummaryStat = this.renderCollection( + this.collection, + summaryStatView, + this.queryByHook("edit-summary-stat-collection"), + ); } - this.editSummaryStat = this.renderCollection( - this.collection, - summaryStatView, - this.queryByHook("edit-summary-stat-collection"), - ); + }, + renderMinimalSummaryStats: function () { + this.collection.forEach((summaryStat) => { + let hook = `minimal-${summaryStat.name.replace('_', '-')}`; + $(this.queryByHook(hook)).prop('checked', true); + }); }, renderViewSummaryStat: function () { if(this.viewSummaryStat) { this.viewSummaryStat.remove(); } - let options = {"viewOptions": { viewMode: true }}; - if(this.summariesType === "identity") { - var summaryStatView = IdentitySummaryStatView; - }else if(this.summariesType === "custom"){ - var summaryStatView = CustomSummaryStatView; + if(this.summariesType === "minimal") { + this.renderMinimalSummaryStats(); + }else{ + let options = {"viewOptions": { viewMode: true }}; + if(this.summariesType === "identity") { + var summaryStatView = IdentitySummaryStatView; + }else if(this.summariesType === "custom"){ + var summaryStatView = CustomSummaryStatView; + } + this.viewSummaryStat = this.renderCollection( + this.collection, + summaryStatView, + this.queryByHook("view-summary-stat-collection"), + options + ); + } + }, + updateMinimalFeatures: function (e) { + let name = e.target.dataset.name; + let model = this.collection.get(name, "name"); + if(e.target.checked && model === undefined) { + this.collection.addSummaryStat({name: name}); + }else if(!(e.target.checked || model === undefined)){ + this.collection.removeSummaryStat(model); } - this.viewSummaryStat = this.renderCollection( - this.collection, - summaryStatView, - this.queryByHook("view-summary-stat-collection"), - options - ); }, updateViewer: function () { this.parent.renderViewSummaryStats(); From 418e3cca87904439075e638a3fa3d4177d8dbb06 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Thu, 23 Feb 2023 14:16:23 -0500 Subject: [PATCH 036/109] Fixed inference settings template. --- .../workflowSettingsTemplate.json | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/stochss_templates/workflowSettingsTemplate.json b/stochss_templates/workflowSettingsTemplate.json index e15d38fe9d..955c3b3f84 100644 --- a/stochss_templates/workflowSettingsTemplate.json +++ b/stochss_templates/workflowSettingsTemplate.json @@ -5,28 +5,44 @@ "priorMethod": "Uniform Prior", "summaryStats": [ { - "sum_values": null + "name": "sum_values", + "args": null, + "formula": "" }, { - "median": null + "name": "median", + "args": null, + "formula": "" }, { - "mean": null + "name": "mean", + "args": null, + "formula": "" }, { - "length": null + "name": "length", + "args": null, + "formula": "" }, { - "standard_deviation": null + "name": "standard_deviation", + "args": null, + "formula": "" }, { - "variance": null + "name": "variance", + "args": null, + "formula": "" }, { - "maximum": null + "name": "maximum", + "args": null, + "formula": "" }, { - "minimum": null + "name": "minimum", + "args": null, + "formula": "" } ], "summaryStatsType": "minimal" From 1ebdd86c3b73b9d438d86e252b8b2767707041eb Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Thu, 23 Feb 2023 14:56:47 -0500 Subject: [PATCH 037/109] Added summary statistics type select. --- client/models/summary-stat.js | 2 +- .../templates/inferenceSettingsView.pug | 14 +++++++- .../views/inference-settings-view.js | 34 +++++++++++-------- 3 files changed, 33 insertions(+), 17 deletions(-) diff --git a/client/models/summary-stat.js b/client/models/summary-stat.js index d515a6c9f3..392ddc26e2 100644 --- a/client/models/summary-stat.js +++ b/client/models/summary-stat.js @@ -56,7 +56,7 @@ module.exports = State.extend({ let argStrs = argStr.replace(/ /g, '').split(','); let args = []; argStrs.forEach((arg) => { - args.push(JSON.parse(arg)); + args.push(JSON.parse(arg.replace(/'/g, '"'))); }); this.args = args; } diff --git a/client/settings-view/templates/inferenceSettingsView.pug b/client/settings-view/templates/inferenceSettingsView.pug index 2e9edd3802..77ece4db69 100644 --- a/client/settings-view/templates/inferenceSettingsView.pug +++ b/client/settings-view/templates/inferenceSettingsView.pug @@ -50,7 +50,19 @@ div#inference-settings.card h4 Summary Statistics - p.hidden(data-hook="tsfresh-docs-link") See TSFresh Documentation for a full list of supported feature calculators. + hr + + div.mr-3.inline + + span.mr-2.inline(for="#summaryStatsTypeSelect") + + h6 Summary Statistics Type: + + div.inline(id="summaryStatsTypeSelect" data-hook="summary-stats-type-select") + + div.inline.hidden(data-hook="tsfresh-docs-link") + + p See TSFresh documentation for a full list of supported feature calculators. div(data-hook="edit-summary-stats-container") diff --git a/client/settings-view/views/inference-settings-view.js b/client/settings-view/views/inference-settings-view.js index 9dbf101ce6..0fe7102a69 100644 --- a/client/settings-view/views/inference-settings-view.js +++ b/client/settings-view/views/inference-settings-view.js @@ -41,7 +41,7 @@ module.exports = View.extend({ } }, events: { - 'change [data-hook=prior-method]' : 'setPriorMethod', + 'change [data-hook=summary-stats-type-select]' : 'setSummaryStatsType', 'change [data-hook=summary-statistics]' : 'updateSummaryStatsView', 'change [data-hook=obs-data-file]' : 'setObsDataFile', 'change [data-hook=obs-data-file-select]' : 'selectObsDataFile', @@ -167,10 +167,6 @@ module.exports = View.extend({ if(this.editSummaryStats) { this.editSummaryStats.remove(); } - // This block is for testing only - // this.summaryStatCollections[this.model.summaryStatsType] = this.model.resetSummaryStats(); - // this.model.summaryStatsType = "custom"; - // End testing block this.editSummaryStats = new SummaryStatsView({ collection: this.model.summaryStats, summariesType: this.model.summaryStatsType @@ -256,11 +252,19 @@ module.exports = View.extend({ this.obsDataFile = e.target.files[0]; $(this.queryByHook("import-obs-data-file")).prop('disabled', !this.obsDataFile); }, - setPriorMethod: function (e) { - this.model.priorMethod = e.target.value; - $(this.queryByHook("view-prior-method")).text(this.model.priorMethod); - this.renderEditParameterSpace(); - this.renderViewParameterSpace(); + setSummaryStatsType: function (e) { + if(this.summaryStatCollections[e.target.value] === null) { + var summaryStats = this.model.resetSummaryStats(); + }else{ + var summaryStats = this.model.summaryStats; + this.model.summaryStats = this.summaryStatCollections[e.target.value]; + } + this.summaryStatCollections[this.model.summaryStatsType] = summaryStats; + this.model.summaryStatsType = e.target.value; + let display = e.target.value === "custom" ? "inline-block" : "none"; + $(this.queryByHook("tsfresh-docs-link")).css("display", display); + this.renderEditSummaryStats(); + this.renderViewSummaryStats(); }, selectObsDataFile: function (e) { this.obsFig = null; @@ -315,18 +319,18 @@ module.exports = View.extend({ }, updateValid: function (e) {}, subviews: { - priorMethodView: { - hook: "prior-method", + summaryStatsTypeView: { + hook: "summary-stats-type-select", prepareView: function (el) { let options = [ - "Uniform Prior"//, "Gaussian Prior" + ["identity", "Identity"], ["minimal", "TSFresh Minimal"], ["custom", "Custom TSFresh"] ] return new SelectView({ - name: 'prior-method', + name: 'summary-statistics-type', required: true, eagerValidate: true, options: options, - value: this.model.priorMethod + value: this.model.summaryStatsType }); } }, From 3b96406facf296a6fdcc750fb5e341fa5c23d153 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Fri, 24 Feb 2023 10:33:51 -0500 Subject: [PATCH 038/109] Added validation for custom summary statistics. --- client/models/inference-settings.js | 3 ++ client/models/summary-stat.js | 6 ++-- .../views/custom-summary-stat-view.js | 15 +++++++++- .../views/inference-settings-view.js | 4 ++- .../settings-view/views/summary-stats-view.js | 3 ++ client/views/input.js | 28 ++++++++++++++++--- stochss/handlers/util/stochss_workflow.py | 4 +++ 7 files changed, 55 insertions(+), 8 deletions(-) diff --git a/client/models/inference-settings.js b/client/models/inference-settings.js index 6759d7f998..ccf93a6253 100644 --- a/client/models/inference-settings.js +++ b/client/models/inference-settings.js @@ -31,6 +31,9 @@ module.exports = State.extend({ parameters: InferenceParameters, summaryStats: SummaryStats }, + session: { + customCalculators: 'object' + }, derived: { elementID: { deps: ["parent"], diff --git a/client/models/summary-stat.js b/client/models/summary-stat.js index 392ddc26e2..5ce210e752 100644 --- a/client/models/summary-stat.js +++ b/client/models/summary-stat.js @@ -49,15 +49,17 @@ module.exports = State.extend({ initialize: function(attrs, options) { State.prototype.initialize.apply(this, arguments); }, - setArgs: function (argStr) { + setArgs: function (argStr, {dryRun=false}={}) { if(argStr === "") { - this.args = null; + args = null; }else{ let argStrs = argStr.replace(/ /g, '').split(','); let args = []; argStrs.forEach((arg) => { args.push(JSON.parse(arg.replace(/'/g, '"'))); }); + } + if(!dryRun) { this.args = args; } } diff --git a/client/settings-view/views/custom-summary-stat-view.js b/client/settings-view/views/custom-summary-stat-view.js index 862705bfe1..5731cd9829 100644 --- a/client/settings-view/views/custom-summary-stat-view.js +++ b/client/settings-view/views/custom-summary-stat-view.js @@ -34,6 +34,7 @@ module.exports = View.extend({ initialize: function (attrs, options) { View.prototype.initialize.apply(this, arguments); this.viewMode = attrs.viewMode ? attrs.viewMode : false; + this.customCalculators = attrs.customCalculators ? attrs.customCalculators : null; }, render: function (attrs, options) { this.template = this.viewMode ? viewTemplate : editTemplate; @@ -49,7 +50,7 @@ module.exports = View.extend({ update: function () {}, updateValid: function () {}, updateViewer: function () { - this.parent.updateViewer(); + this.parent.parent.updateViewer(); }, subviews: { nameInputView: { @@ -61,6 +62,11 @@ module.exports = View.extend({ name: 'observable-name', modelKey: 'name', tests: tests.nameTests, + changeTests: [(text) => { + if(!this.customCalculators.includes(text)) { + return "This feature calculator is not currently supported." + } + }], valueType: 'string', value: this.model.name, placeholder: "-- i.e. autocorrelation --" @@ -74,6 +80,13 @@ module.exports = View.extend({ parent: this, required: false, name: 'summary-calculator-args', + changeTests: [(text) => { + try{ + this.model.setArgs(text, {dryRun: true}); + }catch(error) { + return `Args must be in one of the following formats '{"lag": 1}' or '{"lag": 1},{"lag": 2}'.` + } + }], valueType: 'string', value: this.model.argsDisplay === "None" ? "" : this.model.argsDisplay, placeholder: '-- i.e. Requires arguments: {"lag":1} or leave blank if no arguments are required --' diff --git a/client/settings-view/views/inference-settings-view.js b/client/settings-view/views/inference-settings-view.js index 0fe7102a69..93f58fbff4 100644 --- a/client/settings-view/views/inference-settings-view.js +++ b/client/settings-view/views/inference-settings-view.js @@ -93,6 +93,7 @@ module.exports = View.extend({ } this.renderViewSummaryStats(); this.renderViewParameterSpace(); + console.log(this.model.customCalculators) }, changeCollapseButtonText: function (e) { app.changeCollapseButtonText(this, e); @@ -169,7 +170,8 @@ module.exports = View.extend({ } this.editSummaryStats = new SummaryStatsView({ collection: this.model.summaryStats, - summariesType: this.model.summaryStatsType + summariesType: this.model.summaryStatsType, + customCalculators: this.model.customCalculators }); let hook = "edit-summary-stats-container"; app.registerRenderSubview(this, this.editSummaryStats, hook); diff --git a/client/settings-view/views/summary-stats-view.js b/client/settings-view/views/summary-stats-view.js index 9d5fc0c273..7b398932f3 100644 --- a/client/settings-view/views/summary-stats-view.js +++ b/client/settings-view/views/summary-stats-view.js @@ -40,6 +40,7 @@ module.exports = View.extend({ this.readOnly = attrs.readOnly ? attrs.readOnly : false; this.tooltips = Tooltips.summaryStats; this.summariesType = attrs.summariesType; + this.customCalculators = attrs.customCalculators; }, render: function () { if(this.summariesType === "identity") { @@ -75,10 +76,12 @@ module.exports = View.extend({ }else if(this.summariesType === "custom"){ var summaryStatView = CustomSummaryStatView; } + let options = {"viewOptions": { customCalculators: this.customCalculators }}; this.editSummaryStat = this.renderCollection( this.collection, summaryStatView, this.queryByHook("edit-summary-stat-collection"), + options ); } }, diff --git a/client/views/input.js b/client/views/input.js index e510b2f0a4..cdda3e42ee 100644 --- a/client/views/input.js +++ b/client/views/input.js @@ -15,8 +15,8 @@ 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 . */ - -var AmpersandInputView = require('ampersand-input-view'); +let $ = require('jquery'); +let AmpersandInputView = require('ampersand-input-view'); module.exports = AmpersandInputView.extend({ props: { @@ -24,10 +24,12 @@ module.exports = AmpersandInputView.extend({ label: 'string', modelKey: 'string', valueType: 'string', - disabled: 'boolean' + disabled: 'boolean', + changeTests: 'object' }, events: { - 'input input' : 'changeInputHandler', + 'change input' : 'runChangeTests', + 'input input' : 'changeInputHandler' }, initialize: function (attrs, options) { AmpersandInputView.prototype.initialize.apply(this, arguments); @@ -68,4 +70,22 @@ module.exports = AmpersandInputView.extend({ this.parent.updateValid() } }, + runChangeTests: function (e) { + if(this.changeTests === undefined) { return } + let text = e.target.value; + let messages = []; + this.changeTests.forEach((test) => { + let message = test(text); + if(message) { + messages.push(message); + } + }); + if(messages.length > 0){ + let html = messages.join("
    "); + $(this.queryByHook("message-text")).html(html); + $(this.queryByHook("message-container")).css("display", "block"); + }else{ + $(this.queryByHook("message-container")).css("display", "none"); + } + } }); \ No newline at end of file diff --git a/stochss/handlers/util/stochss_workflow.py b/stochss/handlers/util/stochss_workflow.py index dae5e9f362..c8cc796d0e 100644 --- a/stochss/handlers/util/stochss_workflow.py +++ b/stochss/handlers/util/stochss_workflow.py @@ -189,6 +189,10 @@ def __load_settings(self): self.workflow['model'] = settings['model'] self.workflow['settings'] = settings['settings'] self.workflow['type'] = settings['type'] + if settings['type'] == "Model Inference": + from tsfresh.feature_extraction.settings import EfficientFCParameters # pylint: disable=import-outside-toplevel + feature_calculators = list(EfficientFCParameters().keys()) + self.workflow['settings']['inferenceSettings']['customCalculators'] = feature_calculators except FileNotFoundError as err: message = f"Could not find the settings file: {str(err)}" raise StochSSFileNotFoundError(message, traceback.format_exc) from err From 0467ac76b0c98d9e9469102a5fbcc7002d2d7cae Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Fri, 24 Feb 2023 11:11:22 -0500 Subject: [PATCH 039/109] Fixed viewer for summary statistics. --- client/models/summary-stat.js | 7 +++---- .../settings-view/templates/editCustomSummaryStatView.pug | 2 +- client/settings-view/views/custom-summary-stat-view.js | 6 +++++- client/settings-view/views/identity-summary-stat-view.js | 6 +++++- client/settings-view/views/inference-settings-view.js | 1 - 5 files changed, 14 insertions(+), 8 deletions(-) diff --git a/client/models/summary-stat.js b/client/models/summary-stat.js index 5ce210e752..e250ee71b2 100644 --- a/client/models/summary-stat.js +++ b/client/models/summary-stat.js @@ -50,11 +50,10 @@ module.exports = State.extend({ State.prototype.initialize.apply(this, arguments); }, setArgs: function (argStr, {dryRun=false}={}) { - if(argStr === "") { - args = null; - }else{ + var args = null + if(argStr !== "") { let argStrs = argStr.replace(/ /g, '').split(','); - let args = []; + args = []; argStrs.forEach((arg) => { args.push(JSON.parse(arg.replace(/'/g, '"'))); }); diff --git a/client/settings-view/templates/editCustomSummaryStatView.pug b/client/settings-view/templates/editCustomSummaryStatView.pug index 68ff26f09a..f9df9e9b8a 100644 --- a/client/settings-view/templates/editCustomSummaryStatView.pug +++ b/client/settings-view/templates/editCustomSummaryStatView.pug @@ -7,7 +7,7 @@ div.mx-1 div.col-sm-2 - div(data-target="identity-property" data-hook="summary-stat-name") + div(data-target="custom-property" data-hook="summary-stat-name") div.col-sm-8 diff --git a/client/settings-view/views/custom-summary-stat-view.js b/client/settings-view/views/custom-summary-stat-view.js index 5731cd9829..71f0d2f537 100644 --- a/client/settings-view/views/custom-summary-stat-view.js +++ b/client/settings-view/views/custom-summary-stat-view.js @@ -27,7 +27,7 @@ let viewTemplate = require('../templates/viewCustomSummaryStatView.pug'); module.exports = View.extend({ events: { - 'change [data-target=identity-property]' : 'updateViewer', + 'change [data-target=custom-property]' : 'updateViewer', 'change [data-hook=summary-stat-args]' : 'setCalculatorArgs', 'click [data-hook=remove]' : 'removeSummaryStat' }, @@ -50,7 +50,11 @@ module.exports = View.extend({ update: function () {}, updateValid: function () {}, updateViewer: function () { + try{ + this.parent.updateViewer(); + }catch(error){ this.parent.parent.updateViewer(); + } }, subviews: { nameInputView: { diff --git a/client/settings-view/views/identity-summary-stat-view.js b/client/settings-view/views/identity-summary-stat-view.js index 2153e3784d..b453115fb3 100644 --- a/client/settings-view/views/identity-summary-stat-view.js +++ b/client/settings-view/views/identity-summary-stat-view.js @@ -44,7 +44,11 @@ module.exports = View.extend({ update: function () {}, updateValid: function () {}, updateViewer: function () { - this.parent.updateViewer(); + try{ + this.parent.updateViewer(); + }catch(error){ + this.parent.parent.updateViewer(); + } }, subviews: { nameInputView: { diff --git a/client/settings-view/views/inference-settings-view.js b/client/settings-view/views/inference-settings-view.js index 93f58fbff4..1fb698b397 100644 --- a/client/settings-view/views/inference-settings-view.js +++ b/client/settings-view/views/inference-settings-view.js @@ -93,7 +93,6 @@ module.exports = View.extend({ } this.renderViewSummaryStats(); this.renderViewParameterSpace(); - console.log(this.model.customCalculators) }, changeCollapseButtonText: function (e) { app.changeCollapseButtonText(this, e); From 6acd3333b2c13d714cdb1755cfc69171e43d9b2a Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Fri, 24 Feb 2023 11:16:02 -0500 Subject: [PATCH 040/109] Removed the old prior method and summary statistics fields. --- .../templates/inferenceSettingsView.pug | 44 ------------------- .../views/inference-settings-view.js | 17 ------- 2 files changed, 61 deletions(-) diff --git a/client/settings-view/templates/inferenceSettingsView.pug b/client/settings-view/templates/inferenceSettingsView.pug index 77ece4db69..207efe3e23 100644 --- a/client/settings-view/templates/inferenceSettingsView.pug +++ b/client/settings-view/templates/inferenceSettingsView.pug @@ -24,28 +24,6 @@ div#inference-settings.card div.tab-pane.active(id=this.model.elementID + "-edit-inference-settings" data-hook=this.model.elementID + "-edit-inference-settings") - hr - - div.mx-1.row.head.align-items-baseline - - div.col-sm-2 - - h6 Prior Method - - div.col-sm-10 - - h6.inline Summary Statistics - - div.mx-1.my-3.row - - div.col-sm-2 - - div(data-hook="prior-method") - - div.col-sm-10 - - div(data-hook="summary-statistics") - div h4 Summary Statistics @@ -154,28 +132,6 @@ div#inference-settings.card div.tab-pane(id=this.model.elementID + "-view-inference-settings" data-hook=this.model.elementID + "-view-inference-settings") - hr - - div.mx-1.row.head.align-items-baseline - - div.col-sm-2 - - h6 Prior Method - - div.col-sm-10 - - h6.inline Summary Statistics - - div.mx-1.my-3.row - - div.col-sm-2 - - div(data-hook="view-prior-method")=this.model.priorMethod - - div.col-sm-10 - - div(data-hook="view-summary-stats")=this.model.summaryStats - h4 Summary Statistics div.mb-3(data-hook="view-summary-stats-container") diff --git a/client/settings-view/views/inference-settings-view.js b/client/settings-view/views/inference-settings-view.js index 1fb698b397..f8a53bb970 100644 --- a/client/settings-view/views/inference-settings-view.js +++ b/client/settings-view/views/inference-settings-view.js @@ -42,7 +42,6 @@ module.exports = View.extend({ }, events: { 'change [data-hook=summary-stats-type-select]' : 'setSummaryStatsType', - 'change [data-hook=summary-statistics]' : 'updateSummaryStatsView', 'change [data-hook=obs-data-file]' : 'setObsDataFile', 'change [data-hook=obs-data-file-select]' : 'selectObsDataFile', 'change [data-hook=obs-data-location-select]' : 'selectObsDataLocation', @@ -315,9 +314,6 @@ module.exports = View.extend({ } }, update: function (e) {}, - updateSummaryStatsView: function (e) { - $(this.queryByHook("view-summary-stats")).text(this.model.summaryStats ? this.model.summaryStats : 'None') - }, updateValid: function (e) {}, subviews: { summaryStatsTypeView: { @@ -334,19 +330,6 @@ module.exports = View.extend({ value: this.model.summaryStatsType }); } - }, - summaryStatsView: { - hook: "summary-statistics", - prepareView: function (el) { - return new InputView({ - parent: this, - required: false, - name: 'summary-statistics', - modelKey: 'summaryStats', - valueType: 'string', - value: this.model.summaryStats - }); - } } } }); From f8ec828b712dbb39ecd94b0ee4b7605ab3460118 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Mon, 27 Feb 2023 10:37:22 -0500 Subject: [PATCH 041/109] Added type and title entries for model inference jobs. --- client/pages/workflow-manager.js | 3 ++- stochss/handlers/util/stochss_job.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/client/pages/workflow-manager.js b/client/pages/workflow-manager.js index f9a0ca994b..7b5e140252 100644 --- a/client/pages/workflow-manager.js +++ b/client/pages/workflow-manager.js @@ -106,7 +106,8 @@ let WorkflowManager = PageView.extend({ let types = { "Ensemble Simulation": "gillespy", "Parameter Sweep": "parameterSweep", - "Spatial Ensemble Simulation": "spatial" + "Spatial Ensemble Simulation": "spatial", + "Model Inference": "inference" }; let data = { "settings": this.model.settings.toJSON(), "mdl_path": this.model.model, diff --git a/stochss/handlers/util/stochss_job.py b/stochss/handlers/util/stochss_job.py index 040845fb2e..f4bc7eec86 100644 --- a/stochss/handlers/util/stochss_job.py +++ b/stochss/handlers/util/stochss_job.py @@ -50,11 +50,11 @@ class StochSSJob(StochSSBase): ''' TYPES = { - "gillespy":"_ES", "spatial":"_SES", "parameterSweep":"_PS" + "gillespy":"_ES", "spatial":"_SES", "parameterSweep":"_PS", "inference":"_MI" } TITLES = { "gillespy":"Ensemble Simulation", "spatial":"Spatial Ensemble Simulation", - "parameterSweep":"Parameter Sweep" + "parameterSweep":"Parameter Sweep", "inference":"Model Inference" } def __init__(self, path, new=False, data=None): From 3da259cf062f6cb0d084804a1f015457fcb371fc Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Mon, 27 Feb 2023 10:39:47 -0500 Subject: [PATCH 042/109] Added model inference job to the start job script. --- stochss/handlers/util/scripts/start_job.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/stochss/handlers/util/scripts/start_job.py b/stochss/handlers/util/scripts/start_job.py index 008f942f09..842187bb92 100755 --- a/stochss/handlers/util/scripts/start_job.py +++ b/stochss/handlers/util/scripts/start_job.py @@ -32,6 +32,7 @@ from util.ensemble_simulation import EnsembleSimulation from util.spatial_simulation import SpatialSimulation from util.parameter_sweep import ParameterSweep +from util.model_inference import ModelInference from handlers.log import init_log init_log() @@ -107,9 +108,10 @@ def setup_logger(log_path): if __name__ == "__main__": args = get_parsed_args() jobs = { - "gillespy":EnsembleSimulation, + "gillespy": EnsembleSimulation, "spatial": SpatialSimulation, - "parameterSweep":ParameterSweep + "parameterSweep": ParameterSweep, + "inference": ModelInference } try: os.chdir(args.path) From 17dc0523df9296ce9f7aac210e7a3821e5fcc3c3 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Mon, 27 Feb 2023 17:13:00 -0500 Subject: [PATCH 043/109] Added observed data to the inference settings viewer. --- client/settings-view/templates/inferenceSettingsView.pug | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/settings-view/templates/inferenceSettingsView.pug b/client/settings-view/templates/inferenceSettingsView.pug index 207efe3e23..3648686fa8 100644 --- a/client/settings-view/templates/inferenceSettingsView.pug +++ b/client/settings-view/templates/inferenceSettingsView.pug @@ -137,3 +137,7 @@ div#inference-settings.card div.mb-3(data-hook="view-summary-stats-container") div(data-hook="view-parameter-space-container") + + h4 Observed Data + + div=this.model.obsData From c2e226a4cf396cf9325a20934a15232dbef6d9c4 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Mon, 27 Feb 2023 17:13:52 -0500 Subject: [PATCH 044/109] Added the model inference job. --- stochss/handlers/util/model_inference.py | 192 +++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 stochss/handlers/util/model_inference.py diff --git a/stochss/handlers/util/model_inference.py b/stochss/handlers/util/model_inference.py new file mode 100644 index 0000000000..5a484171f1 --- /dev/null +++ b/stochss/handlers/util/model_inference.py @@ -0,0 +1,192 @@ +''' +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 . +''' +import os +import csv +import copy +import pickle +import logging +import traceback + +import numpy + +import gillespy2 + +from sciope.inference import smc_abc +from sciope.utilities.priors import uniform_prior +from sciope.utilities.summarystats import auto_tsfresh, identity + +from .stochss_job import StochSSJob +from .stochss_errors import StochSSJobError, StochSSJobResultsError + +log = logging.getLogger("stochss") + +class ModelInference(StochSSJob): + ''' + ################################################################################################ + StochSS model inference job object + ################################################################################################ + ''' + + TYPE = "inference" + + def __init__(self, path): + ''' + Intitialize a model inference job object + + Attributes + ---------- + path : str + Path to the model inference job + ''' + super().__init__(path=path) + self.g_model, self.s_model = self.load_models() + self.settings = self.load_settings() + + @classmethod + def __get_csv_data(cls, path): + with open(path, "r", newline="", encoding="utf-8") as csv_fd: + csv_reader = csv.reader(csv_fd, delimiter=",") + rows = [] + for i, row in enumerate(csv_reader): + if i != 0: + rows.append(row) + data = numpy.array(rows).swapaxes(0, 1).astype("float") + return data + + def __get_prior_function(self): + dmin = [] + dmax = [] + for parameter in self.settings['inferenceSettings']['parameters']: + dmin.append(parameter['min']) + dmax.append(parameter['max']) + return uniform_prior.UniformPrior(dmin, dmax) + + def __get_run_settings(self): + solver_map = {"ODE":self.g_model.get_best_solver_algo("ODE"), + "SSA":self.g_model.get_best_solver_algo("SSA"), + "CLE":self.g_model.get_best_solver_algo("CLE"), + "Tau-Leaping":self.g_model.get_best_solver_algo("Tau-Leaping"), + "Hybrid-Tau-Leaping":self.g_model.get_best_solver_algo("Tau-Hybrid")} + run_settings = self.get_run_settings(settings=self.settings, solver_map=solver_map) + instance_solvers = ["ODECSolver", "SSACSolver", "TauLeapingCSolver", "TauHybridCSolver"] + if run_settings['solver'].name in instance_solvers : + run_settings['solver'] = run_settings['solver'](model=self.g_model) + return run_settings + + def __get_summaries_function(self): + summary_type = self.settings['inferenceSettings']['summaryStatsType'] + if summary_type == "identity": + return identity.Identity() + if summary_type == "minimal" and len(self.settings['inferenceSettings']['summaryStats']) == 8: + return auto_tsfresh.SummariesTSFRESH() + features = {} + for feature_calculator in self.settings['inferenceSettings']['summaryStats']: + features[feature_calculator['name']] = feature_calculator['args'] + return auto_tsfresh.SummariesTSFRESH(features=features) + + def __load_obs_data(self, path=None, data=None): + if path is None: + path = self.get_new_path(self.settings['inferenceSettings']['obsData']) + if not (path.endswith(".csv") or path.endswith(".obsd")): + raise StochSSJobError("Observed data must be a CSV file (.csv) or a directory (.obsd) of CSV files.") + if path.endswith(".csv"): + new_data = self.__get_csv_data(path) + data.append(new_data) + return data + for file in os.listdir(path): + data = self.__load_obs_data(path=os.path.join(path, file), data=data) + return data + + @classmethod + def __report_result_error(cls, trace): + message = "An unexpected error occured with the result object" + raise StochSSJobResultsError(message, trace) + + @classmethod + def __store_pickled_results(cls, results): + try: + with open('results/results.p', 'wb') as results_fd: + pickle.dump(results, results_fd) + except Exception as err: + message = f"Error storing pickled results: {err}\n{traceback.format_exc()}" + log.error(message) + return message + return False + + def process(self, raw_results): + """ + Post processing function used to reshape simulator results and + process results for identity summary statistics. + """ + if self.settings['inferenceSettings']['summaryStatsType'] != "identity": + return raw_results.to_array().swapaxes(1, 2)[:,1:, :] + + definitions = {"time": "time"} + for feature_calculator in self.settings['inferenceSettings']['summaryStats']: + definitions[feature_calculator['name']] = feature_calculator['formula'] + + trajectories = [] + for result in raw_results: + evaluations = {} + for label, formula in definitions.items(): + evaluations[label] = eval(formula, {}, result.data) + trajectories.append(gillespy2.Trajectory( + data=evaluations, model=result.model, solver_name=result.solver_name, rc=result.rc + )) + processed_results = gillespy2.Results([evaluations]) + return processed_results.to_array().swapaxes(1, 2)[:,1:, :] + + def run(self, verbose=True): + ''' + Run a model inference job + + Attributes + ---------- + verbose : bool + Indicates whether or not to print debug statements + ''' + obs_data = numpy.array(self.__load_obs_data(data=[]))[:,1:, :] + prior = self.__get_prior_function() + summaries = self.__get_summaries_function() + if verbose: + log.info("Running the model inference") + smc_abc_inference = smc_abc.SMCABC( + obs_data, sim=self.simulator, prior_function=prior, summaries_function=summaries.compute + ) + results = smc_abc_inference.infer(num_samples=100, batch_size=10) + if verbose: + log.info("The model inference has completed") + log.info("Storing the results as pickle.") + if not 'results' in os.listdir(): + os.mkdir('results') + pkl_err = self.__store_pickled_results(results) + if pkl_err: + self.__report_result_error(trace=pkl_err) + + def simulator(self, parameter_point): + """ Wrapper function for inference simulations. """ + model = copy.deepcopy(self.g_model) + + labels = list(map(lambda parameter: parameter['name'], self.settings['inferenceSettings']['parameters'])) + for ndx, parameter in enumerate(parameter_point): + model.listOfParameters[labels[ndx]].expression = str(parameter) + + kwargs = self.__get_run_settings() + raw_results = model.run(**kwargs) + + return self.process(raw_results) From a17aceb9f1db52cd07b8008aebef6c1d2b43b318 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Tue, 28 Feb 2023 11:08:06 -0500 Subject: [PATCH 045/109] Updated tooltips for inference settings. --- .../settings-view/views/inference-parameters-view.js | 2 +- client/tooltips.js | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/client/settings-view/views/inference-parameters-view.js b/client/settings-view/views/inference-parameters-view.js index 1e17290304..ba341f397c 100644 --- a/client/settings-view/views/inference-parameters-view.js +++ b/client/settings-view/views/inference-parameters-view.js @@ -32,7 +32,7 @@ module.exports = View.extend({ initialize: function (attrs, options) { View.prototype.initialize.apply(this, arguments); this.readOnly = attrs.readOnly ? attrs.readOnly : false; - this.tooltips = Tooltips.parameterSweepSettings; + this.tooltips = Tooltips.inferenceSettings; this.stochssModel = attrs.stochssModel; this.priorMethod = attrs.priorMethod; if(!this.readOnly) { diff --git a/client/tooltips.js b/client/tooltips.js index 9ec4bcb133..88d284e9ec 100644 --- a/client/tooltips.js +++ b/client/tooltips.js @@ -153,7 +153,7 @@ module.exports = { variable: "The parameter(s) to sweep through.", - value: "The current value for the parameter you with to sweep through.", + value: "The current value for the parameter you wish to sweep through.", min: "The initial value of the sweep range. Defaults to half of the current value.", @@ -161,6 +161,15 @@ module.exports = { steps: "The number of steps used to determine the sweep values across the sweep range." }, + inferenceSettings: { + variable: "The parameter(s) to infer.", + + value: "The current value for the parameter you wish to infer.", + + min: "The lower bound of the parameter/dimension. Defaults to half of the current value.", + + max: "The upper bound of the parameter/dimension. Defaults to 1.5 times the current value." + }, summaryStats: { customArgs: "Arguments to the TSFresh feature calculator.
  • Leave the feild empty if no arguments are required.
  • " + '
  • To run once provide a single dictionary. i.e. {"lag":1}
  • ' + From 004c6b34012534aa42b51afb5937c94f8aa19304 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Tue, 28 Feb 2023 11:08:56 -0500 Subject: [PATCH 046/109] Fixed issue with types of prior args. --- stochss/handlers/util/model_inference.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stochss/handlers/util/model_inference.py b/stochss/handlers/util/model_inference.py index 5a484171f1..7015bbd768 100644 --- a/stochss/handlers/util/model_inference.py +++ b/stochss/handlers/util/model_inference.py @@ -74,7 +74,7 @@ def __get_prior_function(self): for parameter in self.settings['inferenceSettings']['parameters']: dmin.append(parameter['min']) dmax.append(parameter['max']) - return uniform_prior.UniformPrior(dmin, dmax) + return uniform_prior.UniformPrior(numpy.array(dmin, dtype="float"), numpy.array(dmax, dtype="float")) def __get_run_settings(self): solver_map = {"ODE":self.g_model.get_best_solver_algo("ODE"), From 9c92bce50cb59a79fcf44819322b9ae03368c351 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Wed, 1 Mar 2023 11:14:46 -0500 Subject: [PATCH 047/109] Added model attributes for 'batch size', 'chunk size', 'number of epochs', and 'number of samples'. Fixed viewer for sammaries type and obs data. --- client/models/inference-settings.js | 4 + .../templates/inferenceSettingsView.pug | 54 ++++++++++++- .../views/inference-settings-view.js | 79 ++++++++++++++----- .../workflowSettingsTemplate.json | 4 + 4 files changed, 122 insertions(+), 19 deletions(-) diff --git a/client/models/inference-settings.js b/client/models/inference-settings.js index ccf93a6253..7483da6d0e 100644 --- a/client/models/inference-settings.js +++ b/client/models/inference-settings.js @@ -23,6 +23,10 @@ let State = require('ampersand-state'); module.exports = State.extend({ props: { + batchSize: 'number', + chunkSize: 'number', + numEpochs: 'number', + numSamples: 'number', obsData: 'string', priorMethod: 'string', summaryStatsType: 'string' diff --git a/client/settings-view/templates/inferenceSettingsView.pug b/client/settings-view/templates/inferenceSettingsView.pug index 3648686fa8..dba6a5e2ff 100644 --- a/client/settings-view/templates/inferenceSettingsView.pug +++ b/client/settings-view/templates/inferenceSettingsView.pug @@ -24,6 +24,26 @@ div#inference-settings.card div.tab-pane.active(id=this.model.elementID + "-edit-inference-settings" data-hook=this.model.elementID + "-edit-inference-settings") + div + + hr + + div.mx-1.row.head.align-items-baseline + + div.col-sm-3 + + h6.inline Number of Epochs + + div.col-sm-3 + + h6.inline Number of Samples + + div.mx-1.my-3.row + + div.col-sm-3(data-hook="num-epochs") + + div.col-sm-3(data-hook="num-samples") + div h4 Summary Statistics @@ -132,12 +152,44 @@ div#inference-settings.card div.tab-pane(id=this.model.elementID + "-view-inference-settings" data-hook=this.model.elementID + "-view-inference-settings") + div + + hr + + div.mx-1.row.head.align-items-baseline + + div.col-sm-3 + + h6.inline Number of Epochs + + div.col-sm-3 + + h6.inline Number of Samples + + div.mx-1.my-3.row + + div.col-sm-3 + + div(data-hook="view-num-epochs")=this.model.numEpochs + + div.col-sm-3 + + div(data-hook="view-num-samples")=this.model.numSamples + h4 Summary Statistics + hr + + div + + h6.mr-2.inline Summary Statistic Type: + + div.inline(data-hook="view-summary-type")=this.summaryType + div.mb-3(data-hook="view-summary-stats-container") div(data-hook="view-parameter-space-container") h4 Observed Data - div=this.model.obsData + div(data-hook="view-obs-data-file")=this.model.obsData diff --git a/client/settings-view/views/inference-settings-view.js b/client/settings-view/views/inference-settings-view.js index f8a53bb970..3cc0fb48ec 100644 --- a/client/settings-view/views/inference-settings-view.js +++ b/client/settings-view/views/inference-settings-view.js @@ -21,6 +21,7 @@ let path = require('path'); let app = require('../../app'); let modals = require('../../modals'); let Plotly = require('plotly.js-dist'); +let tests = require('../../views/tests'); //views let View = require('ampersand-view'); let InputView = require('../../views/input'); @@ -41,6 +42,8 @@ module.exports = View.extend({ } }, events: { + 'change [data-hook=num-epochs]' : 'updateEpochsView', + 'change [data-hook=num-samples]' : 'updateSamplesView', 'change [data-hook=summary-stats-type-select]' : 'setSummaryStatsType', 'change [data-hook=obs-data-file]' : 'setObsDataFile', 'change [data-hook=obs-data-file-select]' : 'selectObsDataFile', @@ -73,6 +76,8 @@ module.exports = View.extend({ this.summaryStatCollections = { identity: null, minimal: null, custom: null } + this.summaryTypes = {"identity": "Identity", "minimal": "TSFresh Minimal", "custom": "Custom TSFresh"}; + this.summaryType = this.summaryTypes[this.model.summaryStatsType]; }, render: function () { View.prototype.render.apply(this, arguments); @@ -123,6 +128,7 @@ module.exports = View.extend({ this.completeAction(); $(this.queryByHook('collapseUploadObsData')).click(); this.renderObsDataSelects(); + $(this.queryByHook("view-obs-data-file")).text(this.model.obsData); }, error: (err, response, body) => { body = JSON.parse(body); @@ -248,24 +254,6 @@ module.exports = View.extend({ let hook = "view-summary-stats-container"; app.registerRenderSubview(this, this.viewSummaryStats, hook); }, - setObsDataFile: function (e) { - this.obsDataFile = e.target.files[0]; - $(this.queryByHook("import-obs-data-file")).prop('disabled', !this.obsDataFile); - }, - setSummaryStatsType: function (e) { - if(this.summaryStatCollections[e.target.value] === null) { - var summaryStats = this.model.resetSummaryStats(); - }else{ - var summaryStats = this.model.summaryStats; - this.model.summaryStats = this.summaryStatCollections[e.target.value]; - } - this.summaryStatCollections[this.model.summaryStatsType] = summaryStats; - this.model.summaryStatsType = e.target.value; - let display = e.target.value === "custom" ? "inline-block" : "none"; - $(this.queryByHook("tsfresh-docs-link")).css("display", display); - this.renderEditSummaryStats(); - this.renderViewSummaryStats(); - }, selectObsDataFile: function (e) { this.obsFig = null; let value = e.target.value; @@ -283,18 +271,39 @@ module.exports = View.extend({ }else{ this.model.obsData = ""; } + $(this.queryByHook("view-obs-data-file")).text(this.model.obsData ? this.model.obsData : "None"); $(this.queryByHook("obs-data-location-message")).css('display', msgDisplay); $(this.queryByHook("obs-data-location-container")).css("display", contDisplay); }, selectObsDataLocation: function (e) { this.obsFig = null; this.model.obsData = e.target.value ? e.target.value : ""; + $(this.queryByHook("view-obs-data-file")).text(this.model.obsData ? this.model.obsData : "None"); }, startAction: function () { $(this.queryByHook("iodf-complete")).css("display", "none"); $(this.queryByHook("iodf-error")).css("display", "none"); $(this.queryByHook("iodf-in-progress")).css("display", "inline-block"); }, + setObsDataFile: function (e) { + this.obsDataFile = e.target.files[0]; + $(this.queryByHook("import-obs-data-file")).prop('disabled', !this.obsDataFile); + }, + setSummaryStatsType: function (e) { + if(this.summaryStatCollections[e.target.value] === null) { + var summaryStats = this.model.resetSummaryStats(); + }else{ + var summaryStats = this.model.summaryStats; + this.model.summaryStats = this.summaryStatCollections[e.target.value]; + } + this.summaryStatCollections[this.model.summaryStatsType] = summaryStats; + this.model.summaryStatsType = e.target.value; + let display = e.target.value === "custom" ? "inline-block" : "none"; + $(this.queryByHook("view-summary-type")).text(this.summaryTypes[this.model.summaryStatsType]); + $(this.queryByHook("tsfresh-docs-link")).css("display", display); + this.renderEditSummaryStats(); + this.renderViewSummaryStats(); + }, toggleImportFiles: function (e) { let classes = $(this.queryByHook('collapseImportObsData')).attr("class").split(/\s+/); $(this.queryByHook('upload-chevron')).html(this.chevrons.hide); @@ -315,7 +324,41 @@ module.exports = View.extend({ }, update: function (e) {}, updateValid: function (e) {}, + updateEpochsView: function (e) { + $(this.queryByHook("view-num-epochs")).text(this.model.numEpochs); + }, + updateSamplesView: function (e) { + $(this.queryByHook("view-num-samples")).text(this.model.numSamples); + }, subviews: { + numEpochsInputView: { + hook: "num-epochs", + prepareView: function (el) { + return new InputView({ + parent: this, + required: true, + name: 'number-of-epochs', + tests: tests.valueTests, + modelKey: 'numEpochs', + valueType: 'number', + value: this.model.numEpochs + }); + } + }, + numSamplesInputView: { + hook: "num-samples", + prepareView: function (el) { + return new InputView({ + parent: this, + required: true, + name: 'number-of-samples', + tests: tests.valueTests, + modelKey: 'numSamples', + valueType: 'number', + value: this.model.numSamples + }); + } + }, summaryStatsTypeView: { hook: "summary-stats-type-select", prepareView: function (el) { diff --git a/stochss_templates/workflowSettingsTemplate.json b/stochss_templates/workflowSettingsTemplate.json index 955c3b3f84..39d0f0e9b9 100644 --- a/stochss_templates/workflowSettingsTemplate.json +++ b/stochss_templates/workflowSettingsTemplate.json @@ -1,5 +1,9 @@ { "inferenceSettings": { + "batchSize": 10, + "chunkSize": 10, + "numEpochs": 5, + "numSamples": 100, "obsData": "", "parameters": [], "priorMethod": "Uniform Prior", From 3ff75f536a0b40698acfb367c01453c43f309b40 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Wed, 1 Mar 2023 15:25:10 -0500 Subject: [PATCH 048/109] Disabled 'AWS-Cloud' option for running model inference. --- client/pages/workflow-manager.js | 1 + 1 file changed, 1 insertion(+) diff --git a/client/pages/workflow-manager.js b/client/pages/workflow-manager.js index 89537d728b..9616fa8a61 100644 --- a/client/pages/workflow-manager.js +++ b/client/pages/workflow-manager.js @@ -320,6 +320,7 @@ let WorkflowManager = PageView.extend({ $(this.queryByHook("start-job")).prop("disabled", numParams < 1); }, this)) }else if(this.model.type === "Model Inference") { + $(this.queryByHook("aws-start-job")).addClass("disabled") if(this.model.settings.inferenceSettings.parameters.length < 1) { $(this.queryByHook("start-job")).prop("disabled", true); } From caae73e22c324979d5d2df7761b12bd627e4d07c Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Wed, 1 Mar 2023 15:26:00 -0500 Subject: [PATCH 049/109] Added backend support for infer args. --- stochss/handlers/util/model_inference.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/stochss/handlers/util/model_inference.py b/stochss/handlers/util/model_inference.py index 7015bbd768..fdce337fa7 100644 --- a/stochss/handlers/util/model_inference.py +++ b/stochss/handlers/util/model_inference.py @@ -29,6 +29,7 @@ from sciope.inference import smc_abc from sciope.utilities.priors import uniform_prior from sciope.utilities.summarystats import auto_tsfresh, identity +from sciope.utilities.epsilonselectors import RelativeEpsilonSelector from .stochss_job import StochSSJob from .stochss_errors import StochSSJobError, StochSSJobResultsError @@ -68,6 +69,13 @@ def __get_csv_data(cls, path): data = numpy.array(rows).swapaxes(0, 1).astype("float") return data + def __get_infer_args(self): + settings = self.settings['inferenceSettings'] + eps_selector = RelativeEpsilonSelector(20, max_rounds=settings['numEpochs']) + args = [settings['num_samples'], settings['batchSize']] + kwargs = {"eps_selector": eps_selector, "chunk_size": settings['chunkSize']} + return args, kwargs + def __get_prior_function(self): dmin = [] dmax = [] @@ -168,7 +176,8 @@ def run(self, verbose=True): smc_abc_inference = smc_abc.SMCABC( obs_data, sim=self.simulator, prior_function=prior, summaries_function=summaries.compute ) - results = smc_abc_inference.infer(num_samples=100, batch_size=10) + infer_args, infer_kwargs = self.__get_infer_args() + results = smc_abc_inference.infer(*infer_args, **infer_kwargs) if verbose: log.info("The model inference has completed") log.info("Storing the results as pickle.") From deaf7aa621a312763f08b0d27fa06afdf2cb8376 Mon Sep 17 00:00:00 2001 From: BryanRumsey <44621966+BryanRumsey@users.noreply.github.com> Date: Thu, 9 Mar 2023 15:18:05 -0500 Subject: [PATCH 050/109] Updated version to v2.5.2 --- __version__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/__version__.py b/__version__.py index 445149aff1..15e8610eb2 100644 --- a/__version__.py +++ b/__version__.py @@ -5,7 +5,7 @@ # @website https://github.com/stochss/stochss # ============================================================================= -__version__ = '2.5.1' +__version__ = '2.5.2' __title__ = 'StochSS' __description__ = 'StochSS is an integrated development environment (IDE) \ for simulation of biochemical networks.' From a41b1c23549a3cdddb76d790240a17752142a906 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Fri, 10 Mar 2023 12:39:53 -0500 Subject: [PATCH 051/109] Added results template for model inference to the job results section. --- .../templates/inferenceResultsView.pug | 157 ++++++++++++++++++ client/job-view/views/job-results-view.js | 27 ++- 2 files changed, 178 insertions(+), 6 deletions(-) create mode 100644 client/job-view/templates/inferenceResultsView.pug diff --git a/client/job-view/templates/inferenceResultsView.pug b/client/job-view/templates/inferenceResultsView.pug new file mode 100644 index 0000000000..28f0e9bbb5 --- /dev/null +++ b/client/job-view/templates/inferenceResultsView.pug @@ -0,0 +1,157 @@ +div#workflow-results.card + + div.card-header.pb-0 + + h3.inline Results + + button.btn.btn-outline-collapse(data-toggle="collapse" data-target="#collapse-results" data-hook="collapse-results-btn") - + + div.collapse.show(id="collapse-results" data-hook="workflow-results") + + div.card-body + + div.collapse(id="edit-plot-args" data-hook="edit-plot-args") + + table.table + thead + tr + th(scope="col") Title + th(scope="col") X-axis Label + th(scope="col") Y-axis Label + + tbody + tr + td: div(id="title" data-hook="title") + td: div(id="xaxis" data-hook="xaxis") + td: div(id="yaxis" data-hook="yaxis") + + div.card + + div.card-header.pb-0 + + h5.inline Plot Inference + + button.btn.btn-outline-collapse( + data-toggle="collapse" + data-target="#collapse-inference" + id="collapse-inference-btn" + data-hook="collapse-inference-btn" + data-trigger="collapse-plot-container" + data-type="inference" + ) - + + div.collapse.show(id="collapse-inference") + + div.card-body + + div(data-hook="inference-plot") + + div.spinner-border.workflow-plot(data-hook="inference-plot-spinner") + + button.btn.btn-primary.box-shadow(data-hook="inference-edit-plot" data-target="edit-plot" disabled) Edit Plot + + button.btn.btn-primary.box-shadow.dropdown-toggle( + id="inference-download" + data-hook="inference-download" + data-toggle="dropdown", + aria-haspopup="true", + aria-expanded="false", + type="button" + disabled + ) Download + + ul.dropdown-menu(aria-labelledby="#inference-download") + li.dropdown-item( + data-hook="inference-download-png-custom" + data-target="download-png-custom" + data-type="inference" + ) Plot as .png + li.dropdown-item.disabled( + data-hook="inference-download-json" + data-target="download-json" + data-type="inference" + ) Plot as .json + li.dropdown-item.disabled( + data-hook="inference-plot-csv" + data-target="download-plot-csv" + data-type="inference" + ) Plot Results as .csv + + div.card + + div.card-header.pb-0 + + h5.inline Plot Epoch + + button.btn.btn-outline-collapse( + data-toggle="collapse" + data-target="#collapse-epoch" + id="collapse-epoch-btn" + data-hook="collapse-epoch-btn" + data-trigger="collapse-plot-container" + data-type="epoch" + ) + + + div.collapse(id="collapse-epoch") + + div.card-body + + div(data-hook="epoch-plot") + + div.spinner-border.workflow-plot(data-hook="epoch-plot-spinner") + + button.btn.btn-primary.box-shadow(data-hook="epoch-edit-plot" data-target="edit-plot" disabled) Edit Plot + + button.btn.btn-primary.box-shadow(data-hook="multiple-plots", data-type="mltplplt" disabled) Multiple Plots + + button.btn.btn-primary.box-shadow.dropdown-toggle( + id="epoch-download" + data-hook="epoch-download" + data-toggle="dropdown", + aria-haspopup="true", + aria-expanded="false", + type="button" + disabled + ) Download + + ul.dropdown-menu(aria-labelledby="#epoch-download") + li.dropdown-item( + data-hook="epoch-download-png-custom" + data-target="download-png-custom" + data-type="epoch" + ) Plot as .png + li.dropdown-item.disabled( + data-hook="epoch-download-json" + data-target="download-json" + data-type="epoch" + ) Plot as .json + li.dropdown-item.disabled( + data-hook="epoch-plot-csv" + data-target="download-plot-csv" + data-type="epoch" + ) Plot Results as .csv + + div + + button.btn.btn-primary.box-shadow(id="convert-to-notebook" data-hook="convert-to-notebook" disabled) Convert to Notebook + + button.btn.btn-primary.box-shadow(id="download-results-csv" data-hook="download-results-csv" disabled) Download Full Results as .csv + + button.btn.btn-primary.box-shadow(id="job-presentation" data-hook="job-presentation" disabled) Publish + + div.saving-status(data-hook="job-action-start") + + div.spinner-grow + + span Publishing ... + + div.saved-status(data-hook="job-action-end") + + span Published + + div.save-error-status(data-hook="job-action-err") + + span Error + + div.text-info(data-hook="update-format-message" style="display: none;") + | To publish you job the workflows format must be updated. diff --git a/client/job-view/views/job-results-view.js b/client/job-view/views/job-results-view.js index 55f6328ac6..478b6925f4 100644 --- a/client/job-view/views/job-results-view.js +++ b/client/job-view/views/job-results-view.js @@ -29,11 +29,12 @@ let View = require('ampersand-view'); let SelectView = require('ampersand-select-view'); let SweepParametersView = require('./sweep-parameter-range-view'); //templates +let spatialTemplate = require('../templates/spatialResultsView.pug'); let wellMixedTemplate = require('../templates/gillespyResultsView.pug'); -let ensembleTemplate = require('../templates/gillespyResultsEnsembleView.pug'); -let sweepTemplate = require('../templates/parameterSweepResultsView.pug'); let scanTemplate = require('../templates/parameterScanResultsView.pug'); -let spatialTemplate = require('../templates/spatialResultsView.pug'); +let inferenceTemplate = require('../templates/inferenceResultsView.pug'); +let sweepTemplate = require('../templates/parameterSweepResultsView.pug'); +let ensembleTemplate = require('../templates/gillespyResultsEnsembleView.pug'); module.exports = View.extend({ events: { @@ -76,7 +77,8 @@ module.exports = View.extend({ let templates = { "Ensemble Simulation": isEnsemble ? ensembleTemplate : wellMixedTemplate, "Parameter Sweep": isParameterScan ? scanTemplate : sweepTemplate, - "Spatial Ensemble Simulation": spatialTemplate + "Spatial Ensemble Simulation": spatialTemplate, + "Model Inference": inferenceTemplate } this.template = templates[this.titleType]; View.prototype.render.apply(this, arguments); @@ -110,7 +112,7 @@ module.exports = View.extend({ } this.getPlot("psweep"); this.renderSweepParameterView(); - }else{ + }else if(this.titleType === "Spatial Ensemble Simulation") { var type = "spatial"; this.spatialTarget = "type"; this.targetIndex = null; @@ -121,6 +123,15 @@ module.exports = View.extend({ $(this.queryByHook("spatial-trajectory-container")).css("display", "inline-block"); } $(this.queryByHook("spatial-plot-csv")).css('display', 'none'); + }else{ + var type = "inference"; + this.epochIndex = this.model.settings.inferenceSettings.numEpochs; + // TODO: Enable inference convert to notebook when implemented + $(this.queryByHook("convert-to-notebook")).prop("disabled", true); + // TODO: Enable inference CSV download when implemented + $(this.queryByHook("download-results-csv")).prop("disabled", true); + // TODO: Enable inference presentations when implemented + $(this.queryByHook("job-presentation")).prop("disabled", true); } this.getPlot(type); }, @@ -171,7 +182,7 @@ module.exports = View.extend({ let storageKey = JSON.stringify(data); data['plt_data'] = this.getPlotLayoutData(); if(Boolean(this.plots[storageKey])) { - let renderTypes = ['psweep', 'ts-psweep', 'ts-psweep-mp', 'mltplplt', 'spatial']; + let renderTypes = ['psweep', 'ts-psweep', 'ts-psweep-mp', 'mltplplt', 'spatial', 'epoch']; if(renderTypes.includes(type)) { this.plotFigure(this.plots[storageKey], type); } @@ -228,6 +239,10 @@ module.exports = View.extend({ target: this.spatialTarget, index: this.targetIndex, mode: this.targetMode, trajectory: this.trajectoryIndex - 1 }; data['plt_key'] = type; + }else if(["inference", "epoch"].includes(type)) { + data['sim_type'] = "Inference"; + data['data_keys'] = {"epoch": type === "inference" ? null : this.epochIndex - 1}; + data['plt_key'] = "inference"; }else { data['sim_type'] = "GillesPy2"; data['data_keys'] = {}; From c400794520898324ab7f2144491baf82d5738ebb Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Fri, 10 Mar 2023 14:33:25 -0500 Subject: [PATCH 052/109] Added plot function for full model inference results. --- stochss/handlers/util/__init__.py | 1 + stochss/handlers/util/model_inference.py | 69 ++++++++++++++++++++++++ stochss/handlers/workflows.py | 38 +++++++------ 3 files changed, 93 insertions(+), 15 deletions(-) diff --git a/stochss/handlers/util/__init__.py b/stochss/handlers/util/__init__.py index c0b67a89ef..5a3ddbd51b 100644 --- a/stochss/handlers/util/__init__.py +++ b/stochss/handlers/util/__init__.py @@ -30,4 +30,5 @@ from .stochss_project import StochSSProject from .ensemble_simulation import EnsembleSimulation from .parameter_sweep import ParameterSweep +from .model_inference import ModelInference from .stochss_errors import StochSSAPIError, report_error, report_critical_error diff --git a/stochss/handlers/util/model_inference.py b/stochss/handlers/util/model_inference.py index fdce337fa7..65a6c6c992 100644 --- a/stochss/handlers/util/model_inference.py +++ b/stochss/handlers/util/model_inference.py @@ -18,11 +18,13 @@ import os import csv import copy +import json import pickle import logging import traceback import numpy +import plotly import gillespy2 @@ -36,6 +38,17 @@ log = logging.getLogger("stochss") +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' +] + class ModelInference(StochSSJob): ''' ################################################################################################ @@ -69,6 +82,44 @@ def __get_csv_data(cls, path): data = numpy.array(rows).swapaxes(0, 1).astype("float") return data + @classmethod + def __get_full_results_plot(cls, results, names, values, + title=None, xaxis="Values", yaxis="Sample Concentrations"): + cols = 2 + nbins = 50 + + fig = plotly.subplots.make_subplots( + rows=int(numpy.ceil(len(values)/cols)), cols=cols, subplot_titles=names, x_title=xaxis, y_title=yaxis + ) + + for i, result in enumerate(results): + accepted_samples = numpy.vstack(result['accepted_samples']).swapaxes(0, 1) + base_opacity = 0.5 if len(results) <= 1 else (i / (len(results) - 1) * 0.5) + + for j, accepted_values in enumerate(accepted_samples): + name = f"epoch {i + 1}" + color = common_rgb_values[i % len(common_rgb_values)] + opacity = base_opacity + 0.25 + trace = plotly.graph_objs.Histogram( + x=accepted_values, name=name, legendgroup=name, showlegend=j==0, + marker_color=color, opacity=opacity, nbinsx=nbins + ) + + row = int(numpy.ceil((j + 1) / cols)) + col = (j % cols) + 1 + fig.append_trace(trace, row, col) + fig.add_vline( + result['inferred_parameters'][j], row=row, col=col, line={"color": color}, + opacity=base_opacity + 0.5, exclude_empty_subplots=True, layer=True + ) + if i == len(results) - 1: + fig.add_vline(values[j], row=row, col=col, line={"color": "red"}, layer=True) + + fig.update_layout(barmode='overlay') + if title is not None: + fig.update_layout(title=title) + return fig + def __get_infer_args(self): settings = self.settings['inferenceSettings'] eps_selector = RelativeEpsilonSelector(20, max_rounds=settings['numEpochs']) @@ -136,6 +187,24 @@ def __store_pickled_results(cls, results): return message return False + def get_result_plot(self, epoch=None, add_config=False, **kwargs): + """ + Generate a plot for inference results. + """ + model = self.load_models()[0] + results = self.__get_filtered_ensemble_results(None) + parameters = self.load_settings()['inferenceSettings']['parameters'] + if epoch is None: + names = [] + values = [] + for parameter in parameters: + names.append(parameter['name']) + values.append(model.listOfParameters[parameter['name']]) + fig = self.__get_full_results_plot(results, names, values, **kwargs) + if add_config: + fig["config"] = {"responsive": True} + return json.loads(json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder)) + def process(self, raw_results): """ Post processing function used to reshape simulator results and diff --git a/stochss/handlers/workflows.py b/stochss/handlers/workflows.py index 496b8824b6..80dc75684a 100644 --- a/stochss/handlers/workflows.py +++ b/stochss/handlers/workflows.py @@ -29,7 +29,7 @@ from .util import StochSSFolder, StochSSJob, StochSSModel, StochSSSpatialModel, StochSSNotebook, \ StochSSWorkflow, StochSSParamSweepNotebook, StochSSSciopeNotebook, \ - StochSSAPIError, report_error, report_critical_error + StochSSAPIError, report_error, report_critical_error, ModelInference log = logging.getLogger('stochss') @@ -249,21 +249,29 @@ async def get(self): body = json.loads(self.get_query_argument(name='data')) log.debug(f"Plot args passed to the plot: {body}") try: - job = StochSSJob(path=path) - if body['sim_type'] in ("GillesPy2", "GillesPy2_PS"): - fig = job.get_plot_from_results(data_keys=body['data_keys'], - plt_key=body['plt_key'], add_config=True) - job.print_logs(log) - elif body['sim_type'] == "SpatialPy": - fig = job.get_plot_from_spatial_results( - data_keys=body['data_keys'], add_config=True - ) + if body['sim_type'] == "Inference": + job = ModelInference(path=path) + kwargs = {'add_config': True} + kwargs.update(body['data_keys']) + if "plt_data" in body.keys(): + kwargs.update(body['plt_data']) + fig = job.get_result_plot(**kwargs) else: - fig = job.get_psweep_plot_from_results(fixed=body['data_keys'], - kwargs=body['plt_key'], add_config=True) - job.print_logs(log) - if "plt_data" in body.keys(): - fig = job.update_fig_layout(fig=fig, plt_data=body['plt_data']) + job = StochSSJob(path=path) + if body['sim_type'] in ("GillesPy2", "GillesPy2_PS"): + fig = job.get_plot_from_results(data_keys=body['data_keys'], + plt_key=body['plt_key'], add_config=True) + job.print_logs(log) + elif body['sim_type'] == "SpatialPy": + fig = job.get_plot_from_spatial_results( + data_keys=body['data_keys'], add_config=True + ) + else: + fig = job.get_psweep_plot_from_results(fixed=body['data_keys'], + kwargs=body['plt_key'], add_config=True) + job.print_logs(log) + if "plt_data" in body.keys(): + fig = job.update_fig_layout(fig=fig, plt_data=body['plt_data']) log.debug(f"Plot figure: {fig}") self.write(fig) except StochSSAPIError as err: From ab37c7f417fc805260e00e231dd56a629429f237 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Mon, 13 Mar 2023 12:06:55 -0400 Subject: [PATCH 053/109] Added plotting functions for model inference results. --- stochss/handlers/util/model_inference.py | 102 ++++++++++++++++++++--- stochss/handlers/workflows.py | 2 +- 2 files changed, 90 insertions(+), 14 deletions(-) diff --git a/stochss/handlers/util/model_inference.py b/stochss/handlers/util/model_inference.py index 65a6c6c992..34ba58b7b6 100644 --- a/stochss/handlers/util/model_inference.py +++ b/stochss/handlers/util/model_inference.py @@ -25,6 +25,7 @@ import numpy import plotly +from plotly import subplots import gillespy2 @@ -49,6 +50,14 @@ '#ffddee', '#702afb' ] +def combine_colors(colors): + red = int(sum([int(k[:2], 16) * 0.5 for k in colors])) + green = int(sum([int(k[2:4], 16) * 0.5 for k in colors])) + blue = int(sum([int(k[4:6], 16) * 0.5 for k in colors])) + zpad = lambda x: x if len(x)==2 else '0' + x + color = f"#{zpad(hex(red)[2:])}{zpad(hex(green)[2:])}{zpad(hex(blue)[2:])}" + return color + class ModelInference(StochSSJob): ''' ################################################################################################ @@ -82,14 +91,64 @@ def __get_csv_data(cls, path): data = numpy.array(rows).swapaxes(0, 1).astype("float") return data + @classmethod + def __get_epoch_result_plot(cls, results, names, values, dmin, dmax, + title=None, xaxis="Values", yaxis="Sample Concentrations"): + accepted_samples = numpy.vstack(results['accepted_samples']).swapaxes(0, 1) + + nbins = 50 + rows = cols = len(accepted_samples) + fig = subplots.make_subplots( + rows=rows, cols=cols, column_titles=names, row_titles=names, + x_title=xaxis, y_title=yaxis, vertical_spacing=0.075 + ) + + for i in range(rows): + for j in range(cols): + row = i + 1 + col = j + 1 + if i > j: + continue + if i == j: + color = common_rgb_values[(i)%len(common_rgb_values)] + trace = plotly.graph_objs.Histogram( + x=accepted_samples[i], name=names[i], legendgroup=names[i], showlegend=False, + nbinsx=nbins, opacity=0.75, marker_color=color + ) + + fig.append_trace(trace, row, col) + fig.update_xaxes(row=row, col=col, range=[dmin[i], dmax[i]]) + fig.add_vline( + results['inferred_parameters'][i], row=row, col=col, line={"color": "green"}, + exclude_empty_subplots=True, layer='above' + ) + fig.add_vline(values[i], row=row, col=col, line={"color": "red"}, layer='above') + else: + color = combine_colors([ + common_rgb_values[(i)%len(common_rgb_values)][1:], + common_rgb_values[(j)%len(common_rgb_values)][1:] + ]) + trace = plotly.graph_objs.Scatter( + x=accepted_samples[j], y=accepted_samples[i], mode='markers', marker_color=color, + name=f"{names[j]} X {names[i]}", legendgroup=f"{names[j]} X {names[i]}", showlegend=False + ) + fig.append_trace(trace, row, col) + fig.update_xaxes(row=row, col=col, range=[dmin[j], dmax[j]]) + fig.update_yaxes(row=row, col=col, range=[dmin[i], dmax[i]]) + + fig.update_layout(height=500 * rows, title=title) + return fig + @classmethod def __get_full_results_plot(cls, results, names, values, title=None, xaxis="Values", yaxis="Sample Concentrations"): cols = 2 nbins = 50 + rows = int(numpy.ceil(len(results[0]['accepted_samples'][0])/cols)) + + fig = subplots.make_subplots( + rows=rows, cols=cols, subplot_titles=names, x_title=xaxis, y_title=yaxis, vertical_spacing=0.075 - fig = plotly.subplots.make_subplots( - rows=int(numpy.ceil(len(values)/cols)), cols=cols, subplot_titles=names, x_title=xaxis, y_title=yaxis ) for i, result in enumerate(results): @@ -110,12 +169,12 @@ def __get_full_results_plot(cls, results, names, values, fig.append_trace(trace, row, col) fig.add_vline( result['inferred_parameters'][j], row=row, col=col, line={"color": color}, - opacity=base_opacity + 0.5, exclude_empty_subplots=True, layer=True + opacity=base_opacity + 0.5, exclude_empty_subplots=True, layer='above' ) if i == len(results) - 1: - fig.add_vline(values[j], row=row, col=col, line={"color": "red"}, layer=True) + fig.add_vline(values[j], row=row, col=col, line={"color": "red"}, layer='above') - fig.update_layout(barmode='overlay') + fig.update_layout(barmode='overlay', height=500 * rows) if title is not None: fig.update_layout(title=title) return fig @@ -127,6 +186,11 @@ def __get_infer_args(self): kwargs = {"eps_selector": eps_selector, "chunk_size": settings['chunkSize']} return args, kwargs + def __get_pickled_results(self): + path = os.path.join(self.__get_results_path(full=True), "results.p") + with open(path, "rb") as results_file: + return pickle.load(results_file) + def __get_prior_function(self): dmin = [] dmax = [] @@ -135,6 +199,9 @@ def __get_prior_function(self): dmax.append(parameter['max']) return uniform_prior.UniformPrior(numpy.array(dmin, dtype="float"), numpy.array(dmax, dtype="float")) + def __get_results_path(self, full=False): + return os.path.join(self.get_path(full=full), "results") + def __get_run_settings(self): solver_map = {"ODE":self.g_model.get_best_solver_algo("ODE"), "SSA":self.g_model.get_best_solver_algo("SSA"), @@ -192,18 +259,27 @@ def get_result_plot(self, epoch=None, add_config=False, **kwargs): Generate a plot for inference results. """ model = self.load_models()[0] - results = self.__get_filtered_ensemble_results(None) + results = self.__get_pickled_results() parameters = self.load_settings()['inferenceSettings']['parameters'] + names = [] + values = [] + dmin = [] + dmax = [] + for parameter in parameters: + names.append(parameter['name']) + values.append(model.listOfParameters[parameter['name']].value) + dmin.append(parameter['min']) + dmax.append(parameter['max']) if epoch is None: - names = [] - values = [] - for parameter in parameters: - names.append(parameter['name']) - values.append(model.listOfParameters[parameter['name']]) - fig = self.__get_full_results_plot(results, names, values, **kwargs) + fig_obj = self.__get_full_results_plot(results, names, values, **kwargs) + else: + if 'titlt' not in kwargs or kwargs['title'] is None: + kwargs['title'] = f"Epoch {epoch + 1}" + fig_obj = self.__get_epoch_result_plot(results[epoch], names, values, dmin, dmax, **kwargs) + fig = json.loads(json.dumps(fig_obj, cls=plotly.utils.PlotlyJSONEncoder)) if add_config: fig["config"] = {"responsive": True} - return json.loads(json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder)) + return fig def process(self, raw_results): """ diff --git a/stochss/handlers/workflows.py b/stochss/handlers/workflows.py index 80dc75684a..e3f22ffcda 100644 --- a/stochss/handlers/workflows.py +++ b/stochss/handlers/workflows.py @@ -253,7 +253,7 @@ async def get(self): job = ModelInference(path=path) kwargs = {'add_config': True} kwargs.update(body['data_keys']) - if "plt_data" in body.keys(): + if "plt_data" in body.keys() and body['plt_data'] is not None: kwargs.update(body['plt_data']) fig = job.get_result_plot(**kwargs) else: From 036f2fc56759a499edfaa3c8f9d673a67bfbfac6 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Mon, 13 Mar 2023 13:27:48 -0400 Subject: [PATCH 054/109] Tweeks to the inference results plotting . --- stochss/handlers/util/model_inference.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/stochss/handlers/util/model_inference.py b/stochss/handlers/util/model_inference.py index 34ba58b7b6..b6ee8b6d0c 100644 --- a/stochss/handlers/util/model_inference.py +++ b/stochss/handlers/util/model_inference.py @@ -93,7 +93,7 @@ def __get_csv_data(cls, path): @classmethod def __get_epoch_result_plot(cls, results, names, values, dmin, dmax, - title=None, xaxis="Values", yaxis="Sample Concentrations"): + title=None, xaxis=None, yaxis=None): accepted_samples = numpy.vstack(results['accepted_samples']).swapaxes(0, 1) nbins = 50 @@ -136,6 +136,9 @@ def __get_epoch_result_plot(cls, results, names, values, dmin, dmax, fig.update_xaxes(row=row, col=col, range=[dmin[j], dmax[j]]) fig.update_yaxes(row=row, col=col, range=[dmin[i], dmax[i]]) + if title is not None: + title = {'text': title, 'x': 0.5, 'xanchor': 'center'} + fig.update_layout(title=title) fig.update_layout(height=500 * rows, title=title) return fig @@ -176,6 +179,7 @@ def __get_full_results_plot(cls, results, names, values, fig.update_layout(barmode='overlay', height=500 * rows) if title is not None: + title = {'text': title, 'x': 0.5, 'xanchor': 'center'} fig.update_layout(title=title) return fig @@ -273,8 +277,6 @@ def get_result_plot(self, epoch=None, add_config=False, **kwargs): if epoch is None: fig_obj = self.__get_full_results_plot(results, names, values, **kwargs) else: - if 'titlt' not in kwargs or kwargs['title'] is None: - kwargs['title'] = f"Epoch {epoch + 1}" fig_obj = self.__get_epoch_result_plot(results[epoch], names, values, dmin, dmax, **kwargs) fig = json.loads(json.dumps(fig_obj, cls=plotly.utils.PlotlyJSONEncoder)) if add_config: From e01094d328368979b30a5670093d09af6fa323c7 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Mon, 13 Mar 2023 15:07:50 -0400 Subject: [PATCH 055/109] Enabled 'Edit Plot' button functionality. --- .../templates/inferenceResultsView.pug | 24 +++++++ client/job-view/views/job-results-view.js | 64 ++++++++++++++++--- stochss/handlers/util/model_inference.py | 2 +- 3 files changed, 80 insertions(+), 10 deletions(-) diff --git a/client/job-view/templates/inferenceResultsView.pug b/client/job-view/templates/inferenceResultsView.pug index 28f0e9bbb5..9ad69736f8 100644 --- a/client/job-view/templates/inferenceResultsView.pug +++ b/client/job-view/templates/inferenceResultsView.pug @@ -96,6 +96,30 @@ div#workflow-results.card div.card-body + div + + div.mx-1.row.head.align-items-baseline + + div.col-sm-12 + + h6 + + div.inline.mr-2 Epoch: + + div.inline(data-hook="epoch-index-value")=this.epochIndex + + div.mx-1.my-3.row + + div.col-sm-12 + + input.custom-range( + type="range" + min="1" + max=`${this.model.settings.inferenceSettings.numEpochs}` + value=this.epochIndex + data-hook="epoch-index-slider" + ) + div(data-hook="epoch-plot") div.spinner-border.workflow-plot(data-hook="epoch-plot-spinner") diff --git a/client/job-view/views/job-results-view.js b/client/job-view/views/job-results-view.js index 478b6925f4..74347941fa 100644 --- a/client/job-view/views/job-results-view.js +++ b/client/job-view/views/job-results-view.js @@ -44,6 +44,7 @@ module.exports = View.extend({ '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=epoch-index-slider]' : 'getPlotForEpoch', 'change [data-hook=specie-of-interest-list]' : 'getPlotForSpecies', 'change [data-hook=feature-extraction-list]' : 'getPlotForFeatureExtractor', 'change [data-hook=ensemble-aggragator-list]' : 'getPlotForEnsembleAggragator', @@ -58,7 +59,8 @@ module.exports = View.extend({ 'click [data-hook=convert-to-notebook]' : 'handleConvertToNotebookClick', 'click [data-hook=download-results-csv]' : 'handleFullCSVClick', 'click [data-hook=job-presentation]' : 'handlePresentationClick', - 'input [data-hook=trajectory-index-slider]' : 'viewTrajectoryIndex' + 'input [data-hook=trajectory-index-slider]' : 'viewTrajectoryIndex', + 'input [data-hook=epoch-index-slider]' : 'viewEpochIndex' }, initialize: function (attrs, options) { View.prototype.initialize.apply(this, arguments); @@ -68,6 +70,7 @@ module.exports = View.extend({ this.tooltips = Tooltips.jobResults; this.plots = {}; this.plotArgs = {}; + this.activePlots = {}; this.trajectoryIndex = 1; }, render: function (attrs, options) { @@ -126,6 +129,8 @@ module.exports = View.extend({ }else{ var type = "inference"; this.epochIndex = this.model.settings.inferenceSettings.numEpochs; + $(this.queryByHook("epoch-index-value")).text(this.epochIndex); + $(this.queryByHook("epoch-index-slider")).prop("value", this.epochIndex); // TODO: Enable inference convert to notebook when implemented $(this.queryByHook("convert-to-notebook")).prop("disabled", true); // TODO: Enable inference CSV download when implemented @@ -184,6 +189,7 @@ module.exports = View.extend({ if(Boolean(this.plots[storageKey])) { let renderTypes = ['psweep', 'ts-psweep', 'ts-psweep-mp', 'mltplplt', 'spatial', 'epoch']; if(renderTypes.includes(type)) { + this.activePlots[type] = storageKey; this.plotFigure(this.plots[storageKey], type); } }else{ @@ -191,6 +197,17 @@ module.exports = View.extend({ let endpoint = `${path.join(app.getApiPath(), "workflow/plot-results")}${queryStr}`; app.getXHR(endpoint, { success: (err, response, body) => { + if(type === "epoch") { + body.layout.annotations.push({ + font: {size: 16}, showarrow: false, text: "", x: 0.5, xanchor: "center", xref: "paper", + y: 0, yanchor: "top", yref: "paper", yshift: -30 + }); + body.layout.annotations.push({ + font: {size: 16}, showarrow: false, text: "", textangle: -90, x: 0, xanchor: "right", + xref: "paper", xshift: -40, y: 0.5, yanchor: "middle", yref: "paper" + }); + } + this.activePlots[type] = storageKey; this.plots[storageKey] = body; this.plotFigure(body, type); }, @@ -260,6 +277,10 @@ module.exports = View.extend({ this.model.settings.resultsSettings.reducer = e.target.value; this.getPlot('psweep') }, + getPlotForEpoch: function (e) { + this.epochIndex = Number(e.target.value); + this.getPlot('epoch'); + }, getPlotForFeatureExtractor: function (e) { this.model.settings.resultsSettings.mapper = e.target.value; this.getPlot('psweep') @@ -329,8 +350,12 @@ module.exports = View.extend({ }, getType: function (storageKey) { let plotData = JSON.parse(storageKey) - if(plotData.sim_type === "GillesPy2") { return plotData.plt_key } - if(plotData.sim_type === "GillesPy2_PS") { return "ts-psweep"} + if(plotData.sim_type === "GillesPy2") { return plotData.plt_key; } + if(plotData.sim_type === "GillesPy2_PS") { return "ts-psweep"; } + if(plotData.sim_type === "Inference") { + if(plotData.data_keys.epoch === null) { return "inference"; } + return "epoch" + } return "psweep" }, handleCollapsePlotContainerClick: function (e) { @@ -578,8 +603,14 @@ module.exports = View.extend({ for (var storageKey in this.plots) { let type = this.getType(storageKey); let fig = this.plots[storageKey] - fig.layout.title.text = e.target.value - this.plotFigure(fig, type) + if(Object.keys(fig.layout).includes('title')) { + fig.layout.title.text = e.target.value + }else{ + fig.layout.title = {'text': e.target.value, 'x': 0.5, 'xanchor': 'center'} + } + } + for (var [type, storageKey] of Object.entries(this.activePlots)) { + this.plotFigure(this.plots[storageKey], type); } }, setXAxis: function (e) { @@ -587,8 +618,14 @@ module.exports = View.extend({ for (var storageKey in this.plots) { let type = this.getType(storageKey); let fig = this.plots[storageKey] - fig.layout.xaxis.title.text = e.target.value - this.plotFigure(fig, type) + if(['inference', 'epoch'].includes(type)) { + fig.layout.annotations.at(-2).text = e.target.value + }else { + fig.layout.xaxis.title.text = e.target.value + } + } + for (var [type, storageKey] of Object.entries(this.activePlots)) { + this.plotFigure(this.plots[storageKey], type); } }, setYAxis: function (e) { @@ -596,8 +633,14 @@ module.exports = View.extend({ for (var storageKey in this.plots) { let type = this.getType(storageKey); let fig = this.plots[storageKey] - fig.layout.yaxis.title.text = e.target.value - this.plotFigure(fig, type) + if(['inference', 'epoch'].includes(type)) { + fig.layout.annotations.at(-1).text = e.target.value + }else { + fig.layout.xaxis.title.text = e.target.value + } + } + for (var [type, storageKey] of Object.entries(this.activePlots)) { + this.plotFigure(this.plots[storageKey], type); } }, startAction: function () { @@ -607,6 +650,9 @@ module.exports = View.extend({ }, update: function () {}, updateValid: function () {}, + viewEpochIndex: function (e) { + $(this.queryByHook("epoch-index-value")).html(e.target.value); + }, viewTrajectoryIndex: function (e) { $(this.queryByHook("trajectory-index-value")).html(e.target.value); }, diff --git a/stochss/handlers/util/model_inference.py b/stochss/handlers/util/model_inference.py index b6ee8b6d0c..92afd5426b 100644 --- a/stochss/handlers/util/model_inference.py +++ b/stochss/handlers/util/model_inference.py @@ -136,10 +136,10 @@ def __get_epoch_result_plot(cls, results, names, values, dmin, dmax, fig.update_xaxes(row=row, col=col, range=[dmin[j], dmax[j]]) fig.update_yaxes(row=row, col=col, range=[dmin[i], dmax[i]]) + fig.update_layout(height=500 * rows) if title is not None: title = {'text': title, 'x': 0.5, 'xanchor': 'center'} fig.update_layout(title=title) - fig.update_layout(height=500 * rows, title=title) return fig @classmethod From 7850f2fad9cce7aa3ace8d49b9f4bccdfa6b7536 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Tue, 14 Mar 2023 10:39:33 -0400 Subject: [PATCH 056/109] Enabled 'Download as .json' button for results. --- client/job-view/templates/inferenceResultsView.pug | 6 ++---- client/job-view/views/job-results-view.js | 4 +++- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/client/job-view/templates/inferenceResultsView.pug b/client/job-view/templates/inferenceResultsView.pug index 9ad69736f8..7c9aac0319 100644 --- a/client/job-view/templates/inferenceResultsView.pug +++ b/client/job-view/templates/inferenceResultsView.pug @@ -66,7 +66,7 @@ div#workflow-results.card data-target="download-png-custom" data-type="inference" ) Plot as .png - li.dropdown-item.disabled( + li.dropdown-item( data-hook="inference-download-json" data-target="download-json" data-type="inference" @@ -126,8 +126,6 @@ div#workflow-results.card button.btn.btn-primary.box-shadow(data-hook="epoch-edit-plot" data-target="edit-plot" disabled) Edit Plot - button.btn.btn-primary.box-shadow(data-hook="multiple-plots", data-type="mltplplt" disabled) Multiple Plots - button.btn.btn-primary.box-shadow.dropdown-toggle( id="epoch-download" data-hook="epoch-download" @@ -144,7 +142,7 @@ div#workflow-results.card data-target="download-png-custom" data-type="epoch" ) Plot as .png - li.dropdown-item.disabled( + li.dropdown-item( data-hook="epoch-download-json" data-target="download-json" data-type="epoch" diff --git a/client/job-view/views/job-results-view.js b/client/job-view/views/job-results-view.js index 74347941fa..a700317fbb 100644 --- a/client/job-view/views/job-results-view.js +++ b/client/job-view/views/job-results-view.js @@ -390,7 +390,9 @@ module.exports = View.extend({ let jsonData = this.plots[storageKey]; let dataStr = JSON.stringify(jsonData); let dataURI = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr); - let exportFileDefaultName = type + '-plot.json'; + let nameIndex = type === "epoch" ? this.epochIndex : this.trajectoryIndex; + let nameBase = ["spatial", "epoch"].includes(type) ? `${type}${nameIndex}` : type; + let exportFileDefaultName = `${nameBase}-plot.json`; let linkElement = document.createElement('a'); linkElement.setAttribute('href', dataURI); From 729421938edb3b3db62b857b15a5d5117eb62c0c Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Tue, 14 Mar 2023 14:34:31 -0400 Subject: [PATCH 057/109] Implemented results to csv functionality. --- .../templates/inferenceResultsView.pug | 14 +--- client/job-view/views/job-results-view.js | 10 +-- stochss/handlers/util/model_inference.py | 67 +++++++++++++++++-- stochss/handlers/workflows.py | 7 +- 4 files changed, 76 insertions(+), 22 deletions(-) diff --git a/client/job-view/templates/inferenceResultsView.pug b/client/job-view/templates/inferenceResultsView.pug index 7c9aac0319..0470932670 100644 --- a/client/job-view/templates/inferenceResultsView.pug +++ b/client/job-view/templates/inferenceResultsView.pug @@ -71,11 +71,6 @@ div#workflow-results.card data-target="download-json" data-type="inference" ) Plot as .json - li.dropdown-item.disabled( - data-hook="inference-plot-csv" - data-target="download-plot-csv" - data-type="inference" - ) Plot Results as .csv div.card @@ -147,17 +142,12 @@ div#workflow-results.card data-target="download-json" data-type="epoch" ) Plot as .json - li.dropdown-item.disabled( - data-hook="epoch-plot-csv" - data-target="download-plot-csv" - data-type="epoch" - ) Plot Results as .csv div - button.btn.btn-primary.box-shadow(id="convert-to-notebook" data-hook="convert-to-notebook" disabled) Convert to Notebook + button.btn.btn-primary.box-shadow(id="convert-to-notebook" data-hook="convert-to-notebook") Convert to Notebook - button.btn.btn-primary.box-shadow(id="download-results-csv" data-hook="download-results-csv" disabled) Download Full Results as .csv + button.btn.btn-primary.box-shadow(id="download-results-csv" data-hook="download-results-csv") Download Full Results as .csv button.btn.btn-primary.box-shadow(id="job-presentation" data-hook="job-presentation" disabled) Publish diff --git a/client/job-view/views/job-results-view.js b/client/job-view/views/job-results-view.js index a700317fbb..be73a08f50 100644 --- a/client/job-view/views/job-results-view.js +++ b/client/job-view/views/job-results-view.js @@ -131,10 +131,6 @@ module.exports = View.extend({ this.epochIndex = this.model.settings.inferenceSettings.numEpochs; $(this.queryByHook("epoch-index-value")).text(this.epochIndex); $(this.queryByHook("epoch-index-slider")).prop("value", this.epochIndex); - // TODO: Enable inference convert to notebook when implemented - $(this.queryByHook("convert-to-notebook")).prop("disabled", true); - // TODO: Enable inference CSV download when implemented - $(this.queryByHook("download-results-csv")).prop("disabled", true); // TODO: Enable inference presentations when implemented $(this.queryByHook("job-presentation")).prop("disabled", true); } @@ -405,7 +401,11 @@ module.exports = View.extend({ pngButton.click(); }, handleFullCSVClick: function (e) { - this.downloadCSV("full", null); + if(this.titleType === "Model Inference") { + this.downloadCSV("inference", null); + }else { + this.downloadCSV("full", null); + } }, handlePlotCSVClick: function (e) { let type = e.target.dataset.type; diff --git a/stochss/handlers/util/model_inference.py b/stochss/handlers/util/model_inference.py index 92afd5426b..91a3b03eed 100644 --- a/stochss/handlers/util/model_inference.py +++ b/stochss/handlers/util/model_inference.py @@ -19,8 +19,10 @@ import csv import copy import json +import shutil import pickle import logging +import tempfile import traceback import numpy @@ -91,6 +93,13 @@ def __get_csv_data(cls, path): data = numpy.array(rows).swapaxes(0, 1).astype("float") return data + @classmethod + def __get_csvzip(cls, dirname, name): + shutil.make_archive(os.path.join(dirname, name), "zip", dirname, name) + path = os.path.join(dirname, f"{name}.zip") + with open(path, "rb") as zip_file: + return zip_file.read() + @classmethod def __get_epoch_result_plot(cls, results, names, values, dmin, dmax, title=None, xaxis=None, yaxis=None): @@ -170,12 +179,12 @@ def __get_full_results_plot(cls, results, names, values, row = int(numpy.ceil((j + 1) / cols)) col = (j % cols) + 1 fig.append_trace(trace, row, col) - fig.add_vline( - result['inferred_parameters'][j], row=row, col=col, line={"color": color}, - opacity=base_opacity + 0.5, exclude_empty_subplots=True, layer='above' - ) if i == len(results) - 1: fig.add_vline(values[j], row=row, col=col, line={"color": "red"}, layer='above') + fig.add_vline( + result['inferred_parameters'][j], row=row, col=col, line={"color": "green"}, + exclude_empty_subplots=True, layer='above' + ) fig.update_layout(barmode='overlay', height=500 * rows) if title is not None: @@ -258,6 +267,56 @@ def __store_pickled_results(cls, results): return message return False + def __to_csv(self, path='.', nametag="results_csv"): + results = self.__get_pickled_results() + settings = self.settings['inferenceSettings'] + names = list(map(lambda param: param['name'], settings['parameters'])) + + directory = os.path.join(path, str(nametag)) + if not os.path.exists(directory): + os.mkdir(directory) + + infer_csv = [["Epoch", "Accepted Count", "Trial Count"]] + epoch_headers = ["Sample ID", "Distances"] + for name in names: + infer_csv[0].append(name) + epoch_headers.insert(-1, name) + + for i, epoch in enumerate(results): + infer_line = [i + 1, epoch['accepted_count'], epoch['trial_count']] + infer_line.extend(epoch['inferred_parameters']) + infer_csv.append(infer_line) + + epoch_csv = [epoch_headers] + for j, accepted_sample in enumerate(epoch['accepted_samples']): + epoch_line = accepted_sample.tolist() + epoch_line.insert(0, j + 1) + epoch_line.extend(epoch['distances'][j]) + epoch_csv.append(epoch_line) + + epoch_path = os.path.join(directory, f"epoch{i + 1}-details.csv") + self.__write_csv_file(epoch_path, epoch_csv) + + infer_path = os.path.join(directory, "inference-overview.csv") + self.__write_csv_file(infer_path, infer_csv) + + @classmethod + def __write_csv_file(cls, path, data): + with open(path, "w", encoding="utf-8") as csv_file: + csv_writer = csv.writer(csv_file) + for line in data: + csv_writer.writerow(line) + + def get_csv_data(self, name): + """ + Generate the csv results and return the binary of the zipped archive. + """ + self.log("info", "Getting job results...") + nametag = f"Inference - {name} - Results-CSV" + with tempfile.TemporaryDirectory() as tmp_dir: + self.__to_csv(path=tmp_dir, nametag=nametag) + return self.__get_csvzip(tmp_dir, nametag) + def get_result_plot(self, epoch=None, add_config=False, **kwargs): """ Generate a plot for inference results. diff --git a/stochss/handlers/workflows.py b/stochss/handlers/workflows.py index e3f22ffcda..f5391b98c2 100644 --- a/stochss/handlers/workflows.py +++ b/stochss/handlers/workflows.py @@ -504,13 +504,18 @@ async def get(self): if data is not None: data = json.loads(data) try: - job = StochSSJob(path=path) + if csv_type == "inference": + job = ModelInference(path=path) + else: + job = StochSSJob(path=path) name = job.get_name() self.set_header('Content-Disposition', f'attachment; filename="{name}.zip"') if csv_type == "time series": csv_data = job.get_csvzip_from_results(**data, name=name) elif csv_type == "psweep": csv_data = job.get_psweep_csvzip_from_results(fixed=data, name=name) + elif csv_type == "inference": + csv_data = job.get_csv_data(name=name) else: csv_data = job.get_full_csvzip_from_results(name=name) self.write(csv_data) From 4c121441eed3788e0b5a0ee619b556f55bc64664 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Wed, 15 Mar 2023 10:35:57 -0400 Subject: [PATCH 058/109] Added backend support for converting inference jobs to notebooks . --- stochss/handlers/util/model_inference.py | 2 +- stochss/handlers/util/stochss_job.py | 4 ++-- stochss/handlers/workflows.py | 5 +++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/stochss/handlers/util/model_inference.py b/stochss/handlers/util/model_inference.py index 91a3b03eed..9de1ae7bbf 100644 --- a/stochss/handlers/util/model_inference.py +++ b/stochss/handlers/util/model_inference.py @@ -145,7 +145,7 @@ def __get_epoch_result_plot(cls, results, names, values, dmin, dmax, fig.update_xaxes(row=row, col=col, range=[dmin[j], dmax[j]]) fig.update_yaxes(row=row, col=col, range=[dmin[i], dmax[i]]) - fig.update_layout(height=500 * rows) + fig.update_layout(height=1000) if title is not None: title = {'text': title, 'x': 0.5, 'xanchor': 'center'} fig.update_layout(title=title) diff --git a/stochss/handlers/util/stochss_job.py b/stochss/handlers/util/stochss_job.py index 6bb5429267..554efbb376 100644 --- a/stochss/handlers/util/stochss_job.py +++ b/stochss/handlers/util/stochss_job.py @@ -466,7 +466,7 @@ def get_notebook_data(self): file = f"{self.get_name()}.ipynb" dirname = self.get_dir_name() if ".wkfl" in dirname: - codes = {"gillespy": "_ES", "spatial": "_SES", "parameterSweep": "_PS"} + codes = {"gillespy": "_ES", "spatial": "_SES", "parameterSweep": "_PS", "inference": "_MI"} code = codes[info['type']] wkfl_name = self.get_name(path=dirname).replace(code, "_NB") file = f"{wkfl_name}_{file}" @@ -474,7 +474,7 @@ def get_notebook_data(self): path = os.path.join(dirname, file) g_model, s_model = self.load_models() settings = self.load_settings() - if info['type'] in ("gillespy", "spatial"): + if info['type'] in ("gillespy", "spatial", "inference"): wkfl_type = info['type'] elif info['type'] == "parameterSweep" and \ len(settings['parameterSweepSettings']['parameters']) == 1: diff --git a/stochss/handlers/workflows.py b/stochss/handlers/workflows.py index f5391b98c2..37600472b7 100644 --- a/stochss/handlers/workflows.py +++ b/stochss/handlers/workflows.py @@ -325,10 +325,11 @@ async def get(self): notebook = StochSSParamSweepNotebook(**kwargs) notebooks = {"1d_parameter_sweep":notebook.create_1d_notebook, "2d_parameter_sweep":notebook.create_2d_notebook} - elif wkfl_type in ("sciope_model_exploration", "model_inference"): + elif wkfl_type in ("sciope_model_exploration", "model_inference", "inference"): notebook = StochSSSciopeNotebook(**kwargs) notebooks = {"sciope_model_exploration":notebook.create_me_notebook, - "model_inference":notebook.create_mi_notebook} + "model_inference":notebook.create_mi_notebook, + "inference":notebook.create_mi_notebook} else: notebook = StochSSNotebook(**kwargs) notebooks = {"gillespy":notebook.create_es_notebook, From c8827fd43ee814ef66e223f929812749851684b7 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Wed, 15 Mar 2023 13:28:52 -0400 Subject: [PATCH 059/109] Added support for exporting inferred models. --- client/app.js | 1 - .../templates/inferenceResultsView.pug | 8 +++ client/job-view/views/job-results-view.js | 50 +++++++++++++++++++ stochss/handlers/__init__.py | 3 +- stochss/handlers/util/model_inference.py | 21 ++++++-- stochss/handlers/util/stochss_model.py | 3 +- stochss/handlers/workflows.py | 38 ++++++++++++++ 7 files changed, 118 insertions(+), 6 deletions(-) diff --git a/client/app.js b/client/app.js index 57f732cf21..6935bebc8b 100644 --- a/client/app.js +++ b/client/app.js @@ -142,7 +142,6 @@ let newWorkflow = (parent, mdlPath, isSpatial, type) => { "Parameter Sweep": "_PS", "Model Inference": "_MI" } - let self = parent; let ext = isSpatial ? /.smdl/g : /.mdl/g let typeCode = typeCodes[type]; let name = mdlPath.split('/').pop().replace(ext, typeCode) diff --git a/client/job-view/templates/inferenceResultsView.pug b/client/job-view/templates/inferenceResultsView.pug index 0470932670..89c8e1614a 100644 --- a/client/job-view/templates/inferenceResultsView.pug +++ b/client/job-view/templates/inferenceResultsView.pug @@ -48,6 +48,10 @@ div#workflow-results.card div.spinner-border.workflow-plot(data-hook="inference-plot-spinner") + button.btn.btn-primary.box-shadow(data-hook="inference-model-export" data-target="model-export" disabled) Export Model + + button.btn.btn-primary.box-shadow(data-hook="inference-model-explore" data-target="model-explore" disabled) Export & Explore Model + button.btn.btn-primary.box-shadow(data-hook="inference-edit-plot" data-target="edit-plot" disabled) Edit Plot button.btn.btn-primary.box-shadow.dropdown-toggle( @@ -119,6 +123,10 @@ div#workflow-results.card div.spinner-border.workflow-plot(data-hook="epoch-plot-spinner") + button.btn.btn-primary.box-shadow(data-hook="epoch-model-export" data-target="model-export" disabled) Export Model + + button.btn.btn-primary.box-shadow(data-hook="epoch-model-explore" data-target="model-explore" disabled) Export & Explore Model + button.btn.btn-primary.box-shadow(data-hook="epoch-edit-plot" data-target="edit-plot" disabled) Edit Plot button.btn.btn-primary.box-shadow.dropdown-toggle( diff --git a/client/job-view/views/job-results-view.js b/client/job-view/views/job-results-view.js index be73a08f50..d85e5fa9ed 100644 --- a/client/job-view/views/job-results-view.js +++ b/client/job-view/views/job-results-view.js @@ -51,6 +51,7 @@ module.exports = View.extend({ 'change [data-hook=plot-type-select]' : 'getTSPlotForType', 'click [data-hook=collapse-results-btn]' : 'changeCollapseButtonText', 'click [data-trigger=collapse-plot-container]' : 'handleCollapsePlotContainerClick', + 'click [data-target=model-export]' : 'handleExportInferredModel', 'click [data-target=edit-plot]' : 'openPlotArgsSection', 'click [data-hook=multiple-plots]' : 'plotMultiplePlots', 'click [data-target=download-png-custom]' : 'handleDownloadPNGClick', @@ -165,6 +166,9 @@ module.exports = View.extend({ $(this.queryByHook("multiple-plots")).prop("disabled", true); }else if(type === "spatial") { $(this.queryByHook("spatial-plot-loading-msg")).css("display", "block"); + }else if(type === "epoch") { + $(this.queryByHook("epoch-model-export")).prop("disabled", true); + $(this.queryByHook("epoch-model-explore")).prop("disabled", true); } $(this.queryByHook(`${type}-plot-spinner`)).css("display", "block"); }, @@ -176,6 +180,23 @@ module.exports = View.extend({ let endpoint = `${path.join(app.getApiPath(), "job/csv")}${queryStr}`; window.open(endpoint); }, + exportInferredModel: function (type, {cb=null}={}) { + var queryStr = `?path=${this.model.directory}`; + if(type === "epoch") { + queryStr += `&epoch=${this.epochIndex}`; + } + let endpoint = `${path.join(app.getApiPath(), "job/export-inferred-model")}${queryStr}`; + app.getXHR(endpoint, { + success: (err, response, body) => { + if(cb === null) { + let editEP = `${path.join(app.getBasePath(), "stochss/models/edit")}?path=${body.path}`; + window.location.href = editEP; + }else { + cb(err, response, body); + } + } + }); + }, getPlot: function (type) { this.cleanupPlotContainer(type); let data = this.getPlotData(type); @@ -400,6 +421,10 @@ module.exports = View.extend({ let pngButton = $('div[data-hook=' + type + '-plot] a[data-title*="Download plot as a png"]')[0]; pngButton.click(); }, + handleExportInferredModel: function (e) { + let type = e.target.dataset.hook.split('-')[0]; + this.exportInferredModel(type); + }, handleFullCSVClick: function (e) { if(this.titleType === "Model Inference") { this.downloadCSV("inference", null); @@ -456,6 +481,27 @@ module.exports = View.extend({ } }); }, + newWorkflow: function (err, response, body) { + let type = "Parameter Sweep" + let model = new Model({ directory: body.path }); + app.getXHR(model.url(), { + success: (err, response, body) => { + model.set(body); + model.updateValid(); + if(model.valid){ + app.newWorkflow(this, model.directory, model.is_spatial, type); + }else{ + if(document.querySelector("#errorModal")) { + document.querySelector("#errorModal").remove(); + } + let title = "Model Errors Detected"; + let endpoint = `${path.join(app.getBasePath(), "stochss/models/edit")}?path=${model.directory}&validate`; + let message = `Errors were detected in you model click here to fix your model`; + $(modals.errorHtml(title, message)).modal(); + } + } + }); + }, openPlotArgsSection: function (e) { $(this.queryByHook("edit-plot-args")).collapse("show"); $(document).ready(function () { @@ -471,6 +517,10 @@ module.exports = View.extend({ if(type === "spatial") { $(this.queryByHook("spatial-plot-loading-msg")).css("display", "none"); } + if(["inference", "epoch"].includes(type)) { + $(this.queryByHook(`${type}-model-export`)).prop("disabled", false); + $(this.queryByHook(`${type}-model-explore`)).prop("disabled", false); + } $(this.queryByHook(`${type}-plot-spinner`)).css("display", "none"); $(this.queryByHook(`${type}-edit-plot`)).prop("disabled", false); $(this.queryByHook(`${type}-download`)).prop("disabled", false); diff --git a/stochss/handlers/__init__.py b/stochss/handlers/__init__.py index ae2c79b3ca..4410a91650 100644 --- a/stochss/handlers/__init__.py +++ b/stochss/handlers/__init__.py @@ -132,7 +132,8 @@ def get_page_handlers(route_start): (r"/stochss/api/workflow/obs-data-files\/?", LoadObsDataFiles), (r"/stochss/api/workflow/preview-obs-data\/?", PreviewOBSDataAPIHandler), (r"/stochss/api/job/presentation\/?", JobPresentationAPIHandler), - (r"/stochss/api/job/csv\/?", DownloadCSVZipAPIHandler) + (r"/stochss/api/job/csv\/?", DownloadCSVZipAPIHandler), + (r"/stochss/api/job/export-inferred-model\/?", ExportInferredModelAPIHandler) ] full_handlers = list(map(lambda h: (url_path_join(route_start, h[0]), h[1]), handlers)) return full_handlers diff --git a/stochss/handlers/util/model_inference.py b/stochss/handlers/util/model_inference.py index 9de1ae7bbf..fbe3d6cb6a 100644 --- a/stochss/handlers/util/model_inference.py +++ b/stochss/handlers/util/model_inference.py @@ -307,6 +307,22 @@ def __write_csv_file(cls, path, data): for line in data: csv_writer.writerow(line) + def export_inferred_model(self, epoch_ndx=-1): + """ + Export the jobs model after updating the inferred parameter values. + """ + model = copy.deepcopy(self.g_model) + epoch = self.__get_pickled_results()[epoch_ndx] + parameters = self.settings['inferenceSettings']['parameters'] + + for i, parameter in enumerate(parameters): + model.listOfParameters[parameter['name']].expression = str(epoch['inferred_parameters'][i]) + + inf_model = gillespy2.export_StochSS(model, return_stochss_model=True) + inf_model['name'] = f"Inferred-{model.name}" + inf_model['modelSettings'] = self.s_model['modelSettings'] + return inf_model + def get_csv_data(self, name): """ Generate the csv results and return the binary of the zipped archive. @@ -321,16 +337,15 @@ def get_result_plot(self, epoch=None, add_config=False, **kwargs): """ Generate a plot for inference results. """ - model = self.load_models()[0] results = self.__get_pickled_results() - parameters = self.load_settings()['inferenceSettings']['parameters'] + parameters = self.settings['inferenceSettings']['parameters'] names = [] values = [] dmin = [] dmax = [] for parameter in parameters: names.append(parameter['name']) - values.append(model.listOfParameters[parameter['name']].value) + values.append(self.g_model.listOfParameters[parameter['name']].value) dmin.append(parameter['min']) dmax.append(parameter['max']) if epoch is None: diff --git a/stochss/handlers/util/stochss_model.py b/stochss/handlers/util/stochss_model.py index 001f5ba58d..de5568b925 100644 --- a/stochss/handlers/util/stochss_model.py +++ b/stochss/handlers/util/stochss_model.py @@ -26,6 +26,7 @@ from escapism import escape from gillespy2.sbml.SBMLexport import export +from gillespy2.core.jsonify import ComplexJsonCoder from gillespy2 import ( Model, Species, Parameter, Reaction, Event, EventTrigger, EventAssignment, RateRule, AssignmentRule, FunctionDefinition, TimeSpan @@ -67,7 +68,7 @@ def __init__(self, path, new=False, model=None): if changed: self.path = new_path.replace(self.user_dir + '/', "") with open(new_path, "w", encoding="utf-8") as mdl_file: - json.dump(model, mdl_file, indent=4, sort_keys=True) + json.dump(model, mdl_file, indent=4, sort_keys=True, cls=ComplexJsonCoder) else: self.model = None diff --git a/stochss/handlers/workflows.py b/stochss/handlers/workflows.py index 37600472b7..65622e39e5 100644 --- a/stochss/handlers/workflows.py +++ b/stochss/handlers/workflows.py @@ -627,3 +627,41 @@ async def get(self): except Exception as err: report_critical_error(self, log, err) self.finish() + +class ExportInferredModelAPIHandler(APIHandler): + ''' + ################################################################################################ + Handler for exporting an inferred model. + ################################################################################################ + ''' + @web.authenticated + async def get(self): + ''' + Preview the observed data files. + + Attributes + ---------- + ''' + self.set_header('Content-Type', 'application/json') + path = self.get_query_argument(name="path") + epoch = int(self.get_query_argument(name="epoch", default=-1)) + try: + job = ModelInference(path=path) + inf_model = job.export_inferred_model(epoch_ndx=epoch) + + end = -3 if '.wkgp' in path else -2 + dirname = '/'.join(path.split('/')[:end]) + if ".proj" in dirname: + dst = os.path.join(dirname, f"{inf_model['name']}.wkgp", f"{inf_model['name']}.mdl") + else: + dst = os.path.join(dirname, f"{inf_model['name']}.mdl") + model = StochSSModel(path=dst, new=True, model=inf_model) + + resp = {"path": model.get_path()} + 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() From f9fa92c1dd050e4676b1c98e04234147257d065d Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Wed, 15 Mar 2023 13:38:22 -0400 Subject: [PATCH 060/109] Added support for exporting and exploring inferred models. --- client/job-view/views/job-results-view.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/client/job-view/views/job-results-view.js b/client/job-view/views/job-results-view.js index d85e5fa9ed..17976404a1 100644 --- a/client/job-view/views/job-results-view.js +++ b/client/job-view/views/job-results-view.js @@ -18,11 +18,14 @@ along with this program. If not, see . let $ = require('jquery'); let path = require('path'); +let _ = require('underscore'); //support files let app = require('../../app'); let modals = require('../../modals'); let Tooltips = require('../../tooltips'); let Plotly = require('plotly.js-dist'); +//models +let Model = require('../../models/model'); //views let InputView = require('../../views/input'); let View = require('ampersand-view'); @@ -52,6 +55,7 @@ module.exports = View.extend({ 'click [data-hook=collapse-results-btn]' : 'changeCollapseButtonText', 'click [data-trigger=collapse-plot-container]' : 'handleCollapsePlotContainerClick', 'click [data-target=model-export]' : 'handleExportInferredModel', + 'click [data-target=model-explore]' : 'handleExploreInferredModel', 'click [data-target=edit-plot]' : 'openPlotArgsSection', 'click [data-hook=multiple-plots]' : 'plotMultiplePlots', 'click [data-target=download-png-custom]' : 'handleDownloadPNGClick', @@ -421,6 +425,10 @@ module.exports = View.extend({ let pngButton = $('div[data-hook=' + type + '-plot] a[data-title*="Download plot as a png"]')[0]; pngButton.click(); }, + handleExploreInferredModel: function (e) { + let type = e.target.dataset.hook.split('-')[0]; + this.exportInferredModel(type, {cb: _.bind(this.newWorkflow, this)}); + }, handleExportInferredModel: function (e) { let type = e.target.dataset.hook.split('-')[0]; this.exportInferredModel(type); From ba772ae8f9fc311bf435c63d5da9f5120c9c4a2a Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Wed, 15 Mar 2023 15:43:47 -0400 Subject: [PATCH 061/109] Added export link tracking. --- client/job-view/views/job-results-view.js | 51 ++++++++++++++++------- client/models/job.js | 1 + stochss/handlers/util/model_inference.py | 18 ++++++++ stochss/handlers/util/stochss_job.py | 11 +++++ stochss/handlers/workflows.py | 1 + 5 files changed, 68 insertions(+), 14 deletions(-) diff --git a/client/job-view/views/job-results-view.js b/client/job-view/views/job-results-view.js index 17976404a1..b75c129b1d 100644 --- a/client/job-view/views/job-results-view.js +++ b/client/job-view/views/job-results-view.js @@ -134,6 +134,10 @@ module.exports = View.extend({ }else{ var type = "inference"; this.epochIndex = this.model.settings.inferenceSettings.numEpochs; + if(this.model.exportLinks[this.epochIndex] !== null) { + $(this.queryByHook("inference-model-export")).text("Open Model"); + $(this.queryByHook("inference-model-explore")).text("Explore Model"); + } $(this.queryByHook("epoch-index-value")).text(this.epochIndex); $(this.queryByHook("epoch-index-slider")).prop("value", this.epochIndex); // TODO: Enable inference presentations when implemented @@ -185,21 +189,30 @@ module.exports = View.extend({ window.open(endpoint); }, exportInferredModel: function (type, {cb=null}={}) { - var queryStr = `?path=${this.model.directory}`; - if(type === "epoch") { - queryStr += `&epoch=${this.epochIndex}`; - } - let endpoint = `${path.join(app.getApiPath(), "job/export-inferred-model")}${queryStr}`; - app.getXHR(endpoint, { - success: (err, response, body) => { - if(cb === null) { - let editEP = `${path.join(app.getBasePath(), "stochss/models/edit")}?path=${body.path}`; - window.location.href = editEP; - }else { - cb(err, response, body); - } + let epoch = type === "epoch" ? this.epochIndex : this.model.settings.inferenceSettings.numEpochs + if(this.model.exportLinks[epoch] === null) { + var queryStr = `?path=${this.model.directory}`; + if(type === "epoch") { + queryStr += `&epoch=${this.epochIndex}`; } - }); + let endpoint = `${path.join(app.getApiPath(), "job/export-inferred-model")}${queryStr}`; + app.getXHR(endpoint, { + success: (err, response, body) => { + if(cb === null) { + let editEP = `${path.join(app.getBasePath(), "stochss/models/edit")}?path=${body.path}`; + window.location.href = editEP; + }else { + cb(err, response, body); + } + } + }); + }else if(cb === null){ + let mdPath = this.model.exportLinks[epoch]; + let editEP = `${path.join(app.getBasePath(), "stochss/models/edit")}?path=${mdPath}`; + window.location.href = editEP; + }else { + cb(); + } }, getPlot: function (type) { this.cleanupPlotContainer(type); @@ -491,6 +504,9 @@ module.exports = View.extend({ }, newWorkflow: function (err, response, body) { let type = "Parameter Sweep" + if([undefined, null].includes(body)) { + body = { path: this.model.exportLinks[this.epochIndex] }; + } let model = new Model({ directory: body.path }); app.getXHR(model.url(), { success: (err, response, body) => { @@ -526,6 +542,13 @@ module.exports = View.extend({ $(this.queryByHook("spatial-plot-loading-msg")).css("display", "none"); } if(["inference", "epoch"].includes(type)) { + if(type === "epoch" && this.model.exportLinks[this.epochIndex] !== null) { + $(this.queryByHook("epoch-model-export")).text("Open Model"); + $(this.queryByHook("epoch-model-explore")).text("Explore Model"); + }else { + $(this.queryByHook("epoch-model-export")).text("Export Model"); + $(this.queryByHook("epoch-model-explore")).text("Export & Explore Model"); + } $(this.queryByHook(`${type}-model-export`)).prop("disabled", false); $(this.queryByHook(`${type}-model-explore`)).prop("disabled", false); } diff --git a/client/models/job.js b/client/models/job.js index 97d7b8317e..de495f6a2b 100644 --- a/client/models/job.js +++ b/client/models/job.js @@ -28,6 +28,7 @@ module.exports = State.extend({ }, session: { directory: 'string', + exportLinks: 'object', logs: 'string', startTime: 'string', status: 'string' diff --git a/stochss/handlers/util/model_inference.py b/stochss/handlers/util/model_inference.py index fbe3d6cb6a..91c8cc78e1 100644 --- a/stochss/handlers/util/model_inference.py +++ b/stochss/handlers/util/model_inference.py @@ -407,6 +407,9 @@ def run(self, verbose=True): pkl_err = self.__store_pickled_results(results) if pkl_err: self.__report_result_error(trace=pkl_err) + export_links = {i + 1: None for i in range(len(results))} + with open("export-links.json", "w", encoding="utf-8") as elfd: + json.dump(export_links, elfd, indent=4, sort_keys=True) def simulator(self, parameter_point): """ Wrapper function for inference simulations. """ @@ -420,3 +423,18 @@ def simulator(self, parameter_point): raw_results = model.run(**kwargs) return self.process(raw_results) + + def update_export_links(self, epoch, path): + """ + Updated the export paths for an epoch. + """ + el_path = os.path.join(self.get_path(full=True), "export-links.json") + with open(el_path, "r", encoding="utf-8") as elfd: + export_links = json.load(elfd) + + if epoch < 1: + epoch = list(export_links.keys())[epoch] + export_links[epoch] = path + + with open(el_path, "w", encoding="utf-8") as elfd: + json.dump(export_links, elfd, indent=4, sort_keys=True) diff --git a/stochss/handlers/util/stochss_job.py b/stochss/handlers/util/stochss_job.py index 554efbb376..13245c9a7c 100644 --- a/stochss/handlers/util/stochss_job.py +++ b/stochss/handlers/util/stochss_job.py @@ -730,6 +730,17 @@ def load(self, new=False): "startTime":info['start_time'], "status":status, "timeStamp":self.time_stamp, "titleType":self.TITLES[info['type']], "type":self.type, "directory":self.path, "logs":logs} + if self.type == "inference": + el_path = os.path.join(self.get_path(full=True), "export-links.json") + with open(el_path, "r", encoding="utf-8") as elfd: + self.job['exportLinks'] = json.load(elfd) + + for ndx, link in self.job['exportLinks'].items(): + if link is not None: + epl_path = os.path.join(self.user_dir, link) + if not os.path.exists(epl_path): + self.job['exportLinks'][ndx] = None + if "template_version" not in self.job['settings']: self.job['settings']['template_version'] = 0 self.__update_settings_to_current() diff --git a/stochss/handlers/workflows.py b/stochss/handlers/workflows.py index 65622e39e5..5b51d4b6da 100644 --- a/stochss/handlers/workflows.py +++ b/stochss/handlers/workflows.py @@ -660,6 +660,7 @@ async def get(self): resp = {"path": model.get_path()} log.debug(f"Response: {resp}") self.write(resp) + job.update_export_links(epoch, dst) except StochSSAPIError as err: report_error(self, log, err) except Exception as err: From ac4f36fa041eaaf77c272e8d28fa857572d5b6fe Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Wed, 15 Mar 2023 15:55:45 -0400 Subject: [PATCH 062/109] Switched from 'epoch' to 'round'> --- .../templates/inferenceResultsView.pug | 46 ++++++------ client/job-view/views/job-results-view.js | 72 +++++++++---------- client/models/inference-settings.js | 2 +- .../templates/inferenceSettingsView.pug | 8 +-- .../views/inference-settings-view.js | 16 ++--- stochss/handlers/util/model_inference.py | 56 +++++++-------- stochss/handlers/workflows.py | 6 +- .../workflowSettingsTemplate.json | 2 +- 8 files changed, 104 insertions(+), 104 deletions(-) diff --git a/client/job-view/templates/inferenceResultsView.pug b/client/job-view/templates/inferenceResultsView.pug index 89c8e1614a..9412e90ca5 100644 --- a/client/job-view/templates/inferenceResultsView.pug +++ b/client/job-view/templates/inferenceResultsView.pug @@ -80,18 +80,18 @@ div#workflow-results.card div.card-header.pb-0 - h5.inline Plot Epoch + h5.inline Plot Round button.btn.btn-outline-collapse( data-toggle="collapse" - data-target="#collapse-epoch" - id="collapse-epoch-btn" - data-hook="collapse-epoch-btn" + data-target="#collapse-round" + id="collapse-round-btn" + data-hook="collapse-round-btn" data-trigger="collapse-plot-container" - data-type="epoch" + data-type="round" ) + - div.collapse(id="collapse-epoch") + div.collapse(id="collapse-round") div.card-body @@ -103,9 +103,9 @@ div#workflow-results.card h6 - div.inline.mr-2 Epoch: + div.inline.mr-2 Round: - div.inline(data-hook="epoch-index-value")=this.epochIndex + div.inline(data-hook="round-index-value")=this.roundIndex div.mx-1.my-3.row @@ -114,24 +114,24 @@ div#workflow-results.card input.custom-range( type="range" min="1" - max=`${this.model.settings.inferenceSettings.numEpochs}` - value=this.epochIndex - data-hook="epoch-index-slider" + max=`${this.model.settings.inferenceSettings.numRounds}` + value=this.roundIndex + data-hook="round-index-slider" ) - div(data-hook="epoch-plot") + div(data-hook="round-plot") - div.spinner-border.workflow-plot(data-hook="epoch-plot-spinner") + div.spinner-border.workflow-plot(data-hook="round-plot-spinner") - button.btn.btn-primary.box-shadow(data-hook="epoch-model-export" data-target="model-export" disabled) Export Model + button.btn.btn-primary.box-shadow(data-hook="round-model-export" data-target="model-export" disabled) Export Model - button.btn.btn-primary.box-shadow(data-hook="epoch-model-explore" data-target="model-explore" disabled) Export & Explore Model + button.btn.btn-primary.box-shadow(data-hook="round-model-explore" data-target="model-explore" disabled) Export & Explore Model - button.btn.btn-primary.box-shadow(data-hook="epoch-edit-plot" data-target="edit-plot" disabled) Edit Plot + button.btn.btn-primary.box-shadow(data-hook="round-edit-plot" data-target="edit-plot" disabled) Edit Plot button.btn.btn-primary.box-shadow.dropdown-toggle( - id="epoch-download" - data-hook="epoch-download" + id="round-download" + data-hook="round-download" data-toggle="dropdown", aria-haspopup="true", aria-expanded="false", @@ -139,16 +139,16 @@ div#workflow-results.card disabled ) Download - ul.dropdown-menu(aria-labelledby="#epoch-download") + ul.dropdown-menu(aria-labelledby="#round-download") li.dropdown-item( - data-hook="epoch-download-png-custom" + data-hook="round-download-png-custom" data-target="download-png-custom" - data-type="epoch" + data-type="round" ) Plot as .png li.dropdown-item( - data-hook="epoch-download-json" + data-hook="round-download-json" data-target="download-json" - data-type="epoch" + data-type="round" ) Plot as .json div diff --git a/client/job-view/views/job-results-view.js b/client/job-view/views/job-results-view.js index b75c129b1d..0a323d4284 100644 --- a/client/job-view/views/job-results-view.js +++ b/client/job-view/views/job-results-view.js @@ -47,7 +47,7 @@ module.exports = View.extend({ '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=epoch-index-slider]' : 'getPlotForEpoch', + 'change [data-hook=round-index-slider]' : 'getPlotForRound', 'change [data-hook=specie-of-interest-list]' : 'getPlotForSpecies', 'change [data-hook=feature-extraction-list]' : 'getPlotForFeatureExtractor', 'change [data-hook=ensemble-aggragator-list]' : 'getPlotForEnsembleAggragator', @@ -65,7 +65,7 @@ module.exports = View.extend({ 'click [data-hook=download-results-csv]' : 'handleFullCSVClick', 'click [data-hook=job-presentation]' : 'handlePresentationClick', 'input [data-hook=trajectory-index-slider]' : 'viewTrajectoryIndex', - 'input [data-hook=epoch-index-slider]' : 'viewEpochIndex' + 'input [data-hook=round-index-slider]' : 'viewRoundIndex' }, initialize: function (attrs, options) { View.prototype.initialize.apply(this, arguments); @@ -133,13 +133,13 @@ module.exports = View.extend({ $(this.queryByHook("spatial-plot-csv")).css('display', 'none'); }else{ var type = "inference"; - this.epochIndex = this.model.settings.inferenceSettings.numEpochs; - if(this.model.exportLinks[this.epochIndex] !== null) { + this.roundIndex = this.model.settings.inferenceSettings.numRounds; + if(this.model.exportLinks[this.roundIndex] !== null) { $(this.queryByHook("inference-model-export")).text("Open Model"); $(this.queryByHook("inference-model-explore")).text("Explore Model"); } - $(this.queryByHook("epoch-index-value")).text(this.epochIndex); - $(this.queryByHook("epoch-index-slider")).prop("value", this.epochIndex); + $(this.queryByHook("round-index-value")).text(this.roundIndex); + $(this.queryByHook("round-index-slider")).prop("value", this.roundIndex); // TODO: Enable inference presentations when implemented $(this.queryByHook("job-presentation")).prop("disabled", true); } @@ -174,9 +174,9 @@ module.exports = View.extend({ $(this.queryByHook("multiple-plots")).prop("disabled", true); }else if(type === "spatial") { $(this.queryByHook("spatial-plot-loading-msg")).css("display", "block"); - }else if(type === "epoch") { - $(this.queryByHook("epoch-model-export")).prop("disabled", true); - $(this.queryByHook("epoch-model-explore")).prop("disabled", true); + }else if(type === "round") { + $(this.queryByHook("round-model-export")).prop("disabled", true); + $(this.queryByHook("round-model-explore")).prop("disabled", true); } $(this.queryByHook(`${type}-plot-spinner`)).css("display", "block"); }, @@ -189,11 +189,11 @@ module.exports = View.extend({ window.open(endpoint); }, exportInferredModel: function (type, {cb=null}={}) { - let epoch = type === "epoch" ? this.epochIndex : this.model.settings.inferenceSettings.numEpochs - if(this.model.exportLinks[epoch] === null) { + let round = type === "round" ? this.roundIndex : this.model.settings.inferenceSettings.numRounds + if(this.model.exportLinks[round] === null) { var queryStr = `?path=${this.model.directory}`; - if(type === "epoch") { - queryStr += `&epoch=${this.epochIndex}`; + if(type === "round") { + queryStr += `&round=${this.roundIndex}`; } let endpoint = `${path.join(app.getApiPath(), "job/export-inferred-model")}${queryStr}`; app.getXHR(endpoint, { @@ -207,7 +207,7 @@ module.exports = View.extend({ } }); }else if(cb === null){ - let mdPath = this.model.exportLinks[epoch]; + let mdPath = this.model.exportLinks[round]; let editEP = `${path.join(app.getBasePath(), "stochss/models/edit")}?path=${mdPath}`; window.location.href = editEP; }else { @@ -221,7 +221,7 @@ module.exports = View.extend({ let storageKey = JSON.stringify(data); data['plt_data'] = this.getPlotLayoutData(); if(Boolean(this.plots[storageKey])) { - let renderTypes = ['psweep', 'ts-psweep', 'ts-psweep-mp', 'mltplplt', 'spatial', 'epoch']; + let renderTypes = ['psweep', 'ts-psweep', 'ts-psweep-mp', 'mltplplt', 'spatial', 'round']; if(renderTypes.includes(type)) { this.activePlots[type] = storageKey; this.plotFigure(this.plots[storageKey], type); @@ -231,7 +231,7 @@ module.exports = View.extend({ let endpoint = `${path.join(app.getApiPath(), "workflow/plot-results")}${queryStr}`; app.getXHR(endpoint, { success: (err, response, body) => { - if(type === "epoch") { + if(type === "round") { body.layout.annotations.push({ font: {size: 16}, showarrow: false, text: "", x: 0.5, xanchor: "center", xref: "paper", y: 0, yanchor: "top", yref: "paper", yshift: -30 @@ -290,9 +290,9 @@ module.exports = View.extend({ target: this.spatialTarget, index: this.targetIndex, mode: this.targetMode, trajectory: this.trajectoryIndex - 1 }; data['plt_key'] = type; - }else if(["inference", "epoch"].includes(type)) { + }else if(["inference", "round"].includes(type)) { data['sim_type'] = "Inference"; - data['data_keys'] = {"epoch": type === "inference" ? null : this.epochIndex - 1}; + data['data_keys'] = {"round": type === "inference" ? null : this.roundIndex - 1}; data['plt_key'] = "inference"; }else { data['sim_type'] = "GillesPy2"; @@ -311,9 +311,9 @@ module.exports = View.extend({ this.model.settings.resultsSettings.reducer = e.target.value; this.getPlot('psweep') }, - getPlotForEpoch: function (e) { - this.epochIndex = Number(e.target.value); - this.getPlot('epoch'); + getPlotForRound: function (e) { + this.roundIndex = Number(e.target.value); + this.getPlot('round'); }, getPlotForFeatureExtractor: function (e) { this.model.settings.resultsSettings.mapper = e.target.value; @@ -387,8 +387,8 @@ module.exports = View.extend({ if(plotData.sim_type === "GillesPy2") { return plotData.plt_key; } if(plotData.sim_type === "GillesPy2_PS") { return "ts-psweep"; } if(plotData.sim_type === "Inference") { - if(plotData.data_keys.epoch === null) { return "inference"; } - return "epoch" + if(plotData.data_keys.round === null) { return "inference"; } + return "round" } return "psweep" }, @@ -424,8 +424,8 @@ module.exports = View.extend({ let jsonData = this.plots[storageKey]; let dataStr = JSON.stringify(jsonData); let dataURI = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr); - let nameIndex = type === "epoch" ? this.epochIndex : this.trajectoryIndex; - let nameBase = ["spatial", "epoch"].includes(type) ? `${type}${nameIndex}` : type; + let nameIndex = type === "round" ? this.roundIndex : this.trajectoryIndex; + let nameBase = ["spatial", "round"].includes(type) ? `${type}${nameIndex}` : type; let exportFileDefaultName = `${nameBase}-plot.json`; let linkElement = document.createElement('a'); @@ -505,7 +505,7 @@ module.exports = View.extend({ newWorkflow: function (err, response, body) { let type = "Parameter Sweep" if([undefined, null].includes(body)) { - body = { path: this.model.exportLinks[this.epochIndex] }; + body = { path: this.model.exportLinks[this.roundIndex] }; } let model = new Model({ directory: body.path }); app.getXHR(model.url(), { @@ -541,13 +541,13 @@ module.exports = View.extend({ if(type === "spatial") { $(this.queryByHook("spatial-plot-loading-msg")).css("display", "none"); } - if(["inference", "epoch"].includes(type)) { - if(type === "epoch" && this.model.exportLinks[this.epochIndex] !== null) { - $(this.queryByHook("epoch-model-export")).text("Open Model"); - $(this.queryByHook("epoch-model-explore")).text("Explore Model"); + if(["inference", "round"].includes(type)) { + if(type === "round" && this.model.exportLinks[this.roundIndex] !== null) { + $(this.queryByHook("round-model-export")).text("Open Model"); + $(this.queryByHook("round-model-explore")).text("Explore Model"); }else { - $(this.queryByHook("epoch-model-export")).text("Export Model"); - $(this.queryByHook("epoch-model-explore")).text("Export & Explore Model"); + $(this.queryByHook("round-model-export")).text("Export Model"); + $(this.queryByHook("round-model-explore")).text("Export & Explore Model"); } $(this.queryByHook(`${type}-model-export`)).prop("disabled", false); $(this.queryByHook(`${type}-model-explore`)).prop("disabled", false); @@ -701,7 +701,7 @@ module.exports = View.extend({ for (var storageKey in this.plots) { let type = this.getType(storageKey); let fig = this.plots[storageKey] - if(['inference', 'epoch'].includes(type)) { + if(['inference', 'round'].includes(type)) { fig.layout.annotations.at(-2).text = e.target.value }else { fig.layout.xaxis.title.text = e.target.value @@ -716,7 +716,7 @@ module.exports = View.extend({ for (var storageKey in this.plots) { let type = this.getType(storageKey); let fig = this.plots[storageKey] - if(['inference', 'epoch'].includes(type)) { + if(['inference', 'round'].includes(type)) { fig.layout.annotations.at(-1).text = e.target.value }else { fig.layout.xaxis.title.text = e.target.value @@ -733,8 +733,8 @@ module.exports = View.extend({ }, update: function () {}, updateValid: function () {}, - viewEpochIndex: function (e) { - $(this.queryByHook("epoch-index-value")).html(e.target.value); + viewRoundIndex: function (e) { + $(this.queryByHook("round-index-value")).html(e.target.value); }, viewTrajectoryIndex: function (e) { $(this.queryByHook("trajectory-index-value")).html(e.target.value); diff --git a/client/models/inference-settings.js b/client/models/inference-settings.js index 7483da6d0e..c3574b6162 100644 --- a/client/models/inference-settings.js +++ b/client/models/inference-settings.js @@ -25,7 +25,7 @@ module.exports = State.extend({ props: { batchSize: 'number', chunkSize: 'number', - numEpochs: 'number', + numRounds: 'number', numSamples: 'number', obsData: 'string', priorMethod: 'string', diff --git a/client/settings-view/templates/inferenceSettingsView.pug b/client/settings-view/templates/inferenceSettingsView.pug index dba6a5e2ff..b5dad3d0a8 100644 --- a/client/settings-view/templates/inferenceSettingsView.pug +++ b/client/settings-view/templates/inferenceSettingsView.pug @@ -32,7 +32,7 @@ div#inference-settings.card div.col-sm-3 - h6.inline Number of Epochs + h6.inline Number of Rounds div.col-sm-3 @@ -40,7 +40,7 @@ div#inference-settings.card div.mx-1.my-3.row - div.col-sm-3(data-hook="num-epochs") + div.col-sm-3(data-hook="num-rounds") div.col-sm-3(data-hook="num-samples") @@ -160,7 +160,7 @@ div#inference-settings.card div.col-sm-3 - h6.inline Number of Epochs + h6.inline Number of Rounds div.col-sm-3 @@ -170,7 +170,7 @@ div#inference-settings.card div.col-sm-3 - div(data-hook="view-num-epochs")=this.model.numEpochs + div(data-hook="view-num-rounds")=this.model.numRounds div.col-sm-3 diff --git a/client/settings-view/views/inference-settings-view.js b/client/settings-view/views/inference-settings-view.js index 3cc0fb48ec..0182e0c1a6 100644 --- a/client/settings-view/views/inference-settings-view.js +++ b/client/settings-view/views/inference-settings-view.js @@ -42,7 +42,7 @@ module.exports = View.extend({ } }, events: { - 'change [data-hook=num-epochs]' : 'updateEpochsView', + 'change [data-hook=num-rounds]' : 'updateRoundsView', 'change [data-hook=num-samples]' : 'updateSamplesView', 'change [data-hook=summary-stats-type-select]' : 'setSummaryStatsType', 'change [data-hook=obs-data-file]' : 'setObsDataFile', @@ -324,24 +324,24 @@ module.exports = View.extend({ }, update: function (e) {}, updateValid: function (e) {}, - updateEpochsView: function (e) { - $(this.queryByHook("view-num-epochs")).text(this.model.numEpochs); + updateRoundsView: function (e) { + $(this.queryByHook("view-num-rounds")).text(this.model.numRounds); }, updateSamplesView: function (e) { $(this.queryByHook("view-num-samples")).text(this.model.numSamples); }, subviews: { - numEpochsInputView: { - hook: "num-epochs", + numRoundsInputView: { + hook: "num-rounds", prepareView: function (el) { return new InputView({ parent: this, required: true, - name: 'number-of-epochs', + name: 'number-of-rounds', tests: tests.valueTests, - modelKey: 'numEpochs', + modelKey: 'numRounds', valueType: 'number', - value: this.model.numEpochs + value: this.model.numRounds }); } }, diff --git a/stochss/handlers/util/model_inference.py b/stochss/handlers/util/model_inference.py index 91c8cc78e1..4d27792b07 100644 --- a/stochss/handlers/util/model_inference.py +++ b/stochss/handlers/util/model_inference.py @@ -101,7 +101,7 @@ def __get_csvzip(cls, dirname, name): return zip_file.read() @classmethod - def __get_epoch_result_plot(cls, results, names, values, dmin, dmax, + def __get_round_result_plot(cls, results, names, values, dmin, dmax, title=None, xaxis=None, yaxis=None): accepted_samples = numpy.vstack(results['accepted_samples']).swapaxes(0, 1) @@ -168,7 +168,7 @@ def __get_full_results_plot(cls, results, names, values, base_opacity = 0.5 if len(results) <= 1 else (i / (len(results) - 1) * 0.5) for j, accepted_values in enumerate(accepted_samples): - name = f"epoch {i + 1}" + name = f"round {i + 1}" color = common_rgb_values[i % len(common_rgb_values)] opacity = base_opacity + 0.25 trace = plotly.graph_objs.Histogram( @@ -194,7 +194,7 @@ def __get_full_results_plot(cls, results, names, values, def __get_infer_args(self): settings = self.settings['inferenceSettings'] - eps_selector = RelativeEpsilonSelector(20, max_rounds=settings['numEpochs']) + eps_selector = RelativeEpsilonSelector(20, max_rounds=settings['numRounds']) args = [settings['num_samples'], settings['batchSize']] kwargs = {"eps_selector": eps_selector, "chunk_size": settings['chunkSize']} return args, kwargs @@ -276,26 +276,26 @@ def __to_csv(self, path='.', nametag="results_csv"): if not os.path.exists(directory): os.mkdir(directory) - infer_csv = [["Epoch", "Accepted Count", "Trial Count"]] - epoch_headers = ["Sample ID", "Distances"] + infer_csv = [["Round", "Accepted Count", "Trial Count"]] + round_headers = ["Sample ID", "Distances"] for name in names: infer_csv[0].append(name) - epoch_headers.insert(-1, name) + round_headers.insert(-1, name) - for i, epoch in enumerate(results): - infer_line = [i + 1, epoch['accepted_count'], epoch['trial_count']] - infer_line.extend(epoch['inferred_parameters']) + for i, round in enumerate(results): + infer_line = [i + 1, round['accepted_count'], round['trial_count']] + infer_line.extend(round['inferred_parameters']) infer_csv.append(infer_line) - epoch_csv = [epoch_headers] - for j, accepted_sample in enumerate(epoch['accepted_samples']): - epoch_line = accepted_sample.tolist() - epoch_line.insert(0, j + 1) - epoch_line.extend(epoch['distances'][j]) - epoch_csv.append(epoch_line) + round_csv = [round_headers] + for j, accepted_sample in enumerate(round['accepted_samples']): + round_line = accepted_sample.tolist() + round_line.insert(0, j + 1) + round_line.extend(round['distances'][j]) + round_csv.append(round_line) - epoch_path = os.path.join(directory, f"epoch{i + 1}-details.csv") - self.__write_csv_file(epoch_path, epoch_csv) + round_path = os.path.join(directory, f"round{i + 1}-details.csv") + self.__write_csv_file(round_path, round_csv) infer_path = os.path.join(directory, "inference-overview.csv") self.__write_csv_file(infer_path, infer_csv) @@ -307,16 +307,16 @@ def __write_csv_file(cls, path, data): for line in data: csv_writer.writerow(line) - def export_inferred_model(self, epoch_ndx=-1): + def export_inferred_model(self, round_ndx=-1): """ Export the jobs model after updating the inferred parameter values. """ model = copy.deepcopy(self.g_model) - epoch = self.__get_pickled_results()[epoch_ndx] + round = self.__get_pickled_results()[round_ndx] parameters = self.settings['inferenceSettings']['parameters'] for i, parameter in enumerate(parameters): - model.listOfParameters[parameter['name']].expression = str(epoch['inferred_parameters'][i]) + model.listOfParameters[parameter['name']].expression = str(round['inferred_parameters'][i]) inf_model = gillespy2.export_StochSS(model, return_stochss_model=True) inf_model['name'] = f"Inferred-{model.name}" @@ -333,7 +333,7 @@ def get_csv_data(self, name): self.__to_csv(path=tmp_dir, nametag=nametag) return self.__get_csvzip(tmp_dir, nametag) - def get_result_plot(self, epoch=None, add_config=False, **kwargs): + def get_result_plot(self, round=None, add_config=False, **kwargs): """ Generate a plot for inference results. """ @@ -348,10 +348,10 @@ def get_result_plot(self, epoch=None, add_config=False, **kwargs): values.append(self.g_model.listOfParameters[parameter['name']].value) dmin.append(parameter['min']) dmax.append(parameter['max']) - if epoch is None: + if round is None: fig_obj = self.__get_full_results_plot(results, names, values, **kwargs) else: - fig_obj = self.__get_epoch_result_plot(results[epoch], names, values, dmin, dmax, **kwargs) + fig_obj = self.__get_round_result_plot(results[round], names, values, dmin, dmax, **kwargs) fig = json.loads(json.dumps(fig_obj, cls=plotly.utils.PlotlyJSONEncoder)) if add_config: fig["config"] = {"responsive": True} @@ -424,17 +424,17 @@ def simulator(self, parameter_point): return self.process(raw_results) - def update_export_links(self, epoch, path): + def update_export_links(self, round, path): """ - Updated the export paths for an epoch. + Updated the export paths for an round. """ el_path = os.path.join(self.get_path(full=True), "export-links.json") with open(el_path, "r", encoding="utf-8") as elfd: export_links = json.load(elfd) - if epoch < 1: - epoch = list(export_links.keys())[epoch] - export_links[epoch] = path + if round < 1: + round = list(export_links.keys())[round] + export_links[round] = path with open(el_path, "w", encoding="utf-8") as elfd: json.dump(export_links, elfd, indent=4, sort_keys=True) diff --git a/stochss/handlers/workflows.py b/stochss/handlers/workflows.py index 5b51d4b6da..28b07c922d 100644 --- a/stochss/handlers/workflows.py +++ b/stochss/handlers/workflows.py @@ -644,10 +644,10 @@ async def get(self): ''' self.set_header('Content-Type', 'application/json') path = self.get_query_argument(name="path") - epoch = int(self.get_query_argument(name="epoch", default=-1)) + round = int(self.get_query_argument(name="round", default=-1)) try: job = ModelInference(path=path) - inf_model = job.export_inferred_model(epoch_ndx=epoch) + inf_model = job.export_inferred_model(round_ndx=round) end = -3 if '.wkgp' in path else -2 dirname = '/'.join(path.split('/')[:end]) @@ -660,7 +660,7 @@ async def get(self): resp = {"path": model.get_path()} log.debug(f"Response: {resp}") self.write(resp) - job.update_export_links(epoch, dst) + job.update_export_links(round, dst) except StochSSAPIError as err: report_error(self, log, err) except Exception as err: diff --git a/stochss_templates/workflowSettingsTemplate.json b/stochss_templates/workflowSettingsTemplate.json index 39d0f0e9b9..f91d632448 100644 --- a/stochss_templates/workflowSettingsTemplate.json +++ b/stochss_templates/workflowSettingsTemplate.json @@ -2,7 +2,7 @@ "inferenceSettings": { "batchSize": 10, "chunkSize": 10, - "numEpochs": 5, + "numRounds": 5, "numSamples": 100, "obsData": "", "parameters": [], From ed4970f92915b51e80fd3bf73ebaee5eb602892f Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Wed, 15 Mar 2023 17:27:02 -0400 Subject: [PATCH 063/109] Fixed bin size for histograms. --- stochss/handlers/util/model_inference.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/stochss/handlers/util/model_inference.py b/stochss/handlers/util/model_inference.py index 4d27792b07..58a78e51a1 100644 --- a/stochss/handlers/util/model_inference.py +++ b/stochss/handlers/util/model_inference.py @@ -107,6 +107,8 @@ def __get_round_result_plot(cls, results, names, values, dmin, dmax, nbins = 50 rows = cols = len(accepted_samples) + sizes = (numpy.array(dmax) - numpy.array(dmin)) / nbins + fig = subplots.make_subplots( rows=rows, cols=cols, column_titles=names, row_titles=names, x_title=xaxis, y_title=yaxis, vertical_spacing=0.075 @@ -121,8 +123,8 @@ def __get_round_result_plot(cls, results, names, values, dmin, dmax, if i == j: color = common_rgb_values[(i)%len(common_rgb_values)] trace = plotly.graph_objs.Histogram( - x=accepted_samples[i], name=names[i], legendgroup=names[i], showlegend=False, - nbinsx=nbins, opacity=0.75, marker_color=color + x=accepted_samples[i], name=names[i], legendgroup=names[i], showlegend=False, marker_color=color, + opacity=0.75, xbins={"start": dmin[i], "end": dmax[i], "size": sizes[i]} ) fig.append_trace(trace, row, col) @@ -152,10 +154,11 @@ def __get_round_result_plot(cls, results, names, values, dmin, dmax, return fig @classmethod - def __get_full_results_plot(cls, results, names, values, + def __get_full_results_plot(cls, results, names, values, dmin, dmax, title=None, xaxis="Values", yaxis="Sample Concentrations"): cols = 2 nbins = 50 + sizes = (numpy.array(dmax) - numpy.array(dmin)) / nbins rows = int(numpy.ceil(len(results[0]['accepted_samples'][0])/cols)) fig = subplots.make_subplots( @@ -172,8 +175,8 @@ def __get_full_results_plot(cls, results, names, values, color = common_rgb_values[i % len(common_rgb_values)] opacity = base_opacity + 0.25 trace = plotly.graph_objs.Histogram( - x=accepted_values, name=name, legendgroup=name, showlegend=j==0, - marker_color=color, opacity=opacity, nbinsx=nbins + x=accepted_values, name=name, legendgroup=name, showlegend=j==0, marker_color=color, + opacity=opacity, xbins={"start": dmin[j], "end": dmax[j], "size": sizes[j]} ) row = int(numpy.ceil((j + 1) / cols)) @@ -349,7 +352,7 @@ def get_result_plot(self, round=None, add_config=False, **kwargs): dmin.append(parameter['min']) dmax.append(parameter['max']) if round is None: - fig_obj = self.__get_full_results_plot(results, names, values, **kwargs) + fig_obj = self.__get_full_results_plot(results, names, values, dmin, dmax, **kwargs) else: fig_obj = self.__get_round_result_plot(results[round], names, values, dmin, dmax, **kwargs) fig = json.loads(json.dumps(fig_obj, cls=plotly.utils.PlotlyJSONEncoder)) From 3441e44b494683a3eabd25a1bb073127ac251eb1 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Fri, 24 Mar 2023 10:47:35 -0400 Subject: [PATCH 064/109] Added a reference link property to the frontend model. --- client/models/model.js | 5 +++-- stochss/handlers/util/model_inference.py | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/client/models/model.js b/client/models/model.js index 7d126dae57..4cb720b143 100644 --- a/client/models/model.js +++ b/client/models/model.js @@ -39,10 +39,11 @@ module.exports = Model.extend({ return path.join(app.getApiPath(), "file/json-data")+"?for="+this.for+"&path="+this.directory; }, props: { - is_spatial: 'boolean', + annotation: 'string', defaultID: 'number', defaultMode: 'string', - annotation: 'string', + is_spatial: 'boolean', + refLink: 'string', volume: 'any', template_version: 'number' }, diff --git a/stochss/handlers/util/model_inference.py b/stochss/handlers/util/model_inference.py index 58a78e51a1..5380d81bdb 100644 --- a/stochss/handlers/util/model_inference.py +++ b/stochss/handlers/util/model_inference.py @@ -322,6 +322,7 @@ def export_inferred_model(self, round_ndx=-1): model.listOfParameters[parameter['name']].expression = str(round['inferred_parameters'][i]) inf_model = gillespy2.export_StochSS(model, return_stochss_model=True) + inf_model['refLink'] = self.path inf_model['name'] = f"Inferred-{model.name}" inf_model['modelSettings'] = self.s_model['modelSettings'] return inf_model @@ -429,7 +430,7 @@ def simulator(self, parameter_point): def update_export_links(self, round, path): """ - Updated the export paths for an round. + Updated the export paths for a round. """ el_path = os.path.join(self.get_path(full=True), "export-links.json") with open(el_path, "r", encoding="utf-8") as elfd: From 9e800527ae82162dddef177369a29e4d3a2eb9f7 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Fri, 24 Mar 2023 14:42:30 -0400 Subject: [PATCH 065/109] Finalized inference job reference links. --- client/models/model.js | 2 +- .../includes/workflowGroupListing.pug | 8 ++++ client/views/workflow-group-listing.js | 10 +++++ stochss/handlers/util/model_inference.py | 37 ++++++++++++++++--- stochss/handlers/util/stochss_model.py | 21 +++++++++-- stochss_templates/modelTemplate.json | 1 + 6 files changed, 69 insertions(+), 10 deletions(-) diff --git a/client/models/model.js b/client/models/model.js index 4cb720b143..1927e0addf 100644 --- a/client/models/model.js +++ b/client/models/model.js @@ -43,7 +43,7 @@ module.exports = Model.extend({ defaultID: 'number', defaultMode: 'string', is_spatial: 'boolean', - refLink: 'string', + refLinks: 'object', volume: 'any', template_version: 'number' }, diff --git a/client/templates/includes/workflowGroupListing.pug b/client/templates/includes/workflowGroupListing.pug index 6512af8954..638783734a 100644 --- a/client/templates/includes/workflowGroupListing.pug +++ b/client/templates/includes/workflowGroupListing.pug @@ -50,6 +50,10 @@ div a.nav-link.tab(data-toggle="tab" href="#" + this.model.elementID + "-notes-tab") Notes + li.nav-item + + a.nav-link.tab(data-toggle="tab" href="#" + this.model.elementID + "-reference-tab") Reference Links + button.btn.btn-outline-collapse(data-toggle="collapse" data-target="#" + this.model.elementID + "-tab-container" data-hook=this.model.elementID + "-tab-btn") - div.card.card-body.tab-content.collapse.show(id=this.model.elementID + "-tab-container") @@ -63,3 +67,7 @@ div div textarea(id=this.model.elementID + "-annotation" data-hook=this.model.elementID + "-annotation" rows="7" style="width: 100%;")=this.model.model.annotation + + div.tab-pane(id=this.model.elementID + "-reference-tab") + + div(data-hook=this.model.elementID + "-reference-links") diff --git a/client/views/workflow-group-listing.js b/client/views/workflow-group-listing.js index a1478ea60e..23c15c9fd1 100644 --- a/client/views/workflow-group-listing.js +++ b/client/views/workflow-group-listing.js @@ -43,6 +43,13 @@ module.exports = View.extend({ }, initialize: function (attrs, options) { View.prototype.initialize.apply(this, arguments); + let links = []; + this.model.model.refLinks.forEach((link) => { + links.push( + `${link.name}` + ); + }); + this.htmlLinks = links.join('') }, render: function (attrs, options) { View.prototype.render.apply(this, arguments); @@ -51,6 +58,9 @@ module.exports = View.extend({ let endpoint = path.join(app.getBasePath(), 'stochss/workflow/selection') + queryString; $(this.queryByHook(this.model.elementID + "-jupyter-notebook")).prop("href", endpoint); this.renderWorkflowCollection(); + if(this.htmlLinks) { + $(this.queryByHook(`${this.model.elementID}-reference-links`)).html(this.htmlLinks); + } }, changeCollapseButtonText: function (e) { app.changeCollapseButtonText(this, e); diff --git a/stochss/handlers/util/model_inference.py b/stochss/handlers/util/model_inference.py index 5380d81bdb..9963a7d7a7 100644 --- a/stochss/handlers/util/model_inference.py +++ b/stochss/handlers/util/model_inference.py @@ -27,7 +27,7 @@ import numpy import plotly -from plotly import subplots +from plotly import figure_factory, subplots import gillespy2 @@ -164,6 +164,10 @@ def __get_full_results_plot(cls, results, names, values, dmin, dmax, fig = subplots.make_subplots( rows=rows, cols=cols, subplot_titles=names, x_title=xaxis, y_title=yaxis, vertical_spacing=0.075 + ) + fig2 = subplots.make_subplots( + rows=rows, cols=cols, subplot_titles=names, x_title=xaxis, y_title=yaxis, vertical_spacing=0.075 + ) for i, result in enumerate(results): @@ -171,28 +175,46 @@ def __get_full_results_plot(cls, results, names, values, dmin, dmax, base_opacity = 0.5 if len(results) <= 1 else (i / (len(results) - 1) * 0.5) for j, accepted_values in enumerate(accepted_samples): + row = int(numpy.ceil((j + 1) / cols)) + col = (j % cols) + 1 + name = f"round {i + 1}" color = common_rgb_values[i % len(common_rgb_values)] opacity = base_opacity + 0.25 + # Create histogram trace trace = plotly.graph_objs.Histogram( x=accepted_values, name=name, legendgroup=name, showlegend=j==0, marker_color=color, opacity=opacity, xbins={"start": dmin[j], "end": dmax[j], "size": sizes[j]} ) - - row = int(numpy.ceil((j + 1) / cols)) - col = (j % cols) + 1 fig.append_trace(trace, row, col) + # Create PDF trace + tmp_fig = figure_factory.create_distplot( + [accepted_values], [names[j]], curve_type='normal', bin_size=sizes[j], histnorm="probability" + ) + trace2 = plotly.graph_objs.Scatter( + x=tmp_fig.data[1]['x'], y=tmp_fig.data[1]['y'] * 1000, name=name, legendgroup=name, showlegend=False, + mode='lines', line=dict(color=color) + ) + fig2.append_trace(trace2, row, col) + fig2.update_xaxes(row=row, col=col, range=[dmin[j], dmax[j]]) if i == len(results) - 1: fig.add_vline(values[j], row=row, col=col, line={"color": "red"}, layer='above') fig.add_vline( result['inferred_parameters'][j], row=row, col=col, line={"color": "green"}, exclude_empty_subplots=True, layer='above' ) + fig2.add_vline(values[j], row=row, col=col, line={"color": "red"}, layer='above') + fig2.add_vline( + result['inferred_parameters'][j], row=row, col=col, line={"color": "green"}, + exclude_empty_subplots=True, layer='above' + ) fig.update_layout(barmode='overlay', height=500 * rows) + fig2.update_layout(height=500 * rows) if title is not None: title = {'text': title, 'x': 0.5, 'xanchor': 'center'} fig.update_layout(title=title) + fig2.update_layout(title=title) return fig def __get_infer_args(self): @@ -322,9 +344,14 @@ def export_inferred_model(self, round_ndx=-1): model.listOfParameters[parameter['name']].expression = str(round['inferred_parameters'][i]) inf_model = gillespy2.export_StochSS(model, return_stochss_model=True) - inf_model['refLink'] = self.path + workflow = os.path.dirname(self.path) + name = f"{self.get_file(path=workflow)} - {self.get_file()}" inf_model['name'] = f"Inferred-{model.name}" inf_model['modelSettings'] = self.s_model['modelSettings'] + inf_model['refLinks'] = self.s_model['refLinks'] + inf_model['refLinks'].append({ + "path": f"{workflow}&job={name}", "name": name, "job": True + }) return inf_model def get_csv_data(self, name): diff --git a/stochss/handlers/util/stochss_model.py b/stochss/handlers/util/stochss_model.py index de5568b925..c5b3c0ab5b 100644 --- a/stochss/handlers/util/stochss_model.py +++ b/stochss/handlers/util/stochss_model.py @@ -277,6 +277,19 @@ def __update_events(self, param_ids): for event in self.model['eventsCollection']: self.__update_event_assignments(event=event, param_ids=param_ids) + def __update_model_to_current(self): + if self.model['template_version'] == self.TEMPLATE_VERSION: + return + + param_ids = self.__update_parameters() + self.__update_reactions() + self.__update_events(param_ids=param_ids) + self.__update_rules(param_ids=param_ids) + + if "refLinks" not in self.model.keys(): + self.model['refLinks'] = [] + + self.model['template_version'] = self.TEMPLATE_VERSION def __update_parameters(self): if "parameters" not in self.model.keys(): @@ -486,10 +499,10 @@ def load(self): self.model['volume'] = self.model['modelSettings']['volume'] else: self.model['volume'] = 1 - param_ids = self.__update_parameters() - self.__update_reactions() - self.__update_events(param_ids=param_ids) - self.__update_rules(param_ids=param_ids) + if "template_version" not in self.model: + self.model['template_version'] = 0 + self.__update_model_to_current() + self.model['name'] = self.get_name() self.model['directory'] = self.path return self.model diff --git a/stochss_templates/modelTemplate.json b/stochss_templates/modelTemplate.json index 579d0acfe0..fcd0a95e86 100644 --- a/stochss_templates/modelTemplate.json +++ b/stochss_templates/modelTemplate.json @@ -44,6 +44,7 @@ "initialConditions": [], "parameters": [], "reactions": [], + "refLinks": [], "rules": [], "eventsCollection": [], "functionDefinitions": [], From a5a9250665c85a41f22ebbf6b1b8224ec2de0b2e Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Fri, 24 Mar 2023 15:28:38 -0400 Subject: [PATCH 066/109] Finished reference links for jobs. --- client/pages/model-editor.js | 5 +++ client/pages/workflow-manager.js | 40 ++++++++++++++---------- client/templates/pages/modelEditor.pug | 2 ++ stochss/handlers/util/model_inference.py | 7 +++-- 4 files changed, 34 insertions(+), 20 deletions(-) diff --git a/client/pages/model-editor.js b/client/pages/model-editor.js index 8fd76438b9..0995d80bb4 100644 --- a/client/pages/model-editor.js +++ b/client/pages/model-editor.js @@ -360,6 +360,11 @@ let ModelEditor = PageView.extend({ if(app.getBasePath() === "/") { $(this.queryByHook("presentation")).css("display", "none"); } + let infLink = this.model.refLinks.filter((refLink) => { return refLink.job; })[0] || null; + if(infLink) { + $(this.queryByHook('return-to-inf-btn')).css("display", "inline-block"); + $(this.queryByHook('return-to-inf-btn')).prop("href", `stochss/workflow/edit?path=${infLink.path}&type=none`); + } this.renderModelView(); this.modelSettings = new TimespanSettingsView({ parent: this, diff --git a/client/pages/workflow-manager.js b/client/pages/workflow-manager.js index 9616fa8a61..3f8290a107 100644 --- a/client/pages/workflow-manager.js +++ b/client/pages/workflow-manager.js @@ -54,32 +54,32 @@ let WorkflowManager = PageView.extend({ initialize: function (attrs, options) { PageView.prototype.initialize.apply(this, arguments); let urlParams = new URLSearchParams(window.location.search); + let jobID = urlParams.has('job') ? urlParams.get('job') : null; this.model = new Workflow({ directory: urlParams.get('path') }); - let self = this; app.getXHR(this.model.url(), { - success: function (err, response, body) { - self.model.set(body) - $("#page-title").text("Workflow: " + self.model.name); - if(self.model.directory.includes('.proj')) { - let index = self.model.directory.indexOf('.proj') + 5; - self.projectPath = self.model.directory.slice(0, index); - $(self.queryByHook('project-breadcrumb')).text(self.projectPath.split('/').pop().split('.proj')[0]); - $(self.queryByHook('workflow-breadcrumb')).text(self.model.name); - self.queryByHook("project-breadcrumb-links").style.display = "block"; - self.queryByHook("return-to-project-btn").style.display = "inline-block"; + success: (err, response, body) => { + this.model.set(body) + $("#page-title").text("Workflow: " + this.model.name); + if(this.model.directory.includes('.proj')) { + let index = this.model.directory.indexOf('.proj') + 5; + this.projectPath = this.model.directory.slice(0, index); + $(this.queryByHook('project-breadcrumb')).text(this.projectPath.split('/').pop().split('.proj')[0]); + $(this.queryByHook('workflow-breadcrumb')).text(this.model.name); + this.queryByHook("project-breadcrumb-links").style.display = "block"; + this.queryByHook("return-to-project-btn").style.display = "inline-block"; } if(body.models){ - self.renderModelSelectView(body.models); + this.renderModelSelectView(body.models); } - self.renderSubviews(); - if(!self.model.newFormat) { + this.renderSubviews(jobID); + if(!this.model.newFormat) { let modal = $(modals.updateFormatHtml("Workflow")).modal(); let yesBtn = document.querySelector("#updateWorkflowFormatModal .yes-modal-btn"); yesBtn.addEventListener("click", function (e) { modal.modal("hide"); - let queryStr = "?path=" + self.model.directory + "&action=update-workflow"; + let queryStr = "?path=" + this.model.directory + "&action=update-workflow"; let endpoint = path.join(app.getBasePath(), "stochss/loading-page") + queryStr; window.location.href = endpoint; }); @@ -273,7 +273,7 @@ let WorkflowManager = PageView.extend({ this.settingsView = new SettingsView(options); app.registerRenderSubview(this, this.settingsView, "settings-container"); }, - renderSubviews: function () { + renderSubviews: function (jobID) { let oldFormRdyState = !this.model.newFormat && this.model.activeJob.status === "ready"; let newFormNotArchive = this.model.newFormat && this.model.model; if(!this.models && (oldFormRdyState || newFormNotArchive)) { @@ -287,7 +287,13 @@ let WorkflowManager = PageView.extend({ }else if(this.model.activeJob.status !== "ready") { this.renderStatusView(); } - let detailsStatus = ["error", "complete"] + let detailsStatus = ["error", "complete"]; + if(jobID !== null) { + let activeJob = this.model.jobs.filter((job) => { return job.name === jobID; })[0] || null; + if(activeJob !== null) { + this.model.activeJob = activeJob; + } + } if(this.model.activeJob && detailsStatus.includes(this.model.activeJob.status)) { this.renderActiveJob(); } diff --git a/client/templates/pages/modelEditor.pug b/client/templates/pages/modelEditor.pug index 306bb6e24b..1e27e5c85a 100644 --- a/client/templates/pages/modelEditor.pug +++ b/client/templates/pages/modelEditor.pug @@ -117,6 +117,8 @@ section.page button.btn.btn-primary.box-shadow(data-hook="return-to-project-btn" style="display: none;") Return to Project + a.btn.btn-primary.box-shadow.text-break.hidden(data-hook="return-to-inf-btn" href="" role="button") Return to Inference + button.btn.btn-primary.box-shadow.dropdown-toggle( id="simulate-model", data-hook="simulate-model", diff --git a/stochss/handlers/util/model_inference.py b/stochss/handlers/util/model_inference.py index 9963a7d7a7..ce74ff903c 100644 --- a/stochss/handlers/util/model_inference.py +++ b/stochss/handlers/util/model_inference.py @@ -123,8 +123,9 @@ def __get_round_result_plot(cls, results, names, values, dmin, dmax, if i == j: color = common_rgb_values[(i)%len(common_rgb_values)] trace = plotly.graph_objs.Histogram( - x=accepted_samples[i], name=names[i], legendgroup=names[i], showlegend=False, marker_color=color, - opacity=0.75, xbins={"start": dmin[i], "end": dmax[i], "size": sizes[i]} + x=accepted_samples[i], name=names[i], legendgroup=names[i], showlegend=False, + marker_color=color, opacity=0.75, + xbins={"start": dmin[i], "end": dmax[i], "size": sizes[i]} ) fig.append_trace(trace, row, col) @@ -350,7 +351,7 @@ def export_inferred_model(self, round_ndx=-1): inf_model['modelSettings'] = self.s_model['modelSettings'] inf_model['refLinks'] = self.s_model['refLinks'] inf_model['refLinks'].append({ - "path": f"{workflow}&job={name}", "name": name, "job": True + "path": f"{workflow}&job={self.get_file()}", "name": name, "job": True }) return inf_model From e114bdbf9da647966a33a68fe10585a445453fca Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Fri, 24 Mar 2023 17:10:48 -0400 Subject: [PATCH 067/109] Started working on additional plotting for inference jobs. --- .../templates/inferenceResultsView.pug | 28 +++++- client/job-view/views/job-results-view.js | 85 +++++++++++++------ stochss/handlers/util/model_inference.py | 48 ++++++++--- stochss/handlers/workflows.py | 8 +- 4 files changed, 125 insertions(+), 44 deletions(-) diff --git a/client/job-view/templates/inferenceResultsView.pug b/client/job-view/templates/inferenceResultsView.pug index 9412e90ca5..2d257ab42f 100644 --- a/client/job-view/templates/inferenceResultsView.pug +++ b/client/job-view/templates/inferenceResultsView.pug @@ -29,7 +29,19 @@ div#workflow-results.card div.card-header.pb-0 - h5.inline Plot Inference + h5.inline.mr-2 Plot Inference + + div.inline + + ul.nav.nav-tabs + + li.nav-item + + a.nav-link.tab.active(data-toggle="tab" href="#histogram") Histogram + + li.nav-item + + a.nav-link.tab(data-toggle="tab" href="#pdf") Propability Distribution button.btn.btn-outline-collapse( data-toggle="collapse" @@ -44,10 +56,20 @@ div#workflow-results.card div.card-body - div(data-hook="inference-plot") + div.tab-content - div.spinner-border.workflow-plot(data-hook="inference-plot-spinner") + div.tab-pane.active(id="histogram" data-hook="histogram") + + div(data-hook="inference-histogram-plot") + + div.spinner-border.workflow-plot(data-hook="inference-histogram-plot-spinner") + + div.tab-pane(id="pdf" data-hook="pdf") + div(data-hook="inference-pdf-plot") + + div.spinner-border.workflow-plot(data-hook="inference-pdf-plot-spinner") + button.btn.btn-primary.box-shadow(data-hook="inference-model-export" data-target="model-export" disabled) Export Model button.btn.btn-primary.box-shadow(data-hook="inference-model-explore" data-target="model-explore" disabled) Export & Explore Model diff --git a/client/job-view/views/job-results-view.js b/client/job-view/views/job-results-view.js index 0a323d4284..39bf2c03bf 100644 --- a/client/job-view/views/job-results-view.js +++ b/client/job-view/views/job-results-view.js @@ -165,20 +165,34 @@ module.exports = View.extend({ }, 5000); }, cleanupPlotContainer: function (type) { - let el = this.queryByHook(`${type}-plot`); - Plotly.purge(el); - $(this.queryByHook(type + "-plot")).empty(); - if(type === "ts-psweep" || type === "psweep"){ - $(this.queryByHook(`${type}-download`)).prop("disabled", true); - $(this.queryByHook(`${type}-edit-plot`)).prop("disabled", true); - $(this.queryByHook("multiple-plots")).prop("disabled", true); - }else if(type === "spatial") { - $(this.queryByHook("spatial-plot-loading-msg")).css("display", "block"); - }else if(type === "round") { - $(this.queryByHook("round-model-export")).prop("disabled", true); - $(this.queryByHook("round-model-explore")).prop("disabled", true); + if(["inference", "round"].includes(type)) { + let histoEL = this.queryByHook(`${type}-histogram-plot`); + Plotly.purge(histoEL); + $(this.queryByHook(`${type}-histogram-plot`)).empty(); + $(this.queryByHook(`${type}-histogram-plot-spinner`)).css("display", "block"); + let pdfEL = this.queryByHook(`${type}-pdf-plot`); + Plotly.purge(pdfEL); + $(this.queryByHook(`${type}-pdf-plot`)).empty(); + $(this.queryByHook(`${type}-pdf-plot-spinner`)).css("display", "block"); + if(type === "round") { + $(this.queryByHook("round-model-export")).prop("disabled", true); + $(this.queryByHook("round-model-explore")).prop("disabled", true); + $(this.queryByHook("round-download")).prop("disabled", true); + $(this.queryByHook("round-edit-plot")).prop("disabled", true); + } + }else { + let el = this.queryByHook(`${type}-plot`); + Plotly.purge(el); + $(this.queryByHook(type + "-plot")).empty(); + if(["ts-psweep", "psweep"].includes(type)) { + $(this.queryByHook(`${type}-download`)).prop("disabled", true); + $(this.queryByHook(`${type}-edit-plot`)).prop("disabled", true); + $(this.queryByHook("multiple-plots")).prop("disabled", true); + }else if(type === "spatial") { + $(this.queryByHook("spatial-plot-loading-msg")).css("display", "block"); + } + $(this.queryByHook(`${type}-plot-spinner`)).css("display", "block"); } - $(this.queryByHook(`${type}-plot-spinner`)).css("display", "block"); }, downloadCSV: function (csvType, data) { var queryStr = `?path=${this.model.directory}&type=${csvType}`; @@ -232,14 +246,18 @@ module.exports = View.extend({ app.getXHR(endpoint, { success: (err, response, body) => { if(type === "round") { - body.layout.annotations.push({ + let xLabel = { font: {size: 16}, showarrow: false, text: "", x: 0.5, xanchor: "center", xref: "paper", y: 0, yanchor: "top", yref: "paper", yshift: -30 - }); - body.layout.annotations.push({ + } + let yLabel = { font: {size: 16}, showarrow: false, text: "", textangle: -90, x: 0, xanchor: "right", xref: "paper", xshift: -40, y: 0.5, yanchor: "middle", yref: "paper" - }); + } + body.histogrom.layout.annotations.push(xLabel); + body.histogrom.layout.annotations.push(yLabel); + body.pdf.layout.annotations.push(xLabel); + body.pdf.layout.annotations.push(yLabel); } this.activePlots[type] = storageKey; this.plots[storageKey] = body; @@ -292,7 +310,7 @@ module.exports = View.extend({ data['plt_key'] = type; }else if(["inference", "round"].includes(type)) { data['sim_type'] = "Inference"; - data['data_keys'] = {"round": type === "inference" ? null : this.roundIndex - 1}; + data['data_keys'] = {"epoch": type === "inference" ? null : this.roundIndex - 1}; data['plt_key'] = "inference"; }else { data['sim_type'] = "GillesPy2"; @@ -535,13 +553,17 @@ module.exports = View.extend({ }); }, plotFigure: function (figure, type) { - let hook = `${type}-plot`; - let el = this.queryByHook(hook); - Plotly.newPlot(el, figure); - if(type === "spatial") { - $(this.queryByHook("spatial-plot-loading-msg")).css("display", "none"); - } if(["inference", "round"].includes(type)) { + // Display histogram plot + let histoHook = `${type}-histogram-plot`; + let histoEL = this.queryByHook(histoHook); + Plotly.newPlot(histoEL, figure.histogram); + $(this.queryByHook(`${type}-histogram-plot-spinner`)).css("display", "none"); + // Display pdf plot + let pdfHook = `${type}-pdf-plot`; + let pdfEL = this.queryByHook(pdfHook); + Plotly.newPlot(pdfEL, figure.pdf); + $(this.queryByHook(`${type}-pdf-plot-spinner`)).css("display", "none"); if(type === "round" && this.model.exportLinks[this.roundIndex] !== null) { $(this.queryByHook("round-model-export")).text("Open Model"); $(this.queryByHook("round-model-explore")).text("Explore Model"); @@ -551,13 +573,20 @@ module.exports = View.extend({ } $(this.queryByHook(`${type}-model-export`)).prop("disabled", false); $(this.queryByHook(`${type}-model-explore`)).prop("disabled", false); + }else { + let hook = `${type}-plot`; + let el = this.queryByHook(hook); + Plotly.newPlot(el, figure); + if(type === "spatial") { + $(this.queryByHook("spatial-plot-loading-msg")).css("display", "none"); + } + $(this.queryByHook(`${type}-plot-spinner`)).css("display", "none"); + if(type === "trajectories" || (this.tsPlotData && this.tsPlotData.type === "trajectories")) { + $(this.queryByHook("multiple-plots")).prop("disabled", false); + } } - $(this.queryByHook(`${type}-plot-spinner`)).css("display", "none"); $(this.queryByHook(`${type}-edit-plot`)).prop("disabled", false); $(this.queryByHook(`${type}-download`)).prop("disabled", false); - if(type === "trajectories" || (this.tsPlotData && this.tsPlotData.type === "trajectories")) { - $(this.queryByHook("multiple-plots")).prop("disabled", false); - } }, plotMultiplePlots: function (e) { let type = e.target.dataset.type; diff --git a/stochss/handlers/util/model_inference.py b/stochss/handlers/util/model_inference.py index ce74ff903c..c9130603d9 100644 --- a/stochss/handlers/util/model_inference.py +++ b/stochss/handlers/util/model_inference.py @@ -113,6 +113,10 @@ def __get_round_result_plot(cls, results, names, values, dmin, dmax, rows=rows, cols=cols, column_titles=names, row_titles=names, x_title=xaxis, y_title=yaxis, vertical_spacing=0.075 ) + fig2 = subplots.make_subplots( + rows=rows, cols=cols, column_titles=names, row_titles=names, + x_title=xaxis, y_title=yaxis, vertical_spacing=0.075 + ) for i in range(rows): for j in range(cols): @@ -122,12 +126,12 @@ def __get_round_result_plot(cls, results, names, values, dmin, dmax, continue if i == j: color = common_rgb_values[(i)%len(common_rgb_values)] + # Create histogram trace trace = plotly.graph_objs.Histogram( x=accepted_samples[i], name=names[i], legendgroup=names[i], showlegend=False, marker_color=color, opacity=0.75, xbins={"start": dmin[i], "end": dmax[i], "size": sizes[i]} ) - fig.append_trace(trace, row, col) fig.update_xaxes(row=row, col=col, range=[dmin[i], dmax[i]]) fig.add_vline( @@ -135,6 +139,23 @@ def __get_round_result_plot(cls, results, names, values, dmin, dmax, exclude_empty_subplots=True, layer='above' ) fig.add_vline(values[i], row=row, col=col, line={"color": "red"}, layer='above') + # Create PDF trace + tmp_fig = figure_factory.create_distplot( + [accepted_samples[i]], [names[i]], curve_type='normal', + bin_size=sizes[i], histnorm="probability" + ) + trace2 = plotly.graph_objs.Scatter( + x=tmp_fig.data[1]['x'], y=tmp_fig.data[1]['y'] * 1000, + name=names[i], legendgroup=names[i], showlegend=False, + mode='lines', line=dict(color=color) + ) + fig.append_trace(trace2, row, col) + fig.update_xaxes(row=row, col=col, range=[dmin[i], dmax[i]]) + fig.add_vline( + results['inferred_parameters'][i], row=row, col=col, line={"color": "green"}, + exclude_empty_subplots=True, layer='above' + ) + fig.add_vline(values[i], row=row, col=col, line={"color": "red"}, layer='above') else: color = combine_colors([ common_rgb_values[(i)%len(common_rgb_values)][1:], @@ -147,12 +168,17 @@ def __get_round_result_plot(cls, results, names, values, dmin, dmax, fig.append_trace(trace, row, col) fig.update_xaxes(row=row, col=col, range=[dmin[j], dmax[j]]) fig.update_yaxes(row=row, col=col, range=[dmin[i], dmax[i]]) + fig2.append_trace(trace, row, col) + fig2.update_xaxes(row=row, col=col, range=[dmin[j], dmax[j]]) + fig2.update_yaxes(row=row, col=col, range=[dmin[i], dmax[i]]) fig.update_layout(height=1000) + fig2.update_layout(height=1000) if title is not None: title = {'text': title, 'x': 0.5, 'xanchor': 'center'} fig.update_layout(title=title) - return fig + fig2.update_layout(title=title) + return fig, fig2 @classmethod def __get_full_results_plot(cls, results, names, values, dmin, dmax, @@ -193,8 +219,8 @@ def __get_full_results_plot(cls, results, names, values, dmin, dmax, [accepted_values], [names[j]], curve_type='normal', bin_size=sizes[j], histnorm="probability" ) trace2 = plotly.graph_objs.Scatter( - x=tmp_fig.data[1]['x'], y=tmp_fig.data[1]['y'] * 1000, name=name, legendgroup=name, showlegend=False, - mode='lines', line=dict(color=color) + x=tmp_fig.data[1]['x'], y=tmp_fig.data[1]['y'] * 1000, name=name, legendgroup=name, + showlegend=j==0, mode='lines', line=dict(color=color) ) fig2.append_trace(trace2, row, col) fig2.update_xaxes(row=row, col=col, range=[dmin[j], dmax[j]]) @@ -216,7 +242,7 @@ def __get_full_results_plot(cls, results, names, values, dmin, dmax, title = {'text': title, 'x': 0.5, 'xanchor': 'center'} fig.update_layout(title=title) fig2.update_layout(title=title) - return fig + return fig, fig2 def __get_infer_args(self): settings = self.settings['inferenceSettings'] @@ -365,7 +391,7 @@ def get_csv_data(self, name): self.__to_csv(path=tmp_dir, nametag=nametag) return self.__get_csvzip(tmp_dir, nametag) - def get_result_plot(self, round=None, add_config=False, **kwargs): + def get_result_plot(self, epoch=None, add_config=False, **kwargs): """ Generate a plot for inference results. """ @@ -380,14 +406,16 @@ def get_result_plot(self, round=None, add_config=False, **kwargs): values.append(self.g_model.listOfParameters[parameter['name']].value) dmin.append(parameter['min']) dmax.append(parameter['max']) - if round is None: - fig_obj = self.__get_full_results_plot(results, names, values, dmin, dmax, **kwargs) + if epoch is None: + fig_obj, fig_obj2 = self.__get_full_results_plot(results, names, values, dmin, dmax, **kwargs) else: - fig_obj = self.__get_round_result_plot(results[round], names, values, dmin, dmax, **kwargs) + fig_obj, fig_obj2 = self.__get_round_result_plot(results[epoch], names, values, dmin, dmax, **kwargs) fig = json.loads(json.dumps(fig_obj, cls=plotly.utils.PlotlyJSONEncoder)) + fig2 = json.loads(json.dumps(fig_obj2, cls=plotly.utils.PlotlyJSONEncoder)) if add_config: fig["config"] = {"responsive": True} - return fig + fig2["config"] = {"responsive": True} + return fig, fig2 def process(self, raw_results): """ diff --git a/stochss/handlers/workflows.py b/stochss/handlers/workflows.py index 28b07c922d..7eb02b0cd0 100644 --- a/stochss/handlers/workflows.py +++ b/stochss/handlers/workflows.py @@ -255,7 +255,9 @@ async def get(self): kwargs.update(body['data_keys']) if "plt_data" in body.keys() and body['plt_data'] is not None: kwargs.update(body['plt_data']) - fig = job.get_result_plot(**kwargs) + fig, fig2 = job.get_result_plot(**kwargs) + log.debug(f"Histogram figure: {fig}, PDF figure: {fig2}") + self.write({'histogram': fig, 'pdf': fig2}) else: job = StochSSJob(path=path) if body['sim_type'] in ("GillesPy2", "GillesPy2_PS"): @@ -272,8 +274,8 @@ async def get(self): job.print_logs(log) if "plt_data" in body.keys(): fig = job.update_fig_layout(fig=fig, plt_data=body['plt_data']) - log.debug(f"Plot figure: {fig}") - self.write(fig) + log.debug(f"Plot figure: {fig}") + self.write(fig) except StochSSAPIError as err: report_error(self, log, err) except Exception as err: From a67a74076d6284df849031c74370a6c2c1c701ae Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Mon, 27 Mar 2023 21:43:00 -0400 Subject: [PATCH 068/109] Created an inference round and result objects. --- stochss/handlers/util/model_inference.py | 786 +++++++++++++++++------ 1 file changed, 604 insertions(+), 182 deletions(-) diff --git a/stochss/handlers/util/model_inference.py b/stochss/handlers/util/model_inference.py index c9130603d9..5b3e622ce8 100644 --- a/stochss/handlers/util/model_inference.py +++ b/stochss/handlers/util/model_inference.py @@ -24,6 +24,7 @@ import logging import tempfile import traceback +from collections import UserDict, UserList import numpy import plotly @@ -60,148 +61,387 @@ def combine_colors(colors): color = f"#{zpad(hex(red)[2:])}{zpad(hex(green)[2:])}{zpad(hex(blue)[2:])}" return color -class ModelInference(StochSSJob): - ''' - ################################################################################################ - StochSS model inference job object - ################################################################################################ - ''' - - TYPE = "inference" - - def __init__(self, path): - ''' - Intitialize a model inference job object - - Attributes - ---------- - path : str - Path to the model inference job - ''' - super().__init__(path=path) - self.g_model, self.s_model = self.load_models() - self.settings = self.load_settings() - - @classmethod - def __get_csv_data(cls, path): - with open(path, "r", newline="", encoding="utf-8") as csv_fd: - csv_reader = csv.reader(csv_fd, delimiter=",") - rows = [] - for i, row in enumerate(csv_reader): - if i != 0: - rows.append(row) - data = numpy.array(rows).swapaxes(0, 1).astype("float") - return data - - @classmethod - def __get_csvzip(cls, dirname, name): - shutil.make_archive(os.path.join(dirname, name), "zip", dirname, name) - path = os.path.join(dirname, f"{name}.zip") - with open(path, "rb") as zip_file: - return zip_file.read() - - @classmethod - def __get_round_result_plot(cls, results, names, values, dmin, dmax, - title=None, xaxis=None, yaxis=None): - accepted_samples = numpy.vstack(results['accepted_samples']).swapaxes(0, 1) - +class InferenceRound(UserDict): + """ + Inference Round Dict created by a StochSS Inference Simulation containing single round, extends the UserDict object. + + :param accepted_samples: A dictionary of accepted sample values created by an inference. + :type accepted_samples: dict + + :param distances: A list of distances values created by an inference. + :type distances: list + + :param accepted_count: The number of accepted samples for this round. + :type accepted_count: int + + :param trial_count: The number of total trials performed in order to converge this round. + :type trial_count: int + + :param inferred_parameters: The mean of accepted parameter samples. + :type inferred_parameters: dict + """ + def __init__(self, accepted_samples, distances, accepted_count, trial_count, inferred_parameters): + super().__init__(accepted_samples) + self.distances = distances + self.accepted_count = accepted_count + self.trial_count = trial_count + self.inferred_parameters = inferred_parameters + + def __getitem__(self, key): + print(key) + if isinstance(key, int): + param = list(self.data.keys())[key] + msg = "InferenceRound is of type dictionary. " + msg += f"Use inference_round['[{param}]'] instead of inference_round[{key}]['{param}']. " + msg += f"Retrieving inference_round['[{param}]']" + log.warning(msg) + return self.data[param] + if key in self.data: + return self.data[key] + if hasattr(self.__class__, "__missing__"): + return self.__class__.__missing__(self, key) + raise KeyError(key) + + def __plotplotly(self, parameters, bounds, include_pdf=True, include_orig_values=True, + include_inferred_values=False, title=None, xaxis_label=None, yaxis_label=None): nbins = 50 - rows = cols = len(accepted_samples) - sizes = (numpy.array(dmax) - numpy.array(dmin)) / nbins + names = list(self.data.keys()) + sizes = (numpy.array(bounds[1]) - numpy.array(bounds[0])) / nbins + plotly.offline.init_notebook_mode() fig = subplots.make_subplots( - rows=rows, cols=cols, column_titles=names, row_titles=names, - x_title=xaxis, y_title=yaxis, vertical_spacing=0.075 - ) - fig2 = subplots.make_subplots( - rows=rows, cols=cols, column_titles=names, row_titles=names, - x_title=xaxis, y_title=yaxis, vertical_spacing=0.075 + rows=len(names), cols=len(names), column_titles=names, row_titles=names, + x_title=xaxis_label, y_title=yaxis_label, vertical_spacing=0.075 ) - for i in range(rows): - for j in range(cols): - row = i + 1 + for i, (param1, accepted_values1) in enumerate(self.data.items()): + row = i + 1 + for j, (param2, accepted_values2) in enumerate(self.data.items()): col = j + 1 if i > j: continue if i == j: color = common_rgb_values[(i)%len(common_rgb_values)] - # Create histogram trace trace = plotly.graph_objs.Histogram( - x=accepted_samples[i], name=names[i], legendgroup=names[i], showlegend=False, - marker_color=color, opacity=0.75, - xbins={"start": dmin[i], "end": dmax[i], "size": sizes[i]} + x=accepted_values1, name=param1, legendgroup=param1, showlegend=False, marker_color=color, + opacity=0.75, xbins={"start": bounds[0][i], "end": bounds[1][i], "size": sizes[i]} ) fig.append_trace(trace, row, col) - fig.update_xaxes(row=row, col=col, range=[dmin[i], dmax[i]]) - fig.add_vline( - results['inferred_parameters'][i], row=row, col=col, line={"color": "green"}, - exclude_empty_subplots=True, layer='above' - ) - fig.add_vline(values[i], row=row, col=col, line={"color": "red"}, layer='above') - # Create PDF trace - tmp_fig = figure_factory.create_distplot( - [accepted_samples[i]], [names[i]], curve_type='normal', - bin_size=sizes[i], histnorm="probability" - ) - trace2 = plotly.graph_objs.Scatter( - x=tmp_fig.data[1]['x'], y=tmp_fig.data[1]['y'] * 1000, - name=names[i], legendgroup=names[i], showlegend=False, - mode='lines', line=dict(color=color) - ) - fig.append_trace(trace2, row, col) - fig.update_xaxes(row=row, col=col, range=[dmin[i], dmax[i]]) - fig.add_vline( - results['inferred_parameters'][i], row=row, col=col, line={"color": "green"}, - exclude_empty_subplots=True, layer='above' - ) - fig.add_vline(values[i], row=row, col=col, line={"color": "red"}, layer='above') + fig.update_xaxes(row=row, col=col, range=[bounds[0][i], bounds[1][i]]) + if include_pdf: + tmp_fig = figure_factory.create_distplot( + [accepted_values1], [param1], curve_type='normal', bin_size=sizes[i], histnorm="probability" + ) + trace2 = plotly.graph_objs.Scatter( + x=tmp_fig.data[1]['x'], y=tmp_fig.data[1]['y'] * 1000, mode='lines', line=dict(color=color), + name=f"{param1} PDF", legendgroup=f"{param1} PDF", showlegend=False + ) + fig.append_trace(trace2, row, col) + if include_inferred_values: + fig.add_vline( + self.inferred_parameters[param1], row=row, col=col, line={"color": "green"}, + exclude_empty_subplots=True, layer='above' + ) + if include_orig_values: + fig.add_vline(parameters[param1], row=row, col=col, line={"color": "red"}, layer='above') else: color = combine_colors([ common_rgb_values[(i)%len(common_rgb_values)][1:], common_rgb_values[(j)%len(common_rgb_values)][1:] ]) trace = plotly.graph_objs.Scatter( - x=accepted_samples[j], y=accepted_samples[i], mode='markers', marker_color=color, - name=f"{names[j]} X {names[i]}", legendgroup=f"{names[j]} X {names[i]}", showlegend=False + x=accepted_values2, y=accepted_values1, mode='markers', marker_color=color, + name=f"{param2} X {param1}", legendgroup=f"{param2} X {param1}", showlegend=False ) fig.append_trace(trace, row, col) - fig.update_xaxes(row=row, col=col, range=[dmin[j], dmax[j]]) - fig.update_yaxes(row=row, col=col, range=[dmin[i], dmax[i]]) - fig2.append_trace(trace, row, col) - fig2.update_xaxes(row=row, col=col, range=[dmin[j], dmax[j]]) - fig2.update_yaxes(row=row, col=col, range=[dmin[i], dmax[i]]) + fig.update_xaxes(row=row, col=col, range=[bounds[0][j], bounds[1][j]]) + fig.update_yaxes(row=row, col=col, range=[bounds[0][i], bounds[1][i]]) fig.update_layout(height=1000) - fig2.update_layout(height=1000) if title is not None: title = {'text': title, 'x': 0.5, 'xanchor': 'center'} fig.update_layout(title=title) - fig2.update_layout(title=title) - return fig, fig2 - @classmethod - def __get_full_results_plot(cls, results, names, values, dmin, dmax, - title=None, xaxis="Values", yaxis="Sample Concentrations"): - cols = 2 + return fig + + def __plotplotly_intersection(self, parameters, bounds, include_pdf=True, include_orig_values=True, + include_inferred_values=False, title=None, xaxis_label=None, yaxis_label=None): nbins = 50 - sizes = (numpy.array(dmax) - numpy.array(dmin)) / nbins - rows = int(numpy.ceil(len(results[0]['accepted_samples'][0])/cols)) + names = list(parameters.keys()) + sizes = (numpy.array(bounds[1]) - numpy.array(bounds[0])) / nbins + + if xaxis_label is None: + xaxis_label = names[0] + if yaxis_label is None: + yaxis_label = names[1] fig = subplots.make_subplots( - rows=rows, cols=cols, subplot_titles=names, x_title=xaxis, y_title=yaxis, vertical_spacing=0.075 + rows=3, cols=3, x_title=xaxis_label, y_title=yaxis_label, horizontal_spacing=0.01, + vertical_spacing=0.01, column_widths=[0.6, 0.1, 0.3], row_heights=[0.3, 0.1, 0.6], + shared_xaxes=True, shared_yaxes=True + ) + bins = ['xbins', 'ybins'] + histo_row = histo_col = [1, 3] + rug_row, rug_col = [2, 3], [1, 2] + x_key, y_key = ['x', 'y'], ['y', 'x'] + line_func = [fig.add_vline, fig.add_hline] + rug_symbol = ['line-ns-open', 'line-ew-open'] + xaxis_func = [fig.update_xaxes, fig.update_yaxes] + yaxis_func = [fig.update_yaxes, fig.update_xaxes] + + for i, (param, orig_val) in enumerate(parameters.items()): + if i >= 2: + break + + color = common_rgb_values[(i)%len(common_rgb_values)] + # Create histogram traces + histo_trace = plotly.graph_objs.Histogram( + name=param, legendgroup=param, showlegend=False, marker_color=color, opacity=0.75 + ) + histo_trace[x_key[i]] = self[param] + histo_trace[bins[i]] = {"start": bounds[0][i], "end": bounds[1][i], "size": sizes[i]} + fig.append_trace(histo_trace, histo_row[i], histo_col[i]) + xaxis_func[i](row=histo_row[i], col=histo_col[i], range=[bounds[0][i], bounds[1][i]]) + if include_pdf: + tmp_fig = figure_factory.create_distplot( + [self[param]], [param], curve_type='normal', bin_size=sizes[i], histnorm="probability" + ) + histo_trace2 = plotly.graph_objs.Scatter( + mode='lines', line=dict(color=color), + name=f"{param} PDF", legendgroup=f"{param} PDF", showlegend=False + ) + histo_trace2[x_key[i]] = tmp_fig.data[1]['x'] + histo_trace2[y_key[i]] = tmp_fig.data[1]['y'] * 1000 + fig.append_trace(histo_trace2, histo_row[i], histo_col[i]) + if include_inferred_values: + line_func[i]( + self.inferred_parameters[param], row=histo_row[i], col=histo_col[i], line={"color": "green"}, + exclude_empty_subplots=True, layer='above' + ) + if include_orig_values: + line_func[i](orig_val, row=histo_row[i], col=histo_col[i], line={"color": "red"}, layer='above') + # Create rug traces + rug_trace = plotly.graph_objs.Scatter( + mode='markers', marker={'color': color, 'symbol': rug_symbol[i]}, + name=param, legendgroup=param, showlegend=False + ) + rug_trace[x_key[i]] = self[param] + rug_trace[y_key[i]] = [param] * self.accepted_count + fig.append_trace(rug_trace, rug_row[i], rug_col[i]) + xaxis_func[i](row=rug_row[i], col=rug_col[i], range=[bounds[0][i], bounds[1][i]]) + yaxis_func[i](row=rug_row[i], col=rug_col[i], showticklabels=False) + + color = combine_colors([ + common_rgb_values[(0)%len(common_rgb_values)][1:], common_rgb_values[(1)%len(common_rgb_values)][1:] + ]) + trace = plotly.graph_objs.Scatter( + x=self[names[0]], y=self[names[1]], mode='markers', marker_color=color, + name=f"{names[0]} X {names[1]}", legendgroup=f"{names[0]} X {names[1]}", showlegend=False ) - fig2 = subplots.make_subplots( - rows=rows, cols=cols, subplot_titles=names, x_title=xaxis, y_title=yaxis, vertical_spacing=0.075 + fig.append_trace(trace, 3, 1) + fig.update_xaxes(row=3, col=1, range=[bounds[0][0], bounds[1][0]]) + fig.update_yaxes(row=3, col=1, range=[bounds[0][1], bounds[1][1]]) + fig.update_layout(height=900) + if title is not None: + title = {'text': title, 'x': 0.5, 'xanchor': 'center'} + fig.update_layout(title=title) + + return fig + + @classmethod + def build_from_inference_round(cls, data, names): + """ + Build an InferenceRound object using the provided inference round results. + + :param data: The results from an inference round. + :type data: dict + + :param names: List of the parameter names. + :type names: list + + :returns: An InferenceRound object using the provided inference results. + :rtype: InferenceResult + """ + _accepted_samples = numpy.vstack(data['accepted_samples']).swapaxes(0, 1) + + accepted_samples = {} + inferred_parameters = {} + for i, name in enumerate(names): + accepted_samples[name] = _accepted_samples[i] + inferred_parameters[name] = data['inferred_parameters'][i] + + return InferenceRound( + accepted_samples, data['distances'], data['accepted_count'], data['trial_count'], inferred_parameters + ) + + def plot(self, parameters, bounds, use_matplotlib=False, return_plotly_figure=False, **kwargs): + """ + Plot the results of the inference round. + + :param parameters: Dictionary of the parameters and original values. + :type parameters: dict + + :param bounds: List of bounds for of the parameter space. + :type bounds: list + + :param use_matplotlib: Whether or not to plot using MatPlotLib. + :type use_matplotlib: bool + + :param return_plotly_figure: Whether or not to return the figure. Ignored if use_matplotlib is set. + :type return_plotly_figure: bool + + :param include_pdf: Whether or not to include the probability distribution curve. + :type include_pdf: bool + + :param include_orig_values: Whether or not to include a line marking the original parameter values. + :type include_orig_values: bool + + :param include_inferred_values: Whether or not to include a line marking the inferred parameter values. + :type include_inferred_values: bool + + :param xaxis_label: The label for the x-axis + :type xaxis_label: str + + :param yaxis_label: The label for the y-axis + :type yaxis_label: str + + :param title: The title of the graph + :type title: str + + :returns: Plotly figure object if return_plotly_figure is set else None. + :rtype: plotly.Figure + """ + if use_matplotlib: + raise Exception("use_matplotlib has not been implemented.") + fig = self.__plotplotly(parameters, bounds, **kwargs) + + if return_plotly_figure: + return fig + plotly.offline.iplot(fig) + return None + + def plot_intersection(self, parameters, bounds, use_matplotlib=False, return_plotly_figure=False, **kwargs): + """ + Plot the results of the inference round. + + :param parameters: Dictionary of two parameters and their original values. + :type parameters: dict + + :param bounds: List of bounds for the provided parameters. + :type bounds: list + + :param use_matplotlib: Whether or not to plot using MatPlotLib. + :type use_matplotlib: bool + + :param return_plotly_figure: Whether or not to return the figure. Ignored if use_matplotlib is set. + :type return_plotly_figure: bool + + :param include_pdf: Whether or not to include the probability distribution curve. + :type include_pdf: bool + + :param include_orig_values: Whether or not to include a line marking the original parameter values. + :type include_orig_values: bool + + :param include_inferred_values: Whether or not to include a line marking the inferred parameter values. + :type include_inferred_values: bool + + :param xaxis_label: The label for the x-axis + :type xaxis_label: str + + :param yaxis_label: The label for the y-axis + :type yaxis_label: str + + :param title: The title of the graph + :type title: str + + :returns: Plotly figure object if return_plotly_figure is set else None. + :rtype: plotly.Figure + """ + if use_matplotlib: + raise Exception("use_matplotlib has not been implemented.") + fig = self.__plotplotly_intersection(parameters, bounds, **kwargs) + + if return_plotly_figure: + return fig + plotly.offline.iplot(fig) + return None + + def to_csv(self, path): + """ + Generate the csv results for the round. + + :param path: The path to the csv file. + :type path: str + """ + headers = ["Sample ID", *list(self.data.keys()), "Distances"] + accepted_samples = numpy.array(list(self.data.values())).swapaxes(0, 1) + + with open(path, 'w', newline='', encoding="utf-8") as csv_fd: + csv_writer = csv.writer(csv_fd) + csv_writer.writerow(headers) + for i, accepted_sample in enumerate(accepted_samples): + line = accepted_sample.tolist() + line.insert(0, i + 1) + if isinstance(self.distances[i], list): + line.extend(self.distances[i]) + else: + line.append(self.distances[i]) + csv_writer.writerow(line) + + def to_dict(self): + """ + Return the results of the round as a dictionary. + + :returns: The results of the round. + :rtype: dict + """ + accepted_samples = numpy.array(list(self.data.values())).swapaxes(0, 1) + return { + 'accepted_samples': accepted_samples, + 'distances': self.distances, + 'accepted_count': self.accepted_count, + 'trial_count': self.trial_count, + 'inferred_parameters': numpy.array(self.inferred_parameters.values()) + } + +class InferenceResults(UserList): + """ + List of InferenceRound objects created by a StochSS Inference Simulation, extends the UserList object. + + :param data: A list of inference round objects + :type data: list + + :param parameters: Dictionary of the parameters and original values. + :type parameters: dict + + :param bounds: List of bounds for of the parameter space. + :type bounds: list + """ + def __init__(self, data, parameters, bounds): + super().__init__(data) + self.parameters = parameters + self.bounds = bounds + + def __plotplotly(self, include_orig_values=True, include_inferred_values=False, + title=None, xaxis_label="Values", yaxis_label="Sample Concentrations"): + cols = 2 + rows = int(numpy.ceil(len(self.parameters)/cols)) + names = list(self.parameters.keys()) + histo_fig = subplots.make_subplots( + rows=rows, cols=cols, subplot_titles=names, x_title=xaxis_label, y_title=yaxis_label, vertical_spacing=0.075 + ) + pdf_fig = subplots.make_subplots( + rows=rows, cols=cols, subplot_titles=names, x_title=xaxis_label, y_title=yaxis_label, vertical_spacing=0.075 ) - for i, result in enumerate(results): - accepted_samples = numpy.vstack(result['accepted_samples']).swapaxes(0, 1) - base_opacity = 0.5 if len(results) <= 1 else (i / (len(results) - 1) * 0.5) + nbins = 50 + sizes = (numpy.array(self.bounds[1]) - numpy.array(self.bounds[0])) / nbins + for i, inf_round in enumerate(self.data): + base_opacity = 0.5 if len(self.data) <= 1 else (i / (len(self.data) - 1) * 0.5) - for j, accepted_values in enumerate(accepted_samples): + for j, (param, accepted_values) in enumerate(inf_round.data.items()): row = int(numpy.ceil((j + 1) / cols)) col = (j % cols) + 1 @@ -211,9 +451,9 @@ def __get_full_results_plot(cls, results, names, values, dmin, dmax, # Create histogram trace trace = plotly.graph_objs.Histogram( x=accepted_values, name=name, legendgroup=name, showlegend=j==0, marker_color=color, - opacity=opacity, xbins={"start": dmin[j], "end": dmax[j], "size": sizes[j]} + opacity=opacity, xbins={"start": self.bounds[0][j], "end": self.bounds[1][j], "size": sizes[j]} ) - fig.append_trace(trace, row, col) + histo_fig.append_trace(trace, row, col) # Create PDF trace tmp_fig = figure_factory.create_distplot( [accepted_values], [names[j]], curve_type='normal', bin_size=sizes[j], histnorm="probability" @@ -222,27 +462,238 @@ def __get_full_results_plot(cls, results, names, values, dmin, dmax, x=tmp_fig.data[1]['x'], y=tmp_fig.data[1]['y'] * 1000, name=name, legendgroup=name, showlegend=j==0, mode='lines', line=dict(color=color) ) - fig2.append_trace(trace2, row, col) - fig2.update_xaxes(row=row, col=col, range=[dmin[j], dmax[j]]) - if i == len(results) - 1: - fig.add_vline(values[j], row=row, col=col, line={"color": "red"}, layer='above') - fig.add_vline( - result['inferred_parameters'][j], row=row, col=col, line={"color": "green"}, - exclude_empty_subplots=True, layer='above' - ) - fig2.add_vline(values[j], row=row, col=col, line={"color": "red"}, layer='above') - fig2.add_vline( - result['inferred_parameters'][j], row=row, col=col, line={"color": "green"}, - exclude_empty_subplots=True, layer='above' - ) - - fig.update_layout(barmode='overlay', height=500 * rows) - fig2.update_layout(height=500 * rows) + pdf_fig.append_trace(trace2, row, col) + pdf_fig.update_xaxes(row=row, col=col, range=[self.bounds[0][j], self.bounds[1][j]]) + + if i == len(self.data) - 1: + if include_orig_values: + histo_fig.add_vline( + self.parameters[param], row=row, col=col, line={"color": "red"}, layer='above' + ) + pdf_fig.add_vline( + self.parameters[param], row=row, col=col, line={"color": "red"}, layer='above' + ) + if include_inferred_values: + histo_fig.add_vline( + inf_round.inferred_parameters[param], row=row, col=col, line={"color": "green"}, + exclude_empty_subplots=True, layer='above' + ) + pdf_fig.add_vline( + inf_round.inferred_parameters[param], row=row, col=col, line={"color": "green"}, + exclude_empty_subplots=True, layer='above' + ) + + height = 500 * rows + histo_fig.update_layout(barmode='overlay', height=height) + pdf_fig.update_layout(height=height) if title is not None: title = {'text': title, 'x': 0.5, 'xanchor': 'center'} - fig.update_layout(title=title) - fig2.update_layout(title=title) - return fig, fig2 + histo_fig.update_layout(title=title) + pdf_fig.update_layout(title=title) + + return histo_fig, pdf_fig + + @classmethod + def build_from_inference_results(cls, data, parameters, bounds): + """ + Build an InferenceResult object using the provided inference. + + :param data: The results from an inference result. + :type data: list or dict + + :param parameters: Dictionary of the parameters and original values. + :type parameters: dict + + :param bounds: List of bounds for of the parameter space. + :type bounds: list + + :returns: An InferenceResult object using the provided inference results. + :rtype: InferenceResult + """ + if isinstance(data, dict): + data = [data] + + inf_rounds = [] + names = list(parameters.keys()) + for inf_r in data: + inf_round = InferenceRound.build_from_inference_round(inf_r, names) + inf_rounds.append(inf_round) + return InferenceResults(inf_rounds, parameters, bounds) + + def plot(self, histo_only=True, pdf_only=False, use_matplotlib=False, return_plotly_figure=False, **kwargs): + """ + Plot the results. + + :param : Indicates that only the histogram plot should be returned. + :type : bool + + :param : Indicates that only the PDF plot should be returned. + :type : bool + + :param use_matplotlib: Whether or not to plot using MatPlotLib. + :type use_matplotlib: bool + + :param return_plotly_figure: Whether or not to return the figure. Ignored if use_matplotlib is set. + :type return_plotly_figure: bool + + :param include_orig_values: Whether or not to include a line marking the original parameter values. + :type include_orig_values: bool + + :param include_inferred_values: Whether or not to include a line marking the + inferred parameter values of the final round. + :type include_inferred_values: bool + + :param xaxis_label: The label for the x-axis + :type xaxis_label: str + + :param yaxis_label: The label for the y-axis + :type yaxis_label: str + + :param title: The title of the graph + :type title: str + + :returns: Plotly figure object if return_plotly_figure is set else None. + :rtype: plotly.Figure + """ + if use_matplotlib: + raise Exception("use_matplotlib has not been implemented.") + histo_fig, pdf_fig = self.__plotplotly(**kwargs) + + if return_plotly_figure: + if histo_only: + return histo_fig + if pdf_only: + return pdf_fig + return histo_fig, pdf_fig + if histo_only: + plotly.offline.iplot(histo_fig) + elif pdf_only: + plotly.offline.iplot(pdf_fig) + else: + plotly.offline.iplot(histo_fig) + plotly.offline.iplot(pdf_fig) + return None + + def plot_round(self, ndx=None, **kwargs): + """ + Plot the results of a single inference round. + + :param ndx: Index of the inference round to plot. + :type ndx: int + + :param \**kwargs: Additional keyword arguments passed to :py:class:`InferenceRound.plot`. + + :returns: Plotly fig object if return_plotly_figure is set else None. + :rtype: plotly.Figure + """ + if ndx is None: + ndx = -1 + + inf_round = self.data[ndx] + return inf_round.plot(self.parameters, self.bounds, **kwargs) + + def plot_round_intersection(self, ndx=None, names=None, **kwargs): + """ + Plot the results of a inference round intersection. + + :param ndx: Index of the inference round to plot. + :type ndx: int + + :param names: List of two parameters. + :type names: list + + :param \**kwargs: Additional keyword arguments passed to :py:class:`InferenceRound.plot`. + + :returns: Plotly fig object if return_plotly_figure is set else None. + :rtype: plotly.Figure + """ + if ndx is None: + ndx = -1 + + param_names = list(self.parameters.keys()) + if names is None: + names = param_names[:2] + + inf_round = self.data[ndx] + bounds = [ + [self.bounds[0][param_names.index(names[1])], self.bounds[0][param_names.index(names[0])]], + [self.bounds[1][param_names.index(names[1])], self.bounds[1][param_names.index(names[0])]] + ] + parameters = {names[1]: self.parameters[names[1]], names[0]: self.parameters[names[0]]} + return inf_round.plot_intersection(parameters, bounds, **kwargs) + + def to_array(self): + """ + Convert the results object into an array. + """ + return [inf_round.to_dict() for inf_round in self.data] + + def to_csv(self, path='.', nametag="results_csv"): + """ + Convert the results to CSV. + + :param path: The location for the new directory and included files. Defaults to current working directory. + :type path: str + + :param nametag: Unique identifier for to CSV directory. + :type nametag: str + """ + directory = os.path.join(path, str(nametag)) + if not os.path.exists(directory): + os.mkdir(directory) + + headers = ["Round", "Accepted Count", "Trial Count", *list(self.parameters.keys())] + + inf_path = os.path.join(directory, "inference-overview.csv") + with open(inf_path, 'w', newline='', encoding="utf-8") as csv_fd: + csv_writer = csv.writer(csv_fd) + csv_writer.writerow(headers) + for i, inf_round in enumerate(self.data): + inf_round.to_csv(path=os.path.join(directory, f"round{i + 1}-details.csv")) + + line = [i + 1, inf_round.accepted_count, inf_round.trial_count] + line.extend(list(inf_round.inferred_parameters.values())) + csv_writer.writerow(line) + +class ModelInference(StochSSJob): + ''' + ################################################################################################ + StochSS model inference job object + ################################################################################################ + ''' + + TYPE = "inference" + + def __init__(self, path): + ''' + Intitialize a model inference job object + + Attributes + ---------- + path : str + Path to the model inference job + ''' + super().__init__(path=path) + self.g_model, self.s_model = self.load_models() + self.settings = self.load_settings() + + @classmethod + def __get_csv_data(cls, path): + with open(path, "r", newline="", encoding="utf-8") as csv_fd: + csv_reader = csv.reader(csv_fd, delimiter=",") + rows = [] + for i, row in enumerate(csv_reader): + if i != 0: + rows.append(row) + data = numpy.array(rows).swapaxes(0, 1).astype("float") + return data + + @classmethod + def __get_csvzip(cls, dirname, name): + shutil.make_archive(os.path.join(dirname, name), "zip", dirname, name) + path = os.path.join(dirname, f"{name}.zip") + with open(path, "rb") as zip_file: + return zip_file.read() def __get_infer_args(self): settings = self.settings['inferenceSettings'] @@ -319,46 +770,6 @@ def __store_pickled_results(cls, results): return message return False - def __to_csv(self, path='.', nametag="results_csv"): - results = self.__get_pickled_results() - settings = self.settings['inferenceSettings'] - names = list(map(lambda param: param['name'], settings['parameters'])) - - directory = os.path.join(path, str(nametag)) - if not os.path.exists(directory): - os.mkdir(directory) - - infer_csv = [["Round", "Accepted Count", "Trial Count"]] - round_headers = ["Sample ID", "Distances"] - for name in names: - infer_csv[0].append(name) - round_headers.insert(-1, name) - - for i, round in enumerate(results): - infer_line = [i + 1, round['accepted_count'], round['trial_count']] - infer_line.extend(round['inferred_parameters']) - infer_csv.append(infer_line) - - round_csv = [round_headers] - for j, accepted_sample in enumerate(round['accepted_samples']): - round_line = accepted_sample.tolist() - round_line.insert(0, j + 1) - round_line.extend(round['distances'][j]) - round_csv.append(round_line) - - round_path = os.path.join(directory, f"round{i + 1}-details.csv") - self.__write_csv_file(round_path, round_csv) - - infer_path = os.path.join(directory, "inference-overview.csv") - self.__write_csv_file(infer_path, infer_csv) - - @classmethod - def __write_csv_file(cls, path, data): - with open(path, "w", encoding="utf-8") as csv_file: - csv_writer = csv.writer(csv_file) - for line in data: - csv_writer.writerow(line) - def export_inferred_model(self, round_ndx=-1): """ Export the jobs model after updating the inferred parameter values. @@ -386,30 +797,41 @@ def get_csv_data(self, name): Generate the csv results and return the binary of the zipped archive. """ self.log("info", "Getting job results...") + inf_params = self.settings['inferenceSettings']['parameters'] + parameters = {} + bounds = [[], []] + for inf_param in inf_params: + parameters[inf_param['name']] = self.g_model.listOfParameters[inf_param['name']].value + bounds[0].append(inf_param['min']) + bounds[1].append(inf_param['max']) + results = InferenceResults.build_from_inference_results(self.__get_pickled_results(), parameters, bounds) + nametag = f"Inference - {name} - Results-CSV" with tempfile.TemporaryDirectory() as tmp_dir: - self.__to_csv(path=tmp_dir, nametag=nametag) + results.to_csv(path=tmp_dir, nametag=nametag) return self.__get_csvzip(tmp_dir, nametag) - def get_result_plot(self, epoch=None, add_config=False, **kwargs): + def get_result_plot(self, epoch=None, names=None, add_config=False, **kwargs): """ Generate a plot for inference results. """ - results = self.__get_pickled_results() - parameters = self.settings['inferenceSettings']['parameters'] - names = [] - values = [] - dmin = [] - dmax = [] - for parameter in parameters: - names.append(parameter['name']) - values.append(self.g_model.listOfParameters[parameter['name']].value) - dmin.append(parameter['min']) - dmax.append(parameter['max']) + inf_params = self.settings['inferenceSettings']['parameters'] + parameters = {} + bounds = [[], []] + for inf_param in inf_params: + parameters[inf_param['name']] = self.g_model.listOfParameters[inf_param['name']].value + bounds[0].append(inf_param['min']) + bounds[1].append(inf_param['max']) + results = InferenceResults.build_from_inference_results(self.__get_pickled_results(), parameters, bounds) + if epoch is None: - fig_obj, fig_obj2 = self.__get_full_results_plot(results, names, values, dmin, dmax, **kwargs) + fig_obj, fig_obj2 = results.plot(histo_only=False, return_plotly_figure=True, **kwargs) else: - fig_obj, fig_obj2 = self.__get_round_result_plot(results[epoch], names, values, dmin, dmax, **kwargs) + fig_obj = results.plot_round(ndx=epoch, return_plotly_figure=True, include_inferred_values=True, **kwargs) + fig_obj2 = results.plot_round( + ndx=epoch, names=names, return_plotly_figure=True, include_inferred_values=True, **kwargs + ) + fig = json.loads(json.dumps(fig_obj, cls=plotly.utils.PlotlyJSONEncoder)) fig2 = json.loads(json.dumps(fig_obj2, cls=plotly.utils.PlotlyJSONEncoder)) if add_config: From 2e37a807e26957a344c041dade38d58582f1757d Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Tue, 28 Mar 2023 10:22:44 -0400 Subject: [PATCH 069/109] Final tweaks to probability distribution plot. --- .../templates/inferenceResultsView.pug | 2 +- client/job-view/views/job-results-view.js | 20 +++++++++++++++++++ stochss/handlers/util/model_inference.py | 1 + 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/client/job-view/templates/inferenceResultsView.pug b/client/job-view/templates/inferenceResultsView.pug index 2d257ab42f..db419877fb 100644 --- a/client/job-view/templates/inferenceResultsView.pug +++ b/client/job-view/templates/inferenceResultsView.pug @@ -41,7 +41,7 @@ div#workflow-results.card li.nav-item - a.nav-link.tab(data-toggle="tab" href="#pdf") Propability Distribution + a.nav-link.tab(data-toggle="tab" href="#pdf" data-hook="inference-pdf-tab") Propability Distribution button.btn.btn-outline-collapse( data-toggle="collapse" diff --git a/client/job-view/views/job-results-view.js b/client/job-view/views/job-results-view.js index 39bf2c03bf..b18bb3ce12 100644 --- a/client/job-view/views/job-results-view.js +++ b/client/job-view/views/job-results-view.js @@ -53,6 +53,7 @@ module.exports = View.extend({ 'change [data-hook=ensemble-aggragator-list]' : 'getPlotForEnsembleAggragator', 'change [data-hook=plot-type-select]' : 'getTSPlotForType', 'click [data-hook=collapse-results-btn]' : 'changeCollapseButtonText', + 'click [data-hook=inference-pdf-tab]' : 'handlePDFResize', 'click [data-trigger=collapse-plot-container]' : 'handleCollapsePlotContainerClick', 'click [data-target=model-export]' : 'handleExportInferredModel', 'click [data-target=model-explore]' : 'handleExploreInferredModel', @@ -77,6 +78,7 @@ module.exports = View.extend({ this.plotArgs = {}; this.activePlots = {}; this.trajectoryIndex = 1; + this.pdfResized = false; }, render: function (attrs, options) { let isEnsemble = this.model.settings.simulationSettings.realizations > 1 && @@ -228,6 +230,15 @@ module.exports = View.extend({ cb(); } }, + fixPlotSize: function (type, plotID) { + let plotEL = this.queryByHook(`${type}-${plotID}-plot`); + // Clear plot + Plotly.purge(plotEL); + $(plotEL).empty(); + // Re-render the plot + let figure = this.plots[this.activePlots[type]] + Plotly.newPlot(plotEL, figure[plotID]); + }, getPlot: function (type) { this.cleanupPlotContainer(type); let data = this.getPlotData(type); @@ -471,6 +482,15 @@ module.exports = View.extend({ this.downloadCSV("full", null); } }, + handlePDFResize: function (e) { + if(!this.pdfResized) { + setTimeout(() => { + console.log("Fixing plot size") + this.fixPlotSize("inference", "pdf"); + this.pdfResized = true; + }, 0) + } + }, handlePlotCSVClick: function (e) { let type = e.target.dataset.type; if(type !== "psweep") { diff --git a/stochss/handlers/util/model_inference.py b/stochss/handlers/util/model_inference.py index 5b3e622ce8..c110863b6d 100644 --- a/stochss/handlers/util/model_inference.py +++ b/stochss/handlers/util/model_inference.py @@ -825,6 +825,7 @@ def get_result_plot(self, epoch=None, names=None, add_config=False, **kwargs): results = InferenceResults.build_from_inference_results(self.__get_pickled_results(), parameters, bounds) if epoch is None: + kwargs['include_inferred_values'] = True fig_obj, fig_obj2 = results.plot(histo_only=False, return_plotly_figure=True, **kwargs) else: fig_obj = results.plot_round(ndx=epoch, return_plotly_figure=True, include_inferred_values=True, **kwargs) From 523bbd7ad770648b0ff3b5b7e8b041297b2716c3 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Tue, 28 Mar 2023 15:14:36 -0400 Subject: [PATCH 070/109] Added support for intersection plotting. --- .../templates/inferenceResultsView.pug | 32 +++- client/job-view/views/job-results-view.js | 167 +++++++++++++----- stochss/handlers/util/model_inference.py | 43 +++-- 3 files changed, 183 insertions(+), 59 deletions(-) diff --git a/client/job-view/templates/inferenceResultsView.pug b/client/job-view/templates/inferenceResultsView.pug index db419877fb..01b3814c33 100644 --- a/client/job-view/templates/inferenceResultsView.pug +++ b/client/job-view/templates/inferenceResultsView.pug @@ -37,7 +37,7 @@ div#workflow-results.card li.nav-item - a.nav-link.tab.active(data-toggle="tab" href="#histogram") Histogram + a.nav-link.tab.active(data-toggle="tab" href="#inference-histogram" data-hook="inference-histogram-tab") Histogram li.nav-item @@ -58,7 +58,7 @@ div#workflow-results.card div.tab-content - div.tab-pane.active(id="histogram" data-hook="histogram") + div.tab-pane.active(id="inference-histogram" data-hook="inference-histogram") div(data-hook="inference-histogram-plot") @@ -102,7 +102,19 @@ div#workflow-results.card div.card-header.pb-0 - h5.inline Plot Round + h5.inline.mr-2 Plot Round + + div.inline + + ul.nav.nav-tabs + + li.nav-item + + a.nav-link.tab.active(data-toggle="tab" href="#round-histogram" data-hook="round-histogram-tab") Histogram + + li.nav-item + + a.nav-link.tab(data-toggle="tab" href="#intersection" data-hook="round-intersection-tab") Intersection button.btn.btn-outline-collapse( data-toggle="collapse" @@ -141,9 +153,19 @@ div#workflow-results.card data-hook="round-index-slider" ) - div(data-hook="round-plot") + div.tab-content + + div.tab-pane.active(id="round-histogram" data-hook="round-histogram") + + div(data-hook="round-histogram-plot") + + div.spinner-border.workflow-plot(data-hook="round-histogram-plot-spinner") + + div.tab-pane(id="intersection" data-hook="intersection") + + div(data-hook="round-intersection-plot") - div.spinner-border.workflow-plot(data-hook="round-plot-spinner") + div.spinner-border.workflow-plot(data-hook="round-intersection-plot-spinner") button.btn.btn-primary.box-shadow(data-hook="round-model-export" data-target="model-export" disabled) Export Model diff --git a/client/job-view/views/job-results-view.js b/client/job-view/views/job-results-view.js index b18bb3ce12..8504e371ae 100644 --- a/client/job-view/views/job-results-view.js +++ b/client/job-view/views/job-results-view.js @@ -53,7 +53,10 @@ module.exports = View.extend({ 'change [data-hook=ensemble-aggragator-list]' : 'getPlotForEnsembleAggragator', 'change [data-hook=plot-type-select]' : 'getTSPlotForType', 'click [data-hook=collapse-results-btn]' : 'changeCollapseButtonText', + 'click [data-hook=inference-histogram-tab]' : 'handleInferenceResize', 'click [data-hook=inference-pdf-tab]' : 'handlePDFResize', + 'click [data-hook=round-histogram-tab]' : 'handleRoundResize', + 'click [data-hook=round-intersection-tab]' : 'handleIntersectionResize', 'click [data-trigger=collapse-plot-container]' : 'handleCollapsePlotContainerClick', 'click [data-target=model-export]' : 'handleExportInferredModel', 'click [data-target=model-explore]' : 'handleExploreInferredModel', @@ -79,6 +82,9 @@ module.exports = View.extend({ this.activePlots = {}; this.trajectoryIndex = 1; this.pdfResized = false; + this.inferenceResized = false; + this.roundHistoResized = false; + this.intersectionResized = false; }, render: function (attrs, options) { let isEnsemble = this.model.settings.simulationSettings.realizations > 1 && @@ -136,6 +142,8 @@ module.exports = View.extend({ }else{ var type = "inference"; this.roundIndex = this.model.settings.inferenceSettings.numRounds; + let parameters = this.model.settings.inferenceSettings.parameters; + this.intersectionNames = [parameters.at(0).name, parameters.at(1).name]; if(this.model.exportLinks[this.roundIndex] !== null) { $(this.queryByHook("inference-model-export")).text("Open Model"); $(this.queryByHook("inference-model-explore")).text("Explore Model"); @@ -166,21 +174,35 @@ module.exports = View.extend({ error.css("display", "none"); }, 5000); }, - cleanupPlotContainer: function (type) { + cleanupPlotContainer: function (type, {pdfOnly=false}={}) { if(["inference", "round"].includes(type)) { let histoEL = this.queryByHook(`${type}-histogram-plot`); - Plotly.purge(histoEL); - $(this.queryByHook(`${type}-histogram-plot`)).empty(); - $(this.queryByHook(`${type}-histogram-plot-spinner`)).css("display", "block"); - let pdfEL = this.queryByHook(`${type}-pdf-plot`); - Plotly.purge(pdfEL); - $(this.queryByHook(`${type}-pdf-plot`)).empty(); - $(this.queryByHook(`${type}-pdf-plot-spinner`)).css("display", "block"); - if(type === "round") { - $(this.queryByHook("round-model-export")).prop("disabled", true); - $(this.queryByHook("round-model-explore")).prop("disabled", true); - $(this.queryByHook("round-download")).prop("disabled", true); - $(this.queryByHook("round-edit-plot")).prop("disabled", true); + if(!pdfOnly) { + Plotly.purge(histoEL); + $(this.queryByHook(`${type}-histogram-plot`)).empty(); + $(this.queryByHook(`${type}-histogram-plot-spinner`)).css("display", "block"); + } + if(type === "inference") { + let pdfEL = this.queryByHook('inference-pdf-plot'); + Plotly.purge(pdfEL); + $(this.queryByHook('inference-pdf-plot')).empty(); + $(this.queryByHook('inference-pdf-plot-spinner')).css("display", "block"); + }else { + let interEL = this.queryByHook('round-intersection-plot'); + Plotly.purge(interEL); + $(this.queryByHook('round-intersection-plot')).empty(); + $(this.queryByHook('round-intersection-plot-spinner')).css("display", "block"); + if(!pdfOnly) { + $(this.queryByHook("round-model-export")).prop("disabled", true); + $(this.queryByHook("round-model-explore")).prop("disabled", true); + $(this.queryByHook("round-download")).prop("disabled", true); + $(this.queryByHook("round-edit-plot")).prop("disabled", true); + try { + histoEL.removeListener('plotly_click', _.bind(this.selectIntersection, this)); + }catch (err) { + //pass + } + } } }else { let el = this.queryByHook(`${type}-plot`); @@ -231,16 +253,24 @@ module.exports = View.extend({ } }, fixPlotSize: function (type, plotID) { + let figID = plotID === "histogram" ? plotID : "pdf" let plotEL = this.queryByHook(`${type}-${plotID}-plot`); // Clear plot Plotly.purge(plotEL); $(plotEL).empty(); // Re-render the plot - let figure = this.plots[this.activePlots[type]] - Plotly.newPlot(plotEL, figure[plotID]); + if(Object.keys(this.plots).includes(this.activePlots[type])) { + let figure = this.plots[this.activePlots[type]] + Plotly.newPlot(plotEL, figure[figID]); + if(type === "round" && plotID === "histogram") { + plotEL.on('plotly_click', _.bind(this.selectIntersection, this)); + } + return true; + } + return false; }, - getPlot: function (type) { - this.cleanupPlotContainer(type); + getPlot: function (type, {pdfOnly=false}={}) { + this.cleanupPlotContainer(type, {pdfOnly: pdfOnly}); let data = this.getPlotData(type); if(data === null) { return }; let storageKey = JSON.stringify(data); @@ -249,7 +279,7 @@ module.exports = View.extend({ let renderTypes = ['psweep', 'ts-psweep', 'ts-psweep-mp', 'mltplplt', 'spatial', 'round']; if(renderTypes.includes(type)) { this.activePlots[type] = storageKey; - this.plotFigure(this.plots[storageKey], type); + this.plotFigure(this.plots[storageKey], type, {pdfOnly: pdfOnly}); } }else{ let queryStr = `?path=${this.model.directory}&data=${JSON.stringify(data)}`; @@ -265,14 +295,14 @@ module.exports = View.extend({ font: {size: 16}, showarrow: false, text: "", textangle: -90, x: 0, xanchor: "right", xref: "paper", xshift: -40, y: 0.5, yanchor: "middle", yref: "paper" } - body.histogrom.layout.annotations.push(xLabel); - body.histogrom.layout.annotations.push(yLabel); + body.histogram.layout.annotations.push(xLabel); + body.histogram.layout.annotations.push(yLabel); body.pdf.layout.annotations.push(xLabel); body.pdf.layout.annotations.push(yLabel); } this.activePlots[type] = storageKey; this.plots[storageKey] = body; - this.plotFigure(body, type); + this.plotFigure(body, type, {pdfOnly: pdfOnly}); }, error: (err, response, body) => { if(type === "spatial") { @@ -321,7 +351,10 @@ module.exports = View.extend({ data['plt_key'] = type; }else if(["inference", "round"].includes(type)) { data['sim_type'] = "Inference"; - data['data_keys'] = {"epoch": type === "inference" ? null : this.roundIndex - 1}; + data['data_keys'] = { + "epoch": type === "inference" ? null : this.roundIndex - 1, + "names": type === "inference" ? null : this.intersectionNames + } data['plt_key'] = "inference"; }else { data['sim_type'] = "GillesPy2"; @@ -342,6 +375,8 @@ module.exports = View.extend({ }, getPlotForRound: function (e) { this.roundIndex = Number(e.target.value); + this.roundHistoResized = false; + this.intersectionResized = false; this.getPlot('round'); }, getPlotForFeatureExtractor: function (e) { @@ -482,12 +517,31 @@ module.exports = View.extend({ this.downloadCSV("full", null); } }, + handleInferenceResize: function (e) { + if(!this.inferenceResized) { + setTimeout(() => { + this.inferenceResized = this.fixPlotSize("inference", "histogram"); + }, 0) + } + }, + handleIntersectionResize: function (e) { + if(!this.intersectionResized) { + setTimeout(() => { + this.intersectionResized = this.fixPlotSize("round", "intersection"); + }, 0) + } + }, handlePDFResize: function (e) { if(!this.pdfResized) { setTimeout(() => { - console.log("Fixing plot size") - this.fixPlotSize("inference", "pdf"); - this.pdfResized = true; + this.pdfResized = this.fixPlotSize("inference", "pdf"); + }, 0) + } + }, + handleRoundResize: function (e) { + if(!this.roundHistoResized) { + setTimeout(() => { + this.roundHistoResized = this.fixPlotSize("round", "histogram"); }, 0) } }, @@ -572,27 +626,39 @@ module.exports = View.extend({ }, false); }); }, - plotFigure: function (figure, type) { + plotFigure: function (figure, type, {pdfOnly=false}={}) { if(["inference", "round"].includes(type)) { - // Display histogram plot let histoHook = `${type}-histogram-plot`; let histoEL = this.queryByHook(histoHook); - Plotly.newPlot(histoEL, figure.histogram); - $(this.queryByHook(`${type}-histogram-plot-spinner`)).css("display", "none"); + if(!pdfOnly) { + // Display histogram plot + Plotly.newPlot(histoEL, figure.histogram); + $(this.queryByHook(`${type}-histogram-plot-spinner`)).css("display", "none"); + $(this.queryByHook(`${type}-model-export`)).prop("disabled", false); + $(this.queryByHook(`${type}-model-explore`)).prop("disabled", false); + } // Display pdf plot - let pdfHook = `${type}-pdf-plot`; - let pdfEL = this.queryByHook(pdfHook); - Plotly.newPlot(pdfEL, figure.pdf); - $(this.queryByHook(`${type}-pdf-plot-spinner`)).css("display", "none"); - if(type === "round" && this.model.exportLinks[this.roundIndex] !== null) { - $(this.queryByHook("round-model-export")).text("Open Model"); - $(this.queryByHook("round-model-explore")).text("Explore Model"); + if(type === "inference") { + let pdfHook = 'inference-pdf-plot'; + let pdfEL = this.queryByHook(pdfHook); + Plotly.newPlot(pdfEL, figure.pdf); + $(this.queryByHook('inference-pdf-plot-spinner')).css("display", "none"); }else { - $(this.queryByHook("round-model-export")).text("Export Model"); - $(this.queryByHook("round-model-explore")).text("Export & Explore Model"); + let interHook = 'round-intersection-plot'; + let interEL = this.queryByHook(interHook); + Plotly.newPlot(interEL, figure.pdf); + $(this.queryByHook('round-intersection-plot-spinner')).css("display", "none"); + if(!pdfOnly) { + histoEL.on('plotly_click', _.bind(this.selectIntersection, this)); + if(this.model.exportLinks[this.roundIndex] !== null) { + $(this.queryByHook("round-model-export")).text("Open Model"); + $(this.queryByHook("round-model-explore")).text("Explore Model"); + }else { + $(this.queryByHook("round-model-export")).text("Export Model"); + $(this.queryByHook("round-model-explore")).text("Export & Explore Model"); + } + } } - $(this.queryByHook(`${type}-model-export`)).prop("disabled", false); - $(this.queryByHook(`${type}-model-explore`)).prop("disabled", false); }else { let hook = `${type}-plot`; let el = this.queryByHook(hook); @@ -605,8 +671,10 @@ module.exports = View.extend({ $(this.queryByHook("multiple-plots")).prop("disabled", false); } } - $(this.queryByHook(`${type}-edit-plot`)).prop("disabled", false); - $(this.queryByHook(`${type}-download`)).prop("disabled", false); + if(!pdfOnly) { + $(this.queryByHook(`${type}-edit-plot`)).prop("disabled", false); + $(this.queryByHook(`${type}-download`)).prop("disabled", false); + } }, plotMultiplePlots: function (e) { let type = e.target.dataset.type; @@ -730,6 +798,21 @@ module.exports = View.extend({ }); app.registerRenderSubview(this, this.targetModeView, "target-mode-list"); }, + selectIntersection: function (data) { + let subplot = data.event.target.dataset.subplot; + let subplotID = subplot === "xy" ? 0 : Number(subplot.split('y').pop()) - 1; + let parameters = this.model.settings.inferenceSettings.parameters + let col = subplotID % parameters.length; + let row = (subplotID - col) / parameters.length; + if(row < col) { + this.intersectionNames = [parameters.at(row).name, parameters.at(col).name]; + this.queryByHook('round-intersection-tab').click(); + setTimeout(() => { + this.getPlot("round", {pdfOnly: true}); + this.intersectionResized = false; + }, 0); + } + }, setTitle: function (e) { this.plotArgs['title'] = e.target.value for (var storageKey in this.plots) { diff --git a/stochss/handlers/util/model_inference.py b/stochss/handlers/util/model_inference.py index c110863b6d..fa056b9ac3 100644 --- a/stochss/handlers/util/model_inference.py +++ b/stochss/handlers/util/model_inference.py @@ -164,7 +164,7 @@ def __plotplotly(self, parameters, bounds, include_pdf=True, include_orig_values return fig - def __plotplotly_intersection(self, parameters, bounds, include_pdf=True, include_orig_values=True, + def __plotplotly_intersection(self, parameters, bounds, colors, include_pdf=True, include_orig_values=True, include_inferred_values=False, title=None, xaxis_label=None, yaxis_label=None): nbins = 50 names = list(parameters.keys()) @@ -194,10 +194,9 @@ def __plotplotly_intersection(self, parameters, bounds, include_pdf=True, includ if i >= 2: break - color = common_rgb_values[(i)%len(common_rgb_values)] # Create histogram traces histo_trace = plotly.graph_objs.Histogram( - name=param, legendgroup=param, showlegend=False, marker_color=color, opacity=0.75 + name=param, legendgroup=param, showlegend=False, marker_color=colors[i], opacity=0.75 ) histo_trace[x_key[i]] = self[param] histo_trace[bins[i]] = {"start": bounds[0][i], "end": bounds[1][i], "size": sizes[i]} @@ -208,7 +207,7 @@ def __plotplotly_intersection(self, parameters, bounds, include_pdf=True, includ [self[param]], [param], curve_type='normal', bin_size=sizes[i], histnorm="probability" ) histo_trace2 = plotly.graph_objs.Scatter( - mode='lines', line=dict(color=color), + mode='lines', line=dict(color=colors[i]), name=f"{param} PDF", legendgroup=f"{param} PDF", showlegend=False ) histo_trace2[x_key[i]] = tmp_fig.data[1]['x'] @@ -223,7 +222,7 @@ def __plotplotly_intersection(self, parameters, bounds, include_pdf=True, includ line_func[i](orig_val, row=histo_row[i], col=histo_col[i], line={"color": "red"}, layer='above') # Create rug traces rug_trace = plotly.graph_objs.Scatter( - mode='markers', marker={'color': color, 'symbol': rug_symbol[i]}, + mode='markers', marker={'color': colors[i], 'symbol': rug_symbol[i]}, name=param, legendgroup=param, showlegend=False ) rug_trace[x_key[i]] = self[param] @@ -232,11 +231,8 @@ def __plotplotly_intersection(self, parameters, bounds, include_pdf=True, includ xaxis_func[i](row=rug_row[i], col=rug_col[i], range=[bounds[0][i], bounds[1][i]]) yaxis_func[i](row=rug_row[i], col=rug_col[i], showticklabels=False) - color = combine_colors([ - common_rgb_values[(0)%len(common_rgb_values)][1:], common_rgb_values[(1)%len(common_rgb_values)][1:] - ]) trace = plotly.graph_objs.Scatter( - x=self[names[0]], y=self[names[1]], mode='markers', marker_color=color, + x=self[names[0]], y=self[names[1]], mode='markers', marker_color=colors[2], name=f"{names[0]} X {names[1]}", legendgroup=f"{names[0]} X {names[1]}", showlegend=False ) fig.append_trace(trace, 3, 1) @@ -322,7 +318,8 @@ def plot(self, parameters, bounds, use_matplotlib=False, return_plotly_figure=Fa plotly.offline.iplot(fig) return None - def plot_intersection(self, parameters, bounds, use_matplotlib=False, return_plotly_figure=False, **kwargs): + def plot_intersection(self, parameters, bounds, colors=None, color_ndxs=None, + use_matplotlib=False, return_plotly_figure=False, **kwargs): """ Plot the results of the inference round. @@ -332,6 +329,12 @@ def plot_intersection(self, parameters, bounds, use_matplotlib=False, return_plo :param bounds: List of bounds for the provided parameters. :type bounds: list + :param colors: List of three colors. + :type colors: list + + :param color_ndxs: List of two color indicies. Ignored if colors is set. + :type color_ndxs: list + :param use_matplotlib: Whether or not to plot using MatPlotLib. :type use_matplotlib: bool @@ -361,7 +364,20 @@ def plot_intersection(self, parameters, bounds, use_matplotlib=False, return_plo """ if use_matplotlib: raise Exception("use_matplotlib has not been implemented.") - fig = self.__plotplotly_intersection(parameters, bounds, **kwargs) + if colors is None: + if color_ndxs is None: + colors = [ + common_rgb_values[(0)%len(common_rgb_values)], + common_rgb_values[(1)%len(common_rgb_values)] + ] + else: + colors = [ + common_rgb_values[(color_ndxs[0])%len(common_rgb_values)], + common_rgb_values[(color_ndxs[1])%len(common_rgb_values)] + ] + colors.append(combine_colors([colors[0][1:], colors[1][1:]])) + + fig = self.__plotplotly_intersection(parameters, bounds, colors, **kwargs) if return_plotly_figure: return fig @@ -614,6 +630,9 @@ def plot_round_intersection(self, ndx=None, names=None, **kwargs): if names is None: names = param_names[:2] + if not ("colors" in kwargs or "color_ndxs" in kwargs): + kwargs['color_ndxs'] = [param_names.index(names[1]), param_names.index(names[0])] + inf_round = self.data[ndx] bounds = [ [self.bounds[0][param_names.index(names[1])], self.bounds[0][param_names.index(names[0])]], @@ -829,7 +848,7 @@ def get_result_plot(self, epoch=None, names=None, add_config=False, **kwargs): fig_obj, fig_obj2 = results.plot(histo_only=False, return_plotly_figure=True, **kwargs) else: fig_obj = results.plot_round(ndx=epoch, return_plotly_figure=True, include_inferred_values=True, **kwargs) - fig_obj2 = results.plot_round( + fig_obj2 = results.plot_round_intersection( ndx=epoch, names=names, return_plotly_figure=True, include_inferred_values=True, **kwargs ) From 4b10e6f0d07147009728bf3ef6f6cdc4256afd80 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Fri, 31 Mar 2023 09:24:37 -0400 Subject: [PATCH 071/109] Fixed issue with loading running inference jobs. --- stochss/handlers/util/model_inference.py | 5 ++++- stochss/handlers/util/stochss_job.py | 8 ++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/stochss/handlers/util/model_inference.py b/stochss/handlers/util/model_inference.py index fa056b9ac3..08384c5926 100644 --- a/stochss/handlers/util/model_inference.py +++ b/stochss/handlers/util/model_inference.py @@ -29,6 +29,7 @@ import numpy import plotly from plotly import figure_factory, subplots +from dask.distributed import Client import gillespy2 @@ -717,7 +718,7 @@ def __get_csvzip(cls, dirname, name): def __get_infer_args(self): settings = self.settings['inferenceSettings'] eps_selector = RelativeEpsilonSelector(20, max_rounds=settings['numRounds']) - args = [settings['num_samples'], settings['batchSize']] + args = [settings['numSamples'], settings['batchSize']] kwargs = {"eps_selector": eps_selector, "chunk_size": settings['chunkSize']} return args, kwargs @@ -896,6 +897,7 @@ def run(self, verbose=True): summaries = self.__get_summaries_function() if verbose: log.info("Running the model inference") + dask_client = Client() smc_abc_inference = smc_abc.SMCABC( obs_data, sim=self.simulator, prior_function=prior, summaries_function=summaries.compute ) @@ -912,6 +914,7 @@ def run(self, verbose=True): export_links = {i + 1: None for i in range(len(results))} with open("export-links.json", "w", encoding="utf-8") as elfd: json.dump(export_links, elfd, indent=4, sort_keys=True) + dask_client.close() def simulator(self, parameter_point): """ Wrapper function for inference simulations. """ diff --git a/stochss/handlers/util/stochss_job.py b/stochss/handlers/util/stochss_job.py index 13245c9a7c..6ba2add76c 100644 --- a/stochss/handlers/util/stochss_job.py +++ b/stochss/handlers/util/stochss_job.py @@ -732,8 +732,12 @@ def load(self, new=False): "type":self.type, "directory":self.path, "logs":logs} if self.type == "inference": el_path = os.path.join(self.get_path(full=True), "export-links.json") - with open(el_path, "r", encoding="utf-8") as elfd: - self.job['exportLinks'] = json.load(elfd) + try: + with open(el_path, "r", encoding="utf-8") as elfd: + self.job['exportLinks'] = json.load(elfd) + except FileNotFoundError: + num_params = len(settings['inferenceSettings']['parameters']) + self.job['exportLinks'] = {i + 1: None for i in range(num_params)} for ndx, link in self.job['exportLinks'].items(): if link is not None: From 03761f4c4e7441ac28f38d0208adbfde5c5e2a1b Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Sat, 1 Apr 2023 08:22:26 -0400 Subject: [PATCH 072/109] Updated notebook template for model inference jobs. --- stochss/handlers/util/sciope_notebook.py | 317 ++++++++++++++++------- 1 file changed, 223 insertions(+), 94 deletions(-) diff --git a/stochss/handlers/util/sciope_notebook.py b/stochss/handlers/util/sciope_notebook.py index 63ec8d093a..216cdea14f 100644 --- a/stochss/handlers/util/sciope_notebook.py +++ b/stochss/handlers/util/sciope_notebook.py @@ -15,7 +15,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' - +import json from nbformat import v4 as nbf from .stochss_notebook import StochSSNotebook @@ -36,22 +36,31 @@ 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_abc_cells(self, cells): + def __create_infer_cells(self, cells, index, results=None): pad = ' ' - cell1 = [ - "abc = ABC(", f"{pad}fixed_data, sim=simulator2, prior_function=uni_prior,", - f"{pad}summaries_function=summ_func.compute, distance_function=ns", ")" + samples = self.settings['inferenceSettings']['numSamples'] + b_size = self.settings['inferenceSettings']['batchSize'] + c_size = self.settings['inferenceSettings']['chunkSize'] + infer_lines = [ + "smc_abc_results = smc_abc_inference.infer(", + f"{pad}num_samples={samples}, batch_size={b_size}, eps_selector=eps_selector, chunk_size={c_size}", ")", + "parameters = {}", "for i, name in enumerate(parameter_names):", f"{pad}parameters[name] = values[i]", + "results = InferenceResults.build_from_inference_results(", + f"{pad}smc_abc_results, parameters, [lower_bounds, upper_bounds]", ")" ] - 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'])" - )) + if results is not None: + for i in range(3): + infer_lines[i] = f"# {infer_lines[i]}" + load_lines = "\n".join([ + f"path = '{results}'", "with open(os.path.join(os.path.expanduser('~'), path), 'rb') as results_file:", + f"{pad}smc_abc_results = pickle.load(results_file)" + ]) + infer_lines.insert(3, load_lines) + cells.insert(index, nbf.new_code_cell("\n".join(infer_lines))) + return index + 2 - def __create_explore_cells(self, cells): + @classmethod + def __create_explore_cells(cls, cells): cell1 = [ "met.data.configurations['listOfParameters'] = list(model.listOfParameters.keys())", "met.data.configurations['listOfSpecies'] = list(model.listOfSpecies.keys())", @@ -75,18 +84,16 @@ def __create_explore_cells(self, cells): 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"] + def __create_import_cells(self, cells, results=None): + base_imports = ["import csv", "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 = [ + "import sciope", + "from sciope.inference import smc_abc", "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" + "from sciope.utilities.summarystats import auto_tsfresh, identity", + "from sciope.utilities.epsilonselectors import RelativeEpsilonSelector", + "from stochss.handlers.util.model_inference import InferenceResults" ] else: sciope_imports = [ @@ -98,8 +105,12 @@ def __create_import_cells(self, cells): ] cells.insert(1, nbf.new_code_cell("\n".join(base_imports))) cells.insert(3, nbf.new_code_cell("\n".join(sciope_imports))) + if results is not None: + result_imports = ["import os", "import pickle"] + cells.insert(1, nbf.new_code_cell("\n".join(result_imports))) - def __create_me_header_cells(self, cells): + @classmethod + def __create_me_header_cells(cls, cells): header1 = "***\n## Model Exploration\n***\n" + \ "### Define simulator function (using gillespy2 wrapper)" header6 = "***\n## Explore the result\n***\n" + \ @@ -119,72 +130,163 @@ def __create_me_header_cells(self, cells): 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" + @classmethod + def __create_mi_header_cells(cls, cells): + obsd_header = nbf.new_markdown_cell("\n".join([ + "***", "## Observed (fixed) Data", "***", + "Sciope assumes the format of a time series to be a matrix of the form,", "$N \times S \times T$,", + "where $N$ is the number of trajectories, $S$ is the number of species and $T$ is the number of time " + + "points or intervals.", "", "Steps:", + "- load some observed (fixed) data based on default parameters of model.", "- Reshape data to (N x S x T)", + "- Remove timepoints array", "- Verify the shape of the time series" + ])) + sf_header = nbf.new_markdown_cell("***\n## Simulator Function\n***") + p_header = nbf.new_markdown_cell("\n".join([ + "***", "## Priors", "***", "Steps:", "- Get the current values", "- Calculate the bounds", + "- Create the prior function. Here we use uniform prior" + ])) + ss_header = nbf.new_markdown_cell("\n".join([ + "***", "## Summary Statistics", "***", "Function to generate summary statistics" + ])) + es_header = nbf.new_markdown_cell("***\n## Epsilon Selector\n***") + inf_header = nbf.new_markdown_cell("\n".join([ + "***", "## SMC-ABC Inference", "***", "### Start local cluster using dask client" + ])) + run_header = nbf.new_markdown_cell("***\n## Run the ABC Inference\n***") + vis_header = nbf.new_markdown_cell("***\n## Visualization\n***") + c_header = nbf.new_markdown_cell("***\n## Close Dask Client\n***") 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") + obsd_header, sf_header, p_header, ss_header, es_header, inf_header, run_header, vis_header, c_header ]) - 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:, :]" + def __create_observed_data(self, cells, results=None): + pad = " " + start = 11 if results is None else 12 + load_cell = nbf.new_code_cell("\n".join([ + "def load_obs_data(path=None, data=None):", + f"{pad}if not (path.endswith('.csv') or path.endswith('.obsd')):", + f"{pad*2}raise ValueError('Observed data must be a CSV file (.csv) or a directory (.obsd) of CSV files.')", + f"{pad}if path.endswith('.csv'):", f"{pad*2}new_data = get_csv_data(path)", f"{pad*2}data.append(new_data)", + f"{pad*2}return data", f"{pad}for file in os.listdir(path):", + f"{pad*2}data = load_obs_data(path=os.path.join(path, file), data=data)", f"{pad}return data" + ])) + csv_cell = nbf.new_code_cell("\n".join([ + "def get_csv_data(path):", f"{pad}with open(path, 'r', newline='', encoding='utf-8') as csv_fd:", + f"{pad*2}csv_reader = csv.reader(csv_fd, delimiter=',')", f"{pad*2}rows = []", + f"{pad*2}for i, row in enumerate(csv_reader):", f"{pad*3}if i != 0:", f"{pad*4}rows.append(row)", + f"{pad*2}data = numpy.array(rows).swapaxes(0, 1).astype('float')", f"{pad}return data" + ])) + cells.insert(start, csv_cell) + cells.insert(start + 1, load_cell) + obsd_lines = [ + "kwargs = configure_simulation()", "results = model.run(**kwargs)", "", + "unshaped_obs_data = results.to_array()", "shaped_obs_data = unshaped_obs_data.swapaxes(1, 2)", + "obs_data = shaped_obs_data[:,1:, :]", "print(obs_data.shape)", "", + "obs_data = load_obs_data(", + f"{pad}path='../{self.get_file(self.settings['inferenceSettings']['obsData'])}', data=[]", ")" ] - 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))) + if self.settings['inferenceSettings']['obsData'] == "": + for i in range(len(obsd_lines) - 3, len(obsd_lines)): + obsd_lines[i] = f"# {obsd_lines[i]}" + else: + for i in range(len(obsd_lines) - 3): + if obsd_lines[i] != "": + obsd_lines[i] = f"# {obsd_lines[i]}" + obsd_cell = nbf.new_code_cell("\n".join(obsd_lines)) + cells.insert(start + 2, obsd_cell) + return start + 4 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)" - ] + pad = ' ' + if len(self.settings['inferenceSettings']['parameters']) > 0: + v_lines = ["values = numpy.array([", "])"] + n_lines = ["parameter_names = [", "]"] + l_lines = ["lower_bounds = numpy.array([", "])"] + m_lines = ["upper_bounds = numpy.array([", "])"] + values = names = f"{pad}" + mins = maxs = f"{pad}" + for param in self.settings['inferenceSettings']['parameters']: + values = f"{values}{self.model.listOfParameters[param['name']].value}, " + names = f"{names}'{param['name']}', " + mins = f"{mins}{param['min']}, " + maxs = f"{maxs}{param['max']}, " + if len(values) > 100: + v_lines.insert(-1, values[:-1]) + values = f"{pad}" + if len(names) > 100: + n_lines.insert(-1, names[:-1]) + names = f"{pad}" + if len(mins) > 100: + l_lines.insert(-1, mins[:-1]) + mins = f"{pad}" + if len(maxs) > 100: + m_lines.insert(-1, maxs[:-1]) + maxs = f"{pad}" + v_lines.insert(-1, values[:-2]) + n_lines.insert(-1, names[:-2]) + l_lines.insert(-1, mins[:-2]) + m_lines.insert(-1, maxs[:-2]) + nb_prior = [ + "\n".join(v_lines), "\n".join(n_lines), "", "\n".join(l_lines), "\n".join(m_lines), "", + ] + else: + nb_prior = [ + "values = numpy.array([param.expression for param in model.listOfParameters.values()], dtype='float')", + "parameter_names = [param.name for param in model.listOfParameters.values()]", "", + "lower_bounds = values * 0.1", "upper_bounds = values * 2.0", "" + ] + nb_prior.append("uni_prior = uniform_prior.UniformPrior(lower_bounds, upper_bounds)") return nbf.new_code_cell("\n".join(nb_prior)) - def __create_simulator_cells(self, cells): + def __create_simulator_cells(self, cells, index): 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" + proc_lines = [ + "def process(raw_results):" ] - 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)" - )) + if self.settings['inferenceSettings']['summaryStatsType'] == "identity": + ident_lines = [ + f"{pad}definitions = dict(", f"{pad*2}time='time'", f"{pad})", "", f"{pad}trajectories = []", + f"{pad}for result in raw_results:", f"{pad*2}evaluations = dict()", + f"{pad*2}for label, formula in definitions.items():", + f"{pad*3}evaluations[label] = eval(formula, dict(), result.data)", + f"{pad*2}trajectories.append(gillespy2.Trajectory(", + f"{pad*3}data=evaluations, model=result.model, solver_name=result.solver_name, rc=result.rc", + f"{pad*2}))", f"{pad}processed_results = gillespy2.Results([evaluations])", + f"{pad}return processed_results.to_array().swapaxes(1, 2)[:,1:, :]" + ] + lines = [] + for feature_calculator in self.settings['inferenceSettings']['summaryStats']: + line = f"{pad*2}{feature_calculator['name']}='{feature_calculator['formula']}'," + ident_lines.insert(2, "\n".join(lines)[:-1]) + proc_lines.extend(ident_lines) + else: + proc_lines.append(f"{pad}return raw_results.to_array().swapaxes(1, 2)[:,1:, :]") + proc_cell = nbf.new_code_cell("\n".join(proc_lines)) + cells.insert(index, proc_cell) + if len(self.settings['inferenceSettings']['parameters']) > 0: + lines = [f"{pad}labels = [", f"{pad}]"] + line = f"{pad*2}" + for param in self.settings['inferenceSettings']['parameters']: + line = f"{line}'{param['name']}', " + if len(line) > 98: + lines.insert(-1, line[:-1]) + line = f"{pad*2}" + lines.insert(-1, line[:-2]) + labels = "\n".join(lines) + else: + labels = f"{pad}labels = list(map(lambda param: param.name, model.listOfParameters.values()))" + sim_cell = nbf.new_code_cell("\n".join([ + "def simulator(parameter_point):", f"{pad}model = {self.get_function_name()}()", "", labels, + f"{pad}for ndx, parameter in enumerate(parameter_point):", + f"{pad*2}model.listOfParameters[labels[ndx]].expression = str(parameter)", "", + f"{pad}kwargs = configure_simulation()", f"{pad}raw_results = model.run(**kwargs)", + "", f"{pad}return process(raw_results)" + ])) + cells.insert(index + 1, sim_cell) + return index + 3 - def __create_simulator_func(self): + @classmethod + def __create_simulator_func(cls): nb_sim_func = [ "settings = configure_simulation()", "simulator = wrapper.get_simulator(", " gillespy_model=model, run_settings=settings, species_of_interest=['U', 'V']", @@ -193,14 +295,35 @@ def __create_simulator_func(self): 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()" - ] + if self.nb_type == self.MODEL_INFERENCE: + pad = ' ' + summary_type = self.settings['inferenceSettings']['summaryStatsType'] + if summary_type == "identity": + nb_sum_stats = ['summ_func = identity.Identity()'] + elif summary_type == "minimal" and len(self.settings['inferenceSettings']['summaryStats']) == 8: + nb_sum_stats = ["summ_func = auto_tsfresh.SummariesTSFRESH()"] + else: + nb_sum_stats = ["summ_func = auto_tsfresh.SummariesTSFRESH(features={", "})"] + lines = [] + for feature_calculator in self.settings['inferenceSettings']['summaryStats']: + args = "None" if feature_calculator['args'] is None else json.dumps(feature_calculator['args']) + lines.append(f"{pad}'{feature_calculator['name']}': {args},") + nb_sum_stats.insert(-1, "\n".join(lines)[:-1]) + else: + 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_visualization_cells(self, cells, index): + cells.insert(index, nbf.new_code_cell("results.plot()")) + cells.insert(index + 1, nbf.new_code_cell( + f"results.plot_round(ndx={self.settings['inferenceSettings']['numRounds'] - 1})" + )) + 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 @@ -228,19 +351,25 @@ 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_import_cells(cells, results=results) self.__create_mi_header_cells(cells) self.settings['solver'] = self.get_gillespy2_solver_name() - 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()" + index = self.__create_observed_data(cells, results=results) + index = self.__create_simulator_cells(cells, index) + cells.insert(index, self.__create_prior()) + cells.insert(index + 2, self.__create_summary_stats()) + cells.insert(index + 4, nbf.new_code_cell( + f"eps_selector = RelativeEpsilonSelector(20, max_rounds={self.settings['inferenceSettings']['numRounds']})" )) - cells.insert(25, nbf.new_code_cell("c = Client()\nc")) - self.__create_abc_cells(cells) + cells.insert(index + 6, nbf.new_code_cell("c = Client()\nc")) + cells.insert(index + 7, nbf.new_code_cell("\n".join([ + "smc_abc_inference = smc_abc.SMCABC(", + " obs_data, sim=simulator, prior_function=uni_prior, summaries_function=summ_func.compute", ")" + ]))) + index = self.__create_infer_cells(cells, index + 9, results) + self.__create_visualization_cells(cells, index) + cells.append(nbf.new_code_cell("# c.close()")) if compute != "StochSS": self.log( "warning", From db6c954417a38d34ad0c09982d58029c87b04614 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Mon, 3 Apr 2023 13:57:05 -0400 Subject: [PATCH 073/109] Changed the color and opacity for current and inferred vlines to 'black' and '0.75'. Changed the inferred vline to a dashed line. --- stochss/handlers/util/model_inference.py | 31 +++++++++++++++--------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/stochss/handlers/util/model_inference.py b/stochss/handlers/util/model_inference.py index 08384c5926..55d6512e6c 100644 --- a/stochss/handlers/util/model_inference.py +++ b/stochss/handlers/util/model_inference.py @@ -140,11 +140,13 @@ def __plotplotly(self, parameters, bounds, include_pdf=True, include_orig_values fig.append_trace(trace2, row, col) if include_inferred_values: fig.add_vline( - self.inferred_parameters[param1], row=row, col=col, line={"color": "green"}, - exclude_empty_subplots=True, layer='above' + self.inferred_parameters[param1], row=row, col=col, exclude_empty_subplots=True, + layer='above', opacity=0.75, line={"color": "black", "dash": "dash"} ) if include_orig_values: - fig.add_vline(parameters[param1], row=row, col=col, line={"color": "red"}, layer='above') + fig.add_vline( + parameters[param1], row=row, col=col, layer='above', opacity=0.75, line={"color": "black"} + ) else: color = combine_colors([ common_rgb_values[(i)%len(common_rgb_values)][1:], @@ -216,11 +218,14 @@ def __plotplotly_intersection(self, parameters, bounds, colors, include_pdf=True fig.append_trace(histo_trace2, histo_row[i], histo_col[i]) if include_inferred_values: line_func[i]( - self.inferred_parameters[param], row=histo_row[i], col=histo_col[i], line={"color": "green"}, - exclude_empty_subplots=True, layer='above' + self.inferred_parameters[param], row=histo_row[i], col=histo_col[i], exclude_empty_subplots=True, + layer='above', opacity=0.75, line={"color": "black", "dash": "dash"} + ) if include_orig_values: - line_func[i](orig_val, row=histo_row[i], col=histo_col[i], line={"color": "red"}, layer='above') + line_func[i]( + orig_val, row=histo_row[i], col=histo_col[i], layer='above', opacity=0.75, line={"color": "black"} + ) # Create rug traces rug_trace = plotly.graph_objs.Scatter( mode='markers', marker={'color': colors[i], 'symbol': rug_symbol[i]}, @@ -485,19 +490,21 @@ def __plotplotly(self, include_orig_values=True, include_inferred_values=False, if i == len(self.data) - 1: if include_orig_values: histo_fig.add_vline( - self.parameters[param], row=row, col=col, line={"color": "red"}, layer='above' + self.parameters[param], row=row, col=col, layer='above', opacity=0.75, + line={"color": "black"} ) pdf_fig.add_vline( - self.parameters[param], row=row, col=col, line={"color": "red"}, layer='above' + self.parameters[param], row=row, col=col, layer='above', opacity=0.75, + line={"color": "black"} ) if include_inferred_values: histo_fig.add_vline( - inf_round.inferred_parameters[param], row=row, col=col, line={"color": "green"}, - exclude_empty_subplots=True, layer='above' + inf_round.inferred_parameters[param], row=row, col=col, exclude_empty_subplots=True, + layer='above', opacity=0.75, line={"color": "black", "dash": "dash"} ) pdf_fig.add_vline( - inf_round.inferred_parameters[param], row=row, col=col, line={"color": "green"}, - exclude_empty_subplots=True, layer='above' + inf_round.inferred_parameters[param], row=row, col=col, exclude_empty_subplots=True, + layer='above', opacity=0.75, line={"color": "black", "dash": "dash"} ) height = 500 * rows From 74bead9378af9feff304c2a651b367c78a331e27 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Mon, 3 Apr 2023 14:21:59 -0400 Subject: [PATCH 074/109] Fixed issue with downloading .png files. --- client/job-view/views/job-results-view.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/client/job-view/views/job-results-view.js b/client/job-view/views/job-results-view.js index 8504e371ae..c5a0e01e55 100644 --- a/client/job-view/views/job-results-view.js +++ b/client/job-view/views/job-results-view.js @@ -499,7 +499,19 @@ module.exports = View.extend({ }, handleDownloadPNGClick: function (e) { let type = e.target.dataset.type; - let pngButton = $('div[data-hook=' + type + '-plot] a[data-title*="Download plot as a png"]')[0]; + if(["inference", "round"].includes(type)) { + if(type === "inference") { + let classList = this.queryByHook("inference-histogram-tab").classList.value.split(" "); + var key = classList.includes("active") ? "histogram" : "pdf"; + }else{ + let classList = this.queryByHook("round-histogram-tab").classList.value.split(" "); + var key = classList.includes("active") ? "histogram" : "intersection"; + } + var divEL = `div[data-hook=${type}-${key}-plot]`; + }else{ + var divEL = `div[data-hook=${type}-plot]`; + } + let pngButton = $(`${divEL} a[data-title*="Download plot as a png"]`)[0]; pngButton.click(); }, handleExploreInferredModel: function (e) { From 4b399058d0690a491e930d8a2ec1887224e17fea Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Mon, 3 Apr 2023 15:32:11 -0400 Subject: [PATCH 075/109] Fixed issue with extra content in .json download. --- client/job-view/views/job-results-view.js | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/client/job-view/views/job-results-view.js b/client/job-view/views/job-results-view.js index c5a0e01e55..821cc98db1 100644 --- a/client/job-view/views/job-results-view.js +++ b/client/job-view/views/job-results-view.js @@ -484,12 +484,27 @@ module.exports = View.extend({ }, handleDownloadJSONClick: function (e) { let type = e.target.dataset.type; - let storageKey = JSON.stringify(this.getPlotData(type)) - let jsonData = this.plots[storageKey]; + let storageKey = JSON.stringify(this.getPlotData(type)); + if(["inference", "round"].includes(type)) { + if(type === "inference") { + let classList = this.queryByHook("inference-histogram-tab").classList.value.split(" "); + var key = classList.includes("active") ? "histogram" : "pdf"; + var nameBase = `${type}-${key}`; + }else{ + let classList = this.queryByHook("round-histogram-tab").classList.value.split(" "); + var key = classList.includes("active") ? "histogram" : "pdf"; + let pltKey = key === "pdf" ? `${this.intersectionNames.join("-X-")}-intersection` : key; + var nameBase = `${type}${this.roundIndex}-${pltKey}`; + } + var jsonData = this.plots[storageKey][key]; + }else if(type === "spatial") { + var nameBase = `${type}${this.trajectoryIndex}` + }else{ + var jsonData = this.plots[storageKey]; + var nameBase = type + } let dataStr = JSON.stringify(jsonData); let dataURI = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr); - let nameIndex = type === "round" ? this.roundIndex : this.trajectoryIndex; - let nameBase = ["spatial", "round"].includes(type) ? `${type}${nameIndex}` : type; let exportFileDefaultName = `${nameBase}-plot.json`; let linkElement = document.createElement('a'); From d9b208c3b4ffd77971aedf2b1a3e3db08ee1e067 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Mon, 3 Apr 2023 16:10:10 -0400 Subject: [PATCH 076/109] Updated the plot titles. --- stochss/handlers/util/model_inference.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/stochss/handlers/util/model_inference.py b/stochss/handlers/util/model_inference.py index 55d6512e6c..5ee4dc8bf8 100644 --- a/stochss/handlers/util/model_inference.py +++ b/stochss/handlers/util/model_inference.py @@ -447,15 +447,22 @@ def __init__(self, data, parameters, bounds): self.bounds = bounds def __plotplotly(self, include_orig_values=True, include_inferred_values=False, - title=None, xaxis_label="Values", yaxis_label="Sample Concentrations"): + title=None, xaxis_label="Parameter Values", yaxis_label=None): + if yaxis_label is None: + yaxis_label = {"histo": "Accepted Samples", "pdf": "Probability"} + elif isinstance(yaxis_label, str): + yaxis_label = {"histo": yaxis_label, "pdf": yaxis_label} + cols = 2 rows = int(numpy.ceil(len(self.parameters)/cols)) names = list(self.parameters.keys()) histo_fig = subplots.make_subplots( - rows=rows, cols=cols, subplot_titles=names, x_title=xaxis_label, y_title=yaxis_label, vertical_spacing=0.075 + rows=rows, cols=cols, subplot_titles=names, vertical_spacing=0.075, + x_title=xaxis_label, y_title=yaxis_label['histo'] ) pdf_fig = subplots.make_subplots( - rows=rows, cols=cols, subplot_titles=names, x_title=xaxis_label, y_title=yaxis_label, vertical_spacing=0.075 + rows=rows, cols=cols, subplot_titles=names, vertical_spacing=0.075, + x_title=xaxis_label, y_title=yaxis_label['pdf'] ) nbins = 50 @@ -570,8 +577,9 @@ def plot(self, histo_only=True, pdf_only=False, use_matplotlib=False, return_plo :param xaxis_label: The label for the x-axis :type xaxis_label: str - :param yaxis_label: The label for the y-axis - :type yaxis_label: str + :param yaxis_label: The label for the y-axis. Dictionaries should be in + the following format {'histo':<