diff --git a/__version__.py b/__version__.py index 0956b15fd9..a9f7c69a73 100644 --- a/__version__.py +++ b/__version__.py @@ -5,7 +5,7 @@ # @website https://github.com/stochss/stochss # ============================================================================= -__version__ = '2.4.6' +__version__ = '2.4.7' __title__ = 'StochSS' __description__ = 'StochSS is an integrated development environment (IDE) \ for simulation of biochemical networks.' diff --git a/client/models/workflow-groups.js b/client/models/workflow-groups.js index 5f67914c98..c64bb2dbec 100644 --- a/client/models/workflow-groups.js +++ b/client/models/workflow-groups.js @@ -22,5 +22,6 @@ var Collection = require('ampersand-collection'); var WorkflowGroup = require('./workflow-group'); module.exports = Collection.extend({ - model: WorkflowGroup + model: WorkflowGroup, + comparator: "name" }); diff --git a/client/models/workflows.js b/client/models/workflows.js index 8530261841..d783c5e3c0 100644 --- a/client/models/workflows.js +++ b/client/models/workflows.js @@ -22,5 +22,6 @@ var Collection = require('ampersand-collection'); var Workflow = require('./workflow'); module.exports = Collection.extend({ - model: Workflow + model: Workflow, + comparator: "name" }); diff --git a/client/pages/loading-page.js b/client/pages/loading-page.js index b555aaa83c..70eb4b8891 100644 --- a/client/pages/loading-page.js +++ b/client/pages/loading-page.js @@ -70,12 +70,23 @@ let LoadingPage = PageView.extend({ let yesBtn = document.querySelector("#uploadFileExistsModal .yes-modal-btn"); let noBtn = document.querySelector("#uploadFileExistsModal .btn-secondary") yesBtn.addEventListener('click', function (e) { + modal.modal('hide'); self.uploadFileFromLink(filePath, true); }); noBtn.addEventListener('click', function (e) { window.location.href = self.homeLink; }); } + }, + error: function(err, response, body) { + if(document.querySelector("#errorModal")) { + document.querySelector("#errorModal").remove(); + } + $(self.queryByHook("loading-spinner")).css("display", "none"); + let modal = $(modals.errorHtml(body.Reason, body.Message)).modal(); + modal.on('hidden.bs.modal', function (e) { + window.location.href = self.homeLink; + }); } }); }, diff --git a/client/pages/model-editor.js b/client/pages/model-editor.js index efc9b0fbdd..fe16d3b4dc 100644 --- a/client/pages/model-editor.js +++ b/client/pages/model-editor.js @@ -260,11 +260,13 @@ let ModelEditor = PageView.extend({ }else if(e.target.dataset.type === "notebook"){ this.notebookWorkflow(e); }else if(!this.model.is_spatial) { - if(e.target.dataset.type === "ensemble") { - app.newWorkflow(this, this.model.directory, this.model.is_spatial, "Ensemble Simulation"); - }else if(e.target.dataset.type === "psweep") { - app.newWorkflow(this, this.model.directory, this.model.is_spatial, "Parameter Sweep"); - } + this.saveModel(() => { + if(e.target.dataset.type === "ensemble") { + app.newWorkflow(this, this.model.directory, this.model.is_spatial, "Ensemble Simulation"); + }else if(e.target.dataset.type === "psweep") { + app.newWorkflow(this, this.model.directory, this.model.is_spatial, "Parameter Sweep"); + } + }); } } }, diff --git a/client/settings-view/templates/simulationSettingsView.pug b/client/settings-view/templates/simulationSettingsView.pug index d06f917689..c5be4b898d 100644 --- a/client/settings-view/templates/simulationSettingsView.pug +++ b/client/settings-view/templates/simulationSettingsView.pug @@ -24,7 +24,7 @@ div#simulation-settings.card | Consider the following when selecting an algorithm for your job. If you would like StochSS to choose the algorithm, select Choose for me. ul.mb-0 - li Algorithms ODE and Hybrid ODE/SSA work best to simulate models with a mode of Concentration. + li Algorithms ODE, CLE, and Hybrid ODE/SSA work best to simulate models with a mode of Concentration. li Algorithns SSA, Tau Leaping, and Hybrid ODE/SSA work best to simulate models with a mode of Population. li Algorithm Hybrid ODE/SSA is required if the model contains advanced components. @@ -58,6 +58,14 @@ div#simulation-settings.card div.tooltip-icon(data-html="true" data-toggle="tooltip" title=this.tooltips.ssa) + div.col-sm-2 + + input.mr-2.inline(type="radio", name="simAlgorithm", data-hook="select-cle", data-name="CLE") + + span.inline CLE + + div.tooltip-icon(data-html="true" data-toggle="tooltip" title=this.tooltips.cle) + div.col-sm-2 input.mr-2.inline(type="radio", name="simAlgorithm", data-hook="select-tau-leaping", data-name="Tau-Leaping") @@ -66,7 +74,7 @@ div#simulation-settings.card div.tooltip-icon(data-html="true" data-toggle="tooltip" title=this.tooltips.tauLeaping) - div.col-sm-3 + div.col-sm-2 input.mr-2.inline(type="radio", name="simAlgorithm", data-hook="select-hybrid-tau", data-name="Hybrid-Tau-Leaping") @@ -74,7 +82,7 @@ div#simulation-settings.card div.tooltip-icon(data-html="true" data-toggle="tooltip" title=this.tooltips.hybrid) - div.col-sm-3 + div.col-sm-2 input.mr-2.inline(type="radio", name="simAlgorithm", data-hook="select-automatic", data-name="Automatic") diff --git a/client/settings-view/views/simulation-settings-view.js b/client/settings-view/views/simulation-settings-view.js index fc0b68ab4f..171ff924db 100644 --- a/client/settings-view/views/simulation-settings-view.js +++ b/client/settings-view/views/simulation-settings-view.js @@ -32,6 +32,7 @@ module.exports = View.extend({ events: { 'change [data-hook=select-ode]' : 'handleSelectSimulationAlgorithmClick', 'change [data-hook=select-ssa]' : 'handleSelectSimulationAlgorithmClick', + 'change [data-hook=select-cle]' : 'handleSelectSimulationAlgorithmClick', 'change [data-hook=select-tau-leaping]' : 'handleSelectSimulationAlgorithmClick', 'change [data-hook=select-hybrid-tau]' : 'handleSelectSimulationAlgorithmClick', 'change [data-hook=select-automatic]' : 'handleSelectSimulationAlgorithmClick', @@ -81,13 +82,14 @@ module.exports = View.extend({ let isAutomatic = this.model.isAutomatic; let isODE = this.model.algorithm === "ODE"; let isSSA = this.model.algorithm === "SSA"; + let isCLE = this.model.algorithm === "CLE"; let isLeaping = this.model.algorithm === "Tau-Leaping"; let isHybrid = this.model.algorithm === "Hybrid-Tau-Leaping"; $(this.queryByHook("relative-tolerance")).find('input').prop('disabled', !(isODE || isHybrid || isAutomatic)); $(this.queryByHook("absolute-tolerance")).find('input').prop('disabled', !(isODE || isHybrid || isAutomatic)); - $(this.queryByHook("trajectories")).find('input').prop('disabled', !(isSSA || isLeaping || isHybrid || isAutomatic)); - $(this.queryByHook("seed")).find('input').prop('disabled', !(isSSA || isLeaping || isHybrid || isAutomatic)); - $(this.queryByHook("tau-tolerance")).find('input').prop('disabled', !(isHybrid || isLeaping || isAutomatic)); + $(this.queryByHook("trajectories")).find('input').prop('disabled', !(isSSA || isCLE || isLeaping || isHybrid || isAutomatic)); + $(this.queryByHook("seed")).find('input').prop('disabled', !(isSSA || isCLE || isLeaping || isHybrid || isAutomatic)); + $(this.queryByHook("tau-tolerance")).find('input').prop('disabled', !(isHybrid || isCLE || isLeaping || isAutomatic)); }, handleSelectSimulationAlgorithmClick: function (e) { let value = e.target.dataset.name; @@ -128,7 +130,7 @@ module.exports = View.extend({ updateValid: function (e) {}, updateViewer: function () { $(this.queryByHook("view-algorithm")).html(this.algorithm); - let hideDeterministic = this.model.isAutomatic || this.model.algorithm === "SSA" || this.model.algorithm === "Tau-Leaping"; + let hideDeterministic = this.model.isAutomatic || this.model.algorithm === "SSA" || this.model.algorithm === "CLE" || this.model.algorithm === "Tau-Leaping"; let hideStochastic = this.model.isAutomatic || this.model.algorithm === "ODE" ; if(hideDeterministic) { $(this.queryByHook("view-deterministic-settings")).css("display", "none"); diff --git a/stochss/handlers/file_browser.py b/stochss/handlers/file_browser.py index c24944eb95..7bccf87585 100644 --- a/stochss/handlers/file_browser.py +++ b/stochss/handlers/file_browser.py @@ -685,10 +685,13 @@ async def get(self): log.debug(f"The command for the upload script: {cmd}") script = '/stochss/stochss/handlers/util/scripts/upload_remote_file.py' if cmd == "validate": - folder = StochSSFolder(path="") - resp = {'exists': folder.validate_upload_link(remote_path=path)} - log.debug(f"Response: {resp}") - self.write(resp) + try: + folder = StochSSFolder(path="") + resp = {'exists': folder.validate_upload_link(remote_path=path)} + log.debug(f"Response: {resp}") + self.write(resp) + except StochSSAPIError as err: + report_error(self, log, err) elif cmd is None: overwrite = self.get_query_argument(name='overwrite', default=False) outfile = f"{str(uuid.uuid4()).replace('-', '_')}.tmp" diff --git a/stochss/handlers/util/ensemble_simulation.py b/stochss/handlers/util/ensemble_simulation.py index 6c88ebf657..b960f546a6 100644 --- a/stochss/handlers/util/ensemble_simulation.py +++ b/stochss/handlers/util/ensemble_simulation.py @@ -18,6 +18,7 @@ import os +import numpy import pickle import logging import traceback @@ -88,7 +89,7 @@ def __update_timespan(self): end = self.settings['timespanSettings']['endSim'] step_size = self.settings['timespanSettings']['timeStep'] self.g_model.timespan( - TimeSpan.arange(t=end + step_size, increment=step_size) + TimeSpan(numpy.arange(0, end + step_size, step_size)) ) diff --git a/stochss/handlers/util/parameter_sweep.py b/stochss/handlers/util/parameter_sweep.py index 454d0fe0bd..b8e68a9332 100644 --- a/stochss/handlers/util/parameter_sweep.py +++ b/stochss/handlers/util/parameter_sweep.py @@ -18,6 +18,7 @@ import os import json +import numpy import pickle import logging import traceback @@ -120,7 +121,7 @@ def configure(self): end = self.settings['timespanSettings']['endSim'] step_size = self.settings['timespanSettings']['timeStep'] self.g_model.timespan( - TimeSpan.arange(t=end + step_size, increment=step_size) + TimeSpan(numpy(0, end + step_size, step_size)) ) kwargs = {"model":self.g_model, "settings":run_settings} parameters = [] diff --git a/stochss/handlers/util/stochss_folder.py b/stochss/handlers/util/stochss_folder.py index 0443654b5e..b5d29f732a 100644 --- a/stochss/handlers/util/stochss_folder.py +++ b/stochss/handlers/util/stochss_folder.py @@ -22,6 +22,7 @@ import shutil import string import zipfile +import tempfile import datetime import traceback @@ -33,7 +34,7 @@ from .stochss_model import StochSSModel from .stochss_sbml import StochSSSBMLModel from .stochss_errors import StochSSFileExistsError, StochSSFileNotFoundError, \ - StochSSPermissionsError + StochSSPermissionsError, StochSSUnzipError class StochSSFolder(StochSSBase): @@ -66,6 +67,38 @@ def __init__(self, path, new=False): raise StochSSFileExistsError(message, traceback.format_exc()) from err + def __get_file_from_link(self, remote_path): + ext = remote_path.split('.').pop().split('?')[0] + if os.path.exists("/stochss/.proxies.txt"): + with open("/stochss/.proxies.txt", "r") as proxy_file: + proxy_ip = proxy_file.read().strip() + proxies = { + "https": f"https://{proxy_ip}", + "http": f"http://{proxy_ip}" + } + else: + proxies = None + response = requests.get(remote_path, allow_redirects=True, proxies=proxies) + body = response.content + if "download_presentation" in remote_path: + if ext in ("mdl", "smdl"): + file = f"{json.loads(body)['name']}.{ext}" + elif ext == "ipynb": + file = json.loads(body)['file'] + body = json.dumps(json.loads(body)['notebook']) + elif ext == "job": + file = self.get_file(path=remote_path) + elif "?" in remote_path: + file = self.get_file(path=remote_path.split("?")[0]) + else: + file = self.get_file(path=remote_path) + if response.status_code == 404: + message = f"Could not upload this file as {file} was not found." + if "?token=" in remote_path: + message += " The token for this file may be out of date." + raise StochSSFileNotFoundError(message, traceback.format_exc()) + return ext, file, body + def __get_rmt_upld_path(self, file): if not file.endswith(".zip"): return file @@ -150,18 +183,30 @@ def __get_presentation_notebook_name(cls, file_path): return name - @classmethod - def __overwrite(cls, path, ext): - if ext == "zip": - with zipfile.ZipFile(path, "r") as zip_file: - members = zip_file.namelist() - for name in members: - if os.path.isdir(name): - shutil.rmtree(name) - elif os.path.exists(name): - os.remove(name) - elif os.path.exists(path): + def __overwrite(self, path, ext, body): + if ext != "zip": os.remove(path) + else: + if os.path.exists(path): + os.remove(path) + file = self.get_file(path=path) + with tempfile.TemporaryDirectory() as tmp_dir: + ext_path = os.path.join(tmp_dir, file) + with open(ext_path, "wb") as zip_file: + zip_file.write(body) + try: + with zipfile.ZipFile(ext_path, 'r') as zip_file: + members = set([name.split('/')[0] for name in zip_file.namelist()]) + for name in members: + m_path = self.get_new_path(dst_path=name) + if os.path.exists(m_path): + if os.path.isdir(m_path): + shutil.rmtree(m_path) + else: + os.remove(m_path) + except zipfile.BadZipFile as err: + message = "File is not a zip file" + raise StochSSFileNotFoundError(message, traceback.format_exc()) from err def __upload_file(self, file, body, new_name=None): @@ -529,38 +574,13 @@ def upload_from_link(self, remote_path, overwrite=False): overwrite : bool Overwrite the existing files. ''' - ext = remote_path.split('.').pop() - if os.path.exists("/stochss/.proxies.txt"): - with open("/stochss/.proxies.txt", "r") as proxy_file: - proxy_ip = proxy_file.read().strip() - proxies = { - "https": f"https://{proxy_ip}", - "http": f"http://{proxy_ip}" - } - else: - proxies = None - body = requests.get(remote_path, allow_redirects=True, proxies=proxies).content - if "download_presentation" in remote_path: - if ext in ("mdl", "smdl"): - file = f"{json.loads(body)['name']}.{ext}" - elif ext == "ipynb": - file = json.loads(body)['file'] - body = json.dumps(json.loads(body)['notebook']) - else: - file = self.get_file(path=remote_path) - if "404: Not Found" in body.decode(): - message = f"Could not upload this file as {file} was not found." - if "?token=" in file: - message += " The token for this file may be out of date." - return {"message": message, "reason":"File Not Found"} - if "?token=" in file: - file = file.split("?token=")[0] + ext, file, body = self.__get_file_from_link(remote_path) path = self.get_new_path(dst_path=file) - if os.path.exists(path): - if not overwrite: - message = f"Could not upload this file as {file} already exists" - return {"message":message, "reason":"File Already Exists"} - self.__overwrite(path=path, ext=ext) + if overwrite: + self.__overwrite(path=path, ext=ext, body=body) + elif os.path.exists(path): + message = f"Could not upload this file as {file} already exists" + return {"message":message, "reason":"File Already Exists"} try: file_types = {"mdl":"model", "smdl":"model", "sbml":"sbml"} file_type = file_types[ext] if ext in file_types.keys() else "file" @@ -581,25 +601,22 @@ def validate_upload_link(self, remote_path): remote_path : str Path to the remote file ''' - ext = remote_path.split('.').pop() - body = requests.get(remote_path, allow_redirects=True).content - if "download_presentation" in remote_path: - if ext in ("mdl", "smdl"): - file = f"{json.loads(body)['name']}.{ext}" - elif ext == "ipynb": - file = json.loads(body)['file'] - body = json.dumps(json.loads(body)['notebook']) - elif ext == "job": - file = self.get_file(path=remote_path) - else: - file = self.get_file(path=remote_path) - if "?token=" in file: - file = file.split("?token=")[0] + ext, file, body = self.__get_file_from_link(remote_path) path = self.get_new_path(dst_path=file) - if ext == "zip": - with zipfile.ZipFile(path, "r") as zip_file: - members = zip_file.namelist() - for name in members: - if os.path.exists(name): - return True - return os.path.exists(path) + exists = os.path.exists(path) + if ext != "zip" or exists: + return exists + with tempfile.TemporaryDirectory() as tmp_dir: + ext_path = os.path.join(tmp_dir, file) + with open(ext_path, "wb") as zip_file: + zip_file.write(body) + try: + with zipfile.ZipFile(ext_path, 'r') as zip_file: + members = set([name.split('/')[0] for name in zip_file.namelist()]) + for name in members: + if os.path.exists(self.get_new_path(dst_path=name)): + return True + except zipfile.BadZipFile as err: + message = "File is not a zip file" + raise StochSSFileNotFoundError(message, traceback.format_exc()) from err + return False diff --git a/stochss/handlers/util/stochss_job.py b/stochss/handlers/util/stochss_job.py index b9b4ff6228..686149b791 100644 --- a/stochss/handlers/util/stochss_job.py +++ b/stochss/handlers/util/stochss_job.py @@ -356,11 +356,11 @@ def get_csvzip_from_results(self, data_keys, proc_key, name): result = result.average_ensemble() self.log("info", "Generating CSV files...") with tempfile.TemporaryDirectory() as tmp_dir: - result.to_csv(path=tmp_dir.name, nametag=name, stamp="") + result.to_csv(path=tmp_dir, nametag=name, stamp="") if data_keys: - self.__write_parameters_csv(path=tmp_dir.name, name=name, data_keys=data_keys) + self.__write_parameters_csv(path=tmp_dir, name=name, data_keys=data_keys) self.log("info", "Generating zip archive...") - return self.__get_csvzip(dirname=tmp_dir.name, name=name) + return self.__get_csvzip(dirname=tmp_dir, name=name) except FileNotFoundError as err: message = f"Could not find the results pickle file: {str(err)}" raise StochSSFileNotFoundError(message, traceback.format_exc()) from err @@ -383,15 +383,15 @@ def get_full_csvzip_from_results(self, name): with tempfile.TemporaryDirectory() as tmp_dir: if not isinstance(results, dict): self.log("info", "Generating CSV files...") - results.to_csv(path=tmp_dir.name, nametag=name, stamp="") + results.to_csv(path=tmp_dir, nametag=name, stamp="") self.log("info", "Generating zip archive...") - return self.__get_csvzip(dirname=tmp_dir.name, name=name) + return self.__get_csvzip(dirname=tmp_dir, name=name) def get_name(b_name, tag): return f"{b_name}_{tag}" - b_path = os.path.join(tmp_dir.name, get_name(name, "full")) + b_path = os.path.join(tmp_dir, get_name(name, "full")) self.log("info", "Generating time series CSV files...") self.__get_full_timeseries_csv(b_path, results, get_name, name) - return self.__get_csvzip(dirname=tmp_dir.name, name=get_name(name, "full")) + return self.__get_csvzip(dirname=tmp_dir, name=get_name(name, "full")) def get_model_path(self, full=False, external=False): @@ -511,18 +511,18 @@ def get_psweep_csvzip_from_results(self, fixed, name): kwargs["species"] = list(kwargs['results'][0][0].model.listOfSpecies.keys()) self.log("info", "Generating CSV files...") ParameterSweep1D.to_csv( - param=params[0], kwargs=kwargs, path=tmp_dir.name, nametag=name + param=params[0], kwargs=kwargs, path=tmp_dir, nametag=name ) else: kwargs = {"results": self.__get_filtered_2d_results(f_keys, params[0])} kwargs["species"] = list(kwargs['results'][0][0][0].model.listOfSpecies.keys()) self.log("info", "Generating CSV files...") ParameterSweep2D.to_csv( - params=params, kwargs=kwargs, path=tmp_dir.name, nametag=name + params=params, kwargs=kwargs, path=tmp_dir, nametag=name ) if fixed: - self.__write_parameters_csv(path=tmp_dir.name, name=name, data_keys=fixed) - return self.__get_csvzip(dirname=tmp_dir.name, name=name) + self.__write_parameters_csv(path=tmp_dir, name=name, data_keys=fixed) + return self.__get_csvzip(dirname=tmp_dir, name=name) except FileNotFoundError as err: message = f"Could not find the results pickle file: {str(err)}" raise StochSSFileNotFoundError(message, traceback.format_exc()) from err @@ -586,7 +586,7 @@ def get_run_settings(cls, settings, solver_map): settings = settings['simulationSettings'] kwargs = {"solver":solver_map[settings['algorithm']]} if settings['algorithm'] in ("ODE", "Hybrid-Tau-Leaping") and \ - "CSolver" not in kwargs['solver']: + "CSolver" not in kwargs['solver'].name: integrator_options = {"atol":settings['absoluteTol'], "rtol":settings['relativeTol']} kwargs["integrator_options"] = integrator_options if settings['algorithm'] == "ODE": diff --git a/stochss/handlers/util/stochss_model.py b/stochss/handlers/util/stochss_model.py index b9c7047936..a090934d24 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 numpy import string import hashlib import tempfile @@ -138,7 +139,7 @@ def __convert_model_settings(self): try: end = self.model['modelSettings']['endSim'] step_size = self.model['modelSettings']['timeStep'] - return TimeSpan.arange(t=end, increment=step_size) + return TimeSpan(numpy.arange(0, end + step_size, step_size)) except KeyError as err: message = "Model settings are not properly formatted or " message += f"are referenced incorrectly: {str(err)}" diff --git a/stochss/handlers/util/stochss_notebook.py b/stochss/handlers/util/stochss_notebook.py index 2601fde90f..e96ba80ed8 100644 --- a/stochss/handlers/util/stochss_notebook.py +++ b/stochss/handlers/util/stochss_notebook.py @@ -405,7 +405,7 @@ def __create_species_strings(self, model, pad, type_refs=None): spec_str += f"diffusion_coefficient={spec['diffusionConst']}, " spec_str += f"restrict_to=[{', '.join(types)}])" else: - spec_str += f'initial_value={spec["value"]}, mode="{spec["mode"]}"))' + spec_str += f'initial_value={spec["value"]}, mode="{spec["mode"]}")' species.append(spec_str) species.append(f"{pad}self.add_species([{', '.join(names)}])") model.extend(species) @@ -437,10 +437,10 @@ def __create_tspan_string(self, model, pad): step_size = self.s_model['modelSettings']['timestepSize'] ts_str = f'{pad}self.timespan(np.arange(0, {end + step_size}, {output_freq})' ts_str += f", timestep_size={step_size})" + tspan.append(ts_str) else: - ts_str = f'{pad}tspan = TimeSpan(t={end + output_freq}, increment={output_freq})' - ts_str = f'{pad}self.timespan(tspan)' - tspan.append(ts_str) + tspan.append(f'{pad}tspan = TimeSpan(np.arange(0, {end + output_freq}, {output_freq}))') + tspan.append(f'{pad}self.timespan(tspan)') model.extend(tspan)