diff --git a/EXAMPLE_DIRECTORY.md b/EXAMPLE_DIRECTORY.md new file mode 100644 index 0000000000..03713a95fc --- /dev/null +++ b/EXAMPLE_DIRECTORY.md @@ -0,0 +1,90 @@ +# Example Directory + +This file contains a directory to all GillesPy2 and SpatialPy example notebooks. + +## GillesPy2 + +["Start Here" Model](https://github.com/StochSS/GillesPy2/blob/main/examples/StartHere.ipynb) + +### Starting Models + +- [Michaelis Menten](https://github.com/StochSS/GillesPy2/tree/main/examples/StartingModels/MichaelisMenten) + - [SSA C Solver](https://github.com/StochSS/GillesPy2/blob/main/examples/StartingModels/MichaelisMenten/Michaelis-Menten_SSA_C.ipynb) + - [SSA NumPy Solver](https://github.com/StochSS/GillesPy2/blob/main/examples/StartingModels/MichaelisMenten/Michaelis-Menten_NumPy_SSA.ipynb) + - [Tau Leaping NumPy](https://github.com/StochSS/GillesPy2/blob/main/examples/StartingModels/MichaelisMenten/Michaelis-Menten_Basic_Tau_Leaping.ipynb) + - [Tau Hybrid NumPy](https://github.com/StochSS/GillesPy2/blob/main/examples/StartingModels/MichaelisMenten/Michaelis-Menten_Basic_Tau_Hybrid.ipynb) + +- [Vilar Oscillator](https://github.com/StochSS/GillesPy2/tree/main/examples/StartingModels/VilarOscillator) + - [SSA](https://github.com/StochSS/GillesPy2/blob/main/examples/StartingModels/VilarOscillator/VilarOscillator_SSA.ipynb) + - [Tau Leaping](https://github.com/StochSS/GillesPy2/blob/main/examples/StartingModels/VilarOscillator/VilarOscillator_Tau_Leaping.ipynb) + - [Tau Hybrid](https://github.com/StochSS/GillesPy2/blob/main/examples/StartingModels/VilarOscillator/VilarOscillator_Tau_Hybrid.ipynb) + + +### Results Management + +- [Basic Results Management](https://github.com/StochSS/GillesPy2/blob/main/examples/ResultsManagement/basic-results-management.ipynb) +- [Single Trajectory to CSV](https://github.com/StochSS/GillesPy2/blob/main/examples/ResultsManagement/to_csv-single-trajectory.ipynb) +- [Multiple Trajectories to CSV](https://github.com/StochSS/GillesPy2/blob/main/examples/ResultsManagement/to_csv-multi-trajectory.ipynb) +- [Using Pickle](https://github.com/StochSS/GillesPy2/blob/main/examples/ResultsManagement/using-pickle.ipynb) + + +### Data Visualization + +- [Data Visualization](https://github.com/StochSS/GillesPy2/blob/main/examples/DataVisualization/DataVisualization.ipynb) +- [Live Output](https://github.com/StochSS/GillesPy2/blob/main/examples/DataVisualization/LiveOutput.ipynb) + + +### Advanced Features + +- [Events](https://github.com/StochSS/GillesPy2/blob/main/examples/AdvancedFeatures/Events.ipynb) +- [Kinetic Model for Styrene Polymerization](https://github.com/StochSS/GillesPy2/blob/main/examples/AdvancedFeatures/KineticModelForStyrenePolymerization.ipynb) +- [Photosynthesis](https://github.com/StochSS/GillesPy2/blob/main/examples/AdvancedFeatures/Photosynthesis.ipynb) +- [GillesPy2 in Parallel](https://github.com/StochSS/GillesPy2/blob/main/examples/AdvancedFeatures/Run%20GillesPy2%20Simulations%20in%20Parallel.ipynb) +- [SBML Import](https://github.com/StochSS/GillesPy2/blob/main/examples/AdvancedFeatures/SBML_Import_Test.ipynb) +- [Variable Inputs (SSA)](https://github.com/StochSS/GillesPy2/blob/main/examples/AdvancedFeatures/Variable_SSA_C_Example.ipynb) +- [Volume Test](https://github.com/StochSS/GillesPy2/blob/main/examples/AdvancedFeatures/Volume_test.py) +- [Hybrid Continuous Species](https://github.com/StochSS/GillesPy2/blob/main/examples/AdvancedFeatures/hybrid_continuous_species.ipynb) +- [Hybrid Switching](https://github.com/StochSS/GillesPy2/blob/main/examples/AdvancedFeatures/hybrid_switching_example.ipynb) +- [Parameter Changing](https://github.com/StochSS/GillesPy2/blob/main/examples/AdvancedFeatures/parameter_changing.py) + + +### Extra Models + +- [Brusselator](https://github.com/StochSS/GillesPy2/blob/main/examples/ExtraModels/Brusselator.ipynb) +- [Genetic Toggle Switch](https://github.com/StochSS/GillesPy2/blob/main/examples/ExtraModels/GeneticToggleSwitch.ipynb) +- [Opioid Model](https://github.com/StochSS/GillesPy2/blob/main/examples/ExtraModels/Opioid_Model.ipynb) +- [Oregonator](https://github.com/StochSS/GillesPy2/blob/main/examples/ExtraModels/Oregonator.ipynb) +- [Tyson Oscillator](https://github.com/StochSS/GillesPy2/blob/main/examples/ExtraModels/Tyson%20Oscillator.ipynb) +- [Degradation Model](https://github.com/StochSS/GillesPy2/blob/main/examples/ExtraModels/degradation_example.py) +- [Dimer Model](https://github.com/StochSS/GillesPy2/blob/main/examples/ExtraModels/dimer_example.py) +- [Tyson Oscillator](https://github.com/StochSS/GillesPy2/blob/main/examples/ExtraModels/tyson_oscillator.py) + + + +## SpatialPy + +### General Examples + +- [Bistable Elf Ehrenberg](https://github.com/StochSS/SpatialPy/blob/develop/examples/Bistable_Biochem_Elf_Ehrenberg/Bistable_Biochem_Elf_Ehrenberg.ipynb) +- [Boundary Conditions](https://github.com/StochSS/SpatialPy/blob/develop/examples/BoundaryCondition/BoundaryCondition.ipynb) +- [SpatialPy Gravity](https://github.com/StochSS/SpatialPy/blob/develop/examples/Gravity/Spatialpy_gravity.ipynb) +- [Coral Reef](https://github.com/StochSS/SpatialPy/blob/develop/examples/coral_reef/CoralModel.ipynb) +- [3D Cylinder Diffusion](https://github.com/StochSS/SpatialPy/blob/develop/examples/cylinderDemo/SpatialPy_cylinderDemo3D.ipynb) +- [hes1](https://github.com/StochSS/SpatialPy/blob/develop/examples/hes1/hes1.ipynb) +- [Lid Driven Cavity](https://github.com/StochSS/SpatialPy/blob/develop/examples/lid_driven_cavity/Lid%20driven%20cavity.ipynb) +- [MinCDE](https://github.com/StochSS/SpatialPy/blob/develop/examples/mincde/mincde.ipynb) +- [Turing Pattern (Old)](https://github.com/StochSS/SpatialPy/blob/develop/examples/turing_pattern/turing_pattern.ipynb) +- [Turing Pattern (New)](https://github.com/StochSS/SpatialPy/blob/develop/examples/turing_pattern/new_turing_pattern.ipynb) +- [Weir](https://github.com/StochSS/SpatialPy/blob/develop/examples/weir/weir.ipynb) +- [Yeast Polarization](https://github.com/StochSS/SpatialPy/tree/develop/examples/yeast_polarization) + - [G-Protein 1D](https://github.com/StochSS/SpatialPy/blob/develop/examples/yeast_polarization/G-Protein_1D.ipynb) + - [Polarisome 1D](https://github.com/StochSS/SpatialPy/blob/develop/examples/yeast_polarization/Polarisome_1D.ipynb) + - [cdc42](https://github.com/StochSS/SpatialPy/blob/develop/examples/yeast_polarization/cdc42.ipynb) + +### Tests + +- [Diffusion Validation](https://github.com/StochSS/SpatialPy/blob/develop/examples/tests/Diffusion_validation.ipynb) +- [Spatial Birth-Death](https://github.com/StochSS/SpatialPy/blob/develop/examples/tests/Spatial_Birth_Death.ipynb) +- [Compile Time Test](https://github.com/StochSS/SpatialPy/blob/develop/examples/tests/compile_time_comparison.ipynb) +- [Read Meshes](https://github.com/StochSS/SpatialPy/blob/develop/examples/tests/read_meshes.ipynb) + diff --git a/__version__.py b/__version__.py index 93d8b3f6da..06125f64be 100644 --- a/__version__.py +++ b/__version__.py @@ -5,7 +5,7 @@ # @website https://github.com/stochss/stochss # ============================================================================= -__version__ = '2.4.1' +__version__ = '2.4.2' __title__ = 'StochSS' __description__ = 'StochSS is an integrated development environment (IDE) \ for simulation of biochemical networks.' diff --git a/client/models/presentation.js b/client/models/presentation.js index 17a07f2295..3e027ade20 100644 --- a/client/models/presentation.js +++ b/client/models/presentation.js @@ -20,8 +20,10 @@ let State = require('ampersand-state'); module.exports = State.extend({ session: { + ctime: 'string', file: 'string', link: 'string', + name: 'string', size: 'number', tag: 'string' }, diff --git a/client/pages/loading-page.js b/client/pages/loading-page.js index 1b553faef3..be496cdced 100644 --- a/client/pages/loading-page.js +++ b/client/pages/loading-page.js @@ -91,7 +91,7 @@ let LoadingPage = PageView.extend({ $(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 = this.homeLink; + window.location.href = self.homeLink; }); } app.getXHR(endpoint, { diff --git a/client/pages/model-editor.js b/client/pages/model-editor.js index 850b8a9946..78a13d5d68 100644 --- a/client/pages/model-editor.js +++ b/client/pages/model-editor.js @@ -197,17 +197,22 @@ let ModelEditor = PageView.extend({ errorCB(err, response, body); } else if(!body.Running){ + Plotly.purge(this.queryByHook('preview-plot-container')); if(body.Results.timeout){ $(this.queryByHook('model-timeout-message')).collapse('show'); } this.plotResults(body.Results.results); }else{ + if(body.Results) { + Plotly.purge(this.queryByHook('preview-plot-container')); + this.plotResults(body.Results.results); + } this.getResults(); } }, error: errorCB }); - }, 2000); + }, 1000); }, handlePresentationClick: function (e) { let errorMsg = $(this.queryByHook("error-detected-msg")); diff --git a/client/pages/project-manager.js b/client/pages/project-manager.js index ccae2200b1..976c7706ed 100644 --- a/client/pages/project-manager.js +++ b/client/pages/project-manager.js @@ -92,8 +92,8 @@ let ProjectManager = PageView.extend({ always: function (err, response, body) { let modal = $(modals.importModelHtml(body.files)).modal(); let okBtn = document.querySelector('#importModelModal .ok-model-btn'); - let select = document.querySelector('#importModelModal #modelFileInput'); - let location = document.querySelector('#importModelModal #modelPathInput'); + let select = document.querySelector('#importModelModal #modelFileSelect'); + let location = document.querySelector('#importModelModal #modelPathSelect'); select.addEventListener("change", function (e) { okBtn.disabled = e.target.value && body.paths[e.target.value].length >= 2; if(body.paths[e.target.value].length >= 2) { @@ -314,7 +314,7 @@ let ProjectManager = PageView.extend({ } }, handleUploadModelClick: function (e) { - this.projectFileBrowser.uploadFile(undefined, "model") + this.projectFileBrowser.uploadFile(null, this.model.directory, "model", true) }, renderArchiveCollection: function () { if(this.archiveCollectionView) { diff --git a/client/project-config.js b/client/project-config.js index edf1cef1b0..9c0629e0ff 100644 --- a/client/project-config.js +++ b/client/project-config.js @@ -151,7 +151,7 @@ let getNotebookContext = (view, node) => { open: open, publish: view.getPublishNotebookContext(node), download: download, rename: rename, - duplicate: duplicate, delete: deleteFile + duplicate: duplicate, moveToTrash: moveToTrash } } diff --git a/client/templates/body.pug b/client/templates/body.pug index 7cab6d1365..a2ab6c0467 100644 --- a/client/templates/body.pug +++ b/client/templates/body.pug @@ -64,6 +64,8 @@ body button.my-0.btn.btn-outline-collapse.inline(data-hook="user-logs-collapse") + + button.my-0.btn.btn-light.inline(data-hook="clear-user-logs" style="float: right;") clear + div.pl-1.overflow-auto(id="user-logs") main.col-sm-12.col-md-10.col-xl-9.col-xxl-6.body(role="main" data-hook="page-main") diff --git a/client/templates/includes/presentationView.pug b/client/templates/includes/presentationView.pug index 113ec43540..9b14ffa957 100644 --- a/client/templates/includes/presentationView.pug +++ b/client/templates/includes/presentationView.pug @@ -5,14 +5,18 @@ div.mx-1 div.row - div.col-sm-6 + div.col-sm-4 - a.pl-2(href=this.model.link)=this.model.file + a.pl-2(href=this.model.link)=this.model.name div.col-sm-2 button.btn.btn-outline-secondary.box-shadow(data-hook="copy-link") Copy Link + div.col-sm-2 + + div=this.model.ctime + div.col-sm-2 div=this.model.size + " " + this.model.tag diff --git a/client/templates/pages/browser.pug b/client/templates/pages/browser.pug index a634614fe5..a0b9dc16eb 100644 --- a/client/templates/pages/browser.pug +++ b/client/templates/pages/browser.pug @@ -36,7 +36,9 @@ section.page div.mx-1.row.head.align-items-baseline - div.col-sm-8: h6 File + div.col-sm-6: h6 File + + div.col-sm-2: h6 Date div.col-sm-2: h6 Size diff --git a/client/views/jstree-view.js b/client/views/jstree-view.js index 5bed5b9f8b..1fe55f6ab8 100644 --- a/client/views/jstree-view.js +++ b/client/views/jstree-view.js @@ -900,7 +900,7 @@ module.exports = View.extend({ msg.html(reason); msg.css("display", "inline-block"); } - app.copyToClipboard(links.presentation, onFulfilled, onReject); + app.copyToClipboard(body.links.presentation, onFulfilled, onReject); }); }, error: (err, response, body) => { diff --git a/client/views/main.js b/client/views/main.js index c77d0ce9ce..22a5f4a9cd 100644 --- a/client/views/main.js +++ b/client/views/main.js @@ -38,67 +38,6 @@ String.prototype.toHtmlEntities = function() { }); }; -let operationInfoModalHtml = (infoKey) => { - let fileBrowserInfo = ` -

