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):