diff --git a/client/job-view/job-view.js b/client/job-view/job-view.js
index 439a4684f..5b40f6505 100644
--- a/client/job-view/job-view.js
+++ b/client/job-view/job-view.js
@@ -46,10 +46,10 @@ module.exports = View.extend({
},
render: function (attrs, options) {
View.prototype.render.apply(this, arguments);
- if(this.model.status !== "error") {
+ if(this.model.status === "complete") {
this.renderResultsView();
}
- if(!this.readOnly) {
+ if(!this.readOnly && this.model.status !== "running") {
this.renderLogsView();
}
this.renderSettingsView();
diff --git a/client/model-view/model-view.js b/client/model-view/model-view.js
index 48ea47eba..178f329fd 100644
--- a/client/model-view/model-view.js
+++ b/client/model-view/model-view.js
@@ -106,11 +106,9 @@ module.exports = View.extend({
});
this.model.eventsCollection.on("add change remove", () => {
this.updateSpeciesInUse();
- this.updateParametersInUse();
});
this.model.rules.on("add change remove", () => {
this.updateSpeciesInUse();
- this.updateParametersInUse();
});
}
},
@@ -429,14 +427,6 @@ module.exports = View.extend({
updateInUse(reaction.rate);
}
});
- events.forEach((event) => {
- event.eventAssignments.forEach((assignment) => {
- updateInUse(assignment.variable);
- });
- });
- rules.forEach((rule) => {
- updateInUse(rule.variable);
- });
},
updateSpeciesInUse: function () {
let species = this.model.species;
@@ -450,6 +440,10 @@ module.exports = View.extend({
specie.inUse = true;
});
}
+ let updateInUseForRate = (rate) => {
+ specie = species.get(rate.compID, 'compID')
+ if(specie) { specie.inUse = true; }
+ }
let updateInUseForOther = (specie) => {
_.where(species.models, { compID: specie.compID })
.forEach((specie) => {
@@ -459,6 +453,9 @@ module.exports = View.extend({
reactions.forEach((reaction) => {
reaction.products.forEach(updateInUseForReaction);
reaction.reactants.forEach(updateInUseForReaction);
+ if(reaction.reactionType !== 'custom-propensity') {
+ updateInUseForRate(reaction.rate);
+ }
});
events.forEach((event) => {
event.eventAssignments.forEach((assignment) => {
diff --git a/client/model-view/templates/editSpecie.pug b/client/model-view/templates/editSpecie.pug
index 3c1ad7208..d2a4c5f19 100644
--- a/client/model-view/templates/editSpecie.pug
+++ b/client/model-view/templates/editSpecie.pug
@@ -9,10 +9,16 @@ div.mx-1
div.pl-2(data-hook="input-name-container")
- div.col-sm-5
+ div.col-sm-3
div(data-hook="input-value-container")
+ div.col-sm-2
+
+ div.pl-3
+
+ input(type="checkbox" data-hook="observable")
+
div.col-sm-2
div.tooltip-icon-large(data-hook="annotation-tooltip" data-html="true" data-toggle="tooltip" title=this.model.annotation || "Click 'Add' to add an annotation")
diff --git a/client/model-view/templates/rulesView.pug b/client/model-view/templates/rulesView.pug
index 353b6073f..f3b029bd5 100644
--- a/client/model-view/templates/rulesView.pug
+++ b/client/model-view/templates/rulesView.pug
@@ -22,7 +22,7 @@ div#rules-editor.card
div.card-body
p.mb-0
- | Equations that determine the value (assignment rule) or rates of change (rate rule) of Variables or Parameter.
+ | Equations that determine the value (assignment rule) or rates of change (rate rule) of Variables.
ul.mb-0
li A 'Rate Rule' corresponds to a function in the form of
li An 'Assignment Rule' correspond to a function in the form of
diff --git a/client/model-view/templates/speciesView.pug b/client/model-view/templates/speciesView.pug
index 5cc6ad9ed..13f499588 100644
--- a/client/model-view/templates/speciesView.pug
+++ b/client/model-view/templates/speciesView.pug
@@ -38,12 +38,18 @@ div#species-editor.card
div.tooltip-icon(data-html="true" data-toggle="tooltip" title=this.tooltips.name)
- div.col-sm-5
+ div.col-sm-3
h6.inline Initial Condition
div.tooltip-icon(data-html="true" data-toggle="tooltip" title=this.tooltips.initialValue)
+ div.col-sm-2
+
+ h6.inline Observable
+
+ div.tooltip-icon(data-html="true" data-toggle="tooltip" title=this.tooltips.observable)
+
div.col-sm-2
h6.inline Annotation
@@ -80,10 +86,14 @@ div#species-editor.card
h6.inline Mode
- div.col-sm-4(data-hook="species-switching-header")
+ div.col-sm-2(data-hook="species-switching-header")
h6.inline Switch Tolerance/Minimum Value for Switching
+ div.col-sm-2
+
+ h6.inline Observable
+
div.col-sm-2(data-hook="species-annotation-header")
h6.inline Annotation
diff --git a/client/model-view/templates/viewSpecie.pug b/client/model-view/templates/viewSpecie.pug
index a792c4492..9c203e455 100644
--- a/client/model-view/templates/viewSpecie.pug
+++ b/client/model-view/templates/viewSpecie.pug
@@ -18,9 +18,15 @@ div.mx-1
div=this.model.mode
if this.model.mode === "dynamic"
- div.col-sm-4
+ div.col-sm-2
div=this.switchingValWithLabel
+ div.col-sm-2
+
+ div.pl-3
+
+ input(type="checkbox" data-hook="observable" checked=this.model.observable)
+
div.col-sm-2
if this.model.annotation
diff --git a/client/model-view/views/event-assignment-view.js b/client/model-view/views/event-assignment-view.js
index 1871f7991..367a618a4 100644
--- a/client/model-view/views/event-assignment-view.js
+++ b/client/model-view/views/event-assignment-view.js
@@ -43,15 +43,9 @@ module.exports = View.extend({
},
getOptions: function () {
var species = this.model.collection.parent.collection.parent.species;
- var parameters = this.model.collection.parent.collection.parent.parameters;
- var specs = species.map(function (specie) {
+ var options = species.map(function (specie) {
return [specie.compID, specie.name];
});
- var params = parameters.map(function (parameter) {
- return [parameter.compID, parameter.name];
- });
- let options = [{groupName: Boolean(specs) ? "Variables" : "Variables (empty)", options: specs},
- {groupName: Boolean(params) ? "Parameters" : "Parameters (empty)", options: params}];
return options;
},
removeAssignment: function () {
@@ -60,22 +54,8 @@ module.exports = View.extend({
this.collection.parent.collection.trigger('change');
},
selectAssignmentVariable: function (e) {
- var species = this.model.collection.parent.collection.parent.species;
- var parameters = this.model.collection.parent.collection.parent.parameters;
- var val = Number(e.target.value);
- var eventVar = species.filter(function (specie) {
- if(specie.compID === val) {
- return specie;
- }
- });
- if(!eventVar.length) {
- eventVar = parameters.filter(function (parameter) {
- if(parameter.compID === val) {
- return parameter;
- }
- });
- }
- this.model.variable = eventVar[0];
+ let species = this.model.collection.parent.collection.parent.species;
+ this.model.variable = species.get(Number(e.target.value), 'compID');
this.updateViewer();
this.model.collection.parent.collection.trigger('change');
},
@@ -107,7 +87,7 @@ module.exports = View.extend({
name: 'variable',
required: true,
idAttributes: 'cid',
- groupOptions: options,
+ options: options,
value: this.model.variable.compID
});
}
diff --git a/client/model-view/views/parameters-view.js b/client/model-view/views/parameters-view.js
index 2a8eceb8f..bda946808 100644
--- a/client/model-view/views/parameters-view.js
+++ b/client/model-view/views/parameters-view.js
@@ -49,24 +49,6 @@ module.exports = View.extend({
}
}
});
- this.collection.parent.eventsCollection.map((event) => {
- event.eventAssignments.map((assignment) => {
- if(assignment.variable.compID === compID) {
- assignment.variable = parameter;
- }
- })
- if(event.selected) {
- event.trigger('change-event');
- }
- });
- this.collection.parent.rules.map((rule) => {
- if(rule.variable.compID === compID) {
- rule.variable = parameter;
- }
- });
- if(this.parent.rulesView) {
- this.parent.rulesView.renderEditRules();
- }
});
},
render: function () {
diff --git a/client/model-view/views/reaction-view.js b/client/model-view/views/reaction-view.js
index 9c4b81c06..d580d255b 100644
--- a/client/model-view/views/reaction-view.js
+++ b/client/model-view/views/reaction-view.js
@@ -155,6 +155,19 @@ module.exports = View.extend({
self.parent.renderEditReactionView();
});
},
+ getOptions: function () {
+ var species = this.model.collection.parent.species;
+ var parameters = this.model.collection.parent.parameters;
+ var specs = species.map(function (specie) {
+ return [specie.compID, specie.name];
+ });
+ var params = parameters.map(function (parameter) {
+ return [parameter.compID, parameter.name];
+ });
+ let options = [{groupName: Boolean(specs) ? "Variables" : "Variables (empty)", options: specs},
+ {groupName: Boolean(params) ? "Parameters" : "Parameters (empty)", options: params}];
+ return options;
+ },
getReactionTypes: function () {
let disableTypes = this.model.collection.parent.parameters.length == 0;
let options = _.map(ReactionTypes, function (val, key) {
@@ -289,14 +302,18 @@ module.exports = View.extend({
viewOptions['options'] = [];
viewOptions['unselectedText'] = "N/A";
}else{
- // make sure the reaction has a rate and that rate exists in the parameters collection
+ // make sure the reaction has a rate and that rate exists in the species or parameters collection
let paramIDs = this.model.collection.parent.parameters.map(function (param) {
return param.compID;
});
- if(!this.model.rate.compID || !paramIDs.includes(this.model.rate.compID)) {
+ let specIDs = this.model.collection.parent.species.map(function (spec) {
+ return spec.compID;
+ });
+ let rateExists = paramIDs.includes(this.model.rate.compID) || specIDs.includes(this.model.rate.compID);
+ if(!this.model.rate.compID || !rateExists) {
this.model.rate = this.model.collection.getDefaultRate();
}
- viewOptions['options'] = this.model.collection.parent.parameters;
+ viewOptions['groupOptions'] = this.getOptions();
viewOptions['value'] = this.model.rate.compID;
}
@@ -368,9 +385,13 @@ module.exports = View.extend({
},
selectRateParam: function (e) {
let val = e.target.selectedOptions.item(0).value;
- let param = this.model.collection.parent.parameters.get(val, 'compID');
- if(param) {
- this.model.rate = param;
+ console.log(val)
+ var rate = this.model.collection.parent.parameters.get(val, 'compID');
+ if(rate === undefined) {
+ rate = this.model.collection.parent.species.get(val, 'compID');
+ }
+ if(rate) {
+ this.model.rate = rate;
this.updateViewer();
this.model.trigger('change-reaction');
this.model.collection.trigger("change");
diff --git a/client/model-view/views/rule-view.js b/client/model-view/views/rule-view.js
index 0723cc05f..d5ea0b644 100644
--- a/client/model-view/views/rule-view.js
+++ b/client/model-view/views/rule-view.js
@@ -77,14 +77,9 @@ module.exports = View.extend({
getOptions: function () {
let species = this.model.collection.parent.species;
let parameters = this.model.collection.parent.parameters;
- let specs = species.map(function (specie) {
+ let options = species.map(function (specie) {
return [specie.compID, specie.name];
});
- let params = parameters.map(function (parameter) {
- return [parameter.compID, parameter.name];
- });
- let options = [{groupName: Boolean(specs) ? "Variables" : "Variables (empty)", options: specs},
- {groupName: Boolean(params) ? "Parameters" : "Parameters (empty)", options: params}];
return options;
},
removeRule: function () {
@@ -95,21 +90,7 @@ module.exports = View.extend({
},
selectRuleVariable: function (e) {
let species = this.model.collection.parent.species;
- let parameters = this.model.collection.parent.parameters;
- let compID = Number(e.target.value);
- let ruleVar = species.filter(function (specie) {
- if(specie.compID === compID) {
- return specie;
- }
- });
- if(!ruleVar.length) {
- ruleVar = parameters.filter(function (parameter) {
- if(parameter.compID === compID) {
- return parameter;
- }
- });
- }
- this.model.variable = ruleVar[0];
+ this.model.variable = species.get(Number(e.target.value), 'compID')
},
update: function (e) {},
updateValid: function () {},
@@ -152,7 +133,7 @@ module.exports = View.extend({
name: 'variable',
required: true,
idAttributes: 'cid',
- groupOptions: options,
+ options: options,
value: this.model.variable.compID
});
}
diff --git a/client/model-view/views/specie-view.js b/client/model-view/views/specie-view.js
index 3fc2cafb6..e4baf6d25 100644
--- a/client/model-view/views/specie-view.js
+++ b/client/model-view/views/specie-view.js
@@ -40,12 +40,19 @@ module.exports = View.extend({
type: 'booleanAttribute',
name: 'disabled',
},
+ 'model.observable': {
+ type: function (el, value, previousValue) {
+ el.checked = value;
+ },
+ hook: 'observable'
+ }
},
events: {
'change [data-hook=input-name-container]' : 'setSpeciesName',
'change [data-hook=specie-mode]' : 'setSpeciesMode',
'change [data-hook=switching-tol]' : 'setSwitchingType',
'change [data-hook=switching-min]' : 'setSwitchingType',
+ 'change [data-hook=observable]' : 'setObservable',
'click [data-hook=edit-annotation-btn]' : 'editAnnotation',
'click [data-hook=remove]' : 'removeSpecie',
'click [data-hook=collapse-advanced]' : 'changeCollapseButtonText'
@@ -152,6 +159,9 @@ module.exports = View.extend({
}}
);
},
+ setObservable: function (e) {
+ this.model.observable = !this.model.observable;
+ },
setSpeciesMode: function (e) {
this.model.mode = e.target.value;
this.model.collection.trigger('update-species', this.model.compID, this.model, false, false);
diff --git a/client/model-view/views/species-view.js b/client/model-view/views/species-view.js
index f0bb0eb25..44a397131 100644
--- a/client/model-view/views/species-view.js
+++ b/client/model-view/views/species-view.js
@@ -46,6 +46,12 @@ module.exports = View.extend({
this.collection.on('update-species', (compID, specie, isNameUpdate, isDefaultMode) => {
this.collection.parent.reactions.forEach((reaction) => {
var changedReaction = false;
+ if(reaction.rate && reaction.rate.compID === compID){
+ reaction.rate = specie;
+ if(reaction.reactionType !== 'custom-propensity') {
+ changedReaction = true;
+ }
+ }
reaction.reactants.forEach((reactant) => {
if(reactant.specie.compID === compID) {
reactant.specie = specie;
diff --git a/client/models/job.js b/client/models/job.js
index de495f6a2..622d2e8e0 100644
--- a/client/models/job.js
+++ b/client/models/job.js
@@ -74,6 +74,13 @@ module.exports = State.extend({
// timeZone = timeZone.replace('(', '').replace(')', '') // remove the '()' from the timezone
return stamp + hours + ":" + minutes + " " + ampm + " " + timeZone;
}
+ },
+ sortTime: {
+ deps: ["startTime"],
+ fn: function () {
+ let date = new Date(this.startTime);
+ return Math.round(date.getTime() / 1000);
+ }
}
}
});
\ No newline at end of file
diff --git a/client/models/jobs.js b/client/models/jobs.js
index c42797fb5..cba1f6543 100644
--- a/client/models/jobs.js
+++ b/client/models/jobs.js
@@ -22,5 +22,6 @@ let Collection = require('ampersand-collection');
let Job = require('./job');
module.exports = Collection.extend({
- model: Job
+ model: Job,
+ comparator: 'sortTime'
});
diff --git a/client/models/specie.js b/client/models/specie.js
index a8b2a9d67..d4ccdf357 100644
--- a/client/models/specie.js
+++ b/client/models/specie.js
@@ -22,16 +22,17 @@ var State = require('ampersand-state');
module.exports = State.extend({
props: {
+ annotation: 'string',
compID: 'number',
- name: 'string',
- value: 'any',
+ diffusionConst: 'number',
+ isSwitchTol: 'boolean',
mode: 'string',
+ name: 'string',
+ observable: 'boolean',
switchTol: 'any',
switchMin: 'any',
- isSwitchTol: 'boolean',
- annotation: 'string',
- diffusionConst: 'number',
- types: 'object'
+ types: 'object',
+ value: 'any'
},
session: {
inUse: {
diff --git a/client/models/species.js b/client/models/species.js
index c35b3bd2b..c22dc1502 100644
--- a/client/models/species.js
+++ b/client/models/species.js
@@ -29,16 +29,17 @@ module.exports = Collection.extend({
var id = this.parent.getDefaultID();
var name = this.getDefaultName();
var specie = this.add({
+ annotation: "",
compID: id,
- name: name,
- value: 0,
+ diffusionConst: 0.0,
+ isSwitchTol: true,
mode: this.parent.defaultMode,
+ name: name,
+ observable: true,
switchTol: 0.03,
switchMin: 100,
- isSwitchTol: true,
- annotation: "",
- diffusionConst: 0.0,
- types: types
+ types: types,
+ value: 0
});
this.parent.updateValid()
},
diff --git a/client/pages/workflow-manager.js b/client/pages/workflow-manager.js
index 3f8290a10..c86b176e0 100644
--- a/client/pages/workflow-manager.js
+++ b/client/pages/workflow-manager.js
@@ -49,12 +49,14 @@ let WorkflowManager = PageView.extend({
'click [data-target=start-job]' : 'clickStartJobHandler',
'click [data-hook=edit-model]' : 'clickEditModelHandler',
'click [data-hook=collapse-jobs]' : 'changeCollapseButtonText',
- 'click [data-hook=return-to-project-btn]' : 'handleReturnToProject'
+ 'click [data-hook=return-to-project-btn]' : 'handleReturnToProject',
+ 'click [data-hook=manual-view-control]' : 'setActiveJobViewControl'
},
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.viewingActiveJob = false;
this.model = new Workflow({
directory: urlParams.get('path')
});
@@ -227,8 +229,9 @@ let WorkflowManager = PageView.extend({
this.jobListingView = this.renderCollection(
this.model.jobs,
JobListingView,
- this.queryByHook("job-listing")
+ this.queryByHook("job-listing"),
);
+ this.setActiveJobIndicator();
},
renderModelLocationSelectView: function (model) {
if(this.modelLocationSelectView) {
@@ -287,14 +290,13 @@ let WorkflowManager = PageView.extend({
}else if(this.model.activeJob.status !== "ready") {
this.renderStatusView();
}
- 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)) {
+ if(this.model.activeJob) {
this.renderActiveJob();
}
},
@@ -309,8 +311,20 @@ let WorkflowManager = PageView.extend({
setActiveJob: function (job) {
this.removeActiveJob();
this.model.activeJob = job;
+ this.viewingActiveJob = false;
+ this.setActiveJobViewControl();
this.renderActiveJob();
},
+ setActiveJobIndicator: function () {
+ this.jobListingView.views.forEach((view) => {
+ view.updateActiveJob(this.model.activeJob.directory);
+ });
+ },
+ setActiveJobViewControl: function (e) {
+ this.viewingActiveJob = !this.viewingActiveJob;
+ $(this.queryByHook("manual-view-control")).text(this.viewingActiveJob ? "Finished" : "View");
+ this.setActiveJobIndicator();
+ },
setupSettingsView: function () {
if(!this.model.newFormat) {
$(this.queryByHook("of-start-job")).css('display', 'inline-block');
@@ -375,31 +389,46 @@ let WorkflowManager = PageView.extend({
let runEndpoint = `${path.join(app.getApiPath(), "workflow/run-job")}${runQuery}`;
app.getXHR(runEndpoint, {
success: (err, response, body) => {
- this.updateWorkflow(true);
+ this.updateWorkflow({newJob: true});
}
});
}
});
},
- updateWorkflow: function (newJob) {
- let self = this;
+ updateWorkflow: function ({newJob=false}={}) {
if(this.model.newFormat) {
- let hadActiveJob = Boolean(this.model.activeJob.status)
app.getXHR(this.model.url(), {
- success: function (err, response, body) {
- self.model.set({jobs: body.jobs, activeJob: body.activeJob});
- if(!Boolean(self.model.activeJob.status)){
- self.removeActiveJob();
- }else if(!hadActiveJob && Boolean(self.model.activeJob.status)) {
- self.renderActiveJob();
+ success: (err, response, body) => {
+ this.model.set({jobs: body.jobs});
+ if(newJob || this.viewingActiveJob) {
+ setTimeout(() => { this.setActiveJobIndicator(); });
+ }else if (!Boolean(body.activeJob.status)) {
+ this.removeActiveJob();
+ }else {
+ this.model.set({activeJob: body.activeJob});
+ this.setActiveJobViewControl();
+ this.renderActiveJob();
+ }
+ if(newJob) {
+ setTimeout(() => { this.setActiveJobIndicator(); });
+ }else {
+ if(!Boolean(this.model.activeJob.status)) {
+ this.removeActiveJob();
+ }else if(!this.viewingActiveJob){
+ this.model.set({activeJob: body.activeJob});
+ this.renderActiveJob();
+ this.setActiveJobViewControl();
+ }else {
+ setTimeout(() => { this.setActiveJobIndicator(); });
+ }
}
}
});
}else if(!this.model.newFormat){
app.getXHR(this.model.url(), {
- success: function (err, response, body) {
- self.model.set(body)
- self.renderSubviews();
+ success: (err, response, body) => {
+ this.model.set(body)
+ this.renderSubviews();
}
});
}
diff --git a/client/settings-view/views/well-mixed-settings-view.js b/client/settings-view/views/well-mixed-settings-view.js
index 1f93e4ec8..89f1945a8 100644
--- a/client/settings-view/views/well-mixed-settings-view.js
+++ b/client/settings-view/views/well-mixed-settings-view.js
@@ -63,7 +63,8 @@ module.exports = View.extend({
}else {
if(!this.model.isAutomatic){
$(this.queryByHook('select-ode')).prop('checked', Boolean(this.model.algorithm === "ODE"));
- $(this.queryByHook('select-ssa')).prop('checked', Boolean(this.model.algorithm === "SSA"));
+ $(this.queryByHook('select-ssa')).prop('checked', Boolean(this.model.algorithm === "SSA"));
+ $(this.queryByHook('select-cle')).prop('checked', Boolean(this.model.algorithm === "CLE"));
$(this.queryByHook('select-tau-leaping')).prop('checked', Boolean(this.model.algorithm === "Tau-Leaping"));
$(this.queryByHook('select-hybrid-tau')).prop('checked', Boolean(this.model.algorithm === "Hybrid-Tau-Leaping"));
}else{
diff --git a/client/styles/styles.css b/client/styles/styles.css
index b0d830753..64d33801e 100644
--- a/client/styles/styles.css
+++ b/client/styles/styles.css
@@ -884,4 +884,19 @@ input:checked + .slider:before {
position: absolute;
left: 15px;
top: 40%;
-}
\ No newline at end of file
+}
+
+.active-job-listing {
+ background-color: rgba(0, 255, 255, 1.0);
+}
+
+.job-listing-btn {
+ color: rgb(108, 117, 125);
+ background-color: rgb(255, 255, 255) !important;
+}
+
+.job-listing-btn:hover {
+ color: rgb(255, 255, 255);
+ background-color: rgb(108, 117, 125) !important;
+}
+
diff --git a/client/templates/includes/jobListing.pug b/client/templates/includes/jobListing.pug
index c99fc0497..e8c95e9f2 100644
--- a/client/templates/includes/jobListing.pug
+++ b/client/templates/includes/jobListing.pug
@@ -3,24 +3,26 @@ div(data-hook=this.model.elementID)
if(this.model.collection.indexOf(this.model) !== 0)
hr
- div.row
+ div.py-1.pl-1(data-hook="is-active-job")
- div.col-md-3
+ div.row
- button.btn.btn-outline-secondary.box-shadow(data-hook=this.model.elementID + "-open" style="width: 100%")=this.model.name
+ div.col-md-3
- div.col-md-4
+ button.btn.btn-outline-secondary.box-shadow.job-listing-btn(data-hook=this.model.elementID + "-open" style="width: 100%")=this.model.name
- div.py-2=this.model.fmtStartTime
+ div.col-md-4
- div.col-md-3
+ div.py-2=this.model.fmtStartTime
- div.py-2
+ div.col-md-3
- div.inline(data-hook=this.model.elementID + "-status")=this.model.status
+ div.py-2
- div.inline.spinner-border.status(data-hook=this.model.elementID + "-running-spinner")
+ div.inline(data-hook=this.model.elementID + "-status")=this.model.status
- div.col-md-2
+ div.inline.spinner-border.status(data-hook=this.model.elementID + "-running-spinner")
- button.btn.btn-outline-secondary.box-shadow(data-hook=this.model.elementID + "-remove") X
\ No newline at end of file
+ div.col-md-2
+
+ button.btn.btn-outline-secondary.box-shadow.job-listing-btn(data-hook=this.model.elementID + "-remove") X
\ No newline at end of file
diff --git a/client/templates/pages/workflowManager.pug b/client/templates/pages/workflowManager.pug
index 05582cab7..bc9be6064 100644
--- a/client/templates/pages/workflowManager.pug
+++ b/client/templates/pages/workflowManager.pug
@@ -106,7 +106,13 @@ section.page
div.mt-4(data-hook="active-job-header-container" style="display: none")
- h2(data-hook="active-job-header") Job:
+ div.inline
+
+ h2(data-hook="active-job-header") Job:
+
+ div.mr-3.inline(style="float: right;")
+
+ button.btn.btn-outline-secondary.box-shadow(data-hook="manual-view-control") View
div(data-hook="active-job-container")
diff --git a/client/tooltips.js b/client/tooltips.js
index 88d284e9e..79752ddc1 100644
--- a/client/tooltips.js
+++ b/client/tooltips.js
@@ -18,17 +18,19 @@ along with this program. If not, see .
module.exports = {
speciesEditor: {
- name: "Unique identifier for Variable. Cannot share a name with other model components.",
-
initialValue: "Initial population of a variables.",
annotation: "An optional note about the variables.",
- remove: "A variables may only be removed if it is not a part of any reaction, event assignment, or rule.",
-
mode: "Concentration - Variables will only be represented using continuous (floating point) values.
Population - Variables will only be represented "+
"using discrete (integer count) values.
Hybrid Concentration/Population - Allows a variables to be represented using continuous and/or discrete values.",
+ name: "Unique identifier for Variable. Cannot share a name with other model components.",
+
+ observable: "Indicates that the species should be plotted with viewing results.",
+
+ remove: "A variables may only be removed if it is not a part of any reaction, event assignment, or rule.",
+
switchValue: "Switching Tolerance - Tolerance level for considering a dynamic variables deterministically, value is compared to an estimated sd/mean population "+
"of a variables after a given time step. This value will be used if a 'Minimum Value' not provided.
Minimum Value For Switching - Minimum population value "+
"at which variables will be represented as Concentration."
diff --git a/client/views/job-listing.js b/client/views/job-listing.js
index c561d46dd..5362476cf 100644
--- a/client/views/job-listing.js
+++ b/client/views/job-listing.js
@@ -36,11 +36,15 @@ module.exports = View.extend({
},
initialize: function (attrs, options) {
View.prototype.initialize.apply(this, arguments);
+ this.isActiveJob = false;
+ console.log(this.model.startTime, this.model.sortTime)
},
render: function (attrs, options) {
View.prototype.render.apply(this, arguments);
+ if(this.isActiveJob) {
+ $(this.queryByHook("is-active-job")).addClass("active-job-listing");
+ }
if(this.model.status === "running") {
- $(this.queryByHook(this.model.elementID + "-open")).prop("disabled", true);
this.getJobStatus();
}else{
$(this.queryByHook(this.model.elementID + '-running-spinner')).css('display', "none")
@@ -80,5 +84,13 @@ module.exports = View.extend({
},
openActiveJob: function (e) {
this.parent.setActiveJob(this.model);
+ },
+ updateActiveJob: function (activeJob) {
+ this.isActiveJob = Boolean(activeJob) && activeJob === this.model.directory;
+ if(this.isActiveJob) {
+ $(this.queryByHook("is-active-job")).addClass("active-job-listing");
+ }else {
+ $(this.queryByHook("is-active-job")).removeClass("active-job-listing");
+ }
}
});
diff --git a/client/views/workflow-group-listing.js b/client/views/workflow-group-listing.js
index 23c15c9fd..cedb8aea2 100644
--- a/client/views/workflow-group-listing.js
+++ b/client/views/workflow-group-listing.js
@@ -44,12 +44,14 @@ module.exports = View.extend({
initialize: function (attrs, options) {
View.prototype.initialize.apply(this, arguments);
let links = [];
- this.model.model.refLinks.forEach((link) => {
- links.push(
- `${link.name}`
- );
- });
- this.htmlLinks = links.join('')
+ if(this.model.model.refLinks) {
+ this.model.model.refLinks.forEach((link) => {
+ links.push(
+ `${link.name}`
+ );
+ });
+ }
+ this.htmlLinks = links.join('');
},
render: function (attrs, options) {
View.prototype.render.apply(this, arguments);
diff --git a/jupyterhub/.env b/jupyterhub/.env
index 82ce710e2..d452a7505 100644
--- a/jupyterhub/.env
+++ b/jupyterhub/.env
@@ -5,7 +5,7 @@ JUPYTER_CONFIG_DIR=/opt/stochss-config/.jupyter
#AUTH_CLASS=jupyterhub.auth.DummyAuthenticator
-JUPYTERHUB_VERSION=1.1.0
+JUPYTERHUB_VERSION=3.1.1
DOCKER_HUB_IMAGE=stochss-hub
diff --git a/stochss/handlers/util/ensemble_simulation.py b/stochss/handlers/util/ensemble_simulation.py
index 2df3f20bc..bc6fee5e1 100644
--- a/stochss/handlers/util/ensemble_simulation.py
+++ b/stochss/handlers/util/ensemble_simulation.py
@@ -103,7 +103,11 @@ def __run(self, run_func, preview=False, verbose=True):
if verbose:
log.info(f"{self.g_model.name} preview simulation has completed")
log.info(f"Generate result plot for {self.g_model.name} preview")
- plot = results.plotplotly(return_plotly_figure=True)
+ included_species_list = []
+ for species in self.s_model['species']:
+ if species['observable']:
+ included_species_list.append(species['name'])
+ plot = results.plotplotly(return_plotly_figure=True, included_species_list=included_species_list)
plot["layout"]["autosize"] = True
plot["config"] = {"responsive": True, "displayModeBar": True}
return plot
diff --git a/stochss/handlers/util/scripts/run_preview.py b/stochss/handlers/util/scripts/run_preview.py
index f6eb1d656..f242a5703 100755
--- a/stochss/handlers/util/scripts/run_preview.py
+++ b/stochss/handlers/util/scripts/run_preview.py
@@ -108,6 +108,7 @@ def run_gillespy2_preview(args):
model = StochSSModel(path=args.path)
job = EnsembleSimulation(path="", preview=True)
job.g_model = model.convert_to_gillespy2()
+ job.s_model = model.model
plot = job.run(preview=True)
timeout = 'GillesPy2 simulation exceeded timeout.' in log_stm.getvalue()
log_stm.close()
diff --git a/stochss/handlers/util/stochss_job.py b/stochss/handlers/util/stochss_job.py
index 6ba2add76..190d2ebf7 100644
--- a/stochss/handlers/util/stochss_job.py
+++ b/stochss/handlers/util/stochss_job.py
@@ -505,16 +505,24 @@ def get_plot_from_results(self, data_keys, plt_key, add_config=False):
self.log("info", "Loading the results...")
result = self.__get_filtered_ensemble_results(data_keys)
self.log("info", "Generating the plot...")
+ included_species_list = []
+ for species in self.load_models()[1]['species']:
+ if species['observable']:
+ included_species_list.append(species['name'])
if plt_key == "mltplplt":
- fig = result.plotplotly(return_plotly_figure=True, multiple_graphs=True)
+ fig = result.plotplotly(
+ return_plotly_figure=True, multiple_graphs=True, included_species_list=included_species_list
+ )
elif plt_key == "stddevran":
- fig = result.plotplotly_mean_stdev(return_plotly_figure=True)
+ fig = result.plotplotly_mean_stdev(
+ return_plotly_figure=True, included_species_list=included_species_list
+ )
else:
if plt_key == "stddev":
result = result.stddev_ensemble()
elif plt_key == "avg":
result = result.average_ensemble()
- fig = result.plotplotly(return_plotly_figure=True)
+ fig = result.plotplotly(return_plotly_figure=True, included_species_list=included_species_list)
if add_config and plt_key != "mltplplt":
fig["config"] = {"responsive":True}
self.log("info", "Loading the plot...")
@@ -692,6 +700,18 @@ def get_time_stamp(self):
return time_stamp
return None
+ def get_update_time(self):
+ os.chdir(self.get_path(full=True))
+ if os.path.exists("COMPLETE"):
+ mod_time = os.path.getctime("COMPLETE")
+ elif os.path.exists("ERROR"):
+ mod_time = os.path.getctime("ERROR")
+ elif os.path.exists("RUNNING"):
+ mod_time = os.path.getctime("RUNNING")
+ else:
+ mod_time = None
+ os.chdir(self.user_dir)
+ return mod_time
def load(self, new=False):
'''
diff --git a/stochss/handlers/util/stochss_model.py b/stochss/handlers/util/stochss_model.py
index 00278a545..65b188c07 100644
--- a/stochss/handlers/util/stochss_model.py
+++ b/stochss/handlers/util/stochss_model.py
@@ -19,6 +19,7 @@
import os
import ast
import json
+import copy
import string
import hashlib
import tempfile
@@ -42,6 +43,7 @@ class StochSSModel(StochSSBase):
StochSS model object
################################################################################################
'''
+ TEMPLATE_VERSION = 2
def __init__(self, path, new=False, model=None):
'''
@@ -277,8 +279,8 @@ def __update_events(self, param_ids):
for event in self.model['eventsCollection']:
self.__update_event_assignments(event=event, param_ids=param_ids)
- def __update_model_to_current(self):
- if self.model['template_version'] == self.TEMPLATE_VERSION:
+ def __update_model_to_v1(self):
+ if self.model['template_version'] == 1:
return
param_ids = self.__update_parameters()
@@ -289,6 +291,59 @@ def __update_model_to_current(self):
if "refLinks" not in self.model.keys():
self.model['refLinks'] = []
+ def __update_model_to_current(self):
+ if self.model['template_version'] == self.TEMPLATE_VERSION:
+ return
+
+ self.__update_model_to_v1()
+
+ for species in self.model['species']:
+ species['observable'] = True
+
+ spec_template = {
+ 'mode': 'continuous', 'switchTol': 0.03, 'switchMin': 100,
+ 'isSwitchTol': True, 'diffusionConst': 0.0, 'types': [], 'observable': False
+ }
+ param_ids = list(map(lambda param: param['compID'], self.model['parameters']))
+ changes = {}
+ for event in self.model['eventsCollection']:
+ for assignment in event['eventAssignments']:
+ if assignment['variable']['compID'] in param_ids:
+ if assignment['variable']['compID'] in changes:
+ assignment['variable'] = changes[assignment['variable']['compID']]
+ else:
+ species = copy.deepcopy(spec_template)
+ updates = {
+ 'annotation': assignment['variable']['annotation'], 'name': assignment['variable']['name'],
+ 'compID': assignment['variable']['compID'], 'value': assignment['variable']['expression']
+ }
+ species.update(updates)
+ assignment['variable'] = species
+ changes[assignment['variable']['compID']] = species
+ for rule in self.model['rules']:
+ if rule['variable']['compID'] in param_ids:
+ if rule['variable']['compID'] in changes:
+ rule['variable'] = changes[rule['variable']['compID']]
+ else:
+ species = copy.deepcopy(spec_template)
+ updates = {
+ 'annotation': rule['variable']['annotation'], 'compID': rule['variable']['compID'],
+ 'name': rule['variable']['name'], 'value': rule['variable']['expression']
+ }
+ species.update(updates)
+ rule['variable'] = species
+ changes[rule['variable']['compID']] = species
+ if rule['type'] == "Rate Rule":
+ self.model['defaultMode'] = "dynamic"
+
+ self.model['species'].extend(list(changes.values()))
+ self.model['parameters'] = list(filter(
+ lambda param, ids=list(changes.keys()): param['compID'] not in ids, self.model['parameters']
+ ))
+ for reaction in self.model['reactions']:
+ if reaction['massaction'] and reaction['rate']['compID'] in changes:
+ reaction['rate'] = changes[reaction['rate']['compID']]
+
self.model['template_version'] = self.TEMPLATE_VERSION
def __update_parameters(self):
diff --git a/stochss/handlers/util/stochss_workflow.py b/stochss/handlers/util/stochss_workflow.py
index c687aeb08..2584790ad 100644
--- a/stochss/handlers/util/stochss_workflow.py
+++ b/stochss/handlers/util/stochss_workflow.py
@@ -165,20 +165,21 @@ def __load_annotation(self):
def __load_jobs(self):
self.workflow['jobs'] = []
- time = 0
+ mrm_time = 0
last_job = None
for file_obj in os.listdir(self.get_path(full=True)):
if file_obj.startswith("job_"):
path = os.path.join(self.path, file_obj)
- job = StochSSJob(path=path).load()
- self.workflow['jobs'].append(job)
- if os.path.getmtime(path) > time and job['status'] != "running":
- time = os.path.getmtime(path)
- last_job = job
+ job = StochSSJob(path=path)
+ self.workflow['jobs'].append(job.load())
+ cm_time = job.get_update_time()
+ if cm_time > mrm_time:
+ mrm_time = cm_time
+ last_job = len(self.workflow['jobs']) - 1
if last_job is None:
self.workflow['activeJob'] = None
else:
- self.workflow['activeJob'] = last_job
+ self.workflow['activeJob'] = self.workflow['jobs'][last_job]
def __load_settings(self):