diff --git a/__version__.py b/__version__.py
index 445149aff..15e8610eb 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.'
diff --git a/client/app.js b/client/app.js
index f734e9b2d..6935bebc8 100644
--- a/client/app.js
+++ b/client/app.js
@@ -79,25 +79,6 @@ let changeCollapseButtonText = (view, e) => {
text === '+' ? collapseBtn.text('-') : collapseBtn.text('+');
}
}
-let copyToClipboard = (text, success, error) => {
- fullURL = window.location.protocol + '//' + window.location.hostname + text;
- if (window.clipboardData && window.clipboardData.setData) {
- // Internet Explorer-specific code path to prevent textarea being shown while dialog is visible.
- return window.clipboardData.setData("Text", fullURL);
- }
- else {
- navigator.clipboard.writeText(fullURL).then(success, error);
- }
-}
-let documentSetup = () => {
- tooltipSetup();
- $(document).on('shown.bs.modal', (e) => {
- $('[autofocus]', e.target).focus();
- });
- $(document).on('hide.bs.modal', '.modal', (e) => {
- e.target.remove();
- });
-}
let getApiPath = () => path.join(getBasePath(), apiPrefix);
let getBasePath = () => {
try {
@@ -133,6 +114,112 @@ let getXHR = (endpoint, {
error(exception, response, body);
}
}
+
+let validateName = (input, {rename=false, saveAs=true}={}) => {
+ var error = "";
+ if(input.endsWith('/')) {
+ error = 'forward';
+ }
+ var invalidChars = "`~!@#$%^&*=+[{]}\"|:;'<,>?\\";
+ if(rename || !saveAs) {
+ invalidChars += "/";
+ }
+ for(var i = 0; i < input.length; i++) {
+ if(invalidChars.includes(input.charAt(i))) {
+ error = error === "" || error === "special" ? "special" : "both";
+ }
+ }
+ return error;
+}
+
+let newWorkflow = (parent, mdlPath, isSpatial, type) => {
+ if(document.querySelector('#newWorkflowModal')) {
+ document.querySelector('#newWorkflowModal').remove()
+ }
+ let typeCodes = {
+ "Ensemble Simulation": "_ES",
+ "Spatial Ensemble Simulation": "_SES",
+ "Parameter Sweep": "_PS",
+ "Model Inference": "_MI"
+ }
+ let ext = isSpatial ? /.smdl/g : /.mdl/g
+ let typeCode = typeCodes[type];
+ let name = mdlPath.split('/').pop().replace(ext, typeCode)
+ let modal = $(modals.createWorkflowHtml(name, type)).modal();
+ let okBtn = document.querySelector('#newWorkflowModal .ok-model-btn');
+ let input = document.querySelector('#newWorkflowModal #workflowNameInput');
+ okBtn.disabled = false;
+ input.addEventListener("keyup", (event) => {
+ if(event.keyCode === 13){
+ event.preventDefault();
+ okBtn.click();
+ }
+ });
+ input.addEventListener("input", (e) => {
+ let endErrMsg = document.querySelector('#newWorkflowModal #workflowNameInputEndCharError')
+ let charErrMsg = document.querySelector('#newWorkflowModal #workflowNameInputSpecCharError')
+ let error = validateName(input.value)
+ okBtn.disabled = error !== "" || input.value.trim() === ""
+ charErrMsg.style.display = error === "both" || error === "special" ? "block" : "none"
+ endErrMsg.style.display = error === "both" || error === "forward" ? "block" : "none"
+ });
+ okBtn.addEventListener('click', (e) => {
+ modal.modal("hide");
+ let wkflFile = `${input.value.trim()}.wkfl`;
+ if(mdlPath.includes(".proj") && !mdlPath.includes(".wkgp")){
+ var wkflPath = path.join(path.dirname(mdlPath), "WorkflowGroup1.wkgp", wkflFile);
+ }else{
+ var wkflPath = path.join(path.dirname(mdlPath), wkflFile);
+ }
+ let queryString = `?path=${wkflPath}&model=${mdlPath}&type=${type}`;
+ let endpoint = path.join(getApiPath(), "workflow/new") + queryString;
+ getXHR(endpoint, {
+ success: (err, response, body) => {
+ window.location.href = `${path.join(getBasePath(), "stochss/workflow/edit")}?path=${body.path}`;
+ }
+ });
+ });
+}
+
+tooltipSetup = () => {
+ $(function () {
+ $('[data-toggle="tooltip"]').tooltip();
+ $('[data-toggle="tooltip"]').on('click ', function () {
+ $('[data-toggle="tooltip"]').tooltip("hide");
+ });
+ });
+}
+
+documentSetup = () => {
+ tooltipSetup();
+ $(document).on('shown.bs.modal', function (e) {
+ $('[autofocus]', e.target).focus();
+ });
+ $(document).on('hide.bs.modal', '.modal', function (e) {
+ e.target.remove();
+ });
+}
+
+copyToClipboard = (text, success, error) => {
+ fullURL = window.location.protocol + '//' + window.location.hostname + text;
+ if (window.clipboardData && window.clipboardData.setData) {
+ // Internet Explorer-specific code path to prevent textarea being shown while dialog is visible.
+ return window.clipboardData.setData("Text", fullURL);
+ }
+ else {
+ navigator.clipboard.writeText(fullURL).then(success, error)
+ }
+}
+
+let switchToEditTab = (view, section) => {
+ let elementID = Boolean(view.model && view.model.elementID) ? view.model.elementID + "-" : "";
+ if($(view.queryByHook(elementID + 'view-' + section)).hasClass('active')) {
+ $(view.queryByHook(elementID + section + '-edit-tab')).tab('show');
+ $(view.queryByHook(elementID + 'edit-' + section)).addClass('active');
+ $(view.queryByHook(elementID + 'view-' + section)).removeClass('active');
+ }
+}
+
let maintenance = (view) => {
getXHR("stochss/api/message", {
always: (err, response, body) => {
@@ -186,53 +273,6 @@ let maintenance = (view) => {
}
});
}
-let newWorkflow = (parent, mdlPath, isSpatial, type) => {
- if(document.querySelector('#newWorkflowModal')) {
- document.querySelector('#newWorkflowModal').remove();
- }
- let typeCodes = {
- "Ensemble Simulation": "_ES",
- "Spatial Ensemble Simulation": "_SES",
- "Parameter Sweep": "_PS"
- }
- let ext = isSpatial ? /.smdl/g : /.mdl/g;
- let typeCode = typeCodes[type];
- let name = mdlPath.split('/').pop().replace(ext, typeCode);
- let modal = $(modals.createWorkflowHtml(name, type)).modal();
- let okBtn = document.querySelector('#newWorkflowModal .ok-model-btn');
- let input = document.querySelector('#newWorkflowModal #workflowNameInput');
- okBtn.disabled = false;
- input.addEventListener("keyup", (event) => {
- if(event.keyCode === 13){
- event.preventDefault();
- okBtn.click();
- }
- });
- input.addEventListener("input", (e) => {
- let endErrMsg = document.querySelector('#newWorkflowModal #workflowNameInputEndCharError');
- let charErrMsg = document.querySelector('#newWorkflowModal #workflowNameInputSpecCharError');
- let error = validateName(input.value);
- okBtn.disabled = error !== "" || input.value.trim() === "";
- charErrMsg.style.display = error === "both" || error === "special" ? "block" : "none";
- endErrMsg.style.display = error === "both" || error === "forward" ? "block" : "none";
- });
- okBtn.addEventListener('click', (e) => {
- modal.modal("hide");
- let wkflFile = `${input.value.trim()}.wkfl`;
- if(mdlPath.includes(".proj") && !mdlPath.includes(".wkgp")){
- var wkflPath = path.join(path.dirname(mdlPath), "WorkflowGroup1.wkgp", wkflFile);
- }else{
- var wkflPath = path.join(path.dirname(mdlPath), wkflFile);
- }
- let queryString = `?path=${wkflPath}&model=${mdlPath}&type=${type}`;
- let endpoint = path.join(getApiPath(), "workflow/new") + queryString;
- getXHR(endpoint, {
- success: (err, response, body) => {
- window.location.href = `${path.join(getBasePath(), "stochss/workflow/edit")}?path=${body.path}`;
- }
- });
- });
-}
let postXHR = (endpoint, data, {
always = function (err, response, body) {}, success = function (err, response, body) {},
error = function (err, response, body) {}}={}, isJSON) => {
@@ -258,38 +298,6 @@ let registerRenderSubview = (parent, view, hook) => {
parent.registerSubview(view);
parent.renderSubview(view, parent.queryByHook(hook));
}
-let switchToEditTab = (view, section) => {
- let elementID = Boolean(view.model && view.model.elementID) ? view.model.elementID + "-" : "";
- if($(view.queryByHook(elementID + 'view-' + section)).hasClass('active')) {
- $(view.queryByHook(elementID + section + '-edit-tab')).tab('show');
- $(view.queryByHook(elementID + 'edit-' + section)).addClass('active');
- $(view.queryByHook(elementID + 'view-' + section)).removeClass('active');
- }
-}
-let tooltipSetup = () => {
- $(() => {
- $('[data-toggle="tooltip"]').tooltip();
- $('[data-toggle="tooltip"]').on('click ', () => {
- $('[data-toggle="tooltip"]').tooltip("hide");
- });
- });
-}
-let validateName = (input, {rename=false, saveAs=true}={}) => {
- var error = "";
- if(input.endsWith('/')) {
- error = 'forward';
- }
- var invalidChars = "`~!@#$%^&*=+[{]}\"|:;'<,>?\\";
- if(rename || !saveAs) {
- invalidChars += "/";
- }
- for(var i = 0; i < input.length; i++) {
- if(invalidChars.includes(input.charAt(i))) {
- error = error === "" || error === "special" ? "special" : "both";
- }
- }
- return error;
-}
module.exports = {
changeCollapseButtonText: changeCollapseButtonText,
diff --git a/client/domain-view/templates/actionsView.pug b/client/domain-view/templates/actionsView.pug
index 69f253e7f..a3273c3e3 100644
--- a/client/domain-view/templates/actionsView.pug
+++ b/client/domain-view/templates/actionsView.pug
@@ -16,7 +16,7 @@ div#domain-actions-editor.card
a.nav-link.tab(data-hook="actions-view-tab" data-toggle="tab" href="#view-actions") View
- button.btn.btn-outline-collapse.inline(data-toggle="collapse" data-target="#collapse-actions" data-hook="collapse") +
+ button.btn.btn-outline-collapse.inline(data-toggle="collapse" data-target="#collapse-actions" data-hook="collapse-actions") +
div.card-body
diff --git a/client/domain-view/templates/shapesView.pug b/client/domain-view/templates/shapesView.pug
index 7939c2a95..a77c5a8e5 100644
--- a/client/domain-view/templates/shapesView.pug
+++ b/client/domain-view/templates/shapesView.pug
@@ -16,7 +16,7 @@ div#domain-geometries-editor.card
a.nav-link.tab(data-hook="shapes-view-tab" data-toggle="tab" href="#view-shapes") View
- button.btn.btn-outline-collapse.inline(data-toggle="collapse" data-target="#collapse-shapes" data-hook="collapse") +
+ button.btn.btn-outline-collapse.inline(data-toggle="collapse" data-target="#collapse-shapes" data-hook="collapse-shapes") +
div.card-body
p.mb-0
diff --git a/client/domain-view/templates/transformationsView.pug b/client/domain-view/templates/transformationsView.pug
index b9f1eadb4..9320cc43d 100644
--- a/client/domain-view/templates/transformationsView.pug
+++ b/client/domain-view/templates/transformationsView.pug
@@ -16,7 +16,7 @@ div#domain-transformation-editor.card
a.nav-link.tab(data-hook="transformations-view-tab" data-toggle="tab" href="#view-transformations") View
- button.btn.btn-outline-collapse.inline(data-toggle="collapse" data-target="#collapse-transformations" data-hook="collapse") +
+ button.btn.btn-outline-collapse.inline(data-toggle="collapse" data-target="#collapse-transformations" data-hook="collapse-transformations") +
div.card-body
diff --git a/client/domain-view/views/actions-view.js b/client/domain-view/views/actions-view.js
index 9122f820f..b66b6a52f 100644
--- a/client/domain-view/views/actions-view.js
+++ b/client/domain-view/views/actions-view.js
@@ -29,7 +29,7 @@ let template = require('../templates/actionsView.pug');
module.exports = View.extend({
template: template,
events: {
- 'click [data-hook=collapse]' : 'changeCollapseButtonText',
+ 'click [data-hook=collapse-actions]' : 'changeCollapseButtonText',
'click [data-hook=fill-action]' : 'addAction',
'click [data-hook=set-action]' : 'addAction',
'click [data-hook=remove-action]' : 'addAction',
diff --git a/client/domain-view/views/shapes-view.js b/client/domain-view/views/shapes-view.js
index 26aa414bc..22572963a 100644
--- a/client/domain-view/views/shapes-view.js
+++ b/client/domain-view/views/shapes-view.js
@@ -29,7 +29,7 @@ let template = require('../templates/shapesView.pug');
module.exports = View.extend({
template: template,
events: {
- 'click [data-hook=collapse]' : 'changeCollapseButtonText',
+ 'click [data-hook=collapse-shapes]' : 'changeCollapseButtonText',
'click [data-hook=cartesian-lattice]' : 'addShape',
'click [data-hook=spherical-lattice]' : 'addShape',
'click [data-hook=cylindrical-lattice]' : 'addShape'
diff --git a/client/domain-view/views/transformations-view.js b/client/domain-view/views/transformations-view.js
index 09206e457..904b1384c 100644
--- a/client/domain-view/views/transformations-view.js
+++ b/client/domain-view/views/transformations-view.js
@@ -29,7 +29,7 @@ let template = require('../templates/transformationsView.pug');
module.exports = View.extend({
template: template,
events: {
- 'click [data-hook=collapse]' : 'changeCollapseButtonText',
+ 'click [data-hook=collapse-transformations]' : 'changeCollapseButtonText',
'click [data-hook=translate-transformation]' : 'addTransformation',
'click [data-hook=rotate-transformation]' : 'addTransformation',
'click [data-hook=reflect-transformation]' : 'addTransformation',
diff --git a/client/domain-view/views/types-view.js b/client/domain-view/views/types-view.js
index fc60f115a..bb6854a06 100644
--- a/client/domain-view/views/types-view.js
+++ b/client/domain-view/views/types-view.js
@@ -63,6 +63,19 @@ module.exports = View.extend({
changeCollapseButtonText: function (e) {
app.changeCollapseButtonText(this, e);
},
+ getParticleCounts: function (particles) {
+ let particleCounts = {};
+ particles.forEach((particle) => {
+ if(particleCounts[particle.type]) {
+ particleCounts[particle.type] += 1;
+ }else{
+ particleCounts[particle.type] = 1;
+ }
+ });
+ this.collection.forEach((type) => {
+ type.numParticles = particleCounts[type.typeID] ? particleCounts[type.typeID] : 0;
+ });
+ },
handleAddDomainType: function () {
let name = this.collection.addType();
this.collection.parent.trigger('update-particle-type-options', {currName: name});
@@ -135,17 +148,7 @@ module.exports = View.extend({
});
},
updateParticleCounts: function (particles) {
- let particleCounts = {};
- particles.forEach((particle) => {
- if(particleCounts[particle.type]) {
- particleCounts[particle.type] += 1;
- }else{
- particleCounts[particle.type] = 1;
- }
- });
- this.collection.forEach((type) => {
- type.numParticles = particleCounts[type.typeID] ? particleCounts[type.typeID] : 0;
- });
+ this.getParticleCounts(particles);
$(this.queryByHook('unassigned-type-count')).text(this.collection.models[0].numParticles);
this.renderEditTypeView();
this.renderViewTypeView();
diff --git a/client/job-view/templates/gillespyResultsEnsembleView.pug b/client/job-view/templates/gillespyResultsEnsembleView.pug
index 0c8d595de..410311ffe 100644
--- a/client/job-view/templates/gillespyResultsEnsembleView.pug
+++ b/client/job-view/templates/gillespyResultsEnsembleView.pug
@@ -75,7 +75,7 @@ div#workflow-results.card
data-hook="stddevran-plot-csv"
data-target="download-plot-csv"
data-type="stddevran"
- ) Plot Results as .csv
+ ) Plot CSV Results as .zip
div.card
@@ -129,7 +129,7 @@ div#workflow-results.card
data-hook="trajectories-plot-csv"
data-target="download-plot-csv"
data-type="trajectories"
- ) Plot Results as .csv
+ ) Plot CSV Results as .zip
div.card
@@ -181,7 +181,7 @@ div#workflow-results.card
data-hook="stddev-plot-csv"
data-target="download-plot-csv"
data-type="stddev"
- ) Plot Results as .csv
+ ) Plot CSV Results as .zip
div.card
@@ -233,13 +233,13 @@ div#workflow-results.card
data-hook="avg-plot-csv"
data-target="download-plot-csv"
data-type="avg"
- ) Plot Results as .csv
+ ) Plot CSV Results as .zip
div
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") Download Full Results as .csv
+ button.btn.btn-primary.box-shadow(id="download-results-csv" data-hook="download-results-csv") Download Full CSV Results as .zip
button.btn.btn-primary.box-shadow(id="job-presentation" data-hook="job-presentation") Publish
diff --git a/client/job-view/templates/gillespyResultsView.pug b/client/job-view/templates/gillespyResultsView.pug
index 3684e21ee..e408ee96c 100644
--- a/client/job-view/templates/gillespyResultsView.pug
+++ b/client/job-view/templates/gillespyResultsView.pug
@@ -75,13 +75,13 @@ div#workflow-results.card
data-hook="trajectories-plot-csv"
data-target="download-plot-csv"
data-type="trajectories"
- ) Plot Results as .csv
+ ) Plot CSV Results as .zip
div
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") Download Full Results as .csv
+ button.btn.btn-primary.box-shadow(id="download-results-csv" data-hook="download-results-csv") Download Full CSV Results as .zip
button.btn.btn-primary.box-shadow(id="job-presentation" data-hook="job-presentation") Publish
diff --git a/client/job-view/templates/inferenceResultsView.pug b/client/job-view/templates/inferenceResultsView.pug
new file mode 100644
index 000000000..33f04693b
--- /dev/null
+++ b/client/job-view/templates/inferenceResultsView.pug
@@ -0,0 +1,262 @@
+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.mr-2 Plot Inference
+
+ div.inline
+
+ ul.nav.nav-tabs
+
+ li.nav-item
+
+ a.nav-link.tab.active(data-toggle="tab" href="#inference-histogram" data-hook="inference-histogram-tab") Histogram
+
+ li.nav-item
+
+ a.nav-link.tab(data-toggle="tab" href="#pdf" data-hook="inference-pdf-tab") Propability Distribution
+
+ 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.row
+
+ div.col-sm-4
+
+ div.inference-cpv-line Current Parameter Values
+
+ div.col-sm-8
+
+ div.inference-ipv-line Inferred Parameter Values (mean of the accepted samples)
+
+ div.tab-content
+
+ div.tab-pane.active(id="inference-histogram" data-hook="inference-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
+
+ 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(
+ data-hook="inference-download-json"
+ data-target="download-json"
+ data-type="inference"
+ ) Plot as .json
+
+ div.card
+
+ div.card-header.pb-0
+
+ 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"
+ data-target="#collapse-round"
+ id="collapse-round-btn"
+ data-hook="collapse-round-btn"
+ data-trigger="collapse-plot-container"
+ data-type="round"
+ ) +
+
+ div.card-body
+
+ p.mb-0
+ | View the results of any selected round.
+ | Histogram - View the histogram, pdf, and original and inferred values for all parameters as well as intersection between parameters.
+
+ ul.mb-0
+ li Click on any intersection subplot to view it in the intersection tab
+
+ p.mb-0
+ | Intersection - View an intersection of two parameters including their histogram, pdf, and original and inferred values.
+
+ ul.mb-0
+ li Click on the histogram tab to select a different intersection.
+
+ div.collapse(id="collapse-round")
+
+ div
+
+ div(data-hook="round-slider-container")
+
+ hr
+
+ div.mx-1.row.head.align-items-baseline
+
+ div.col-sm-12
+
+ h6
+
+ div.inline.mr-2 Round:
+
+ div.inline(data-hook="round-index-value")=this.roundIndex
+
+ div.mx-1.my-3.row
+
+ div.col-sm-12
+
+ input.custom-range(
+ type="range"
+ min="1"
+ max=`${this.model.settings.inferenceSettings.numRounds}`
+ value=this.roundIndex
+ data-hook="round-index-slider"
+ )
+
+ hr
+
+ div.row
+
+ div.col-sm-4
+
+ div.inference-cpv-line Current Parameter Values
+
+ div.col-sm-8
+
+ div.inference-ipv-line Inferred Parameter Values (mean of the accepted samples)
+
+ div.tab-content
+
+ div.tab-pane.active(id="round-histogram" data-hook="round-histogram")
+
+ div(data-hook="round-histogram-plot" style="min-height: 1000;")
+
+ 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" style="min-height: 1000;")
+
+ div.spinner-border.workflow-plot(data-hook="round-intersection-plot-spinner")
+
+ div
+
+ 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="round-model-explore" data-target="model-explore" disabled) Export & Explore Model
+
+ 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="round-download"
+ data-hook="round-download"
+ data-toggle="dropdown",
+ aria-haspopup="true",
+ aria-expanded="false",
+ type="button"
+ disabled
+ ) Download
+
+ ul.dropdown-menu(aria-labelledby="#round-download")
+ li.dropdown-item(
+ data-hook="round-download-png-custom"
+ data-target="download-png-custom"
+ data-type="round"
+ ) Plot as .png
+ li.dropdown-item(
+ data-hook="round-download-json"
+ data-target="download-json"
+ data-type="round"
+ ) Plot as .json
+
+ div
+
+ 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") Download Full CSV Results as .zip
+
+ 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/templates/parameterScanResultsView.pug b/client/job-view/templates/parameterScanResultsView.pug
index a3704a375..af40cdf14 100644
--- a/client/job-view/templates/parameterScanResultsView.pug
+++ b/client/job-view/templates/parameterScanResultsView.pug
@@ -95,7 +95,7 @@ div#workflow-results.card
data-hook="ts-psweep-plot-csv"
data-target="download-plot-csv"
data-type="ts-psweep"
- ) Plot Results as .csv
+ ) Plot CSV Results as .zip
div.col-md-3
@@ -201,7 +201,7 @@ div#workflow-results.card
data-hook="psweep-plot-csv"
data-target="download-plot-csv"
data-type="psweep"
- ) Plot Results as .csv
+ ) Plot CSV Results as .zip
div.col-md-3
@@ -213,7 +213,7 @@ div#workflow-results.card
div
- 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="download-results-csv" data-hook="download-results-csv") Download Full CSV Results as .zip
button.btn.btn-primary.box-shadow(id="job-presentation" data-hook="job-presentation") Publish Presentation
diff --git a/client/job-view/templates/parameterSweepResultsView.pug b/client/job-view/templates/parameterSweepResultsView.pug
index b0a670bb0..a144521bd 100644
--- a/client/job-view/templates/parameterSweepResultsView.pug
+++ b/client/job-view/templates/parameterSweepResultsView.pug
@@ -95,7 +95,7 @@ div#workflow-results.card
data-hook="ts-psweep-plot-csv"
data-target="download-plot-csv"
data-type="ts-psweep"
- ) Plot Results as .csv
+ ) Plot CSV Results as .zip
div.col-md-3
@@ -187,13 +187,13 @@ div#workflow-results.card
data-hook="psweep-plot-csv"
data-target="download-plot-csv"
data-type="psweep"
- ) Plot Results as .csv
+ ) Plot CSV Results as .zip
div
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") Download Full Results as .csv
+ button.btn.btn-primary.box-shadow(id="download-results-csv" data-hook="download-results-csv") Download Full CSV Results as .zip
button.btn.btn-primary.box-shadow(id="job-presentation" data-hook="job-presentation") Publish
diff --git a/client/job-view/templates/spatialResultsView.pug b/client/job-view/templates/spatialResultsView.pug
index 98ef61d0a..9aa3704d0 100644
--- a/client/job-view/templates/spatialResultsView.pug
+++ b/client/job-view/templates/spatialResultsView.pug
@@ -115,13 +115,13 @@ div#workflow-results.card
data-hook="spatial-plot-csv"
data-target="download-plot-csv"
data-type="spatial"
- ) Plot Results as .csv
+ ) Plot CSV Results as .zip
div
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") Download Full Results as .csv
+ button.btn.btn-primary.box-shadow(id="download-results-csv" data-hook="download-results-csv") Download Full CSV Results as .zip
button.btn.btn-primary.box-shadow(id="job-presentation" data-hook="job-presentation") Publish
diff --git a/client/job-view/views/job-results-view.js b/client/job-view/views/job-results-view.js
index 55f6328ac..39eff0284 100644
--- a/client/job-view/views/job-results-view.js
+++ b/client/job-view/views/job-results-view.js
@@ -18,22 +18,26 @@ 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');
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: {
@@ -43,12 +47,19 @@ 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=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',
'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',
'click [data-target=edit-plot]' : 'openPlotArgsSection',
'click [data-hook=multiple-plots]' : 'plotMultiplePlots',
'click [data-target=download-png-custom]' : 'handleDownloadPNGClick',
@@ -57,7 +68,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=round-index-slider]' : 'viewRoundIndex'
},
initialize: function (attrs, options) {
View.prototype.initialize.apply(this, arguments);
@@ -67,7 +79,12 @@ module.exports = View.extend({
this.tooltips = Tooltips.jobResults;
this.plots = {};
this.plotArgs = {};
+ 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 &&
@@ -76,7 +93,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 +128,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 +139,23 @@ 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.roundIndex = this.model.settings.inferenceSettings.numRounds === 0 ? 1 : 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");
+ }
+ if(this.roundIndex > 1) {
+ $(this.queryByHook("round-index-value")).text(this.roundIndex);
+ $(this.queryByHook("round-index-slider")).prop("value", this.roundIndex);
+ }else{
+ $(this.queryByHook("round-slider-container")).css('display', 'none');
+ }
+ // TODO: Enable inference presentations when implemented
+ $(this.queryByHook("job-presentation")).prop("disabled", true);
}
this.getPlot(type);
},
@@ -143,18 +178,49 @@ module.exports = View.extend({
error.css("display", "none");
}, 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");
+ cleanupPlotContainer: function (type, {pdfOnly=false}={}) {
+ if(["inference", "round"].includes(type)) {
+ let histoEL = this.queryByHook(`${type}-histogram-plot`);
+ 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`);
+ 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}`;
@@ -164,24 +230,83 @@ module.exports = View.extend({
let endpoint = `${path.join(app.getApiPath(), "job/csv")}${queryStr}`;
window.open(endpoint);
},
- getPlot: function (type) {
- this.cleanupPlotContainer(type);
+ exportInferredModel: function (type, {cb=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 === "round") {
+ queryStr += `&round=${this.roundIndex}`;
+ }
+ 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[round];
+ let editEP = `${path.join(app.getBasePath(), "stochss/models/edit")}?path=${mdPath}`;
+ window.location.href = editEP;
+ }else {
+ cb();
+ }
+ },
+ 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
+ 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, {pdfOnly=false}={}) {
+ this.cleanupPlotContainer(type, {pdfOnly: pdfOnly});
let data = this.getPlotData(type);
if(data === null) { return };
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', 'round'];
if(renderTypes.includes(type)) {
- this.plotFigure(this.plots[storageKey], type);
+ this.activePlots[type] = storageKey;
+ this.plotFigure(this.plots[storageKey], type, {pdfOnly: pdfOnly});
}
}else{
let queryStr = `?path=${this.model.directory}&data=${JSON.stringify(data)}`;
let endpoint = `${path.join(app.getApiPath(), "workflow/plot-results")}${queryStr}`;
app.getXHR(endpoint, {
success: (err, response, body) => {
+ if(type === "round") {
+ let xLabel = {
+ font: {size: 16}, showarrow: false, text: "", x: 0.5, xanchor: "center", xref: "paper",
+ y: 0, yanchor: "top", yref: "paper", yshift: -30
+ }
+ 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.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") {
@@ -228,6 +353,13 @@ module.exports = View.extend({
target: this.spatialTarget, index: this.targetIndex, mode: this.targetMode, trajectory: this.trajectoryIndex - 1
};
data['plt_key'] = type;
+ }else if(["inference", "round"].includes(type)) {
+ data['sim_type'] = "Inference";
+ 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";
data['data_keys'] = {};
@@ -245,6 +377,12 @@ module.exports = View.extend({
this.model.settings.resultsSettings.reducer = e.target.value;
this.getPlot('psweep')
},
+ getPlotForRound: function (e) {
+ this.roundIndex = Number(e.target.value);
+ this.roundHistoResized = false;
+ this.intersectionResized = false;
+ this.getPlot('round');
+ },
getPlotForFeatureExtractor: function (e) {
this.model.settings.resultsSettings.mapper = e.target.value;
this.getPlot('psweep')
@@ -314,8 +452,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.round === null) { return "inference"; }
+ return "round"
+ }
return "psweep"
},
handleCollapsePlotContainerClick: function (e) {
@@ -346,11 +488,28 @@ 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 exportFileDefaultName = type + '-plot.json';
+ let exportFileDefaultName = `${nameBase}-plot.json`;
let linkElement = document.createElement('a');
linkElement.setAttribute('href', dataURI);
@@ -359,11 +518,63 @@ 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) {
+ 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);
+ },
handleFullCSVClick: function (e) {
- this.downloadCSV("full", null);
+ if(this.titleType === "Model Inference") {
+ this.downloadCSV("inference", null);
+ }else {
+ 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(() => {
+ this.pdfResized = this.fixPlotSize("inference", "pdf");
+ }, 0)
+ }
+ },
+ handleRoundResize: function (e) {
+ if(!this.roundHistoResized) {
+ setTimeout(() => {
+ this.roundHistoResized = this.fixPlotSize("round", "histogram");
+ }, 0)
+ }
},
handlePlotCSVClick: function (e) {
let type = e.target.dataset.type;
@@ -414,6 +625,30 @@ module.exports = View.extend({
}
});
},
+ newWorkflow: function (err, response, body) {
+ let type = "Parameter Sweep"
+ if([undefined, null].includes(body)) {
+ body = { path: this.model.exportLinks[this.roundIndex] };
+ }
+ 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 () {
@@ -422,18 +657,54 @@ module.exports = View.extend({
}, false);
});
},
- 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");
+ plotFigure: function (figure, type, {pdfOnly=false}={}) {
+ if(["inference", "round"].includes(type)) {
+ let histoHook = `${type}-histogram-plot`;
+ let histoEL = this.queryByHook(histoHook);
+ 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
+ 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 {
+ 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");
+ }
+ }
+ }
+ }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);
+ if(!pdfOnly) {
+ $(this.queryByHook(`${type}-edit-plot`)).prop("disabled", false);
+ $(this.queryByHook(`${type}-download`)).prop("disabled", false);
}
},
plotMultiplePlots: function (e) {
@@ -558,13 +829,34 @@ 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('x').pop().split('y')[0]) - 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) {
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) {
@@ -572,8 +864,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', 'round'].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) {
@@ -581,8 +879,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', 'round'].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 () {
@@ -592,6 +896,9 @@ module.exports = View.extend({
},
update: function () {},
updateValid: function () {},
+ 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/modals.js b/client/modals.js
index 11f76f703..483d42380 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/models/inference-parameter.js b/client/models/inference-parameter.js
new file mode 100644
index 000000000..01a0d48fb
--- /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 000000000..95117cdd1
--- /dev/null
+++ b/client/models/inference-parameters.js
@@ -0,0 +1,54 @@
+/*
+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);
+ },
+ 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
new file mode 100644
index 000000000..c3574b616
--- /dev/null
+++ b/client/models/inference-settings.js
@@ -0,0 +1,60 @@
+/*
+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 SummaryStats = require('./summary-stats');
+let InferenceParameters = require('./inference-parameters');
+//models
+let State = require('ampersand-state');
+
+module.exports = State.extend({
+ props: {
+ batchSize: 'number',
+ chunkSize: 'number',
+ numRounds: 'number',
+ numSamples: 'number',
+ obsData: 'string',
+ priorMethod: 'string',
+ summaryStatsType: 'string'
+ },
+ collections: {
+ parameters: InferenceParameters,
+ summaryStats: SummaryStats
+ },
+ session: {
+ customCalculators: 'object'
+ },
+ 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);
+ },
+ resetSummaryStats: function () {
+ let summaryStats = this.summaryStats;
+ this.summaryStats = new SummaryStats();
+ return summaryStats;
+ }
+});
diff --git a/client/models/job.js b/client/models/job.js
index 97d7b8317..de495f6a2 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/client/models/model.js b/client/models/model.js
index 7d126dae5..1927e0add 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',
+ refLinks: 'object',
volume: 'any',
template_version: 'number'
},
diff --git a/client/models/settings.js b/client/models/settings.js
index 5dba94e6e..c4e865e81 100644
--- a/client/models/settings.js
+++ b/client/models/settings.js
@@ -15,19 +15,23 @@ 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 InferenceSettings = require('./inference-settings');
+let SimulationSettings = require('./simulation-settings');
+let ParameterSweepSettings = require('./parameter-sweep-settings');
module.exports = State.extend({
+ props: {
+ template_version: 'number'
+ },
children: {
timespanSettings: TimespanSettings,
simulationSettings: SimulationSettings,
parameterSweepSettings: ParameterSweepSettings,
+ inferenceSettings: InferenceSettings,
resultsSettings: ResultsSettings
},
initialize: function(attrs, options) {
@@ -44,4 +48,4 @@ module.exports = State.extend({
}
}
}
-});
\ No newline at end of file
+});
diff --git a/client/models/summary-stat.js b/client/models/summary-stat.js
new file mode 100644
index 000000000..e250ee71b
--- /dev/null
+++ b/client/models/summary-stat.js
@@ -0,0 +1,65 @@
+/*
+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));
+ });
+ return argStrs.join();
+ }
+ }
+ },
+ initialize: function(attrs, options) {
+ State.prototype.initialize.apply(this, arguments);
+ },
+ setArgs: function (argStr, {dryRun=false}={}) {
+ var args = null
+ if(argStr !== "") {
+ let argStrs = argStr.replace(/ /g, '').split(',');
+ args = [];
+ argStrs.forEach((arg) => {
+ args.push(JSON.parse(arg.replace(/'/g, '"')));
+ });
+ }
+ if(!dryRun) {
+ this.args = args;
+ }
+ }
+});
diff --git a/client/models/summary-stats.js b/client/models/summary-stats.js
new file mode 100644
index 000000000..80a4f558d
--- /dev/null
+++ b/client/models/summary-stats.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('underscore');
+//models
+let SummaryStat = require('./summary-stat');
+//collections
+let Collection = require('ampersand-collection');
+
+module.exports = Collection.extend({
+ model: SummaryStat,
+ indexes: ['name'],
+ addSummaryStat: function ({name=null}={}) {
+ if(name === null) {
+ name = this.getDefaultName();
+ }
+ let summaryStat = new SummaryStat({
+ formula: '',
+ name: name
+ });
+ summaryStat.setArgs("");
+ 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);
+ }
+});
diff --git a/client/models/user-settings.js b/client/models/user-settings.js
index 44cc65bc3..2177bf255 100644
--- a/client/models/user-settings.js
+++ b/client/models/user-settings.js
@@ -24,7 +24,7 @@ let Model = require('ampersand-model');
module.exports = Model.extend({
url: function () {
- return path.join(app.getApiPath(), "user-settings");
+ return path.join(app.getApiPath(), "load-user-settings");
},
props: {
awsAccessKeyID: 'string',
diff --git a/client/pages/model-editor.js b/client/pages/model-editor.js
index 8fd76438b..0995d80bb 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/model-presentation.js b/client/pages/model-presentation.js
index 2c8fb413d..2c4979147 100644
--- a/client/pages/model-presentation.js
+++ b/client/pages/model-presentation.js
@@ -52,10 +52,22 @@ let ModelPresentationPage = PageView.extend({
let endpoint = `api/file/json-data?file=${this.model.directory}&owner=${owner}`;
app.getXHR(endpoint, {
success: (err, response, body) => {
+ let particles = body.model.domain.particles
this.model.set(body.model);
- if(Object.keys(body.model.domain).includes("particles")) {
+ if(this.model.is_spatial && Object.keys(body.model.domain).includes("particles")) {
let particles = new Particles(body.model.domain.particles);
this.model.domain.particles = particles;
+ this.model.domain.actions.forEach((action) => {
+ action.filename = action.filename.split('/').pop();
+ action.subdomainFile = action.subdomainFile.split('/').pop();
+ });
+ this.model.domain.x_lim = body.domainLimits[0];
+ this.model.domain.y_lim = body.domainLimits[1];
+ this.model.domain.z_lim = body.domainLimits[2];
+ let particleCounts = {};
+ this.model.domain.types.forEach((type) => { particleCounts[type.typeID] = 0; });
+ particles.forEach((particle) => { particleCounts[particle.type] += 1; });
+ this.model.domain.types.forEach((type) => { type.numParticles = particleCounts[type.typeID]; });
}
let domainPlot = Boolean(body.domainPlot) ? body.domainPlot : null;
this.renderSubviews(false, domainPlot);
diff --git a/client/pages/project-manager.js b/client/pages/project-manager.js
index 234669747..554ebc9f7 100644
--- a/client/pages/project-manager.js
+++ b/client/pages/project-manager.js
@@ -89,42 +89,42 @@ let ProjectManager = PageView.extend({
let self = this
let mdlListEP = path.join(app.getApiPath(), 'project/add-existing-model') + "?path=" + self.model.directory;
app.getXHR(mdlListEP, {
- always: function (err, response, body) {
+ always: (err, response, body) => {
let modal = $(modals.importModelHtml(body.files)).modal();
let okBtn = document.querySelector('#importModelModal .ok-model-btn');
let select = document.querySelector('#importModelModal #modelFileSelect');
let location = document.querySelector('#importModelModal #modelPathSelect');
- select.addEventListener("change", function (e) {
+ select.addEventListener("change", (e) => {
okBtn.disabled = e.target.value && body.paths[e.target.value].length >= 2;
if(body.paths[e.target.value].length >= 2) {
- var locations = body.paths[e.target.value].map(function (path) {
+ var locations = body.paths[e.target.value].map((path) => {
return ``;
});
locations.unshift(``);
locations = locations.join(" ");
- $("#modelPathInput").find('option').remove().end().append(locations);
+ $(location).find('option').remove().end().append(locations);
$("#location-container").css("display", "block");
}else{
$("#location-container").css("display", "none");
- $("#modelPathInput").find('option').remove().end();
+ $(location).find('option').remove().end();
}
});
- location.addEventListener("change", function (e) {
+ location.addEventListener("change", (e) => {
okBtn.disabled = !Boolean(e.target.value);
});
- okBtn.addEventListener("click", function (e) {
+ okBtn.addEventListener("click", (e) => {
modal.modal('hide');
let mdlPath = body.paths[select.value].length < 2 ? body.paths[select.value][0] : location.value;
let queryString = "?path=" + self.model.directory + "&mdlPath=" + mdlPath;
let endpoint = path.join(app.getApiPath(), 'project/add-existing-model') + queryString
app.postXHR(endpoint, null, {
- success: function (err, response, body) {
+ success: (err, response, body) => {
if(document.querySelector("#successModal")) {
document.querySelector("#successModal").remove();
}
let successModal = $(modals.successHtml(body.message)).modal();
},
- error: function (err, response, body) {
+ error: (err, response, body) => {
if(document.querySelector("#errorModal")) {
document.querySelector("#errorModal").remove();
}
@@ -423,22 +423,24 @@ let ProjectManager = PageView.extend({
}
let fetchTypes = ["Model", "Workflow", "WorkflowGroup", "Archive"];
if(fetchTypes.includes(target)) {
- let self = this;
app.getXHR(this.model.url(), {
- success: function (err, response, body) {
- self.model.set(body)
- if(self.model.newFormat) {
- self.renderWorkflowGroupCollection();
+ success: (err, response, body) => {
+ this.model.set(body);
+ if(this.model.newFormat) {
+ this.renderWorkflowGroupCollection();
+ if(["Model", "WorkflowGroup", "Archive"].includes(target)) {
+ this.metaDataView.updateView();
+ }
}else{
if(target === "Workflow"){
- self.renderWorkflowsCollection();
+ this.renderWorkflowsCollection();
}
if(target === "Model"){
- self.models = new Collection(body.models, {model: Model});
- self.renderModelsCollection();
+ this.models = new Collection(body.models, {model: Model});
+ this.renderModelsCollection();
}
}
- $(self.queryByHook('empty-project-trash')).prop('disabled', body.trash_empty)
+ $(this.queryByHook('empty-project-trash')).prop('disabled', body.trash_empty);
}
});
}
diff --git a/client/pages/workflow-manager.js b/client/pages/workflow-manager.js
index 63b6d16b6..3f8290a10 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();
}
@@ -315,25 +321,33 @@ 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") {
+ $(this.queryByHook("aws-start-job")).addClass("disabled")
+ 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 {
@@ -344,7 +358,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/client/settings-view/settings-view.js b/client/settings-view/settings-view.js
index 0eb4c271f..9a402affa 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,23 @@ 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();
+ }
+ 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 9a83214c7..b3588a74f 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/editCustomSummaryStatView.pug b/client/settings-view/templates/editCustomSummaryStatView.pug
new file mode 100644
index 000000000..f9df9e9b8
--- /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="custom-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 000000000..b044706c4
--- /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/editIdentitySummaryStatView.pug b/client/settings-view/templates/editIdentitySummaryStatView.pug
new file mode 100644
index 000000000..b83a80bec
--- /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-target="identity-property" data-hook="summary-stat-name")
+
+ div.col-sm-8
+
+ div(data-target="identity-property" 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 000000000..889657825
--- /dev/null
+++ b/client/settings-view/templates/editIdentitySummaryStats.pug
@@ -0,0 +1,27 @@
+div
+
+ div
+
+ hr
+
+ div.mx-1.row.head.align-items-baseline
+
+ div.col-sm-2
+
+ 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-summary-stat") Add Summary Statistic
diff --git a/client/settings-view/templates/editMinimalSummaryStats.pug b/client/settings-view/templates/editMinimalSummaryStats.pug
new file mode 100644
index 000000000..e40d16c67
--- /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/editUniformParameterView.pug b/client/settings-view/templates/editUniformParameterView.pug
new file mode 100644
index 000000000..071d80cc9
--- /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/editUniformParameters.pug b/client/settings-view/templates/editUniformParameters.pug
new file mode 100644
index 000000000..9a90eddf0
--- /dev/null
+++ b/client/settings-view/templates/editUniformParameters.pug
@@ -0,0 +1,44 @@
+div
+
+ div
+
+ h4 Parameter Space Configuration
+
+ 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")
+
+ div(data-hook="mi-parameter-collection-error"): p.text-danger A model inference job must have at least one parameter.
+
+ 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
new file mode 100644
index 000000000..5e25dfd1d
--- /dev/null
+++ b/client/settings-view/templates/inferenceSettingsView.pug
@@ -0,0 +1,207 @@
+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
+
+ hr
+
+ div.mx-1.row.head.align-items-baseline
+
+ div.col-sm-3
+
+ h6.inline Number of Rounds
+
+ div.col-sm-3
+
+ h6.inline Number of Samples
+
+ div.mx-1.my-3.row
+
+ div.col-sm-3(data-hook="num-rounds")
+
+ div.col-sm-3(data-hook="num-samples")
+
+ div
+
+ h4 Summary Statistics
+
+ 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")
+
+ 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")
+
+ 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")
+
+ 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.card-body
+
+ p.mb-0
+ | Valid files are Observed Data Files (a directory with the postfix .odf) compressed to a zip archive (.zip) or Observed Data File (.csv)
+
+ div
+
+ div.collapse(id="collapseImportObsData" aria-labelledby="importObsDataHeader" data-parent="#obs-data-section")
+
+ hr
+
+ 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='.zip, .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")
+
+ 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")
+
+ 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.card-body
+
+ p.mb-0
+ | Valid files are Observed Data Files (a directory with the postfix .odf) or Observed Data File (.csv)
+
+ div
+
+ div.collapse(id="collapseUploadObsData" aria-labelledby="uploadObsDataHeader" data-parent="#obs-data-section")
+
+ hr
+
+ 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: `
+
+ 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.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")
+
+ div
+
+ hr
+
+ div.mx-1.row.head.align-items-baseline
+
+ div.col-sm-3
+
+ h6.inline Number of Rounds
+
+ div.col-sm-3
+
+ h6.inline Number of Samples
+
+ div.mx-1.my-3.row
+
+ div.col-sm-3
+
+ div(data-hook="view-num-rounds")=this.model.numRounds
+
+ 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(data-hook="view-obs-data-file")=this.model.obsData
diff --git a/client/settings-view/templates/viewCustomSummaryStatView.pug b/client/settings-view/templates/viewCustomSummaryStatView.pug
new file mode 100644
index 000000000..25c8f5c31
--- /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 000000000..c80854873
--- /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/viewIdentitySummaryStatView.pug b/client/settings-view/templates/viewIdentitySummaryStatView.pug
new file mode 100644
index 000000000..c99c9e973
--- /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 ? this.model.formula : "None"
diff --git a/client/settings-view/templates/viewIdentitySummaryStats.pug b/client/settings-view/templates/viewIdentitySummaryStats.pug
new file mode 100644
index 000000000..0831ba2ea
--- /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")
diff --git a/client/settings-view/templates/viewMinimalSummaryStats.pug b/client/settings-view/templates/viewMinimalSummaryStats.pug
new file mode 100644
index 000000000..a919236c9
--- /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/templates/viewUniformParameterView.pug b/client/settings-view/templates/viewUniformParameterView.pug
new file mode 100644
index 000000000..316233caf
--- /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/templates/viewUniformParameters.pug b/client/settings-view/templates/viewUniformParameters.pug
new file mode 100644
index 000000000..3062b7460
--- /dev/null
+++ b/client/settings-view/templates/viewUniformParameters.pug
@@ -0,0 +1,25 @@
+div
+
+ h4.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/custom-summary-stat-view.js b/client/settings-view/views/custom-summary-stat-view.js
new file mode 100644
index 000000000..71f0d2f53
--- /dev/null
+++ b/client/settings-view/views/custom-summary-stat-view.js
@@ -0,0 +1,101 @@
+/*
+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=custom-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;
+ this.customCalculators = attrs.customCalculators ? attrs.customCalculators : null;
+ },
+ 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 () {
+ try{
+ this.parent.updateViewer();
+ }catch(error){
+ this.parent.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,
+ 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 --"
+ });
+ }
+ },
+ formulaInputView: {
+ hook: "summary-stat-args",
+ prepareView: function (el) {
+ return new InputView({
+ 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/identity-summary-stat-view.js b/client/settings-view/views/identity-summary-stat-view.js
new file mode 100644
index 000000000..b453115fb
--- /dev/null
+++ b/client/settings-view/views/identity-summary-stat-view.js
@@ -0,0 +1,83 @@
+/*
+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/editIdentitySummaryStatView.pug');
+let viewTemplate = require('../templates/viewIdentitySummaryStatView.pug');
+
+module.exports = View.extend({
+ 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;
+ },
+ render: function (attrs, options) {
+ this.template = this.viewMode ? viewTemplate : editTemplate;
+ View.prototype.render.apply(this, arguments);
+ },
+ removeSummaryStat: function () {
+ this.model.collection.removeSummaryStat(this.model);
+ },
+ update: function () {},
+ updateValid: function () {},
+ updateViewer: function () {
+ try{
+ this.parent.updateViewer();
+ }catch(error){
+ this.parent.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: "-- i.e. Juvenile + Susceptible + Exposed + Infected + Diseased --"
+ });
+ }
+ }
+ }
+});
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 000000000..e1e779581
--- /dev/null
+++ b/client/settings-view/views/inference-parameters-view.js
@@ -0,0 +1,125 @@
+/*
+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 UniformParameterView = require('./uniform-parameter-view');
+//templates
+let editUniformTemplate = require('../templates/editUniformParameters.pug');
+let viewUniformTemplate = 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.inferenceSettings;
+ this.stochssModel = attrs.stochssModel;
+ this.priorMethod = attrs.priorMethod;
+ if(!this.readOnly) {
+ this.collection.updateVariables(this.stochssModel.parameters);
+ }
+ },
+ render: function () {
+ 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.renderEditInferenceParameter();
+ this.toggleAddParameter();
+ this.toggleParameterCollectionError();
+ }else{
+ this.renderViewInferenceParameter();
+ }
+ },
+ 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.updateTargetOptions();
+ this.toggleAddParameter();
+ this.toggleParameterCollectionError();
+ },
+ renderEditInferenceParameter: function () {
+ if(this.editInferenceParameter) {
+ this.editInferenceParameter.remove();
+ }
+ let options = {"viewOptions": {
+ parent: this,
+ stochssParams: this.stochssModel.parameters
+ }}
+ 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
+ }}
+ var inferenceParameterView = UniformParameterView;
+ this.viewInferenceParameter = this.renderCollection(
+ this.collection,
+ inferenceParameterView,
+ this.queryByHook("view-mi-parameter-collection"),
+ options
+ );
+ },
+ toggleAddParameter: function () {
+ let disable = this.collection.length >= this.stochssModel.parameters.length;
+ $(this.queryByHook("add-mi-parameter")).prop('disabled', disable);
+ },
+ toggleParameterCollectionError: function () {
+ let errorMsg = $(this.queryByHook('mi-parameter-collection-error'));
+ if(this.collection.length <= 0) {
+ errorMsg.addClass('component-invalid');
+ errorMsg.removeClass('component-valid');
+ }else{
+ errorMsg.addClass('component-valid');
+ errorMsg.removeClass('component-invalid');
+ }
+ },
+ 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
new file mode 100644
index 000000000..d3cd32ecd
--- /dev/null
+++ b/client/settings-view/views/inference-settings-view.js
@@ -0,0 +1,377 @@
+/*
+StochSS is a platform for simulating biochemical systems
+Copyright (C) 2019-2022 StochSS developers.
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+let $ = require('jquery');
+let path = require('path');
+//support files
+let app = require('../../app');
+let 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');
+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');
+
+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=num-rounds]' : 'updateRoundsView',
+ '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',
+ 'change [data-hook=obs-data-location-select]' : 'selectObsDataLocation',
+ '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=preview-obs-data]' : 'handlePreviewObsData'
+ },
+ 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.obsFig = null;
+ this.chevrons = {
+ hide: `
+
+ `,
+ show: `
+
+ `
+ }
+ this.occordianStates = {import: "hide", select: "hide"}
+ 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);
+ 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.renderEditSummaryStats();
+ this.renderEditParameterSpace();
+ this.renderObsDataSelects();
+ }
+ this.renderViewSummaryStats();
+ this.renderViewParameterSpace();
+ },
+ 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.obsFig = null;
+ this.model.obsData = path.join(body.obsDataPath, body.obsDataFile);
+ 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);
+ this.errorAction(body.Message);
+ }
+ }, 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');
+ Plotly.newPlot(plotEl, this.obsFig);
+ },
+ renderEditParameterSpace: function () {
+ if(this.editParameterSpace) {
+ this.editParameterSpace.remove();
+ }
+ this.editParameterSpace = new InferenceParametersView({
+ collection: this.model.parameters,
+ stochssModel: this.stochssModel,
+ priorMethod: this.model.priorMethod
+ });
+ let hook = "edit-parameter-space-container";
+ app.registerRenderSubview(this, this.editParameterSpace, hook);
+ },
+ renderEditSummaryStats: function () {
+ if(this.editSummaryStats) {
+ this.editSummaryStats.remove();
+ }
+ this.editSummaryStats = new SummaryStatsView({
+ collection: this.model.summaryStats,
+ summariesType: this.model.summaryStatsType,
+ customCalculators: this.model.customCalculators
+ });
+ let hook = "edit-summary-stats-container";
+ app.registerRenderSubview(this, this.editSummaryStats, hook);
+ },
+ renderObsDataSelects: function () {
+ let queryStr = "?ext=.odf,.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 (index) {
+ if(this.obsDataLocationSelectView) {
+ this.obsDataLocationSelectView.remove();
+ }
+ let value = 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();
+ }
+ this.viewParameterSpace = new InferenceParametersView({
+ collection: this.model.parameters,
+ readOnly: true,
+ stochssModel: this.stochssModel,
+ priorMethod: this.model.priorMethod
+ });
+ 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);
+ },
+ selectObsDataFile: function (e) {
+ this.obsFig = null;
+ 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.model.obsData = "";
+ this.renderObsDataLocationSelectView(value);
+ }else{
+ this.model.obsData = this.obsDataFiles.paths[value][0];
+ }
+ }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) {
+ setTimeout(() => {
+ this.occordianStates.select = "hide";
+ this.occordianStates.import = this.occordianStates.import === "hide" ? "show" : "hide";
+ $(this.queryByHook('upload-chevron')).html(this.chevrons.hide);
+ $(this.queryByHook('import-chevron')).html(this.occordianStates.import === "show" ? this.chevrons.show : this.chevrons.hide);
+ });
+ },
+ toggleUploadFiles: function (e) {
+ setTimeout(() => {
+ this.occordianStates.import = "hide";
+ this.occordianStates.select = this.occordianStates.select === "hide" ? "show" : "hide";
+ $(this.queryByHook('import-chevron')).html(this.chevrons.hide);
+ $(this.queryByHook('upload-chevron')).html(this.occordianStates.select === "show" ? this.chevrons.show : this.chevrons.hide);
+ });
+ },
+ update: function (e) {},
+ updateValid: function (e) {},
+ 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: {
+ numRoundsInputView: {
+ hook: "num-rounds",
+ prepareView: function (el) {
+ return new InputView({
+ parent: this,
+ required: true,
+ name: 'number-of-rounds',
+ tests: tests.valueTests,
+ modelKey: 'numRounds',
+ valueType: 'number',
+ value: this.model.numRounds
+ });
+ }
+ },
+ 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) {
+ let options = [
+ ["identity", "Identity"], ["minimal", "TSFresh Minimal"], ["custom", "Custom TSFresh"]
+ ]
+ return new SelectView({
+ name: 'summary-statistics-type',
+ required: true,
+ eagerValidate: true,
+ options: options,
+ value: this.model.summaryStatsType
+ });
+ }
+ }
+ }
+});
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 000000000..7b398932f
--- /dev/null
+++ b/client/settings-view/views/summary-stats-view.js
@@ -0,0 +1,127 @@
+/*
+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 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 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) {
+ View.prototype.initialize.apply(this, arguments);
+ 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") {
+ this.template = this.readOnly ? viewIdentityTemplate : editIdentityTemplate;
+ }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) {
+ this.renderEditSummaryStat();
+ }else{
+ this.renderViewSummaryStat();
+ }
+ },
+ 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 === "minimal") {
+ this.renderMinimalSummaryStats();
+ }else{
+ if(this.summariesType === "identity") {
+ var summaryStatView = IdentitySummaryStatView;
+ }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
+ );
+ }
+ },
+ 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();
+ }
+ 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);
+ }
+ },
+ updateViewer: function () {
+ this.parent.renderViewSummaryStats();
+ }
+});
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 000000000..a282efac0
--- /dev/null
+++ b/client/settings-view/views/uniform-parameter-view.js
@@ -0,0 +1,147 @@
+/*
+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.parent.updateTargetOptions();
+ this.parent.toggleAddParameter();
+ this.parent.toggleParameterCollectionError();
+ this.remove();
+ },
+ 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();
+ }
+});
diff --git a/client/styles/styles.css b/client/styles/styles.css
index c6eb3bbd9..b0d830753 100644
--- a/client/styles/styles.css
+++ b/client/styles/styles.css
@@ -428,6 +428,10 @@ input[type="file"]::-ms-browse {
margin: 0.5rem auto;
}
+.modal-dialog.preview-plot {
+ max-width: 1000px;
+}
+
.modal-backdrop {
width: 100%;
height: 100%;
@@ -442,6 +446,10 @@ input[type="file"]::-ms-browse {
width: 60%;
}
+.modal-content.preview-plot {
+ max-width: 1000px;
+}
+
.spinner-border {
display: none;
position: relative;
@@ -849,3 +857,31 @@ input:checked + .slider:before {
.slider.round:before {
border-radius: 50%;
}
+
+.inference-cpv-line {
+ margin-left: 19%;
+}
+
+.inference-cpv-line:before {
+ content: "";
+ display: block;
+ width: 15%;
+ border-top: 4px solid black;
+ position: absolute;
+ left: 15px;
+ top: 40%;
+}
+
+.inference-ipv-line {
+ margin-left: 10%;
+}
+
+.inference-ipv-line:before {
+ content: "";
+ display: block;
+ width: 8%;
+ border-top: 4px dashed black;
+ position: absolute;
+ left: 15px;
+ top: 40%;
+}
\ No newline at end of file
diff --git a/client/templates/includes/archiveListing.pug b/client/templates/includes/archiveListing.pug
index 0e3838b49..1605952df 100644
--- a/client/templates/includes/archiveListing.pug
+++ b/client/templates/includes/archiveListing.pug
@@ -1,6 +1,6 @@
div
- div.row
+ div.ml-1.row
div.col-md-11
@@ -10,7 +10,7 @@ div
button.btn.btn-outline-secondary.box-shadow(data-hook=this.model.elementID + "-remove")
- div.ml-2
+ div.ml-2.mb-3.pl-2
div
diff --git a/client/templates/includes/modelListing.pug b/client/templates/includes/modelListing.pug
index c369e2092..453704fc7 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 eb5f735f4..638783734 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
@@ -49,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")
@@ -62,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/templates/pages/modelEditor.pug b/client/templates/pages/modelEditor.pug
index 306bb6e24..1e27e5c85 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/client/tooltips.js b/client/tooltips.js
index 816537794..88d284e9e 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,26 @@ 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}' +
+ '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",
diff --git a/client/views/input.js b/client/views/input.js
index e510b2f0a..cdda3e42e 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/client/views/jstree-view.js b/client/views/jstree-view.js
index a11020125..f7fd33f6d 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)
}
});
@@ -781,11 +787,11 @@ module.exports = View.extend({
});
locations.unshift(``);
locations = locations.join(" ");
- $("#modelPathSelect").find('option').remove().end().append(locations);
+ $(location).find('option').remove().end().append(locations);
$("#location-container").css("display", "block");
}else{
$("#location-container").css("display", "none");
- $("#modelPathSelect").find('option').remove().end();
+ $(location).find('option').remove().end();
}
});
location.addEventListener("change", (e) => {
diff --git a/client/views/main.js b/client/views/main.js
index f81ef9fa6..cec56b031 100644
--- a/client/views/main.js
+++ b/client/views/main.js
@@ -69,6 +69,14 @@ module.exports = View.extend({
$("#presentation-nav-link").css("display", "none");
}
this.setupUserLogs();
+ let endpoint = path.join(app.getApiPath(), "load-user-settings");
+ app.getXHR(endpoint, {
+ always: (err, response, body) => {
+ if(!body.settings.userLogs) {
+ this.closeUserLogs();
+ }
+ }
+ });
return this;
},
addNewLogBlock: function () {
@@ -133,11 +141,13 @@ module.exports = View.extend({
let logs = $("#user-logs");
let classes = logs.attr("class").split(/\s+/);
if(classes.includes("show")) {
+ $(this.queryByHook('close-user-logs')).css('display', 'block');
logs.removeClass("show");
$(this.queryByHook(e.target.dataset.hook)).html("+");
$(".user-logs").removeClass("expand-logs");
$(".side-navbar").css("z-index", 0);
}else{
+ $(this.queryByHook('close-user-logs')).css('display', 'none');
logs.addClass("show");
$(this.queryByHook(e.target.dataset.hook)).html("-");
if($(".sidebar-sticky").css("position") === "fixed") {
diff --git a/client/views/meta-data.js b/client/views/meta-data.js
index b683d404f..a0da12ada 100644
--- a/client/views/meta-data.js
+++ b/client/views/meta-data.js
@@ -36,7 +36,7 @@ let template = require('../templates/includes/metaData.pug');
module.exports = View.extend({
template: template,
events: {
- 'change [data-hook=file-select-view]' : 'updateMetaData',
+ 'change [data-hook=file-select-view]' : 'handleUpdateMetaData',
'change [data-hook=description-input-view]' : 'updateFileDescription',
'change [data-hook=creator-select-view]' : 'updateCreator',
'change [data-hook=email-input-view]' : 'toggleAddCreatorBtn',
@@ -46,13 +46,14 @@ module.exports = View.extend({
},
initialize: function(attrs, options) {
View.prototype.initialize.apply(this, arguments);
- this.files = this.model.workflowGroups.map(function (wkgp) {
+ this.files = this.model.workflowGroups.map((wkgp) => {
return wkgp.name;
});
this.files.unshift(this.model.name);
+ this.selectedFile = this.model.name
this.metadata = this.model.metadata;
this.selectedCreator = "New Creator";
- this.model.creators.forEach(function (creator) {
+ this.model.creators.forEach((creator) => {
if(!creator.elementID) {
creator.elementID = "C" + (creator.collection.indexOf(creator) + 1);
}
@@ -96,13 +97,16 @@ module.exports = View.extend({
this.addCreator("existing", creator);
}
},
+ handleUpdateMetaData: function (e) {
+ this.selectedFile = e.target.selectedOptions.item(0).text;
+ this.updateMetaData();
+ },
renderCreatorListingView: function () {
if(this.creatorListingView) {
this.creatorListingView.remove();
}
- let self = this;
- let listOfCreators = this.metadata.creators.map(function (key) {
- return self.model.creators.get(key, "elementID");
+ let listOfCreators = this.metadata.creators.map((key) => {
+ return this.model.creators.get(key, "elementID");
});
let creators = new Creators(listOfCreators);
this.creatorListingView = this.renderCollection(creators, CreatorListingView, this.queryByHook("list-of-creators"));
@@ -163,16 +167,15 @@ module.exports = View.extend({
textAttribute: 'name',
eagerValidate: true,
options: this.files,
- value: this.model.name
+ value: this.selectedFile
});
app.registerRenderSubview(this, this.filesSelectView, "file-select-view");
},
saved: function () {
$(this.queryByHook('md-in-progress')).css("display", "none");
$(this.queryByHook('md-complete')).css("display", "inline-block");
- let self = this;
- setTimeout(function () {
- $(self.queryByHook("md-complete")).css("display", "none");
+ setTimeout(() => {
+ $(this.queryByHook("md-complete")).css("display", "none");
}, 5000);
},
saving: function () {
@@ -183,14 +186,13 @@ module.exports = View.extend({
this.saving();
let data = {}
data[this.model.directory] = {"metadata":this.model.metadata, "creators":this.model.creators};
- this.model.workflowGroups.forEach(function (wkgp) {
+ this.model.workflowGroups.forEach((wkgp) => {
data[wkgp.name + ".wkgp"] = {"metadata": wkgp.metadata};
});
- let self = this;
let endpoint = path.join(app.getApiPath(), "project/meta-data")+"?path="+this.model.directory;
app.postXHR(endpoint, data, {
- success: function (err, response, body) {
- self.saved();
+ success: (err, response, body) => {
+ this.saved();
}
});
},
@@ -212,13 +214,23 @@ module.exports = View.extend({
$(this.queryByHook("add-creator-btn")).text("Add");
}
},
- updateMetaData: function (e) {
- let selectedFile = e.target.selectedOptions.item(0).text;
- this.metadata = selectedFile !== this.model.name ? this.model.workflowGroups.filter(function (wkgp) {
- return wkgp.name === selectedFile;
+ updateMetaData: function () {
+ this.metadata = this.selectedFile !== this.model.name ? this.model.workflowGroups.filter((wkgp) => {
+ return wkgp.name === this.selectedFile;
})[0].metadata : this.model.metadata;
$(this.queryByHook("description-input-view")).val(this.metadata.description);
this.renderCreatorListingView();
},
- updateValid: function () {}
+ updateValid: function () {},
+ updateView: function () {
+ this.files = this.model.workflowGroups.map((wkgp) => {
+ return wkgp.name;
+ });
+ this.files.unshift(this.model.name);
+ if(!this.files.includes(this.selectedFile)) {
+ this.selectedFile = this.model.name;
+ this.updateMetaData();
+ }
+ this.renderFilesSelectView();
+ }
});
\ No newline at end of file
diff --git a/client/views/model-listing.js b/client/views/model-listing.js
index c4b9756b5..0c827b71a 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 820bf4ba0..23c15c9fd 100644
--- a/client/views/workflow-group-listing.js
+++ b/client/views/workflow-group-listing.js
@@ -36,12 +36,20 @@ 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;
},
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);
@@ -50,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);
@@ -58,8 +69,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/jupyterhub/model_presentation.py b/jupyterhub/model_presentation.py
index 825be8b89..53725a37d 100644
--- a/jupyterhub/model_presentation.py
+++ b/jupyterhub/model_presentation.py
@@ -18,6 +18,7 @@
import os
import ast
import json
+import hashlib
import logging
import traceback
@@ -106,7 +107,7 @@ async def get(self, owner, file):
self.finish()
-def process_wmmodel_presentation(path, file=None, for_download=False):
+def process_wmmodel_presentation(path, for_download=False, **kwargs):
'''
Get the model presentation data from the file.
@@ -114,8 +115,6 @@ def process_wmmodel_presentation(path, file=None, for_download=False):
----------
path : str
Path to the model presentation file.
- file : str
- Name of the presentation file.
for_download : bool
Whether or not the model presentation is being downloaded.
'''
@@ -128,7 +127,7 @@ def process_wmmodel_presentation(path, file=None, for_download=False):
file_obj.print_logs(log)
return model_pres
-def process_smodel_presentation(path, file=None, for_download=False):
+def process_smodel_presentation(path, for_download=False, **kwargs):
'''
Get the model presentation data from the file.
@@ -136,8 +135,6 @@ def process_smodel_presentation(path, file=None, for_download=False):
----------
path : str
Path to the model presentation file.
- file : str
- Name of the presentation file.
for_download : bool
Whether or not the model presentation is being downloaded.
'''
@@ -157,6 +154,24 @@ def process_smodel_presentation(path, file=None, for_download=False):
file_obj.print_logs(log)
return model_pres
+template = {
+ "is_spatial": False, "defaultID": 1, "defaultMode": "", "annotation": "", "volume": 1,
+ "modelSettings": {"endSim": 20, "timeStep": 0.05, "timestepSize": 1e-5 },
+ "domain": {
+ "actions": [],
+ "boundary_condition": {"reflect_x": True, "reflect_y": True, "reflect_z": True},
+ "c_0": 10, "gravity": [0, 0, 0], "p_0": 100.0, "rho_0": 1.0, "shapes": [],
+ "size": None, "static": True, "template_version": 2, "transformations": [],
+ "types": [{
+ "c":10, "fixed":False, "mass":1.0, "name":"Un-Assigned",
+ "nu":0.0, "rho":1.0, "typeID":0, "volume":1.0
+ }],
+ "x_lim": [0, 0], "y_lim": [0, 0], "z_lim": [0, 0]
+ },
+ "species": [], "initialConditions": [], "parameters": [], "reactions": [], "rules": [],
+ "eventsCollection": [], "functionDefinitions": [], "boundaryConditions": []
+}
+
class StochSSModel(StochSSBase):
'''
################################################################################################
@@ -215,6 +230,8 @@ def __update_reactions(self):
if "reactions" not in self.model.keys():
return
for reaction in self.model['reactions']:
+ if "odePropensity" not in reaction.keys():
+ reaction['odePropensity'] = reaction['propensity']
try:
if reaction['rate'].keys() and isinstance(reaction['rate']['expression'], str):
expression = ast.literal_eval(reaction['rate']['expression'])
@@ -237,13 +254,10 @@ def __update_rules(self, param_ids):
except ValueError:
pass
- def load(self):
- '''
- Reads the model file, updates the model to the current format, and stores it in self.model
+ def __update_model_to_current(self):
+ if self.model['template_version'] == self.TEMPLATE_VERSION:
+ return
- Attributes
- ----------
- '''
if "annotation" not in self.model.keys():
self.model['annotation'] = ""
if "volume" not in self.model.keys():
@@ -251,11 +265,29 @@ 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)
- return {"model": self.model}
+
+ if "refLinks" not in self.model.keys():
+ self.model['refLinks'] = []
+
+ self.model['template_version'] = self.TEMPLATE_VERSION
+
+ def load(self):
+ '''
+ Reads the model file, updates the model to the current format, and stores it in self.model
+
+ Attributes
+ ----------
+ '''
+ if "template_version" not in self.model:
+ self.model['template_version'] = 0
+ self.__update_model_to_current()
+
+ return {"model": self.model, "diff": self.diff}
class StochSSSpatialModel(StochSSBase):
@@ -294,10 +326,8 @@ def __init__(self):
pass
def inside(self, point, on_boundary): # pylint: disable=no-self-use
- '''
- Custom inside function for geometry
- '''
- namespace = {'x': point[0], 'y': point[1], 'z': point[2]}
+ ''' Custom inside function for geometry. '''
+ namespace = {'x': point[0], 'y': point[1], 'z': point[2], 'on_boundary': on_boundary}
return eval(formula, {}, namespace)
return NewGeometry()
@@ -317,37 +347,31 @@ def __build_stochss_domain_particles(cls, domain):
return particles
def __convert_actions(self, domain, s_domain, type_ids):
- geometries = self.__convert_geometries(s_domain)
- lattices = self.__convert_lattices(s_domain)
- transformations = self.__convert_transformations(s_domain, geometries, lattices)
+ geometries, lattices = self.__convert_shapes(s_domain)
+ transformations = self.__convert_transformations(s_domain)
try:
- actions = sorted(s_domain['actions'], key=lambda action: action['priority'])
+ actions = list(filter(lambda action: action['enable'], s_domain['actions']))
+ actions = sorted(actions, key=lambda action: action['priority'])
for i, action in enumerate(actions):
- # Get geometry. 'Multi Particle' scope uses geometry from action
- if action['geometry'] == "":
- geometry = None
- elif action['geometry'] in geometries:
- geometry = geometries[action['geometry']]
- else:
- geometry = transformations[action['geometry']]
# Build props arg
- if action['type'] in ('Fill Action', 'Set Action'):
+ if action['type'] in ('Fill Action', 'Set Action', 'XML Mesh', 'Mesh IO'):
kwargs = {
- 'type_id': type_ids[action['typeID']], 'mass': action['mass'],
- 'vol': action['vol'], 'rho': action['rho'], 'nu': action['nu'],
- 'c': action['c'], 'fixed': action['fixed']
+ 'mass': action['mass'], 'vol': action['vol'], 'rho': action['rho'],
+ 'nu': action['nu'], 'c': action['c'], 'fixed': action['fixed']
}
+ if action['type'] in ('Fill Action', 'Set Action'):
+ kwargs['type_id'] = type_ids[action['typeID']].replace("-", "")
else:
kwargs = {}
# Apply actions
if action['type'] == "Fill Action":
- if not action['useProps']:
- kwargs = {}
if action['scope'] == 'Multi Particle':
- if action['lattice'] in lattices:
- lattice = lattices[action['lattice']]
+ geometry = geometries[f"{action['shape']}_geom"]
+ if action['transformation'] == "":
+ lattice = lattices[f"{action['shape']}_latt"]
else:
- lattice = transformations[action['lattice']]
+ lattice = transformations[action['transformation']]
+ lattice.lattice = lattices[f"{action['shape']}_latt"]
_ = domain.add_fill_action(
lattice=lattice, geometry=geometry, enable=action['enable'],
apply_action=action['enable'], **kwargs
@@ -355,6 +379,20 @@ def __convert_actions(self, domain, s_domain, type_ids):
else:
point = [action['point']['x'], action['point']['y'], action['point']['z']]
domain.add_point(point, **kwargs)
+ elif action['type'] in ('XML Mesh', 'Mesh IO', 'StochSS Domain'):
+ lattices = {
+ 'XML Mesh': spatialpy.XMLMeshLattice, 'Mesh IO': spatialpy.MeshIOLattice,
+ 'StochSS Domain': spatialpy.StochSSLattice
+ }
+ filename = os.path.join(self.user_dir, action['filename'])
+ if action['type'] == "StochSS Domain" or action['subdomainFile'] == "":
+ lattice = lattices[action['type']](filename)
+ else:
+ subdomain_file = os.path.join(self.user_dir, action['subdomainFile'])
+ lattice = lattices[action['type']](filename, subdomain_file=subdomain_file)
+ _ = domain.add_fill_action(
+ lattice=lattice, enable=action['enable'], apply_action=action['enable'], **kwargs
+ )
else:
# Get proper geometry for scope
# 'Single Particle' scope creates a geometry using actions point.
@@ -366,6 +404,11 @@ def __convert_actions(self, domain, s_domain, type_ids):
geometry = self.__build_geometry(
None, name=f"SPAGeometry{i + 1}", formula=formula
)
+ elif action['transformation'] == "":
+ geometry = geometries[f"{action['shape']}_geom"]
+ else:
+ geometry = transformations[action['transformation']]
+ geometry.geometry = geometries[f"{action['shape']}_geom"]
if action['type'] == "Set Action":
domain.add_set_action(
geometry=geometry, enable=action['enable'],
@@ -405,9 +448,7 @@ def __convert_domain(self, type_ids, s_domain):
gravity = s_domain['gravity']
if gravity == [0, 0, 0]:
gravity = None
- domain = spatialpy.Domain(
- 0, xlim, ylim, zlim, rho0=rho0, c0=c_0, P0=p_0, gravity=gravity
- )
+ domain = spatialpy.Domain(0, xlim, ylim, zlim, rho0=rho0, c0=c_0, P0=p_0, gravity=gravity)
self.__convert_actions(domain, s_domain, type_ids)
self.__convert_types(domain, type_ids)
return domain
@@ -416,122 +457,114 @@ def __convert_domain(self, type_ids, s_domain):
message += f"are referenced incorrectly: {str(err)}"
raise StochSSModelFormatError(message, traceback.format_exc()) from err
- @classmethod
- def __convert_types(cls, domain, type_ids):
- domain.typeNdxMapping = {"type_UnAssigned": 0}
- domain.typeNameMapping = {0: "type_UnAssigned"}
- domain.listOfTypeIDs = [0]
- for ndx, name in type_ids.items():
- name = f"type_{name}"
- domain.typeNdxMapping[name] = ndx
- domain.typeNameMapping[ndx] = name
- domain.listOfTypeIDs.append(ndx)
-
- def __convert_geometries(self, s_domain):
+ def __convert_shapes(self, s_domain):
try:
geometries = {}
comb_geoms = []
- for s_geometry in s_domain['geometries']:
- if s_geometry['type'] == "Standard Geometry":
- geometries[s_geometry['name']] = self.__build_geometry(s_geometry)
+ lattices = {}
+ for s_shape in s_domain['shapes']:
+ # Create geometry from shape
+ geo_name = f"{s_shape['name']}_geom"
+ if s_shape['type'] == "Standard":
+ if s_shape['formula'] in ("", "True"):
+ geometries[geo_name] = spatialpy.GeometryAll()
+ elif s_shape['formula'] == "on_boundary":
+ geometries[geo_name] = spatialpy.GeometryExterior()
+ elif s_shape['formula'] == "not on_boundary":
+ geometries[geo_name] = spatialpy.GeometryInterior()
+ else:
+ geometries[geo_name] = self.__build_geometry(None, name=geo_name, formula=s_shape['formula'])
else:
- name = s_geometry['name']
- comb_geometry = spatialpy.CombinatoryGeometry("", {})
- comb_geometry.formula = s_geometry['formula']
- geometries[name] = comb_geometry
- comb_geoms.append(name)
+ geometry = spatialpy.CombinatoryGeometry("", {})
+ geometry.formula = s_shape['formula']
+ geometries[geo_name] = geometry
+ comb_geoms.append(geo_name)
+ # Create lattice from shape if fillable
+ if s_shape['fillable']:
+ lat_name = f"{s_shape['name']}_latt"
+ if s_shape['lattice'] == "Cartesian Lattice":
+ half_length = s_shape['length'] / 2
+ half_height = s_shape['height'] / 2
+ half_depth = s_shape['depth'] / 2
+ lattice = spatialpy.CartesianLattice(
+ -half_length, half_length, s_shape['deltax'],
+ ymin=-half_height, ymax=half_height, deltay=s_shape['deltay'],
+ zmin=-half_depth, zmax=half_depth, deltaz=s_shape['deltaz']
+ )
+ elif s_shape['lattice'] == "Spherical Lattice":
+ lattice = spatialpy.SphericalLattice(
+ s_shape['radius'], s_shape['deltas'], deltar=s_shape['deltar']
+ )
+ elif s_shape['lattice'] == "Cylindrical Lattice":
+ lattice = spatialpy.CylindricalLattice(
+ s_shape['radius'], s_shape['length'], s_shape['deltas'], deltar=s_shape['deltar']
+ )
+ lattices[lat_name] = lattice
+ items = [' and ', ' or ', ' not ', '(', ')']
for name in comb_geoms:
- geo_namespace = {
- key: geometry for key, geometry in geometries.items() if key != name
- }
+ formula = geometries[name].formula
+ if formula.startswith("not "):
+ formula = formula.replace("not ", "")
+ for item in items:
+ formula = formula.replace(item, " ")
+ formula = formula.split(" ")
+ geo_namespace = {}
+ for key, geometry in geometries.items():
+ if key != name and key[:-5] in formula:
+ geo_namespace[key[:-5]] = geometry
geometries[name].geo_namespace = geo_namespace
- return geometries
+ return geometries, lattices
except KeyError as err:
- message = "Spatial geometries are not properly formatted or "
+ message = "Spatial domain shapes are not properly formatted or "
message += f"are referenced incorrectly: {str(err)}"
raise StochSSModelFormatError(message, traceback.format_exc()) from err
@classmethod
- def __convert_lattices(cls, s_domain):
- try:
- lattices = {}
- for s_lattice in s_domain['lattices']:
- name = s_lattice['name']
- center = [
- s_lattice['center']['x'], s_lattice['center']['y'], s_lattice['center']['z']
- ]
- if s_lattice['type'] == "Cartesian Lattice":
- lattice = spatialpy.CartesianLattice(
- s_lattice['xmin'], s_lattice['xmax'], s_lattice['deltax'], center=center,
- ymin=s_lattice['ymin'], ymax=s_lattice['ymax'], deltay=s_lattice['deltay'],
- zmin=s_lattice['zmin'], zmax=s_lattice['zmax'], deltaz=s_lattice['deltaz']
- )
- elif s_lattice['type'] == "Spherical Lattice":
- lattice = spatialpy.SphericalLattice(
- s_lattice['radius'], s_lattice['deltas'],
- center=center, deltar=s_lattice['deltar']
- )
- elif s_lattice['type'] == "Cylindrical Lattice":
- lattice = spatialpy.CylindricalLattice(
- s_lattice['radius'], s_lattice['length'], s_lattice['deltas'],
- center=center, deltar=s_lattice['deltar']
- )
- elif s_lattice['type'] == "XML Mesh Lattice":
- lattice = spatialpy.XMLMeshLattice(
- s_lattice['filename'], center=center,
- subdomain_file=s_lattice['subdomainFile']
- )
- elif s_lattice['type'] == "Mesh IO Lattice":
- lattice = spatialpy.MeshIOLattice(
- s_lattice['filename'], center=center,
- subdomain_file=s_lattice['subdomainFile']
- )
- else:
- lattice = spatialpy.StochSSLattice(s_lattice['filename'], center=center)
- lattices[name] = lattice
- return lattices
- except KeyError as err:
- message = "Spatial lattices are not properly formatted or "
- message += f"are referenced incorrectly: {str(err)}"
- raise StochSSModelFormatError(message, traceback.format_exc()) from err
+ def __convert_types(cls, domain, type_ids):
+ domain.typeNdxMapping = {"type_UnAssigned": 0}
+ domain.typeNameMapping = {0: "type_UnAssigned"}
+ domain.listOfTypeIDs = [0]
+ for ndx, name in type_ids.items():
+ if ndx not in domain.typeNameMapping:
+ name = f"type_{name}"
+ domain.typeNdxMapping[name] = ndx
+ domain.typeNameMapping[ndx] = name
+ domain.listOfTypeIDs.append(ndx)
+ types = list(set(domain.type_id))
+ for name in types:
+ if name not in domain.typeNdxMapping:
+ ndx = len(domain.typeNdxMapping)
+ domain.typeNdxMapping[name] = ndx
+ domain.typeNameMapping[ndx] = name
+ domain.listOfTypeIDs.append(ndx)
@classmethod
- def __convert_transformations(cls, s_domain, geometries, lattices):
+ def __convert_transformations(cls, s_domain):
try:
transformations = {}
nested_trans = {}
for s_transformation in s_domain['transformations']:
name = s_transformation['name']
- vector = [
- [
- s_transformation['vector'][0]['x'],
- s_transformation['vector'][0]['y'],
- s_transformation['vector'][0]['z']
- ],
- [
- s_transformation['vector'][1]['x'],
- s_transformation['vector'][1]['y'],
- s_transformation['vector'][1]['z']
- ]
- ]
- if s_transformation['geometry'] != "":
- geometry = geometries[s_transformation['geometry']]
- else:
- geometry = None
- if s_transformation['lattice'] != "":
- lattice = lattices[s_transformation['lattice']]
- else:
- lattice = None
if s_transformation['transformation'] != "":
nested_trans[name] = s_transformation['transformation']
- if s_transformation['type'] == "Translate Transformation":
- transformation = spatialpy.TranslationTransformation(
- vector, geometry=geometry, lattice=lattice
- )
- elif s_transformation['type'] == "Rotate Transformation":
- transformation = spatialpy.RotationTransformation(
- vector, s_transformation['angle'], geometry=geometry, lattice=lattice
- )
+ if s_transformation['type'] in ("Translate Transformation", "Rotate Transformation"):
+ vector = [
+ [
+ s_transformation['vector'][0]['x'],
+ s_transformation['vector'][0]['y'],
+ s_transformation['vector'][0]['z']
+ ],
+ [
+ s_transformation['vector'][1]['x'],
+ s_transformation['vector'][1]['y'],
+ s_transformation['vector'][1]['z']
+ ]
+ ]
+
+ if s_transformation['type'] == "Translate Transformation":
+ transformation = spatialpy.TranslationTransformation(vector)
+ else:
+ transformation = spatialpy.RotationTransformation(vector, s_transformation['angle'])
elif s_transformation['type'] == "Reflect Transformation":
normal = numpy.array([
s_transformation['normal']['x'], s_transformation['normal']['y'],
@@ -556,18 +589,14 @@ def __convert_transformations(cls, s_domain, geometries, lattices):
else:
normal = None
transformation = spatialpy.ReflectionTransformation(
- point1, normal=normal, point2=point2, point3=point3,
- geometry=geometry, lattice=lattice
+ point1, normal=normal, point2=point2, point3=point3
)
else:
center = numpy.array([
s_transformation['center']['x'], s_transformation['center']['y'],
s_transformation['center']['z']
])
- transformation = spatialpy.ScalingTransformation(
- s_transformation['factor'], center=center,
- geometry=geometry, lattice=lattice
- )
+ transformation = spatialpy.ScalingTransformation(s_transformation['factor'], center=center)
transformations[name] = transformation
for trans, nested_tran in nested_trans.items():
transformations[trans].transformation = transformations[nested_tran]
@@ -578,7 +607,7 @@ def __convert_transformations(cls, s_domain, geometries, lattices):
raise StochSSModelFormatError(message, traceback.format_exc()) from err
@classmethod
- def __get_trace_data(cls, particles, name="", index=None):
+ def __get_trace_data(cls, particles, name="", index=None, dimensions=3):
common_rgb_values = [
'#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2', '#7f7f7f',
'#bcbd22', '#17becf', '#ff0000', '#00ff00', '#0000ff', '#ffff00', '#00ffff', '#ff00ff',
@@ -600,8 +629,13 @@ def __get_trace_data(cls, particles, name="", index=None):
marker = {"size":5}
if index is not None:
marker["color"] = common_rgb_values[(index) % len(common_rgb_values)]
- return plotly.graph_objs.Scatter3d(ids=ids, x=x_data, y=y_data, z=z_data,
- name=name, mode="markers", marker=marker)
+ if dimensions == 2:
+ return plotly.graph_objs.Scatter(
+ ids=ids, x=x_data, y=y_data, name=name, mode="markers", marker=marker
+ )
+ return plotly.graph_objs.Scatter3d(
+ ids=ids, x=x_data, y=y_data, z=z_data, name=name, mode="markers", marker=marker
+ )
def __update_domain_to_current(self, domain=None):
domain = self.model['domain']
@@ -637,28 +671,21 @@ def __update_domain_to_current(self, domain=None):
'pres_path': filename
}
- geometries = []
+ shapes = []
for d_type in domain['types']:
if 'geometry' in d_type and d_type['geometry']:
- geometries.append({
- 'name': f"geometry{len(geometries) + 1}",
- 'type': 'Standard Geometry',
- 'formula': d_type['geometry']
+ shapes.append({
+ 'deltar': 0, 'deltas': 0, 'deltax': 0, 'deltay': 0, 'deltaz': 0, 'depth': 0, 'fillable': False,
+ 'formula': d_type['geometry'], 'height': 0, 'lattice': 'Cartesian Lattice',
+ 'length': 0, 'name': f"shape{len(shapes) + 1}", 'radius': 0, 'type': 'Standard'
})
domain['actions'] = [{
- 'type': 'Fill Action', 'scope': 'Multi Particle', 'priority': 1, 'enable': True,
- 'geometry': '', 'lattice': 'lattice1', 'useProps': False,
+ 'type': 'StochSS Domain', 'scope': 'Multi Particle', 'priority': 1, 'enable': True, 'shape': '',
+ 'transformation': '', 'filename': filename.replace(f'{self.user_dir}/', ''), 'subdomainFile': '',
'point': {'x': 0, 'y': 0, 'z': 0}, 'newPoint': {'x': 0, 'y': 0, 'z': 0},
'c': 10, 'fixed': False, 'mass': 1.0, 'nu': 0.0, 'rho': 1.0, 'typeID': 0, 'vol': 0.0
}]
- domain['geometries'] = geometries
- domain['lattices'] = [{
- 'name': 'lattice1', 'type': 'StochSS Lattice',
- 'filename': filename.replace(f'{self.user_dir}/', ''), 'subdomainFile': '',
- 'center': {'x': 0, 'y': 0, 'z': 0}, 'length': 0, 'radius': 0,
- 'deltar': 0,'deltas': 0,'deltax': 0,'deltay': 0,'deltaz': 0,
- 'xmax': 0,'xmin': 0,'ymax': 0,'ymin': 0,'zmax': 0,'zmin': 0
- }]
+ domain['shapes'] = shapes
domain['transformations'] = []
domain['template_version'] = self.DOMAIN_TEMPLATE_VERSION
@@ -723,17 +750,15 @@ def __update_model_to_current(self):
@classmethod
def get_presentation(cls, model=None, files=None):
''' Get the presentation for download. '''
- # Check if the domain has lattices
- if len(model['domain']['lattices']) == 0:
- return {'model': model, 'files': files}
# Process file based lattices
- file_based_types = ('XML Mesh Lattice', 'Mesh IO Lattice', 'StochSS Lattice')
- for lattice in model['domain']['lattices']:
- if lattice['type'] in file_based_types:
- lattice['filename'] = files[lattice['name']]['dwn_path']
- if lattice['subdomainFile'] != "":
- entry = files[f"{lattice['name']}_sdf"]
- lattice['subdomainFile'] = entry['dwn_path']
+ file_based_types = ('XML Mesh', 'Mesh IO', 'StochSS Domain')
+ for action in model['domain']['actions']:
+ if action['type'] in file_based_types:
+ action_id = hashlib.md5(json.dumps(action, sort_keys=True, indent=4).encode('utf-8')).hexdigest()
+ action['filename'] = files[action_id]['dwn_path']
+ if action['subdomainFile'] != "":
+ entry = files[f"{action_id}_sdf"]
+ action['subdomainFile'] = entry['dwn_path']
return {'model': model, 'files': files}
def get_domain_plot(self, domain, s_domain):
@@ -751,7 +776,7 @@ def get_domain_plot(self, domain, s_domain):
# Case #3: 1 or more particles and one type
if len(s_domain['types']) == 1:
fig['data'][0]['name'] = "Un-Assigned"
- ids = list(filter(lambda particle: particle['particle_id'], s_domain['particles']))
+ ids = list(map(lambda particle: particle['particle_id'], s_domain['particles']))
fig['data'][0]['ids'] = ids
# Case #4: 1 or more particles and multiple types
else:
@@ -763,7 +788,7 @@ def get_domain_plot(self, domain, s_domain):
traces = list(filter(t_test, fig['data']))
if len(traces) == 0:
fig['data'].insert(index, self.__get_trace_data(
- particles=[], name=d_type['name'], index=index
+ particles=[], name=d_type['name'], index=index, dimensions=domain.dimensions
))
else:
particles = list(filter(
@@ -786,22 +811,25 @@ def load(self, v1_domain=False):
'''
if "template_version" not in self.model:
self.model['template_version'] = 0
+ self.__update_model_to_current()
+
if "template_version" not in self.model['domain']:
self.model['domain']['template_version'] = 0
-
- self.__update_model_to_current()
files = self.__update_domain_to_current()
+
if v1_domain:
return {'model': self.model, 'files': files}
- plot = self.load_action_preview()
- return {"model": self.model, "domainPlot": json.loads(plot)}
+ plot, limits = self.load_action_preview()
+ return {"model": self.model, "domainPlot": json.loads(plot), "domainLimits": limits}
def load_action_preview(self):
''' Get a domain preview of all enabled actions. '''
s_domain = self.model['domain']
types = sorted(s_domain['types'], key=lambda d_type: d_type['typeID'])
type_ids = {d_type['typeID']: d_type['name'] for d_type in types}
- domain = self.__convert_domain(type_ids, s_domain)
- # xlim, ylim, zlim = domain.get_bounding_box()
+ domain = self.__convert_domain(type_ids, s_domain=s_domain)
+ xlim, ylim, zlim = domain.get_bounding_box()
+ limits = [list(xlim), list(ylim), list(zlim)]
s_domain['particles'] = self.__build_stochss_domain_particles(domain)
- return self.get_domain_plot(domain, s_domain)#, [list(xlim), list(ylim), list(zlim)]
+ plot = self.get_domain_plot(domain, s_domain)
+ return plot, limits
diff --git a/package.json b/package.json
index 77a5bccd4..18a77692f 100644
--- a/package.json
+++ b/package.json
@@ -39,7 +39,7 @@
"jstree": "^3.3.8",
"katex": "^0.11.0",
"local-links": "^1.4.1",
- "plotly.js-dist": "^1.58.5",
+ "plotly.js-dist": "^2.20.0",
"popper.js": "^1.15.0",
"pug": "^2.0.3",
"underscore": "^1.9.1",
diff --git a/requirements.txt b/requirements.txt
index 671f6e35f..0f04c8eac 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,7 +3,7 @@ python-libsedml==2.0.9
python-libcombine==0.2.7
pyabc==0.12.6
escapism==1.0.1
-gillespy2==1.8.1
-spatialpy==1.2.1
-stochss-compute[AWS]==1.0.1
+gillespy2==1.8.2
+spatialpy==1.2.2
+stochss-compute[AWS]==1.0.2
git+https://github.com/StochSS/sciope.git@master
diff --git a/stochss/handlers/__init__.py b/stochss/handlers/__init__.py
index ba5eadd17..24133bd65 100644
--- a/stochss/handlers/__init__.py
+++ b/stochss/handlers/__init__.py
@@ -53,7 +53,7 @@ def get_page_handlers(route_start):
(r'/stochss/settings\/?', UserSettingsHandler),
(r'/stochss/api/user-logs\/?', UserLogsAPIHandler),
(r'/stochss/api/clear-user-logs\/?', ClearUserLogsAPIHandler),
- (r'/stochss/api/user-settings\/?', LoadUserSettings),
+ (r'/stochss/api/load-user-settings\/?', LoadUserSettings),
(r'/stochss/api/aws/job-config-check\/?', ConfirmAWSConfigHandler),
(r'/stochss/api/aws/launch-cluster\/?', LaunchAWSClusterHandler),
(r'/stochss/api/aws/cluster-status\/?', AWSClusterStatusHandler),
@@ -128,8 +128,12 @@ 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/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/__init__.py b/stochss/handlers/util/__init__.py
index c0b67a89e..5a3ddbd51 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
new file mode 100644
index 000000000..ea2eed8fd
--- /dev/null
+++ b/stochss/handlers/util/model_inference.py
@@ -0,0 +1,1448 @@
+'''
+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 json
+import shutil
+import pickle
+import logging
+import tempfile
+import traceback
+from collections import UserDict, UserList
+
+import numpy
+from scipy import stats
+
+try:
+ import plotly
+ from plotly import subplots
+ plotly_installed = True
+except ImportError:
+ plotly_installed = False
+import matplotlib.pyplot as plt
+import matplotlib.text as mtext
+from dask.distributed import Client
+
+import gillespy2
+
+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
+
+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'
+]
+
+def combine_colors(colors):
+ """
+ Combine two colors into one with concentrations of 50% of the original colors.
+
+ :param colors: Colors in hexidecimal form to combine.
+ :type colors: list(2)
+
+ :returns: Returns the new color in hexidecimal form.
+ :rtype: str
+ """
+ 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 LegendTitle(object):
+ """
+ Custom handler map for legend group titles.
+
+ :param text_props: \**kwargs: Keyword arguments passed to :py:class:`matplotlib.text`.
+ :type text_props: dict
+ """
+ def __init__(self, text_props=None):
+ self.text_props = text_props or {}
+ super(LegendTitle, self).__init__()
+
+ def legend_artist(self, legend, orig_handle, fontsize, handlebox):
+ """
+ Return the artist that this HandlerBase generates for the given original artist/handle.
+ Full documentation can be found here:
+ https://matplotlib.org/stable/api/legend_handler_api.html#matplotlib.legend_handler.HandlerBase.legend_artist
+ """
+ x0, y0 = handlebox.xdescent, handlebox.ydescent
+ title = mtext.Text(x0, y0, orig_handle, **self.text_props)
+ handlebox.add_artist(title)
+ return title
+
+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
+
+ :param inferred_method: Label for the method used to calculate the inferred parameters.
+ :type inferred_method: str
+ """
+ def __init__(self, accepted_samples, distances, accepted_count, trial_count, inferred_parameters, inferred_method):
+ super().__init__(accepted_samples)
+ self.distances = distances
+ self.accepted_count = accepted_count
+ self.trial_count = trial_count
+ self.inferred_method = inferred_method
+ self.__inferred_parameters = {inferred_method: inferred_parameters}
+
+ def __getattribute__(self, key):
+ # attribute_map = {
+ # 'accepted_samples': self.data,
+ # 'distances': self.distances,
+ # 'accepted_count': self.accepted_count,
+ # 'trial_count': self.trial_count,
+ # 'inferred_method': self.inferred_method,
+ # 'inferred_parameters': self.__inferred_parameters[self.inferred_method]
+ # }
+ # if key in attribute_map:
+ # return attribute_map[key]
+ if key == 'inferred_parameters':
+ return self.__inferred_parameters[self.inferred_method]
+ return UserList.__getattribute__(self, key)
+
+ def __getitem__(self, 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]
+ attribute_map = {
+ 'accepted_samples': self.data,
+ 'distances': self.distances,
+ 'accepted_count': self.accepted_count,
+ 'trial_count': self.trial_count,
+ 'inferred_method': self.inferred_method,
+ 'inferred_parameters': self.__inferred_parameters[self.inferred_method]
+ }
+ if key in attribute_map:
+ return attribute_map[key]
+ if hasattr(self.__class__, "__missing__"):
+ return self.__class__.__missing__(self, key)
+ raise KeyError(key)
+
+ def __plot(self, parameters, bounds, include_pdf=True, include_orig_values=True,
+ include_inferred_values=False, title=None, xaxis_label=None, yaxis_label=None):
+ nbins = 50
+ names = list(self.data.keys())
+ dims = len(names)
+ fig, axes = plt.subplots(nrows=dims, ncols=dims, figsize=[14, 14])
+ if xaxis_label is not None:
+ _ = fig.text(0.5, 0.09, xaxis_label, size=18, ha='center', va='center')
+ if yaxis_label is not None:
+ _ = fig.text(0.08, 0.5, yaxis_label, size=18, ha='center', va='center', rotation='vertical')
+
+ sing_param = []
+ doub_param = [(["Parameter Intersections"], [""])]
+ pdf_axes = [None] * dims
+ for i, (param1, accepted_values1) in enumerate(self.data.items()):
+ row = i
+ for j, (param2, accepted_values2) in enumerate(self.data.items()):
+ col = j
+ if i == 0:
+ axes[row, col].set_title(names[j], size=16)
+ if j == dims - 1:
+ axes[row, col].set_ylabel(names[i], size=16, rotation=270)
+ axes[row, col].yaxis.set_label_position("right")
+ axes[row, col].yaxis.set_label_coords(1.4, 0.5)
+
+ if i > j:
+ axes[row, col].axis('off')
+ elif i == j:
+ color = common_rgb_values[(i)%len(common_rgb_values)]
+ axes[row, col].hist(
+ accepted_values1, label="Histogram", color=color, alpha=0.75,
+ bins=nbins, range=(bounds[0][i], bounds[1][i])
+ )
+ axes[row, col].set_xlim(bounds[0][i], bounds[1][i])
+ sing_param.append(([param1], [""]))
+ sing_param.append(axes[row, col].get_legend_handles_labels())
+ if include_pdf:
+ mean, std = stats.norm.fit(accepted_values1)
+ points = numpy.linspace(min(accepted_values1), max(accepted_values1), 500)
+ pdf = stats.norm.pdf(points, loc=mean, scale=std)
+
+ pdf_axes[row] = axes[row, col].twinx()
+ pdf_axes[row].plot(points, pdf, label="PDF", color=color)
+ y_ticks = pdf_axes[row].get_yticks()
+ pdf_axes[row].set_ylim(0, max(y_ticks))
+ sing_param.append(pdf_axes[row].get_legend_handles_labels())
+ if include_orig_values:
+ axes[row, col].axvline(parameters[param1], alpha=0.75, color='black')
+ if include_inferred_values:
+ axes[row, col].axvline(
+ self.inferred_parameters[param1], alpha=0.75, color='black', ls='dashed'
+ )
+ else:
+ color = combine_colors([
+ common_rgb_values[(i)%len(common_rgb_values)][1:],
+ common_rgb_values[(j)%len(common_rgb_values)][1:]
+ ])
+ axes[row, col].scatter(accepted_values2, accepted_values1, c=color, label=f"{param2} X {param1}")
+ axes[row, col].set_xlim(bounds[0][j], bounds[1][j])
+ axes[row, col].set_ylim(bounds[0][i], bounds[1][i])
+ doub_param.append(axes[row, col].get_legend_handles_labels())
+
+ labels = numpy.array(sing_param)[:, 1, 0].tolist()
+ handles = numpy.array(sing_param)[:, 0, 0].tolist()
+ labels.extend(numpy.array(doub_param)[:, 1, 0].tolist())
+ handles.extend(numpy.array(doub_param)[:, 0, 0].tolist())
+ fig.legend(
+ handles, labels, loc=(0.05, 0.06), fontsize=12.5, frameon=False, markerscale=1.75,
+ handler_map={str: LegendTitle({'fontsize': 14})}
+ )
+ fig.subplots_adjust(left=0.1, right=0.9, wspace=0.5, hspace=0.25)
+ if title is not None:
+ _ = fig.text(0.5, 0.92, title, size=20, ha='center', va='center')
+
+ return fig
+
+ def __plot_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())
+
+ if xaxis_label is None:
+ xaxis_label = names[0]
+ if yaxis_label is None:
+ yaxis_label = names[1]
+
+ fig, axes = plt.subplots(
+ nrows=3, ncols=3, figsize=[14, 14], sharex='col', sharey='row',
+ gridspec_kw={'width_ratios': [6, 1, 3], 'height_ratios': [3, 1, 6]}
+ )
+ _ = fig.text(0.5, 0.09, xaxis_label, size=18, ha='center', va='center')
+ _ = fig.text(0.08, 0.5, yaxis_label, size=18, ha='center', va='center', rotation='vertical')
+ for row in range(2):
+ for col in range(1, 3):
+ axes[row, col].axis('off')
+
+ rug_symbol = ['|', '_']
+ histo_row = histo_col = [0, 2]
+ rug_row, rug_col = [1, 2], [0, 1]
+ x_key, y_key = ['x', 'y'], ['y', 'x']
+ orientation = ['vertical', 'horizontal']
+ line_func = [axes[histo_row[0], histo_col[0]].axvline, axes[histo_row[1], histo_col[1]].axhline]
+
+ rugs = [(["Rug"], [""])]
+ legend = []
+ pdf_axes = [None] * 2
+ for i, (param, orig_val) in enumerate(parameters.items()):
+ if i >= 2:
+ break
+
+ # Create histogram traces
+ axes[histo_row[i], histo_col[i]].hist(
+ self[param], label="Histogram", color=colors[i], alpha=0.75, orientation=orientation[i],
+ bins=nbins, range=(bounds[0][i], bounds[1][i])
+ )
+ legend.append(([param], [""]))
+ legend.append(axes[histo_row[i], histo_col[i]].get_legend_handles_labels())
+ if include_pdf:
+ mean, std = stats.norm.fit(self[param])
+ points = numpy.linspace(min(self[param]), max(self[param]), 500)
+ pdf = stats.norm.pdf(points, loc=mean, scale=std)
+
+ if i == 0:
+ pdf_axes[i] = axes[histo_row[i], histo_col[i]].twinx()
+ pdf_axes[i].plot(points, pdf, label="PDF", color=colors[i])
+ y_ticks = pdf_axes[i].get_yticks()
+ pdf_axes[i].set_ylim(0, max(y_ticks))
+ else:
+ pdf_axes[i] = axes[histo_row[i], histo_col[i]].twiny()
+ pdf_axes[i].plot(pdf, points, label="PDF", color=colors[i])
+ x_ticks = pdf_axes[i].get_xticks()
+ pdf_axes[i].set_xlim(0, max(x_ticks))
+ legend.append(pdf_axes[i].get_legend_handles_labels())
+ if include_orig_values:
+ line_func[i](orig_val, alpha=0.75, color='black')
+ if include_inferred_values:
+ line_func[i](self.inferred_parameters[param], alpha=0.75, color='black', ls='dashed')
+ # Create rug traces
+ rug_args = {
+ x_key[i]: self[param], y_key[i]: [param] * self.accepted_count, 's': 50,
+ 'c': colors[i], 'label': param, 'marker': rug_symbol[i]
+ }
+ axes[rug_row[i], rug_col[i]].scatter(**rug_args)
+ if i == 0:
+ axes[rug_row[i], rug_col[i]].set_yticks([])
+ axes[rug_row[i], rug_col[i]].yaxis.set_tick_params(labelleft=False)
+ else:
+ axes[rug_row[i], rug_col[i]].set_xticks([])
+ axes[rug_row[i], rug_col[i]].xaxis.set_tick_params(labelbottom=False)
+ rugs.append(axes[rug_row[i], rug_col[i]].get_legend_handles_labels())
+
+ legend.extend(rugs)
+
+ axes[2, 0].scatter(self[names[0]], self[names[1]], c=colors[2], label=f"{names[0]} X {names[1]}")
+ legend.append((["Intersection"], [""]))
+ legend.append(axes[2, 0].get_legend_handles_labels())
+
+ labels = numpy.array(legend)[:, 1, 0].tolist()
+ handles = numpy.array(legend)[:, 0, 0].tolist()
+ fig.legend(
+ handles, labels, loc=(0.78, 0.68), fontsize=12.5, frameon=False, markerscale=1.75,
+ handler_map={str: LegendTitle({'fontsize': 14})}
+ )
+ axes[2, 0].set_xlim(bounds[0][0], bounds[1][0])
+ axes[2, 0].set_ylim(bounds[0][1], bounds[1][1])
+ fig.subplots_adjust(wspace=0.04, hspace=0.04)
+ if title is not None:
+ _ = fig.text(0.5, 0.92, title, size=20, ha='center', va='center')
+
+ return fig
+
+ 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
+ names = list(self.data.keys())
+ sizes = (numpy.array(bounds[1]) - numpy.array(bounds[0])) / nbins
+
+ plotly.offline.init_notebook_mode()
+ dims = len(names)
+ specs = [[{"secondary_y": True} if x == y else {} for y in range(dims)] for x in range(dims)]
+ fig = subplots.make_subplots(
+ rows=dims, cols=dims, column_titles=names, row_titles=names, specs=specs,
+ x_title=xaxis_label, y_title=yaxis_label, vertical_spacing=0.05, horizontal_spacing=0.05
+ )
+
+ 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)]
+ trace = plotly.graph_objs.Histogram(
+ x=accepted_values1, name="Histogram", legendgroup=param1, showlegend=True, marker_color=color,
+ opacity=0.75, xbins={"start": bounds[0][i], "end": bounds[1][i], "size": sizes[i]},
+ legendgrouptitle={'text': param1}, legendrank=1
+ )
+ fig.add_trace(trace, row, col, secondary_y=False)
+ fig.update_xaxes(row=row, col=col, range=[bounds[0][i], bounds[1][i]])
+ if include_pdf:
+ mean, std = stats.norm.fit(accepted_values1)
+ points = numpy.linspace(min(accepted_values1), max(accepted_values1), 500)
+ pdf = stats.norm.pdf(points, loc=mean, scale=std)
+ trace2 = plotly.graph_objs.Scatter(
+ x=points, y=pdf, mode='lines', line=dict(color=color),
+ name="PDF", legendgroup=param1, showlegend=True, legendrank=1
+ )
+ fig.add_trace(trace2, row, col, secondary_y=True)
+ if include_inferred_values:
+ fig.add_vline(
+ 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, layer='above', opacity=0.75, line={"color": "black"}
+ )
+ else:
+ color = combine_colors([
+ common_rgb_values[(i)%len(common_rgb_values)][1:],
+ common_rgb_values[(j)%len(common_rgb_values)][1:]
+ ])
+ scatter_kwa = {
+ 'x': accepted_values2, 'y': accepted_values1, 'mode': 'markers', 'marker_color': color,
+ 'name': f"{param2} X {param1}", 'legendgroup': "intersections", 'showlegend': True
+ }
+ if i == 0 and j == 1:
+ scatter_kwa['legendgrouptitle'] = {'text': "Parameter Intersections"}
+ trace = plotly.graph_objs.Scatter(**scatter_kwa)
+ fig.append_trace(trace, row, col)
+ 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, legend=dict(
+ groupclick="toggleitem", x=0, y=0, tracegroupgap=0, itemsizing="constant"
+ ))
+ if title is not None:
+ title = {'text': title, 'x': 0.5, 'xanchor': 'center'}
+ fig.update_layout(title=title)
+ if include_pdf:
+ def update_annotations(annotation):
+ if annotation.x >= 0.94:
+ annotation.update(x=0.96)
+ fig.for_each_annotation(update_annotations)
+
+ return fig
+
+ 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())
+ 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]
+
+ specs = [[{"secondary_y": True}, {}, {}], [{}, {}, {}], [{}, {}, {}]]
+ fig = subplots.make_subplots(
+ 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, specs=specs
+ )
+
+ 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]
+ secondary_y = [True, False]
+
+ for i, (param, orig_val) in enumerate(parameters.items()):
+ if i >= 2:
+ break
+
+ # Create histogram traces
+ histo_trace = plotly.graph_objs.Histogram(
+ marker_color=colors[i], opacity=0.75,
+ name="Histogram", legendgroup=param, showlegend=True, legendgrouptitle={'text': param}, legendrank=1
+ )
+ histo_trace[x_key[i]] = self[param]
+ histo_trace[bins[i]] = {"start": bounds[0][i], "end": bounds[1][i], "size": sizes[i]}
+ fig.add_trace(histo_trace, histo_row[i], histo_col[i], secondary_y=False)
+ xaxis_func[i](row=histo_row[i], col=histo_col[i], range=[bounds[0][i], bounds[1][i]])
+ if include_pdf:
+ mean, std = stats.norm.fit(self[param])
+ points = numpy.linspace(min(self[param]), max(self[param]), 500)
+ pdf = stats.norm.pdf(points, loc=mean, scale=std)
+ histo_trace2 = plotly.graph_objs.Scatter(
+ mode='lines', line=dict(color=colors[i]),
+ name="PDF", legendgroup=param, showlegend=True, legendrank=1
+ )
+ fig.add_trace(histo_trace2, histo_row[i], histo_col[i], secondary_y=secondary_y[i])
+ if i > 0:
+ fig.data[-1].update(xaxis='x10')
+ fig.data[-1].update(**{x_key[i]: points, y_key[i]: pdf})
+ fig.update_layout(xaxis10={
+ 'overlaying': 'x9', 'side': 'top', 'layer': 'above traces', 'anchor': 'free', 'position': 0.59
+ })
+ if include_inferred_values:
+ line_func[i](
+ 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], 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]},
+ name=param, legendgroup="rug", showlegend=True, legendrank=2,
+ legendgrouptitle={'text': "Rug"}
+ )
+ 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)
+
+ trace = plotly.graph_objs.Scatter(
+ x=self[names[0]], y=self[names[1]], mode='markers', marker_color=colors[2],
+ name=f"{names[0]} X {names[1]}", legendgroup="intersection", showlegend=True,
+ legendgrouptitle={'text': "Intersection"}
+ )
+ 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=1000, legend=dict(
+ groupclick="toggleitem", x=0.75, y=1, tracegroupgap=0, itemsizing="constant"
+ ))
+ 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, "mean"
+ )
+
+ def calculate_inferred_parameters(self, key=None, method=None):
+ """
+ Calculate the inferred parameters using the given method and cached using the given key.
+
+ :param key: Key used to cache the inferred parameters.
+ If method is None, key is a reference to a supported function.
+ :type key: str
+
+ :param method: A callable function or method used to calculate the inferred parameters.
+ Needs to accept a single numpy.ndarray argument.
+ :type method: Callable
+
+ :returns: The calculated inferred parameters.
+ :rtype: dict
+
+ :raises ValueError: method and key are None or method is not callable.
+ """
+ if key is None and method is None:
+ raise ValueError("key or method must be set.")
+
+ if key is None:
+ i = 0
+ key = "custom"
+ while key in self.__inferred_parameters:
+ i += 1
+ key = f"custom{i}"
+
+ if key in self.__inferred_parameters:
+ self.inferred_method = key
+ return self.__inferred_parameters[key]
+
+ if method is None:
+ key_methods = {"mean": numpy.mean, "median": numpy.median}
+ if not (isinstance(key, str) and key in key_methods):
+ raise ValueError(f"{key} is not a supported function key. Supported keys: {tuple(key_methods.keys())}")
+ method = key_methods[key]
+
+ if not callable(method) or type(method).__name__ not in ("function", "method"):
+ raise ValueError("method must be a callable function or method.")
+
+ inferred_parameters = {}
+ for param, accepted_values in self.items():
+ inferred_parameters[param] = method(accepted_values)
+ self.inferred_method = key
+ self.__inferred_parameters[key] = inferred_parameters
+ return inferred_parameters
+
+ def plot(self, parameters, bounds, use_matplotlib=False, save_fig=None, 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 save_fig: \**kwargs: Keyword arguments passed to :py:class:`matplotlib.pyplot.savefig`
+ for saving round plots. Ignored if use_matplotlib is False.
+ :type save_fig: dict
+
+ :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:
+ fig = self.__plot(parameters, bounds, **kwargs)
+ if save_fig is not None:
+ fig.savefig(**save_fig)
+ return None
+
+ if not plotly_installed:
+ raise ImportError("Unable to plot results. To continue, install plotly or set 'use_matplotlib' to 'True'")
+
+ fig = self.__plotplotly(parameters, bounds, **kwargs)
+
+ if return_plotly_figure:
+ return fig
+ plotly.offline.iplot(fig)
+ return None
+
+ def plot_intersection(self, parameters, bounds, colors=None, color_ndxs=None,
+ use_matplotlib=False, save_fig=None, 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 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
+
+ :param save_fig: \**kwargs: Keyword arguments passed to :py:class:`matplotlib.pyplot.savefig`
+ for saving intersection plots. Ignored if use_matplotlib is False.
+ :type save_fig: dict
+
+ :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 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:]]))
+
+ if use_matplotlib:
+ fig = self.__plot_intersection(parameters, bounds, colors, **kwargs)
+ if save_fig is not None:
+ fig.savefig(**save_fig)
+ return None
+
+ if not plotly_installed:
+ raise ImportError("Unable to plot results. To continue, install plotly or set 'use_matplotlib' to 'True'")
+
+ fig = self.__plotplotly_intersection(parameters, bounds, colors, **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 __getattribute__(self, key):
+ if key in ('distances', 'accepted_count', 'trial_count', 'inferred_parameters'):
+ if len(self.data) > 1:
+ msg = f"Results is of type list. Use results[i]['{key}'] instead of results['{key}']"
+ log.warning(msg)
+ return getattr(InferenceResults.__getattribute__(self, key='data')[-1], key)
+ return UserList.__getattribute__(self, key)
+
+ def __getitem__(self, key):
+ if key == 'data':
+ return UserList.__getitem__(self, key)
+ if isinstance(key, str):
+ if len(self.data) > 1:
+ msg = f"Results is of type list. Use results[i]['{key}'] instead of results['{key}']"
+ log.warning(msg)
+ return self.data[0][key]
+ return UserList.__getitem__(self,key)
+
+ def __add__(self, other):
+ c_type = type(other).__name__
+ if c_type != "InferenceResults":
+ raise ValueError(f'{c_type} cannot be added to InferenceResults.')
+
+ if self.parameters != other.parameters:
+ raise ValueError("InferenceResults object contain difference parameters.")
+
+ if not numpy.all(numpy.array(self.bounds) == numpy.array(other.bounds)):
+ raise ValueError("InferenceResults object contain difference priors.")
+
+ return InferenceResults(
+ data=(self.data + other.data), parameters=self.parameters, bounds=self.bounds
+ )
+
+ def __radd__(self, other):
+ if other == 0:
+ return self
+ return self.__add__(other)
+
+ def __plot(self, include_orig_values=True, include_inferred_values=False,
+ 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, histo_axes = plt.subplots(nrows=rows, ncols=cols, figsize=[14, 7 * rows])
+ _ = histo_fig.text(0.5, 0.09, xaxis_label, size=18, ha='center', va='center')
+ _ = histo_fig.text(0.08, 0.5, yaxis_label['histo'], size=18, ha='center', va='center', rotation='vertical')
+
+ pdf_fig, pdf_axes = plt.subplots(nrows=rows, ncols=cols, figsize=[14, 7 * rows])
+ _ = pdf_fig.text(0.5, 0.09, xaxis_label, size=18, ha='center', va='center')
+ _ = pdf_fig.text(0.08, 0.5, yaxis_label['pdf'], size=18, ha='center', va='center', rotation='vertical')
+
+ if len(self.parameters) < rows * cols:
+ histo_axes[-1, -1].axis('off')
+ pdf_axes[-1, -1].axis('off')
+
+ nbins = 50
+ 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, (param, accepted_values) in enumerate(inf_round.data.items()):
+ row = int(numpy.ceil((j + 1) / cols)) - 1
+ col = (j % cols)
+
+ name = f"round {i + 1}"
+ color = common_rgb_values[i % len(common_rgb_values)]
+ opacity = base_opacity + 0.25
+ # Create histogram trace
+ histo_axes[row, col].hist(
+ accepted_values, label=name, color=color, alpha=opacity,
+ bins=nbins, range=(self.bounds[0][j], self.bounds[1][j])
+ )
+ # Create pdf trace
+ mean, std = stats.norm.fit(accepted_values)
+ points = numpy.linspace(min(accepted_values), max(accepted_values), 500)
+ pdf = stats.norm.pdf(points, loc=mean, scale=std)
+ pdf_axes[row, col].plot(points, pdf, label=name, color=color)
+
+ if i == len(self.data) - 1:
+ histo_axes[row, col].set_title(names[j], size=16)
+ pdf_axes[row, col].set_title(names[j], size=16)
+ if include_orig_values:
+ histo_axes[row, col].axvline(self.parameters[param], alpha=0.75, color='black')
+ pdf_axes[row, col].axvline(self.parameters[param], alpha=0.75, color='black')
+ if include_inferred_values:
+ histo_axes[row, col].axvline(
+ inf_round.inferred_parameters[param], alpha=0.75, color='black', ls='dashed'
+ )
+ pdf_axes[row, col].axvline(
+ inf_round.inferred_parameters[param], alpha=0.75, color='black', ls='dashed'
+ )
+
+ histo_handles, histo_labels = histo_axes[0, 0].get_legend_handles_labels()
+ histo_fig.legend(
+ histo_handles, histo_labels, loc=(0.905, 0.83), fontsize=12.5, frameon=False, labelspacing=1.2
+ )
+ pdf_handles, pdf_labels = pdf_axes[0, 0].get_legend_handles_labels()
+ pdf_fig.legend(
+ pdf_handles, pdf_labels, loc=(0.905, 0.83), fontsize=12.5, frameon=False, labelspacing=1.2
+ )
+ if title is not None:
+ _ = histo_fig.text(0.5, 0.92, title, size=20, ha='center', va='center')
+ _ = pdf_fig.text(0.5, 0.92, title, size=20, ha='center', va='center')
+
+ return histo_fig, pdf_fig
+
+ def __plotplotly(self, include_orig_values=True, include_inferred_values=False,
+ 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, 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, vertical_spacing=0.075,
+ x_title=xaxis_label, y_title=yaxis_label['pdf']
+ )
+
+ 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, (param, accepted_values) in enumerate(inf_round.data.items()):
+ 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": self.bounds[0][j], "end": self.bounds[1][j], "size": sizes[j]}
+ )
+ histo_fig.append_trace(trace, row, col)
+ # Create PDF trace
+ mean, std = stats.norm.fit(accepted_values)
+ points = numpy.linspace(min(accepted_values), max(accepted_values), 500)
+ pdf = stats.norm.pdf(points, loc=mean, scale=std)
+ trace2 = plotly.graph_objs.Scatter(
+ x=points, y=pdf, name=name, legendgroup=name, showlegend=j==0, mode='lines', line=dict(color=color)
+ )
+ 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, layer='above', opacity=0.75,
+ line={"color": "black"}
+ )
+ pdf_fig.add_vline(
+ 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, 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, exclude_empty_subplots=True,
+ layer='above', opacity=0.75, line={"color": "black", "dash": "dash"}
+ )
+
+ 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'}
+ 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 calculate_inferred_parameters(self, key=None, method=None, ndx=None):
+ """
+ Calculate the inferred parameters using the given method and cached using the given key.
+
+ :param key: Key used to cache the inferred parameters.
+ If method is None, key is a reference to a supported function.
+ :type key: str
+
+ :param method: A callable function or method used to calculate the inferred parameters.
+ Needs to accept a single numpy.ndarray argument.
+ :type method: Callable
+
+ :param ndx: Index of the inference round to plot.
+ :type ndx: int
+
+ :returns: The calculated inferred parameters for the indicated round if ndx is set else the final round.
+ :rtype: dict
+
+ :raises ValueError: method and key are None or method is not callable.
+ """
+ if ndx is not None:
+ return self[ndx].calculate_inferred_parameters(key=key, method=method)
+
+ for inf_round in self:
+ inferred_parameters = inf_round.calculate_inferred_parameters(key=key, method=method)
+ return inferred_parameters
+
+ def plot(self, histo_only=True, pdf_only=False, use_matplotlib=False,
+ save_histo=None, save_pdf=None, 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 save_histo: \**kwargs: Keyword arguments passed to :py:class:`matplotlib.pyplot.savefig`
+ for saving histogram plots. Ignored if use_matplotlib is False.
+ :type save_histo: dict
+
+ :param save_pdf: \**kwargs: Keyword arguments passed to :py:class:`matplotlib.pyplot.savefig`
+ for saving pdf plots. Ignored if use_matplotlib is False.
+ :type save_pdf: dict
+
+ :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. Dictionaries should be in
+ the following format {'histo':<