In StochSS we use custom file extensions for a number of files we work with. Here is a list of our extentions with the files they are associated with:

- - - - - - - - - - - - - - - - - -
StochSS Model .mdl
StochSS Spatial Model .smdl
SBML Model .sbml
Workflows .wkfl
-
-

Other useful file extensions include the following:

- - - - - -
Jupyter Notebook .ipynb
- `; - let modelInfo = ` - Model Information - `; - let workflowInfo = ` - Workflow Information - `; - - let infoList = {"File Browser":fileBrowserInfo, "Models":modelInfo, "Workflows":workflowInfo} - - return ` - - ` -} - module.exports = View.extend({ template: bodyTemplate, autoRender: true, @@ -109,8 +48,8 @@ module.exports = View.extend({ }, events: { 'click [data-hook=registration-link-button]' : 'handleRegistrationLinkClick', - 'click [data-hook=user-logs-collapse]' : 'collapseExpandLogs' - //'click a[href]': 'handleLinkClick' + 'click [data-hook=user-logs-collapse]' : 'collapseExpandLogs', + 'click [data-hook=clear-user-logs]' : 'clearUserLogs' }, render: function () { @@ -132,18 +71,7 @@ module.exports = View.extend({ if(app.getBasePath() === "/") { $("#presentation-nav-link").css("display", "none"); } - let self = this; - let message = app.getBasePath() === "/" ? "Welcome to StochSS!" : "Welcome to StochSS Live!"; - $("#user-logs").html(message) - this.logBlock = []; - this.logs = []; - this.getUserLogs(); - this.scrolled = false; - this.scrollCount = 0; - $("#user-logs").on("mousewheel", function(e) { - self.scrolled = true; - self.scrollCount = 0; - }); + this.setupUserLogs(); return this; }, addNewLogBlock: function () { @@ -172,6 +100,14 @@ module.exports = View.extend({ }); this.addNewLogBlock(); }, + clearUserLogs: function (e) { + let endpoint = path.join(app.getApiPath(), "clear-user-logs"); + app.getXHR(endpoint, { + success: (err, response, body) => { + this.setupUserLogs({getLogs: false}); + } + }); + }, collapseExpandLogs: function (e) { let logs = $("#user-logs"); let classes = logs.attr("class").split(/\s+/); @@ -249,6 +185,21 @@ module.exports = View.extend({ navigate: function (page) { window.location = url; }, + setupUserLogs: function ({getLogs = true}={}) { + let message = app.getBasePath() === "/" ? "Welcome to StochSS!" : "Welcome to StochSS Live!"; + $("#user-logs").html(message) + this.logBlock = []; + this.logs = []; + this.scrolled = false; + this.scrollCount = 0; + if(getLogs) { + this.getUserLogs(); + $("#user-logs").on("mousewheel", (e) => { + this.scrolled = true; + this.scrollCount = 0; + }); + } + }, updateUserLogs: function () { setTimeout(_.bind(this.getUserLogs, this), 1000); } diff --git a/launch_webbrowser.py b/launch_webbrowser.py index e9f892e27b..9524b9c75e 100755 --- a/launch_webbrowser.py +++ b/launch_webbrowser.py @@ -4,6 +4,9 @@ import sys import time import webbrowser + +MAX_WAIT_TIME = 60 + try: import docker except ImportError: @@ -25,11 +28,15 @@ #time.sleep(10) print("Checking for running StochSS container: stochss-lab") container_started = False +poll_start_time = time.time() while not container_started: + if (time.time() - MAX_WAIT_TIME) > poll_start_time: + print(f"Stopped checking for running StochSS container after {MAX_WAIT_TIME}s") + sys.exit(1) time.sleep(1) try: stochss_container=docker_client.containers.get("stochss-lab") - print("Generating StochSS webpage...") + print("Checking to see if the server is active.") jupyter_url_generator=stochss_container.exec_run("jupyter notebook list", demux=False) url_regex = r"(?i)\b((?:https?://|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'\".,<>?«»“”‘’]))" jupyter_url_bytes = jupyter_url_generator.output @@ -40,6 +47,7 @@ jupyter_url=jupyter_url_sequence[0] jupyter_url=jupyter_url.replace('0.0.0.0','127.0.0.1') print(f"Opening StochSS webpage...\n") + time.sleep(1) webbrowser.open_new_tab(jupyter_url) print("Welcome to StochSS!\n\nYou can access your local StochSS service with this URL:\n\n") print(jupyter_url+"\n") diff --git a/requirements.txt b/requirements.txt index a5b7759e21..ef68e8a1a7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,8 +2,8 @@ python-libsbml==5.18.0 python-libsedml==2.0.9 python-libcombine==0.2.7 escapism==1.0.1 -gillespy2==1.6.3 -sciope==0.4 +gillespy2==1.6.5 +git+https://github.com/StochSS/sciope.git@master pygmsh==5.0.2 meshio==2.3.10 -git+https://github.com/spatialpy/SpatialPy.git@main +git+https://github.com/StochSS/SpatialPy.git@develop diff --git a/stochss/handlers/__init__.py b/stochss/handlers/__init__.py index 30adcddd42..512ece0016 100644 --- a/stochss/handlers/__init__.py +++ b/stochss/handlers/__init__.py @@ -53,6 +53,7 @@ def get_page_handlers(route_start): ## API Handlers # (r'/stochss/api/user-logs\/?', UserLogsAPIHandler), + (r'/stochss/api/clear-user-logs\/?', ClearUserLogsAPIHandler), (r"/stochss/api/file/browser-list\/?", ModelBrowserFileList), (r"/stochss/api/file/upload\/?", UploadFileAPIHandler), (r"/stochss/api/file/upload-from-link\/?", UploadFileFromLinkAPIHandler), diff --git a/stochss/handlers/log.py b/stochss/handlers/log.py index b8717443c2..c082d97c90 100644 --- a/stochss/handlers/log.py +++ b/stochss/handlers/log.py @@ -16,6 +16,8 @@ along with this program. If not, see . ''' +import os +import sys import logging from tornado.log import LogFormatter @@ -28,12 +30,45 @@ def init_log(): Attributes ---------- ''' + relocate_old_logs() setup_stream_handler() setup_file_handler() log.setLevel(logging.DEBUG) log.propagate = False +def relocate_old_logs(): + ''' + Move the user log file to its new location (/var/log). + ''' + user_dir = os.path.expanduser("~") + src = os.path.join(user_dir, ".user-logs.txt") + if not os.path.exists(src): + return + + src_size = os.path.getsize(src) + if src_size < 500000: + return + + with open(src, "r") as log_file: + logs = log_file.read().rstrip().split('\n') + + mlog_size = src_size % 500000 + mlogs = [logs.pop()] + while sys.getsizeof("\n".join(mlogs)) < mlog_size: + mlogs.insert(0, logs.pop()) + with open(os.path.join(user_dir, ".user-logs.txt"), "w") as main_log_file: + main_log_file.write("\n".join(mlogs)) + + blogs = [logs.pop()] + nlog_size = sys.getsizeof(f"\n{logs[-1]}") + while logs and sys.getsizeof("\n".join(blogs)) + nlog_size < 500000: + blogs.insert(0, logs.pop()) + nlog_size = sys.getsizeof(f"\n{logs[-1]}") + with open(os.path.join(user_dir, ".user-logs.txt.bak"), "w") as backup_log_file: + backup_log_file.write("\n".join(blogs)) + + def setup_stream_handler(): ''' Initialize the StochSS stream handler @@ -57,7 +92,37 @@ def setup_file_handler(): Attributes ---------- ''' - handler = logging.FileHandler(".user-logs.txt") + def namer(name): + ''' + Namer function for the RotatingFileHandler + + Attributes + ---------- + name : str + Default name of the log file. + ''' + return f"{name}.bak" + + def rotator(src, dst): + ''' + Rotator function for the RotatingFileHandler + + Attributes + ---------- + src : str + Path to the main log file. + dst : str + Path to the backup log file. + ''' + if os.path.exists(dst): + os.remove(dst) + os.rename(src, dst) + os.remove(src) + + path = os.path.join(os.path.expanduser("~"), ".user-logs.txt") + handler = logging.handlers.RotatingFileHandler(path, maxBytes=500000, backupCount=1) + handler.namer = namer + handler.rotator = rotator fmt = '%(asctime)s$ %(message)s' formatter = LogFormatter(fmt=fmt, datefmt="%b %d, %Y %I:%M %p UTC") handler.setFormatter(formatter) diff --git a/stochss/handlers/models.py b/stochss/handlers/models.py index 5c4152c8f7..406595e0c3 100644 --- a/stochss/handlers/models.py +++ b/stochss/handlers/models.py @@ -212,6 +212,9 @@ async def get(self): target = self.get_query_argument(name="target", default=None) resp = {"Running":False, "Outfile":outfile, "Results":""} if run_cmd == "start": + model = StochSSModel(path=path) + if os.path.exists(f".{model.get_name()}-preview.json"): + os.remove(f".{model.get_name()}-preview.json") exec_cmd = ['/stochss/stochss/handlers/util/scripts/run_preview.py', f'{path}', f'{outfile}'] if target is not None: @@ -229,6 +232,7 @@ async def get(self): log.debug(f"Results for the model preview: {results}") if results is None: resp['Running'] = True + resp['Results'] = model.get_live_results() log.info("The preview is still running") else: resp['Results'] = results @@ -265,7 +269,7 @@ async def get(self): class ImportMeshAPIHandler(APIHandler): ''' ################################################################################################ - Handler for importing mesh particles from remote file. + Handler for importing domain particles from remote file. ################################################################################################ ''' @web.authenticated diff --git a/stochss/handlers/pages.py b/stochss/handlers/pages.py index 7ca148a5c0..ba03c27060 100644 --- a/stochss/handlers/pages.py +++ b/stochss/handlers/pages.py @@ -264,13 +264,39 @@ async def get(self): ''' self.set_header('Content-Type', 'application/json') log_num = self.get_query_arguments(name="logNum")[0] - user_dir = os.path.expanduser("~") - path = os.path.join(user_dir, ".user-logs.txt") + path = os.path.join(os.path.expanduser("~"), ".user-logs.txt") try: + if os.path.exists(f"{path}.bak"): + with open(path, "r") as log_file: + logs = log_file.read().strip().split("\n") + else: + logs = [] with open(path, "r") as log_file: - logs = log_file.read().strip().split("\n")[int(log_num):] + logs.extend(log_file.read().strip().split("\n")) + logs = logs[int(log_num):] except FileNotFoundError: open(path, "w").close() logs = [] self.write({"logs":logs}) self.finish() + + +class ClearUserLogsAPIHandler(APIHandler): + ''' + ################################################################################################ + Handler for clearing the user logs + ################################################################################################ + ''' + @web.authenticated + async def get(self): + ''' + Clear contents of the user log file. + + Attributes + ---------- + ''' + path = os.path.join(os.path.expanduser("~"), ".user-logs.txt") + if os.path.exists(f'{path}.bak'): + os.remove(f'{path}.bak') + open(path, "w").close() + self.finish() diff --git a/stochss/handlers/project.py b/stochss/handlers/project.py index db3d3d44e0..ccb04bb69f 100644 --- a/stochss/handlers/project.py +++ b/stochss/handlers/project.py @@ -171,7 +171,8 @@ def get(self): folder = StochSSFolder(path="") # file will be excluded if test passes test = lambda ext, root, file: bool(".wkfl" in root or f"{path}" in root or \ - "trash" in root.split("/")) + "trash" in root.split("/") or \ + ".presentations" in root) data = folder.get_file_list(ext=[".mdl", ".smdl"], test=test) log.debug(f"List of models: {data}") self.write(data) diff --git a/stochss/handlers/util/ensemble_simulation.py b/stochss/handlers/util/ensemble_simulation.py index 949fe70b36..31ed2c8253 100644 --- a/stochss/handlers/util/ensemble_simulation.py +++ b/stochss/handlers/util/ensemble_simulation.py @@ -104,7 +104,9 @@ def run(self, preview=False, verbose=True): if preview: if verbose: log.info(f"Running {self.g_model.name} preview simulation") - results = self.g_model.run(timeout=5) + live_file = f".{self.g_model.name}-preview.json" + options = {"file_path": live_file} + results = self.g_model.run(timeout=60, live_output="graph", live_output_options=options) 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") diff --git a/stochss/handlers/util/parameter_scan.py b/stochss/handlers/util/parameter_scan.py index f1871b6138..a3c914b277 100644 --- a/stochss/handlers/util/parameter_scan.py +++ b/stochss/handlers/util/parameter_scan.py @@ -86,7 +86,7 @@ def __setup_model(self, variables): return self.model tmp_mdl = copy.deepcopy(self.model) for name, value in variables.items(): - tmp_mdl.listOfParameters[name].set_expression(value) + tmp_mdl.listOfParameters[name].expression = value return tmp_mdl diff --git a/stochss/handlers/util/parameter_sweep_1d.py b/stochss/handlers/util/parameter_sweep_1d.py index 14e0d2b3df..f93feaa49b 100644 --- a/stochss/handlers/util/parameter_sweep_1d.py +++ b/stochss/handlers/util/parameter_sweep_1d.py @@ -150,7 +150,7 @@ def run(self, job_id, verbose=False): self.settings['variables'] = {self.param['parameter']:val} else: tmp_mdl = copy.deepcopy(self.model) - tmp_mdl.listOfParameters[self.param['parameter']].set_expression(val) + tmp_mdl.listOfParameters[self.param['parameter']].expression = val if verbose: log.info(f"{job_id} --> running: {self.param['parameter']}={val}") try: diff --git a/stochss/handlers/util/parameter_sweep_2d.py b/stochss/handlers/util/parameter_sweep_2d.py index f4cd173d57..abbee80cfd 100644 --- a/stochss/handlers/util/parameter_sweep_2d.py +++ b/stochss/handlers/util/parameter_sweep_2d.py @@ -153,8 +153,8 @@ def run(self, job_id, verbose=False): self.settings['variables'] = variables else: tmp_mdl = copy.deepcopy(self.model) - tmp_mdl.listOfParameters[self.params[0]['parameter']].set_expression(val1) - tmp_mdl.listOfParameters[self.params[1]['parameter']].set_expression(val2) + tmp_mdl.listOfParameters[self.params[0]['parameter']].expression = val1 + tmp_mdl.listOfParameters[self.params[1]['parameter']].expression = val2 if verbose: message = f"{job_id} --> running: {self.params[0]['parameter']}={val1}, " message += f"{self.params[1]['parameter']}={val2}" diff --git a/stochss/handlers/util/parameter_sweep_notebook.py b/stochss/handlers/util/parameter_sweep_notebook.py index 75f586e1c3..165790df63 100644 --- a/stochss/handlers/util/parameter_sweep_notebook.py +++ b/stochss/handlers/util/parameter_sweep_notebook.py @@ -128,7 +128,7 @@ def __create_1d_run_str(self): else: res_str += "tmp_model.run(**kwargs)" run_strs.extend([f"{pad*3}tmp_model = c.ps_class()", - f"{pad*3}tmp_model.listOfParameters[c.p1].set_expression(v1)"]) + f"{pad*3}tmp_model.listOfParameters[c.p1].expression = v1"]) run_strs.extend([f"{pad*3}if c.verbose:", pad * 4 + "print(f'running {c.p1}={v1}')", f"{pad*3}if(c.number_of_trajectories > 1):", @@ -252,8 +252,8 @@ def __create_2d_run_str(self): else: res_str += "tmp_model.run(**kwargs)" run_strs.extend([f"{pad*4}tmp_model = c.ps_class()", - f"{pad*4}tmp_model.listOfParameters[c.p1].set_expression(v1)", - f"{pad*4}tmp_model.listOfParameters[c.p2].set_expression(v2)"]) + f"{pad*4}tmp_model.listOfParameters[c.p1].expression = v1", + f"{pad*4}tmp_model.listOfParameters[c.p2].expression = v2"]) run_strs.extend([f"{pad*4}if c.verbose:", pad * 5 + "print(f'running {c.p1}={v1}, {c.p2}={v2}')", f"{pad*4}if(c.number_of_trajectories > 1):", diff --git a/stochss/handlers/util/sciope_notebook.py b/stochss/handlers/util/sciope_notebook.py index a3ec3b7625..ecfbc87234 100644 --- a/stochss/handlers/util/sciope_notebook.py +++ b/stochss/handlers/util/sciope_notebook.py @@ -141,7 +141,7 @@ def __create_mi_simulator_cell(self): run = f"{pad}res = model.run(**kwargs, variables=variables)" else: func_def = "def set_model_parameters(params, model):" - body = f"{pad*2}model.get_parameter(pname).set_expression(params[e])" + body = f"{pad*2}model.get_parameter(pname).expression = params[e]" return_str = f"{pad}return model" call = f"{pad}model_update = set_model_parameters(params, model)" run = f"{pad}res = model_update.run(**kwargs)" diff --git a/stochss/handlers/util/stochss_base.py b/stochss/handlers/util/stochss_base.py index 43a5b73ea8..f8727ad40b 100644 --- a/stochss/handlers/util/stochss_base.py +++ b/stochss/handlers/util/stochss_base.py @@ -46,6 +46,28 @@ def __init__(self, path): self.logs = [] + def add_presentation_name(self, file, name): + ''' + Add a new presentation to the presentation names file. + + Attributes + ---------- + file : str + Name of the presentation file + name : str + Name of the presentation + ''' + path = os.path.join(self.user_dir, ".presentations", ".presentation_names.json") + if os.path.exists(path): + with open(path, "r") as names_file: + names = json.load(names_file) + else: + names = {} + names[file] = name + with open(path, "w") as names_file: + json.dump(names, names_file) + + @classmethod def check_project_format(cls, path): ''' @@ -87,6 +109,23 @@ def check_workflow_format(cls, path): return True + def delete_presentation_name(self, file): + ''' + Remove a presentation name from the presentation names file. + + Attributes + ---------- + file : str + Name of the presentation file to remove + ''' + path = os.path.join(self.user_dir, ".presentations", ".presentation_names.json") + with open(path, "r") as names_file: + names = json.load(names_file) + del names[file] + with open(path, "w") as names_file: + json.dump(names, names_file) + + @classmethod def get_new_path(cls, dst_path): ''' diff --git a/stochss/handlers/util/stochss_file.py b/stochss/handlers/util/stochss_file.py index 2c621494fc..2e7cb3f815 100644 --- a/stochss/handlers/util/stochss_file.py +++ b/stochss/handlers/util/stochss_file.py @@ -60,6 +60,8 @@ def delete(self): ''' path = self.get_path(full=True) try: + if ".presentations" in path: + self.delete_presentation_name(self.get_file()) os.remove(path) return "The file {0} was successfully deleted.".format(self.get_file()) except FileNotFoundError as err: diff --git a/stochss/handlers/util/stochss_folder.py b/stochss/handlers/util/stochss_folder.py index 3c49af47e1..1e21d5afd6 100644 --- a/stochss/handlers/util/stochss_folder.py +++ b/stochss/handlers/util/stochss_folder.py @@ -18,9 +18,11 @@ import os import json +import pickle import shutil import string import zipfile +import datetime import traceback import requests @@ -98,6 +100,56 @@ def __build_jstree_node(self, path, file): return node + @classmethod + def __get_presentation_job_name(cls, file_path): + with open(file_path, "rb") as job_file: + job = pickle.load(job_file) + name = job['name'] + return name + + + @classmethod + def __get_presentation_model_name(cls, file_path): + with open(file_path, "r") as model_file: + name = json.load(model_file)['name'] + return name + + + @classmethod + def __get_presentation_name(cls, names, file, file_path): + ext = file.split('.').pop() + if file in names.keys(): + name = names[file] + else: + if ext in ("mdl", "smdl"): + name = cls.__get_presentation_model_name(file_path) + elif ext == "job": + name = cls.__get_presentation_job_name(file_path) + elif ext == "ipynb": + name = cls.__get_presentation_notebook_name(file_path) + names[file] = name + return name, ext + + + @classmethod + def __get_names_from_file(cls, path, files): + if not os.path.exists(os.path.join(path, ".presentation_names.json")): + return {} + with open(os.path.join(path, ".presentation_names.json"), "r") as names_file: + names = json.load(names_file) + if len(names.keys()) != len(files): + return {} + return names + + + @classmethod + def __get_presentation_notebook_name(cls, file_path): + with open(file_path, "r") as nb_file: + file = json.load(nb_file)['file'] + name = cls.get_name(cls, path=file) + return name + + @classmethod def __overwrite(cls, path, ext): if ext == "zip": @@ -360,26 +412,34 @@ def get_presentations(cls): ---------- ''' path = os.path.join(cls.user_dir, ".presentations") + files = [file for file in os.listdir(path) if not file.startswith('.')] presentations = [] - if not os.path.isdir(path): + if not files: return presentations + names = cls.__get_names_from_file(path, files) + need_names = not bool(names) safe_chars = set(string.ascii_letters + string.digits) hostname = escape(os.environ.get('JUPYTERHUB_USER'), safe=safe_chars) - for file in os.listdir(path): + for file in files: file_path = os.path.join(path, file) - query_str = f"?owner={hostname}&file={file}" + ctime = os.path.getctime(file_path) routes = { "smdl": "present-model", "mdl": "present-model", "job": "present-job", "ipynb": "present-notebook" } - route = routes[file.split('.').pop()] - link = f"/stochss/{route}{query_str}" + name, ext = cls.__get_presentation_name(names, file, file_path) presentation = { - "file": file, "link": link, "size": os.path.getsize(file_path) + "file": file, "name": f"{name}.{ext}", + "link": f"/stochss/{routes[ext]}?owner={hostname}&file={file}", + "size": os.path.getsize(file_path), + "ctime": datetime.datetime.fromtimestamp(ctime).strftime("%b %d, %Y") } presentations.append(presentation) + if need_names or len(names.keys()) != len(files): + with open(os.path.join(path, ".presentation_names.json"), "w") as names_file: + json.dump(names, names_file) return presentations @@ -479,6 +539,13 @@ def upload_from_link(self, remote_path, overwrite=False): 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] path = self.get_new_path(dst_path=file) if os.path.exists(path): if not overwrite: @@ -515,6 +582,8 @@ def validate_upload_link(self, remote_path): body = json.dumps(json.loads(body)['notebook']) else: file = self.get_file(path=remote_path) + if "?token=" in file: + file = file.split("?token=")[0] path = self.get_new_path(dst_path=file) if ext == "zip": with zipfile.ZipFile(path, "r") as zip_file: diff --git a/stochss/handlers/util/stochss_job.py b/stochss/handlers/util/stochss_job.py index 61663da1b4..11dd944084 100644 --- a/stochss/handlers/util/stochss_job.py +++ b/stochss/handlers/util/stochss_job.py @@ -746,6 +746,7 @@ def publish_presentation(self, name=None): else: exists = False name = self.get_file() if name is None else name + self.add_presentation_name(file, name) path = os.path.join(self.__get_results_path(), "results.p") with open(path, "rb") as results_file: results = pickle.load(results_file) diff --git a/stochss/handlers/util/stochss_model.py b/stochss/handlers/util/stochss_model.py index 3bfe947995..69276e5340 100644 --- a/stochss/handlers/util/stochss_model.py +++ b/stochss/handlers/util/stochss_model.py @@ -406,6 +406,28 @@ def convert_to_spatial(self): return {"Message":message, "File":s_file}, {"spatial":model, "path":s_path} + def get_live_results(self): + ''' + Get the live output figure for the preview. + + Attributes + ---------- + ''' + file_name = f".{self.get_name()}-preview.json" + try: + with open(file_name, "r") as live_fig: + fig = json.load(live_fig) + fig["config"] = { + "displayModeBar": True, + "responsive": True + } + return {"results": fig, "timeout":False} + except FileNotFoundError: + return "" + except json.decoder.JSONDecodeError: + return "" + + def get_notebook_data(self): ''' Get the needed data for converting to notebook @@ -485,6 +507,7 @@ def publish_presentation(self): if os.path.exists(dst): data = None else: + self.add_presentation_name(file, self.model['name']) data = {"path": dst, "new":True, "model":self.model} query_str = f"?owner={hostname}&file={file}" present_link = f"/stochss/present-model{query_str}" diff --git a/stochss/handlers/util/stochss_notebook.py b/stochss/handlers/util/stochss_notebook.py index bc0731e057..9db45a860d 100644 --- a/stochss/handlers/util/stochss_notebook.py +++ b/stochss/handlers/util/stochss_notebook.py @@ -222,8 +222,8 @@ def __create_import_cell(self): imports = ["import numpy as np"] if self.s_model['is_spatial']: imports.append("import spatialpy") - imports.append("from spatialpy import Model, Species, Parameter, Reaction, Mesh,\\") - imports.append(" PlaceInitialCondition, \\") + imports.append("from spatialpy import Model, Species, Parameter, Reaction,\\") + imports.append(" Domain, PlaceInitialCondition, \\") imports.append(" UniformInitialCondition, \\") imports.append(" ScatterInitialCondition") return nbf.new_code_cell("\n".join(imports)) @@ -275,12 +275,12 @@ def __create_initial_condition_strings(self, model, pad): raise StochSSModelFormatError(message, traceback.format_exc()) from err - def __create_mesh_string(self, model, pad): - mesh = ["", f"{pad}# Domain", - f"{pad}mesh = Mesh.read_stochss_domain('{self.s_model['path']}')", - f"{pad}self.add_mesh(mesh)", - "", f"{pad}self.staticDomain = {self.s_model['domain']['static']}"] - model.extend(mesh) + def __create_domain_string(self, model, pad): + domain = ["", f"{pad}# Domain", + f"{pad}domain = Domain.read_stochss_domain('{self.s_model['path']}')", + f"{pad}self.add_domain(domain)", + "", f"{pad}self.staticDomain = {self.s_model['domain']['static']}"] + model.extend(domain) def __create_model_cell(self): @@ -289,7 +289,7 @@ def __create_model_cell(self): model = [f"class {self.get_class_name()}(Model):", " def __init__(self):", f'{pad}Model.__init__(self, name="{self.get_name()}")'] - self.__create_mesh_string(model=model, pad=pad) + self.__create_domain_string(model=model, pad=pad) self.__create_boundary_condition_string(model=model, pad=pad) self.__create_species_strings(model=model, pad=pad) self.__create_initial_condition_strings(model=model, pad=pad) @@ -403,7 +403,7 @@ def __create_species_strings(self, model, pad): if self.s_model['is_spatial']: names.append(spec["name"]) spec_str = f'{pad}{spec["name"]} = Species(name="{spec["name"]}", ' - spec_str += f"diffusion_constant={spec['diffusionConst']})" + spec_str += f"diffusion_coefficient={spec['diffusionConst']})" if len(spec['types']) < len(self.s_model['domain']['types']) - 1: type_str = f"{pad}self.restrict({spec['name']}, {str(spec['types'])})" types_str.append(type_str) @@ -564,7 +564,7 @@ def get_class_name(self): return f"M{name}" if l_char in string.ascii_lowercase: return name.replace(l_char, l_char.upper(), 1) - return name + return name.replace(" ", "") def get_gillespy2_solver_name(self): @@ -604,6 +604,7 @@ def publish_presentation(self): if os.path.exists(dst): exists = True else: + self.add_presentation_name(file, self.get_name()) exists = False with open(dst, "w") as presentation_file: json.dump(notebook_pres, presentation_file) diff --git a/stochss/handlers/util/stochss_spatial_model.py b/stochss/handlers/util/stochss_spatial_model.py index 6e2ea225f9..552c765eaf 100644 --- a/stochss/handlers/util/stochss_spatial_model.py +++ b/stochss/handlers/util/stochss_spatial_model.py @@ -26,7 +26,7 @@ import numpy import plotly from escapism import escape -from spatialpy import Model, Species, Parameter, Reaction, Mesh, MeshError, BoundaryCondition, \ +from spatialpy import Model, Species, Parameter, Reaction, Domain, DomainError, BoundaryCondition, \ PlaceInitialCondition, UniformInitialCondition, ScatterInitialCondition from .stochss_base import StochSSBase @@ -93,7 +93,7 @@ def expression(self): # pylint: disable=no-self-use def __build_stochss_domain(cls, s_domain, data=None): particles = cls.__build_stochss_domain_particles(s_domain=s_domain, data=data) gravity = [0] * 3 if s_domain.gravity is None else s_domain.gravity - domain = {"size":s_domain.mesh_size, + domain = {"size":s_domain.domain_size, "rho_0":s_domain.rho0, # density "c_0":s_domain.c0, # approx./artificial speed of sound "p_0":s_domain.P0, # atmos/background pressure @@ -136,6 +136,8 @@ def __build_stochss_domain_particles(cls, s_domain, data=None): "mass":s_domain.mass[i], "type":type_id, "nu":viscosity, + "rho":s_domain.rho[i], + "c":s_domain.c[i], "fixed":fixed} particles.append(particle) return particles @@ -163,9 +165,9 @@ def __convert_domain(self, model): gravity = self.model['domain']['gravity'] if gravity == [0, 0, 0]: gravity = None - mesh = Mesh(0, xlim, ylim, zlim, rho0=rho0, c0=c_0, P0=p_0, gravity=gravity) - self.__convert_particles(mesh=mesh) - model.add_mesh(mesh) + domain = Domain(0, xlim, ylim, zlim, rho0=rho0, c0=c_0, P0=p_0, gravity=gravity) + self.__convert_particles(domain=domain) + model.add_domain(domain) model.staticDomain = self.model['domain']['static'] except KeyError as err: message = "Spatial model domain properties are not properly formatted or " @@ -221,11 +223,12 @@ def __convert_parameters(self, model): raise StochSSModelFormatError(message, traceback.format_exc()) from err - def __convert_particles(self, mesh): + def __convert_particles(self, domain): try: for particle in self.model['domain']['particles']: - mesh.add_point(particle['point'], particle['volume'], particle['mass'], - particle['type'], particle['nu'], particle['fixed']) + domain.add_point(particle['point'], particle['volume'], particle['mass'], + particle['type'], particle['nu'], particle['fixed'], + particle['rho'], particle['c']) except KeyError as err: message = "Spatial model domain particle properties are not properly formatted or " message += f"are referenced incorrectly: {str(err)}" @@ -264,7 +267,7 @@ def __convert_species(self, model): try: for species in self.model['species']: name = species['name'] - s_species = Species(name=name, diffusion_constant=species['diffusionConst']) + s_species = Species(name=name, diffusion_coefficient=species['diffusionConst']) model.add_species(s_species) model.restrict(s_species, species['types']) except KeyError as err: @@ -321,7 +324,7 @@ def __load_domain_from_file(self, path): if path.endswith(".domn"): with open(path, "r") as domain_file: return json.load(domain_file) - s_domain = Mesh.read_xml_mesh(filename=path) + s_domain = Domain.read_xml_mesh(filename=path) return self.__build_stochss_domain(s_domain=s_domain) except FileNotFoundError as err: message = f"Could not find the domain file: {str(err)}" @@ -329,7 +332,7 @@ def __load_domain_from_file(self, path): except json.decoder.JSONDecodeError as err: message = f"The domain file is not JSON decobable: {str(err)}" raise FileNotJSONFormatError(message, traceback.format_exc()) from err - except MeshError as err: + except DomainError as err: message = f"The domain file is not in proper format: {str(err)}" raise DomainFormatError(message, traceback.format_exc()) from err @@ -346,6 +349,19 @@ def __read_model_file(self): raise FileNotJSONFormatError(message, traceback.format_exc()) from err + def __update_domain(self): + if "domain" not in self.model.keys() or len(self.model['domain'].keys()) < 6: + self.model['domain'] = self.get_model_template()['domain'] + elif "static" not in self.model['domain'].keys(): + self.model['domain']['static'] = True + if self.model['domain']['particles']: + if "rho" not in self.model['domain']['particles'][0].keys() or \ + "c" not in self.model['domain']['particles'][0].keys(): + for particle in self.model['domain']['particles']: + particle['rho'] = particle['mass']/particle['volume'] + particle['c'] = 10 + + def convert_to_model(self): ''' Convert a spatial model to a non_spatial model @@ -425,37 +441,44 @@ def get_domain(self, path=None, new=False): return self.__load_domain_from_file(path=path) - def get_domain_plot(self, domain=None, path=None, new=False): + def get_domain_plot(self, path=None, new=False): ''' Get a plotly plot of the models domain or a prospective domain Attributes ---------- - domain : dict - The domain to be plotted path : str Path to a prospective domain new : bool Indicates whether or not to load an new domain ''' - if domain is None: - domain = self.get_domain(path=path, new=new) - trace_list = [] - for i, d_type in enumerate(domain['types']): - if len(domain['types']) > 1: - particles = list(filter(lambda particle, key=i: particle['type'] == key, - domain['particles'])) - else: - particles = domain['particles'] - trace = self.__get_trace_data(particles=particles, name=d_type['name']) - trace_list.append(trace) - layout = {"scene":{"aspectmode":'data'}, "autosize":True} - if len(domain['x_lim']) == 2: - layout["xaxis"] = {"range":domain['x_lim']} - if len(domain['y_lim']) == 2: - layout["yaxis"] = {"range":domain['y_lim']} - return json.dumps({"data":trace_list, "layout":layout, "config":{"responsive":True}}, - cls=plotly.utils.PlotlyJSONEncoder) + if new: + path = '/stochss/stochss_templates/nonSpatialModelTemplate.json' + elif path is None: + path = self.path + domain = Domain.read_stochss_domain(path) + fig = domain.plot_types(return_plotly_figure=True) + if not fig['data']: + fig['data'].append(self.__get_trace_data(particles=[], name="Un-Assigned")) + else: + s_domain = self.load()['domain'] + for i, d_type in enumerate(s_domain['types']): + if len(s_domain['types']) > 1: + particles = list(filter(lambda partictle, key=i: particle['type'] == key, + s_domain['particles'])) + else: + particles = s_domain['particles'] + ids = list(map(lambda particle: particle['particle_id'], particles)) + index = fig['data'].index( + list(filter(lambda trace: trace['name'].endswith(str(d_type['typeID'])), fig['data']))[0] + ) + fig['data'][index]['name'] = d_type['name'] + fig['data'][index]['ids'] = ids + fig['layout']['width'] = None + fig['layout']['height'] = None + fig['layout']['autosize'] = True + fig['config'] = {"responsive":True} + return json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder) def get_notebook_data(self): @@ -490,8 +513,15 @@ def get_particles_from_3d_domain(cls, data): xlim = [coord + data['transformation'][0] for coord in data['xLim']] ylim = [coord + data['transformation'][1] for coord in data['yLim']] zlim = [coord + data['transformation'][2] for coord in data['zLim']] - s_domain = Mesh.create_3D_domain(xlim=xlim, ylim=ylim, zlim=zlim, nx=data['nx'], - ny=data['ny'], nz=data['nz'], **data['type']) + dimensions = bool(xlim[1] - xlim[0]) + dimensions += bool(ylim[1] - ylim[0]) + dimensions += bool(zlim[1] - zlim[0]) + if dimensions > 2: + s_domain = Domain.create_3D_domain(xlim=xlim, ylim=ylim, zlim=zlim, nx=data['nx'], + ny=data['ny'], nz=data['nz'], **data['type']) + else: + s_domain = Domain.create_2D_domain(xlim=xlim, ylim=ylim, + nx=data['nx'], ny=data['ny'], **data['type']) domain = cls.__build_stochss_domain(s_domain=s_domain) limits = {"x_lim":domain['x_lim'], "y_lim":domain['y_lim'], "z_lim":domain['z_lim']} return {"particles":domain['particles'], "limits":limits} @@ -512,9 +542,9 @@ def get_particles_from_remote(cls, mesh, data, types): List of type discriptions (lines from an uploaded file) ''' file = tempfile.NamedTemporaryFile() - with open(file.name, "w") as mesh_file: - mesh_file.write(mesh) - s_domain = Mesh.read_xml_mesh(filename=file.name) + with open(file.name, "w") as domain_file: + domain_file.write(mesh) + s_domain = Domain.read_xml_mesh(filename=file.name) domain = cls.__build_stochss_domain(s_domain=s_domain, data=data) if types is not None: type_data = cls.get_types_from_file(lines=types) @@ -576,10 +606,7 @@ def load(self): self.model['defaultMode'] = "discrete" if "timestepSize" not in self.model['modelSettings'].keys(): self.model['modelSettings']['timestepSize'] = 1e-5 - if "domain" not in self.model.keys() or len(self.model['domain'].keys()) < 6: - self.model['domain'] = self.get_model_template()['domain'] - elif "static" not in self.model['domain'].keys(): - self.model['domain']['static'] = True + self.__update_domain() if "boundaryConditions" not in self.model.keys(): self.model['boundaryConditions'] = [] for species in self.model['species']: @@ -615,6 +642,7 @@ def publish_presentation(self): if os.path.exists(dst): data = None else: + self.add_presentation_name(file, self.model['name']) data = {"path": dst, "new":True, "model":self.model} query_str = f"?owner={hostname}&file={file}" present_link = f"/stochss/present-model{query_str}" diff --git a/stochss/tests/test_stochss_base.py b/stochss/tests/test_stochss_base.py index 11e2f1f920..89cbc2e068 100644 --- a/stochss/tests/test_stochss_base.py +++ b/stochss/tests/test_stochss_base.py @@ -59,6 +59,30 @@ def tearDown(self): if StochSSBase.user_dir != os.path.expanduser("~"): StochSSBase.user_dir = os.path.expanduser("~") + ################################################################################################ + # Unit tests for the StochSS base class add_presentation_name function. + ################################################################################################ + + def test_add_presentation_name__file_exists(self): + ''' Check if the presentation name is add to the presentation names file. ''' + StochSSBase.user_dir = self.tempdir.name + test_base = StochSSBase(path="") + with mock.patch("os.path.exists", return_value=True): + with mock.patch("builtins.open", mock.mock_open(read_data="{}")): + with mock.patch("json.dump") as mock_json_dump: + test_base.add_presentation_name("foo", "bar") + mock_json_dump.assert_called_once_with({'foo': 'bar'}, mock.ANY) + + + def test_add_presentation_name__file_does_not_exists(self): + ''' Check if the presentation names file was created with the given entry. ''' + StochSSBase.user_dir = self.tempdir.name + test_base = StochSSBase(path="") + with mock.patch("builtins.open", mock.mock_open()): + with mock.patch("json.dump") as mock_json_dump: + test_base.add_presentation_name("foo", "bar") + mock_json_dump.assert_called_once_with({'foo': 'bar'}, mock.ANY) + ################################################################################################ # Unit tests for the StochSS base class check_project_format function. ################################################################################################ @@ -130,6 +154,19 @@ def test_check_workflow_format__new(self): ''' Check if the workflow is identified as old. ''' self.assertTrue(StochSSBase.check_workflow_format(path=self.test_folderpath)) + ################################################################################################ + # Unit tests for the StochSS base class delete_presentation_name function. + ################################################################################################ + + def test_delete_presentation_name(self): + ''' Check if the target presentation was removed from the presentation names file. ''' + StochSSBase.user_dir = self.tempdir.name + test_base = StochSSBase(path="") + with mock.patch("builtins.open", mock.mock_open(read_data='{"foo": "bar"}')): + with mock.patch("json.dump") as mock_json_dump: + test_base.delete_presentation_name("foo") + mock_json_dump.assert_called_once_with({}, mock.ANY) + ################################################################################################ # Unit tests for the StochSS base class get_new_path function. ################################################################################################