diff --git a/client/models/sweep-parameter.js b/client/models/sweep-parameter.js index ca35393999..e9efdf0a38 100644 --- a/client/models/sweep-parameter.js +++ b/client/models/sweep-parameter.js @@ -1,14 +1,17 @@ /* StochSS is a platform for simulating biochemical systems Copyright (C) 2019-2020 StochSS developers. + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + You should have received a copy of the GNU General Public License along with this program. If not, see . */ diff --git a/client/models/sweep-parameters.js b/client/models/sweep-parameters.js index c9f08e3340..eb60b7f7c0 100644 --- a/client/models/sweep-parameters.js +++ b/client/models/sweep-parameters.js @@ -1,14 +1,17 @@ /* StochSS is a platform for simulating biochemical systems Copyright (C) 2019-2020 StochSS developers. + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + You should have received a copy of the GNU General Public License along with this program. If not, see . */ diff --git a/client/pages/file-browser.js b/client/pages/file-browser.js index 1cf0296c34..3ebc068764 100644 --- a/client/pages/file-browser.js +++ b/client/pages/file-browser.js @@ -842,6 +842,24 @@ let FileBrowser = PageView.extend({ } }); }, + extractAll: function (o) { + let self = this; + let queryStr = "?path=" + o.original._path; + let endpoint = path.join(app.getApiPath(), "file/unzip") + queryStr; + app.getXHR(endpoint, { + success: function (err, response, body) { + let node = $('#models-jstree').jstree().get_node(o.parent); + if(node.type === "root"){ + self.refreshJSTree(); + }else{ + $('#models-jstree').jstree().refresh_node(node); + } + }, + error: function (err, response, body) { + let modal = $(modals.newProjectModelErrorHtml(body.Reason, body.message)).modal(); + } + }); + }, setupJstree: function () { var self = this; $.jstree.defaults.contextmenu.items = (o, cb) => { @@ -1235,6 +1253,18 @@ let FileBrowser = PageView.extend({ } } } + //Specific to zip archives + let extractAll = { + "extractAll" : { + "label" : "Extract All", + "_disabled" : false, + "separator_before" : false, + "separator_after" : false, + "action" : function (data) { + self.extractAll(o); + } + } + } if (o.type === 'root'){ return folder } @@ -1253,6 +1283,9 @@ let FileBrowser = PageView.extend({ if (o.type === 'workflow') { return $.extend(open, workflow, common) } + if (o.text.endsWith(".zip")) { + return $.extend(open, extractAll, common) + } if (o.type === 'notebook' || o.type === "other") { return $.extend(open, common) } diff --git a/client/styles/styles.css b/client/styles/styles.css index 5c1f35ea3a..29403e2122 100644 --- a/client/styles/styles.css +++ b/client/styles/styles.css @@ -1,3 +1,21 @@ +/* +StochSS is a platform for simulating biochemical systems +Copyright (C) 2019-2020 StochSS developers. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + body { min-width: 650px } diff --git a/client/templates/includes/reactionsEditor.pug b/client/templates/includes/reactionsEditor.pug index 0ec990c9c6..b27791913b 100644 --- a/client/templates/includes/reactionsEditor.pug +++ b/client/templates/includes/reactionsEditor.pug @@ -35,6 +35,8 @@ div#reactions-editor.card.card-body tbody(data-hook="reaction-list") div.col-md-5.container-part(data-hook="reaction-details-container") + + div(data-hook="massaction-message"): p.text-info To add a mass action reaction the model must have at least one parameter div(data-hook="process-component-error"): p.text-danger A model must have at least one reaction, event, or rule diff --git a/client/views/file-browser-view.js b/client/views/file-browser-view.js index aff25ff023..6a317fbb74 100644 --- a/client/views/file-browser-view.js +++ b/client/views/file-browser-view.js @@ -924,6 +924,24 @@ module.exports = View.extend({ } }); }, + extractAll: function (o) { + let self = this; + let queryStr = "?path=" + o.original._path; + let endpoint = path.join(app.getApiPath(), "file/unzip") + queryStr; + app.getXHR(endpoint, { + success: function (err, response, body) { + let node = $('#models-jstree-view').jstree().get_node(o.parent); + if(node.type === "root"){ + self.refreshJSTree(); + }else{ + $('#models-jstree-view').jstree().refresh_node(node); + } + }, + error: function (err, response, body) { + let modal = $(modals.newProjectModelErrorHtml(body.Reason, body.message)).modal(); + } + }); + }, setupJstree: function () { var self = this; $.jstree.defaults.contextmenu.items = (o, cb) => { @@ -1343,6 +1361,18 @@ module.exports = View.extend({ } } } + //Specific to zip archives + let extractAll = { + "extractAll" : { + "label" : "Extract All", + "_disabled" : false, + "separator_before" : false, + "separator_after" : false, + "action" : function (data) { + self.extractAll(o); + } + } + } if (o.type === 'root'){ return $.extend(refresh, project, commonFolder, downloadWCombine, {"Rename": common.Rename}) } @@ -1367,6 +1397,9 @@ module.exports = View.extend({ if (o.type === 'workflow') { return $.extend(open, workflow, downloadWCombine, common) } + if (o.text.endsWith(".zip")) { + return $.extend(open, extractAll, download, common) + } if (o.type === 'notebook' || o.type === "other") { return $.extend(open, download, common) } diff --git a/client/views/reactions-editor.js b/client/views/reactions-editor.js index fe3bbd03ef..10647b431e 100644 --- a/client/views/reactions-editor.js +++ b/client/views/reactions-editor.js @@ -133,9 +133,11 @@ module.exports = View.extend({ }, toggleReactionTypes: function (e, prev, curr) { if(curr && curr.add && this.collection.parent.parameters.length === 1){ + $(this.queryByHook('massaction-message')).prop('hidden', true); $(this.queryByHook('add-reaction-full')).prop('hidden', false); $(this.queryByHook('add-reaction-partial')).prop('hidden', true); }else if(curr && !curr.add && this.collection.parent.parameters.length === 0){ + $(this.queryByHook('massaction-message')).prop('hidden', false); $(this.queryByHook('add-reaction-full')).prop('hidden', true); $(this.queryByHook('add-reaction-partial')).prop('hidden', false); } diff --git a/stochss/handlers/__init__.py b/stochss/handlers/__init__.py index 1703f4b6b4..f52ecc0747 100644 --- a/stochss/handlers/__init__.py +++ b/stochss/handlers/__init__.py @@ -63,6 +63,7 @@ def get_page_handlers(route_start): (r"/stochss/api/file/download-zip\/?", DownloadZipFileAPIHandler), (r"/stochss/api/file/json-data\/?", JsonFileAPIHandler), (r"/stochss/api/file/duplicate\/?", DuplicateModelHandler), + (r"/stochss/api/file/unzip\/?", UnzipFileAPIHandler), (r"/stochss/api/directory/duplicate\/?", DuplicateDirectoryHandler), (r"/stochss/api/directory/create\/?", CreateDirectoryHandler), (r"/stochss/api/spatial/to-model\/?", ConvertToModelAPIHandler), diff --git a/stochss/handlers/file_browser.py b/stochss/handlers/file_browser.py index 11fce21679..7d35d52423 100644 --- a/stochss/handlers/file_browser.py +++ b/stochss/handlers/file_browser.py @@ -674,3 +674,30 @@ async def get(self): log.debug("Response: %s", resp) self.write(resp) self.finish() + + +class UnzipFileAPIHandler(APIHandler): + ''' + ################################################################################################ + Handler for unzipping zip archives. + ################################################################################################ + ''' + + async def get(self): + ''' + Unzip a zip archive. + + Attributes + ---------- + ''' + self.set_header('Content-Type', 'application/json') + path = self.get_query_argument(name="path") + log.debug("The path to the zip archive: %s", path) + try: + file = StochSSFile(path=path) + resp = file.unzip(from_upload=False) + log.debug("Response Message: %s", resp) + self.write(resp) + except StochSSAPIError as err: + report_error(self, log, err) + self.finish() diff --git a/stochss/handlers/util/ensemble_simulation.py b/stochss/handlers/util/ensemble_simulation.py index e68bea8560..925922e2c2 100644 --- a/stochss/handlers/util/ensemble_simulation.py +++ b/stochss/handlers/util/ensemble_simulation.py @@ -21,13 +21,14 @@ import json import pickle import logging +import traceback import numpy import plotly -from gillespy2 import TauLeapingSolver, TauHybridSolver, SSACSolver, ODESolver +from gillespy2 import TauHybridSolver from .stochss_job import StochSSJob -from .stochss_errors import StochSSAPIError +from .stochss_errors import StochSSAPIError, StochSSJobResultsError log = logging.getLogger("stochss") @@ -59,36 +60,57 @@ def __init__(self, path, preview=False): def __get_run_settings(self): - solver_map = {"SSA":SSACSolver, "Tau-Leaping":TauLeapingSolver, - "ODE":ODESolver, "Hybrid-Tau-Leaping":TauHybridSolver} + solver_map = {"ODE":self.g_model.get_best_solver_algo("ODE"), + "SSA":self.g_model.get_best_solver_algo("SSA"), + "Tau-Leaping":self.g_model.get_best_solver_algo("Tau-Leaping"), + "Hybrid-Tau-Leaping":TauHybridSolver} run_settings = self.get_run_settings(settings=self.settings, solver_map=solver_map) - if run_settings['solver'].name == "SSACSolver": + instance_solvers = ["SSACSolver", "TauLeapingCSolver", "ODECSolver"] + if run_settings['solver'].name in instance_solvers : run_settings['solver'] = run_settings['solver'](model=self.g_model) return run_settings + def __store_csv_results(self, results): + try: + results.to_csv(path="results", nametag="results_csv", stamp=self.get_time_stamp()) + except Exception as err: + log.error("Error storing csv results: %s\n%s", + str(err), traceback.format_exc()) + + @classmethod - def __plot_results(cls, results): - plots = {"trajectories":results.plotplotly(return_plotly_figure=True)} - if len(results) > 1: - plots['stddevran'] = results.plotplotly_std_dev_range(return_plotly_figure=True) - std_res = results.stddev_ensemble() - plots['stddev'] = std_res.plotplotly(return_plotly_figure=True) - avg_res = results.average_ensemble() - plots['avg'] = avg_res.plotplotly(return_plotly_figure=True) - for _, plot in plots.items(): - plot["config"] = {"responsive":True} - with open('results/plots.json', 'w') as plots_file: - json.dump(plots, plots_file, cls=plotly.utils.PlotlyJSONEncoder, - indent=4, sort_keys=True) - - - def __store_results(self, results): - if not 'results' in os.listdir(): - os.mkdir('results') - with open('results/results.p', 'wb') as results_file: - pickle.dump(results, results_file) - results.to_csv(path="results", nametag="results_csv", stamp=self.get_time_stamp()) + def __store_pickled_results(cls, results): + try: + with open('results/results.p', 'wb') as results_file: + pickle.dump(results, results_file) + except Exception as err: + message = f"Error storing pickled results: {err}\n{traceback.format_exc()}" + log.error(message) + return message + return False + + + @classmethod + def __store_result_plots(cls, results): + try: + plots = {"trajectories":results.plotplotly(return_plotly_figure=True)} + if len(results) > 1: + plots['stddevran'] = results.plotplotly_std_dev_range(return_plotly_figure=True) + std_res = results.stddev_ensemble() + plots['stddev'] = std_res.plotplotly(return_plotly_figure=True) + avg_res = results.average_ensemble() + plots['avg'] = avg_res.plotplotly(return_plotly_figure=True) + for _, plot in plots.items(): + plot["config"] = {"responsive":True} + with open('results/plots.json', 'w') as plots_file: + json.dump(plots, plots_file, cls=plotly.utils.PlotlyJSONEncoder, + indent=4, sort_keys=True) + except Exception as err: + message = f"Error storing result plots: {err}\n{traceback.format_exc()}" + log.error(message) + return message + return False def __update_timespan(self): @@ -126,7 +148,7 @@ def run(self, preview=False, verbose=True): log.info("Running the ensemble simulation") if self.settings['simulationSettings']['isAutomatic']: self.__update_timespan() - is_ode = self.g_model.get_best_solver().name == "ODESolver" + is_ode = self.g_model.get_best_solver().name in ["ODESolver", "ODECSolver"] results = self.g_model.run(number_of_trajectories=1 if is_ode else 100) else: kwargs = self.__get_run_settings() @@ -135,8 +157,15 @@ def run(self, preview=False, verbose=True): if verbose: log.info("The ensemble simulation has completed") log.info("Storing the results as pickle and csv") - self.__store_results(results=results) + if not 'results' in os.listdir(): + os.mkdir('results') + self.__store_csv_results(results=results) + pkl_err = self.__store_pickled_results(results=results) if verbose: log.info("Storing the polts of the results") - self.__plot_results(results=results) + plt_err = self.__store_result_plots(results=results) + if pkl_err and plt_err: + message = "An unexpected error occured with the result object" + trace = f"{pkl_err}\n{plt_err}" + raise StochSSJobResultsError(message, trace) return None diff --git a/stochss/handlers/util/parameter_sweep.py b/stochss/handlers/util/parameter_sweep.py index 7fafa79ce7..2b2c1dca85 100644 --- a/stochss/handlers/util/parameter_sweep.py +++ b/stochss/handlers/util/parameter_sweep.py @@ -22,16 +22,18 @@ import pickle import logging import itertools +import traceback import numpy import plotly -from gillespy2 import TauLeapingSolver, TauHybridSolver, SSACSolver, ODESolver +from gillespy2 import TauHybridSolver from .stochss_job import StochSSJob from .parameter_sweep_1d import ParameterSweep1D from .parameter_sweep_2d import ParameterSweep2D from .parameter_scan import ParameterScan +from .stochss_errors import StochSSJobResultsError log = logging.getLogger("stochss") @@ -71,77 +73,115 @@ def __init__(self, path): def __get_run_settings(self): - solver_map = {"SSA":SSACSolver, "Tau-Leaping":TauLeapingSolver, - "ODE":ODESolver, "Hybrid-Tau-Leaping":TauHybridSolver} + instance_solvers = ["SSACSolver", "TauLeapingCSolver", "ODECSolver"] if self.settings['simulationSettings']['isAutomatic']: - solver_name = self.g_model.get_best_solver().name - kwargs = {"number_of_trajectories":1 if solver_name == "ODESolver" else 20} - if solver_name != "SSACSolver": + solver = self.g_model.get_best_solver() + kwargs = {"number_of_trajectories":1 if "ODE" in solver.name else 20} + if solver.name not in instance_solvers: return kwargs - kwargs['solver'] = solver_map['SSA'](model=self.g_model) + kwargs['solver'] = solver(model=self.g_model) return kwargs + solver_map = {"SSA":self.g_model.get_best_solver_algo("SSA"), + "Tau-Leaping":self.g_model.get_best_solver_algo("Tau-Leaping"), + "ODE":self.g_model.get_best_solver_algo("ODE"), + "Hybrid-Tau-Leaping":TauHybridSolver} run_settings = self.get_run_settings(settings=self.settings, solver_map=solver_map) - if run_settings['solver'].name == "SSACSolver": + if run_settings['solver'].name in instance_solvers: run_settings['solver'] = run_settings['solver'](model=self.g_model) return run_settings - def __store_csv_results(self, wkfl): - if "ODE" not in wkfl.settings['solver'].name and \ - wkfl.settings['number_of_trajectories'] > 1: - csv_keys = list(itertools.product(["min", "max", "avg", "var", "final"], + @classmethod + def __report_result_error(cls, trace): + message = "An unexpected error occured with the result object" + raise StochSSJobResultsError(message, trace) + + + def __store_csv_results(self, job): + try: + if "solver" in job.settings.keys(): + solver_name = job.settings['solver'].name + else: + solver_name = job.model.get_best_solver().name + if "ODE" not in solver_name and job.settings['number_of_trajectories'] > 1: + csv_keys = list(itertools.product(["min", "max", "avg", "var", "final"], + ["min", "max", "avg", "var"])) + else: + csv_keys = [["min"], ["max"], ["avg"], ["var"], ["final"]] + stamp = self.get_time_stamp() + dirname = f"results/results_csv{stamp}" + if not os.path.exists(dirname): + os.mkdir(dirname) + for key in csv_keys: + if not isinstance(key, list): + key = list(key) + path = os.path.join(dirname, f"{'-'.join(key)}.csv") + with open(path, "w", newline="") as csv_file: + csv_writer = csv.writer(csv_file) + job.to_csv(keys=key, csv_writer=csv_writer) + except Exception as err: + log.error("Error storing csv results: %s\n%s", + str(err), traceback.format_exc()) + + + @classmethod + def __store_pickled_results(cls, job): + try: + with open('results/results.p', 'wb') as results_file: + pickle.dump(job.ts_results, results_file) + except Exception as err: + message = f"Error storing pickled results: {err}\n{traceback.format_exc()}" + log.error(message) + return message + return False + + + @classmethod + def __store_result_plots(cls, job): + try: + mappers = ["min", "max", "avg", "var", "final"] + if "solver" in job.settings.keys(): + solver_name = job.settings['solver'].name + else: + solver_name = job.model.get_best_solver().name + if "ODE" not in solver_name and job.settings['number_of_trajectories'] > 1: + keys = list(itertools.product(job.list_of_species, mappers, ["min", "max", "avg", "var"])) - else: - csv_keys = [["min"], ["max"], ["avg"], ["var"], ["final"]] - stamp = self.get_time_stamp() - dirname = f"results/results_csv{stamp}" - if not os.path.exists(dirname): - os.mkdir(dirname) - for key in csv_keys: - if not isinstance(key, list): + else: + keys = list(itertools.product(job.list_of_species, mappers)) + plot_figs = {} + for key in keys: key = list(key) - path = os.path.join(dirname, f"{'-'.join(key)}.csv") - with open(path, "w", newline="") as csv_file: - csv_writer = csv.writer(csv_file) - wkfl.to_csv(keys=key, csv_writer=csv_writer) + trace_list = job.get_plotly_traces(keys=key) + plt_data = {'title':f"Parameter Sweep - Variable: {key[0]}"} + job.get_plotly_layout_data(plt_data=plt_data) + layout = plotly.graph_objs.Layout(title=dict(text=plt_data['title'], x=0.5), + xaxis=dict(title=plt_data['xaxis_label']), + yaxis=dict(title=plt_data['yaxis_label'])) + + fig = dict(data=trace_list, layout=layout, config={"responsive": True}) + plot_figs['-'.join(key)] = fig + + with open('results/plots.json', 'w') as plots_file: + json.dump(plot_figs, plots_file, cls=plotly.utils.PlotlyJSONEncoder, + indent=4, sort_keys=True) + except Exception as err: + message = f"Error storing result plots: {err}\n{traceback.format_exc()}" + log.error(message) + return message + return False @classmethod - def __store_plots(cls, wkfl): - mappers = ["min", "max", "avg", "var", "final"] - if "ODE" not in wkfl.settings['solver'].name and \ - wkfl.settings['number_of_trajectories'] > 1: - keys = list(itertools.product(wkfl.list_of_species, mappers, - ["min", "max", "avg", "var"])) - else: - keys = list(itertools.product(wkfl.list_of_species, mappers)) - plot_figs = {} - for key in keys: - key = list(key) - trace_list = wkfl.get_plotly_traces(keys=key) - plt_data = {'title':f"Parameter Sweep - Variable: {key[0]}"} - wkfl.get_plotly_layout_data(plt_data=plt_data) - layout = plotly.graph_objs.Layout(title=dict(text=plt_data['title'], x=0.5), - xaxis=dict(title=plt_data['xaxis_label']), - yaxis=dict(title=plt_data['yaxis_label'])) - - fig = dict(data=trace_list, layout=layout, config={"responsive": True}) - plot_figs['-'.join(key)] = fig - - with open('results/plots.json', 'w') as plots_file: - json.dump(plot_figs, plots_file, cls=plotly.utils.PlotlyJSONEncoder, - indent=4, sort_keys=True) - - - def __store_results(self, wkfl): - if not 'results' in os.listdir(): - os.mkdir('results') - with open('results/results.p', 'wb') as results_file: - pickle.dump(wkfl.ts_results, results_file) - if wkfl.name != "ParameterScan": + def __store_results(cls, job): + try: with open('results/results.json', 'w') as json_file: - json.dump(wkfl.results, json_file, indent=4, sort_keys=True, cls=NumpyEncoder) - self.__store_csv_results(wkfl) + json.dump(job.results, json_file, indent=4, sort_keys=True, cls=NumpyEncoder) + except Exception as err: + message = f"Error storing results dictionary: {err}\n{traceback.format_exc()}" + log.err(message) + return message + return False def configure(self): @@ -181,22 +221,30 @@ def run(self, verbose=True): ''' kwargs = self.configure() if "param" in kwargs.keys(): - wkfl = ParameterSweep1D(**kwargs) + job = ParameterSweep1D(**kwargs) sim_type = "1D parameter sweep" elif len(kwargs['params']) > 2: sim_type = "parameter scan" - wkfl = ParameterScan(**kwargs) + job = ParameterScan(**kwargs) else: sim_type = "2D parameter sweep" - wkfl = ParameterSweep2D(**kwargs) + job = ParameterSweep2D(**kwargs) if verbose: log.info("Running the %s", sim_type) - wkfl.run(job_id=self.get_file(), verbose=verbose) + job.run(job_id=self.get_file(), verbose=verbose) if verbose: log.info("The %s has completed", sim_type) log.info("Storing the results as pickle and csv") - self.__store_results(wkfl=wkfl) - if wkfl.name != "ParameterScan": + if not 'results' in os.listdir(): + os.mkdir('results') + pkl_err = self.__store_pickled_results(job=job) + if job.name != "ParameterScan": if verbose: log.info("Storing the polts of the results") - self.__store_plots(wkfl=wkfl) + res_err = self.__store_results(job=job) + self.__store_csv_results(job=job) + plt_err = self.__store_result_plots(job=job) + if pkl_err and res_err and plt_err: + self.__report_result_error(trace=f"{res_err}\n{pkl_err}\n{plt_err}") + elif pkl_err: + self.__report_result_error(trace=pkl_err) diff --git a/stochss/handlers/util/parameter_sweep_1d.py b/stochss/handlers/util/parameter_sweep_1d.py index 14fb8a4596..33c580e115 100644 --- a/stochss/handlers/util/parameter_sweep_1d.py +++ b/stochss/handlers/util/parameter_sweep_1d.py @@ -91,11 +91,10 @@ def __feature_extraction(self, results, index, verbose=False): log.debug(' %s population %s=%s', key, species, data) - def __setup_results(self): + def __setup_results(self, solver_name): for species in self.list_of_species: spec_res = {} - if "ODE" not in self.settings['solver'].name and \ - self.settings['number_of_trajectories'] > 1: + if "ODE" not in solver_name and self.settings['number_of_trajectories'] > 1: for m_key in self.MAPPER_KEYS: spec_res[m_key] = {} for r_key in self.REDUCER_KEYS: @@ -135,7 +134,8 @@ def get_plotly_traces(self, keys): else: results = self.results[keys[0]][keys[1]] - visible = self.settings['number_of_trajectories'] > 1 + visible = "number_of_trajectories" in self.settings.keys() and \ + self.settings['number_of_trajectories'] > 1 error_y = dict(type="data", array=results[:, 1], visible=visible) trace_list = [plotly.graph_objs.Scatter(x=self.param['range'], @@ -172,10 +172,13 @@ def run(self, job_id, verbose=False): Attributes ---------- ''' - self.__setup_results() + if "solver" in self.settings.keys(): + solver_name = self.settings['solver'].name + else: + solver_name = self.model.get_best_solver().name + self.__setup_results(solver_name=solver_name) for i, val in enumerate(self.param['range']): - if "solver" in self.settings.keys() and \ - self.settings['solver'].name == "SSACSolver": + if solver_name in ["SSACSolver", "TauLeapingCSolver", "ODECSolver"]: tmp_mdl = self.model self.settings['variables'] = {self.param['parameter']:val} else: @@ -190,8 +193,7 @@ def run(self, job_id, verbose=False): else: key = f"{self.param['parameter']}:{val}" self.ts_results[key] = tmp_res - if "ODE" not in self.settings['solver'].name and \ - self.settings['number_of_trajectories'] > 1: + if "ODE" not in solver_name and self.settings['number_of_trajectories'] > 1: self.__ensemble_feature_extraction(results=tmp_res, index=i, verbose=verbose) else: self.__feature_extraction(results=tmp_res, index=i, verbose=verbose) diff --git a/stochss/handlers/util/parameter_sweep_2d.py b/stochss/handlers/util/parameter_sweep_2d.py index 9f0cca708d..0c909181de 100644 --- a/stochss/handlers/util/parameter_sweep_2d.py +++ b/stochss/handlers/util/parameter_sweep_2d.py @@ -90,11 +90,10 @@ def __feature_extraction(self, results, i_ndx, j_ndx, verbose=False): log.debug(' %s population %s=%s', key, species, data) - def __setup_results(self): + def __setup_results(self, solver_name): for species in self.list_of_species: spec_res = {} - if "ODE" not in self.settings['solver'].name and \ - self.settings['number_of_trajectories'] > 1: + if "ODE" not in solver_name and self.settings['number_of_trajectories'] > 1: for m_key in self.MAPPER_KEYS: spec_res[m_key] = {} for r_key in self.REDUCER_KEYS: @@ -176,11 +175,14 @@ def run(self, job_id, verbose=False): Attributes ---------- ''' - self.__setup_results() + if "solver" in self.settings.keys(): + solver_name = self.settings['solver'].name + else: + solver_name = self.model.get_best_solver().name + self.__setup_results(solver_name=solver_name) for i, val1 in enumerate(self.params[0]['range']): for j, val2 in enumerate(self.params[1]['range']): - if "solver" in self.settings.keys() and \ - self.settings['solver'].name == "SSACSolver": + if solver_name in ["SSACSolver", "TauLeapingCSolver", "ODECSolver"]: tmp_mdl = self.model variables = {self.params[0]['parameter']:val1, self.params[1]['parameter']:val2} self.settings['variables'] = variables @@ -200,8 +202,7 @@ def run(self, job_id, verbose=False): key = f"{self.params[0]['parameter']}:{val1}," key += f"{self.params[1]['parameter']}:{val2}" self.ts_results[key] = tmp_res - if "ODE" not in self.settings['solver'].name and \ - self.settings['number_of_trajectories'] > 1: + if "ODE" not in solver_name and self.settings['number_of_trajectories'] > 1: self.__ensemble_feature_extraction(results=tmp_res, i_ndx=i, j_ndx=j, verbose=verbose) else: diff --git a/stochss/handlers/util/scripts/start_job.py b/stochss/handlers/util/scripts/start_job.py index 3f93fac855..0b52ebd1d8 100755 --- a/stochss/handlers/util/scripts/start_job.py +++ b/stochss/handlers/util/scripts/start_job.py @@ -108,9 +108,9 @@ def setup_logger(log_path): jobs = {"gillespy":EnsembleSimulation, "parameterSweep":ParameterSweep} try: os.chdir(args.path) - wkfl = jobs[args.type](path=args.path) + job = jobs[args.type](path=args.path) job_handler, user_handler = setup_logger("logs.txt") - wkfl.run(verbose=args.verbose) + job.run(verbose=args.verbose) except Exception as error: report_error(err=error) else: diff --git a/stochss/handlers/util/stochss_errors.py b/stochss/handlers/util/stochss_errors.py index 66b6eb8257..3ee1cbd411 100644 --- a/stochss/handlers/util/stochss_errors.py +++ b/stochss/handlers/util/stochss_errors.py @@ -140,6 +140,27 @@ def __init__(self, msg, trace=None): ''' super().__init__(403, "Permission Denied", msg, trace) + +class StochSSUnzipError(StochSSAPIError): + ''' + ################################################################################################ + StochSS Un-Zip Zip Archive API Handler Error + ################################################################################################ + ''' + + def __init__(self, msg, trace=None): + ''' + Indicates that a problem occured during the extraction process for the zip archive + + Attributes + ---------- + msg : str + Details on what caused the error + trace : str + Error traceback for the error + ''' + super().__init__(403, "Unable to Extract Contents", msg, trace) + #################################################################################################### # Model Errors #################################################################################################### @@ -271,3 +292,24 @@ def __init__(self, msg, trace=None): Error traceback for the error ''' super().__init__(406, "Plot Figure Not Available", msg, trace) + + +class StochSSJobResultsError(StochSSAPIError): + ''' + ################################################################################################ + StochSS Job Results Error + ################################################################################################ + ''' + + def __init__(self, msg, trace=None): + ''' + Indicates that the job results object was corrupted + + Attributes + ---------- + msg : str + Details on what caused the error + trace : str + Error traceback for the error + ''' + super().__init__(500, "Job Results Error", msg, trace) diff --git a/stochss/handlers/util/stochss_file.py b/stochss/handlers/util/stochss_file.py index 25f5c4f734..fbe99c91d3 100644 --- a/stochss/handlers/util/stochss_file.py +++ b/stochss/handlers/util/stochss_file.py @@ -23,7 +23,7 @@ from .stochss_base import StochSSBase from .stochss_errors import StochSSFileNotFoundError, StochSSPermissionsError, \ - StochSSFileExistsError + StochSSFileExistsError, StochSSUnzipError class StochSSFile(StochSSBase): ''' @@ -142,7 +142,7 @@ def read(self): raise StochSSFileNotFoundError(message, traceback.format_exc()) from err - def unzip(self): + def unzip(self, from_upload=True): ''' Extract the contents of a zip archive @@ -151,7 +151,6 @@ def unzip(self): ''' if not self.path.endswith(".zip"): return [] - try: path = self.get_path(full=True) dirname = self.get_dir_name(full=True) @@ -167,6 +166,12 @@ def unzip(self): zip_file.extractall(dirname) if "__MACOSX" in os.listdir(dirname): shutil.rmtree(os.path.join(dirname, "__MACOSX")) - return [] + if from_upload: + return [] + message = "Successfully extracted the contents of" + return {"message": f"{message} {self.get_file()} to {self.get_dir_name()}"} except zipfile.BadZipFile as err: - return [str(err)] + message = f"{str(err)} so it could not be unzipped." + if not from_upload: + raise StochSSUnzipError(message, traceback.format_exc()) from err + return [message] diff --git a/stochss/handlers/util/stochss_folder.py b/stochss/handlers/util/stochss_folder.py index c2451bbb9f..8d9200e65b 100644 --- a/stochss/handlers/util/stochss_folder.py +++ b/stochss/handlers/util/stochss_folder.py @@ -100,8 +100,6 @@ def __upload_file(self, file, body, new_name=None): path = os.path.join(self.path, file) new_file = StochSSFile(path=path, new=True, body=body) error = new_file.unzip() - if error: - error[0] += " so it could not be unzipped." file = new_file.get_file() dirname = new_file.get_dir_name() message = f"{file} was successfully uploaded to {dirname}" diff --git a/stochss/handlers/util/stochss_notebook.py b/stochss/handlers/util/stochss_notebook.py index f3f2e888cb..2a02836cd8 100644 --- a/stochss/handlers/util/stochss_notebook.py +++ b/stochss/handlers/util/stochss_notebook.py @@ -40,7 +40,8 @@ class StochSSNotebook(StochSSBase): MODEL_EXPLORATION = 5 MODEL_INFERENCE = 6 SOLVER_MAP = {"SSACSolver":"SSA", "NumPySSASolver":"SSA", "ODESolver":"ODE", "Solver":"SSA", - "TauLeapingSolver":"Tau-Leaping", "TauHybridSolver":"Hybrid-Tau-Leaping"} + "TauLeapingSolver":"Tau-Leaping", "TauHybridSolver":"Hybrid-Tau-Leaping", + "ODECSolver":"ODE", "TauLeapingCSolver":"Tau-Leaping"} def __init__(self, path, new=False, models=None, settings=None): '''Intitialize a notebook object and if its new create it on the users file system @@ -71,7 +72,6 @@ def __init__(self, path, new=False, models=None, settings=None): if changed: self.path = n_path.replace(self.user_dir + '/', "") - def __create_common_cells(self, interactive_backend=False): cells = [self.__create_import_cell(interactive_backend=interactive_backend), nbf.new_markdown_cell(f"# {self.get_name()}"), @@ -85,11 +85,15 @@ def __create_configuration_cell(self): pad = " " config = ["def configure_simulation():"] # Add solver instantiation line if the c solver are available - instance_solvers = ["SSACSolver"] + instance_solvers = ["SSACSolver", "ODECSolver", "TauLeapingCSolver"] is_automatic = self.settings['simulationSettings']['isAutomatic'] - if self.is_ssa_c and self.settings['solver'] in instance_solvers: - commented = is_automatic and self.nb_type <= self.ENSEMBLE_SIMULATION and \ - self.settings['solver'] not in instance_solvers + if self.settings['solver'] in instance_solvers: + if is_automatic and self.nb_type <= self.ENSEMBLE_SIMULATION: + commented = True + elif is_automatic and self.settings['solver'] not in instance_solvers: + commented = True + else: + commented = False start = f"{pad}# " if commented else pad config.append(f"{start}solver = {self.settings['solver']}(model=model)") config.append(pad + "kwargs = {") @@ -201,11 +205,12 @@ def __create_import_cell(self, interactive_backend=False): imports.append("from gillespy2 import Model, Species, Parameter, Reaction, Event, \\") imports.append(" EventTrigger, EventAssignment, RateRule, \\") imports.append(" AssignmentRule, FunctionDefinition") - ssa = "SSACSolver" if self.is_ssa_c else "NumPySSASolver" - algorithm_map = {'SSA': f'from gillespy2 import {ssa}', - 'Tau-Leaping': 'from gillespy2 import TauLeapingSolver', - 'Hybrid-Tau-Leaping': 'from gillespy2 import TauHybridSolver', - 'ODE': 'from gillespy2 import ODESolver'} + ssa_import = f'from gillespy2 import {self.model.get_best_solver_algo("SSA").name}' + tau_import = 'from gillespy2 import ' + tau_import += f'{self.model.get_best_solver_algo("Tau-Leaping").name}' + ode_import = f'from gillespy2 import {self.model.get_best_solver_algo("ODE").name}' + algorithm_map = {'SSA': ssa_import, 'Tau-Leaping': tau_import, 'ODE': ode_import, + 'Hybrid-Tau-Leaping': 'from gillespy2 import TauHybridSolver'} algorithm = self.settings['simulationSettings']['algorithm'] for name, alg_import in algorithm_map.items(): if not is_automatic and name == algorithm: @@ -796,9 +801,11 @@ def __get_gillespy2_run_settings(self): settings['realizations'] = 1 # Map algorithm for GillesPy2 ssa_solver = "solver" if self.is_ssa_c else "NumPySSASolver" + ode_solver = "solver" if self.is_ssa_c else "ODESolver" + tau_solver = "solver" if self.is_ssa_c else "TauLeapingSolver" solver_map = {"SSA":f'"solver":{ssa_solver}', - "ODE":'"solver":ODESolver', - "Tau-Leaping":'"solver":TauLeapingSolver', + "ODE":f'"solver":{ode_solver}', + "Tau-Leaping":f'"solver":{tau_solver}', "Hybrid-Tau-Leaping":'"solver":TauHybridSolver'} # Map algorithm settings for GillesPy2. GillesPy2 requires snake case, remap camelCase settings_map = {"number_of_trajectories":settings['realizations'], @@ -824,10 +831,10 @@ def __get_gillespy2_solver_name(self): solver = self.model.get_best_solver().name self.settings['simulationSettings']['algorithm'] = self.SOLVER_MAP[solver] return solver - algorithm_map = {'SSA': "SSACSolver" if self.is_ssa_c else "NumPySSASolver", - 'Tau-Leaping': 'TauLeapingSolver', + algorithm_map = {'SSA': self.model.get_best_solver_algo("SSA").name, + 'Tau-Leaping': self.model.get_best_solver_algo("Tau-Leaping").name, 'Hybrid-Tau-Leaping': 'TauHybridSolver', - 'ODE': 'ODESolver'} + 'ODE': self.model.get_best_solver_algo("ODE").name} return algorithm_map[self.settings['simulationSettings']['algorithm']] def create_1dps_notebook(self): @@ -970,10 +977,7 @@ def create_smi_notebook(self): return {"Message":message, "FilePath":self.get_path(), "File":self.get_file()} def load(self): - '''Read the notebook file and return as a dict - - Attributes - ----------''' + '''Read the notebook file and return as a dict''' try: with open(self.get_path(full=True), "r") as notebook_file: return json.load(notebook_file) diff --git a/stochss/handlers/util/stochss_spatial_model.py b/stochss/handlers/util/stochss_spatial_model.py index c4bd1affe9..e0f444ca0e 100644 --- a/stochss/handlers/util/stochss_spatial_model.py +++ b/stochss/handlers/util/stochss_spatial_model.py @@ -54,6 +54,7 @@ def __init__(self, path, new=False, model=None): if new: if model is None: model = self.get_model_template() + if not model['is_spatial']: model['is_spatial'] = True if isinstance(model, str): model = json.loads(model)