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)