diff --git a/mocos-gui.py b/mocos-gui.py index a96c3f2..c2238d1 100644 --- a/mocos-gui.py +++ b/mocos-gui.py @@ -3,7 +3,7 @@ import os from model.ProjectHandler import ProjectHandler from model.ProjectSettings import Cardinalities -from PyQt5.QtGui import QGuiApplication +from PyQt5.QtWidgets import QApplication from PyQt5.QtQml import QQmlApplicationEngine, qmlRegisterType import PyQt5.QtCore import logging @@ -19,15 +19,15 @@ def shutdown(): qmlRegisterType(Cardinalities, "ProjectSettingTypes", 1, 0, "Cardinalities") - QGuiApplication.setAttribute(PyQt5.QtCore.Qt.AA_EnableHighDpiScaling, True) - QGuiApplication.setAttribute(PyQt5.QtCore.Qt.AA_UseHighDpiPixmaps, True) + QApplication.setAttribute(PyQt5.QtCore.Qt.AA_EnableHighDpiScaling, True) + QApplication.setAttribute(PyQt5.QtCore.Qt.AA_UseHighDpiPixmaps, True) - app = QGuiApplication([]) + app = QApplication([]) engine = QQmlApplicationEngine() app.aboutToQuit.connect(shutdown) - app.setApplicationName("MOCOS") - app.setOrganizationDomain("mocos.pl") + app.setApplicationName("MOCOS Simulator") + app.setOrganizationDomain("https://mocos.pl") engine.rootContext().setContextProperty("projectHandler", projectHandler) engine.rootContext().setContextProperty("initialConditions", projectHandler._settings.initialConditions) diff --git a/mocos-gui.pyproject b/mocos-gui.pyproject index 96b8767..65b55e3 100644 --- a/mocos-gui.pyproject +++ b/mocos-gui.pyproject @@ -2,25 +2,27 @@ "files": [ "mocos-gui.py", "model/ApplicationSettings.py", + "model/charts.py", + "model/ConfigurationValidator.py", "model/ProjectHandler.py", "model/ProjectSettings.py", "model/SimulationRunner.py", - "model/ConfigurationValidator.py", "model/Utilities.py", "views/ApplicationSettingsWindow.qml", - "views/MainWindow.qml", - "views/ModulationSettingsView.qml", - "views/PhoneTrackingSettingsView.qml", - "views/GeneralSettingsView.qml", - "views/InitialConditionsView.qml", - "views/TransmissionProbabilitiesView.qml", "views/ContactTrackingSettingsView.qml", + "views/DailyInfectionsChartWindow.qml", "views/DoubleNumField.qml", + "views/GeneralSettingsView.qml", + "views/InitialConditionsView.qml", "views/IntNumField.qml", "views/KernelEnablingButton.qml", "views/LogWindow.qml", + "views/MainWindow.qml", + "views/ModulationSettingsView.qml", + "views/PhoneTrackingSettingsView.qml", "views/SpreadingView.qml", + "views/TransmissionProbabilitiesView.qml", "model/Configuration.schema", "model/TanhParameters.schema" - ] + ] } diff --git a/model/ApplicationSettings.py b/model/ApplicationSettings.py index 76125ea..2579621 100644 --- a/model/ApplicationSettings.py +++ b/model/ApplicationSettings.py @@ -1,5 +1,5 @@ import os -from model.Utilities import formatPath, getOrEmptyStr, getOr +from model.Utilities import format_path, get_or_empty_str, get_or from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot import json from enum import Enum @@ -62,12 +62,12 @@ def __loadCliOptions(self): appSettingsFileHandle.close() if lines: content = json.loads(lines) - self._juliaCommand = getOr(content, ApplicationSettings.PropertyNames.JULIA_COMMAND.value, "julia") - self._outputDaily = getOrEmptyStr(content, ApplicationSettings.PropertyNames.OUTPUT_DAILY.value) - self._outputSummary = getOrEmptyStr(content, ApplicationSettings.PropertyNames.OUTPUT_SUMMARY.value) - self._outputParamsDump = getOrEmptyStr(content, ApplicationSettings.PropertyNames.OUTPUT_PARAMS_DUMP.value) - self._outputRunDumpPrefix = getOrEmptyStr(content, ApplicationSettings.PropertyNames.OUTPUT_RUN_DUMP_PREFIX.value) - self._numOfThreads = getOr(content, ApplicationSettings.PropertyNames.NUM_OF_THREADS.value, 1) + self._juliaCommand = get_or(content, ApplicationSettings.PropertyNames.JULIA_COMMAND.value, "julia") + self._outputDaily = get_or_empty_str(content, ApplicationSettings.PropertyNames.OUTPUT_DAILY.value) + self._outputSummary = get_or_empty_str(content, ApplicationSettings.PropertyNames.OUTPUT_SUMMARY.value) + self._outputParamsDump = get_or_empty_str(content, ApplicationSettings.PropertyNames.OUTPUT_PARAMS_DUMP.value) + self._outputRunDumpPrefix = get_or_empty_str(content, ApplicationSettings.PropertyNames.OUTPUT_RUN_DUMP_PREFIX.value) + self._numOfThreads = get_or(content, ApplicationSettings.PropertyNames.NUM_OF_THREADS.value, 1) if self._numOfThreads > self.getMaxNumOfThreads(): self._numOfThreads = self.getMaxNumOfThreads() except OSError: @@ -142,7 +142,7 @@ def outputRunDumpPrefix(self): return self._outputRunDumpPrefix def __isPathToOutputFileJld2Correct(self, relpath): - fullpath = formatPath(self._getworkdir() + "\\" + relpath) + fullpath = format_path(self._getworkdir() + "\\" + relpath) return (os.path.dirname(fullpath) != fullpath and relpath.endswith(".jld2") and os.access(os.path.dirname(fullpath), os.W_OK)) @@ -169,7 +169,7 @@ def outputParamsDumpAcceptable(self): @pyqtProperty(bool, notify=outputRunDumpPrefixAcceptabilityCheckReq) def outputRunDumpPrefixAcceptable(self): - fullpath = formatPath(self._getworkdir() + "\\" + self._outputRunDumpPrefix) + fullpath = format_path(self._getworkdir() + "\\" + self._outputRunDumpPrefix) return (self._outputRunDumpPrefix == "" or (os.path.dirname(fullpath) != fullpath and os.access(os.path.dirname(fullpath), os.W_OK))) @@ -185,7 +185,7 @@ def recentFiles(self): @juliaCommand.setter def juliaCommand(self, cmd): if cmd != "julia": - cmd = formatPath(cmd) + cmd = format_path(cmd) if self._juliaCommand != cmd: self._juliaCommand = cmd self.__saveCliOptions() @@ -194,7 +194,7 @@ def juliaCommand(self, cmd): @outputDaily.setter def outputDaily(self, path): - newpath = formatPath(path, makeRelativeTo=self._getworkdir()) + newpath = format_path(path, makeRelativeTo=self._getworkdir()) if self._outputDaily != path: self._outputDaily = newpath self.__saveCliOptions() @@ -203,7 +203,7 @@ def outputDaily(self, path): @outputSummary.setter def outputSummary(self, path): - newpath = formatPath(path, makeRelativeTo=self._getworkdir()) + newpath = format_path(path, makeRelativeTo=self._getworkdir()) if self._outputSummary != path: self._outputSummary = newpath self.__saveCliOptions() @@ -212,7 +212,7 @@ def outputSummary(self, path): @outputParamsDump.setter def outputParamsDump(self, path): - newpath = formatPath(path, makeRelativeTo=self._getworkdir()) + newpath = format_path(path, makeRelativeTo=self._getworkdir()) if self._outputParamsDump != path: self._outputParamsDump = newpath self.__saveCliOptions() @@ -221,7 +221,7 @@ def outputParamsDump(self, path): @outputRunDumpPrefix.setter def outputRunDumpPrefix(self, path): - newpath = formatPath(path, makeRelativeTo=self._getworkdir()) + newpath = format_path(path, makeRelativeTo=self._getworkdir()) if self._outputRunDumpPrefix != path: self._outputRunDumpPrefix = newpath self.__saveCliOptions() @@ -239,3 +239,16 @@ def numOfThreads(self, threadsNum): @pyqtSlot(result=int) def getMaxNumOfThreads(self): return os.cpu_count() + + def abs_path(self, propertyname): + if propertyname == ApplicationSettings.PropertyNames.NUM_OF_THREADS: + raise ValueError + if propertyname == ApplicationSettings.PropertyNames.OUTPUT_DAILY: + return "" if not self._outputDaily else format_path(self._getworkdir() + "//" + self._outputDaily) + if propertyname == ApplicationSettings.PropertyNames.OUTPUT_PARAMS_DUMP: + return "" if not self._outputParamsDump else format_path(self._getworkdir() + "//" + self._outputParamsDump) + if propertyname == ApplicationSettings.PropertyNames.OUTPUT_SUMMARY: + return "" if not self._outputSummary else format_path(self._getworkdir() + "//" + self._outputSummary) + if propertyname == ApplicationSettings.PropertyNames.OUTPUT_RUN_DUMP_PREFIX: + return "" if not self._outputRunDumpPrefix else format_path(self._getworkdir() + "//" + self._outputRunDumpPrefix) + raise NotImplementedError diff --git a/model/ProjectHandler.py b/model/ProjectHandler.py index 95b85a6..013a4eb 100644 --- a/model/ProjectHandler.py +++ b/model/ProjectHandler.py @@ -1,14 +1,15 @@ # This Python file uses the following encoding: utf-8 from model.ProjectSettings import ModulationFunctions, ProjectSettings, EmptyModulationParams, ValueTypes import json -from PyQt5.QtCore import QAbstractTableModel, pyqtSignal, pyqtSlot, Qt, QByteArray, QObject, QVariant, pyqtProperty import os +from PyQt5.QtCore import QAbstractTableModel, pyqtSignal, pyqtSlot, Qt, QByteArray, QObject, QVariant, pyqtProperty from model.ConfigurationValidator import ConfigurationValidator -from model.Utilities import formatPath +from model.Utilities import format_path from model.ApplicationSettings import ApplicationSettings from model.SimulationRunner import SimulationRunner from jsonschema import ValidationError import tempfile +import model.charts as charts class FunctionParametersModel(QAbstractTableModel): @@ -116,22 +117,34 @@ def notifyDataChanged(self, topLeft, bottomRight): class ProjectHandler(QObject): - _settings = ProjectSettings() - _modulationModel = FunctionParametersModel() - _applicationSettings = None - _simulationRunner = None _openedFilePath = None _isOpenedConfModified = False _isModifyingConfOngoing = True + _infectionTrajectories = None showErrorMsg = pyqtSignal(str, arguments=['msg']) modulationFunctionChanged = pyqtSignal() openedNewConf = pyqtSignal() openedConfModified = pyqtSignal() + infectionTrajectoriesChanged = pyqtSignal() + dailyInfectionsDataAvailableChanged = pyqtSignal() + + updateDailyInfectionsChartBegin = pyqtSignal(str, arguments=['filename']) + addDailyInfectionSeries = pyqtSignal(str, list, arguments=['name', 'series']) + updateDailyInfectionsChartDone = pyqtSignal() + logDebug = pyqtSignal(str, arguments=['msg']) def __init__(self): super().__init__() + self._settings = ProjectSettings() + self._modulationModel = FunctionParametersModel() self._applicationSettings = ApplicationSettings(lambda: self.workdir()) self._simulationRunner = SimulationRunner(lambda: self.workdir()) + self._chart_preparer = charts.DailyInfectionsSeriesPreparer( + lambda: self._applicationSettings.abs_path( + ApplicationSettings.PropertyNames.OUTPUT_DAILY)) + self.connect_signals() + + def connect_signals(self): def setModifiedToTrue(): return self.setOpenedConfModifiedIfModificationOngoing() self._settings.generalSettings.numTrajectoriesChanged.connect(setModifiedToTrue) self._settings.generalSettings.populationPathChanged.connect(setModifiedToTrue) @@ -157,16 +170,29 @@ def setModifiedToTrue(): return self.setOpenedConfModifiedIfModificationOngoing( self._settings.spreading.x0Changed.connect(setModifiedToTrue) self._settings.spreading.truncationChanged.connect(setModifiedToTrue) self._modulationModel.dataChanged.connect(lambda tr, bl, role: setModifiedToTrue()) - self.openedNewConf.connect(self._applicationSettings.recheckPaths) + self.openedNewConf.connect(self._applicationSettings.recheckPaths, type=Qt.QueuedConnection) + self.openedNewConf.connect(self.dailyInfectionsDataAvailableChanged, type=Qt.QueuedConnection) + self._applicationSettings.outputDailyChanged.connect( + self.dailyInfectionsDataAvailableChanged, type=Qt.QueuedConnection) + self.dailyInfectionsDataAvailableChanged.connect( + self.prepareDailyInfectionsData, type=Qt.QueuedConnection) + self._simulationRunner.isRunningChanged.connect( + self.dailyInfectionsDataAvailableChanged, type=Qt.QueuedConnection) + self._chart_preparer.series_prepared.connect(self.addDailyInfectionSeries) + self._chart_preparer.preparing_begin.connect( + self.updateDailyInfectionsChartBegin, type=Qt.QueuedConnection) + self._chart_preparer.preparing_done.connect( + self.updateDailyInfectionsChartDone, type=Qt.QueuedConnection) + self._chart_preparer.log_debug.connect(self.logDebug) def setOpenedConfModifiedIfModificationOngoing(self): if self._isModifyingConfOngoing: self.setOpenedConfModified(True) - def __saveConfToTempFile(self): + def _saveConfToTempFile(self): fh = tempfile.NamedTemporaryFile(mode='w', encoding='utf-8', delete=False) json.dump(self._settings.serialize(), fh, indent=4, ensure_ascii=False) - path = formatPath(fh.name) + path = format_path(fh.name) fh.close() return path @@ -177,7 +203,7 @@ def workdir(self): @pyqtSlot(str) def saveAs(self, path): - path = formatPath(path) + path = format_path(path) fh = open(path, "w", encoding='utf-8') json.dump(self._settings.serialize(), fh, indent=4, ensure_ascii=False) fh.close() @@ -193,7 +219,8 @@ def quickSave(self): @pyqtSlot(str) def open(self, path): try: - path = formatPath(path) + self._chart_preparer.stop() + path = format_path(path) inputFileHandle = open(path, 'r', encoding='utf-8') data = json.loads(inputFileHandle.read()) ConfigurationValidator.validateAgainstSchema(data) @@ -210,6 +237,8 @@ def open(self, path): self.showErrorMsg.emit("File: json schema is corrupted: {0}".format(str(error))) except ValidationError as error: self.showErrorMsg.emit("Unable to open file at: {0} due to wrong input data: {1}".format(path, str(error))) + finally: + inputFileHandle.close() @pyqtSlot(result=str) def getOpenedConfName(self): @@ -254,10 +283,6 @@ def loadParamsForFunction(self, funcType, isModifyingConf): else: self._isModifyingConfOngoing = True - @pyqtSlot(str) - def setPopulationFilePath(self, path): - self._settings.generalSettings.populationPath = formatPath(path) - @pyqtSlot() def runSimulation(self): if not self._settings.generalSettings._populationPath: @@ -273,9 +298,10 @@ def runSimulation(self): or not self._applicationSettings.outputRunDumpPrefixAcceptable): self.showErrorMsg.emit("Simulation can't be run: simulation settings are incorrect.") return + self._chart_preparer.stop() self._simulationRunner.openedFilePath = self._openedFilePath if not self._simulationRunner.openedFilePath: - self._simulationRunner.openedFilePath = self.__saveConfToTempFile() + self._simulationRunner.openedFilePath = self._saveConfToTempFile() self._simulationRunner.juliaCommand = self._applicationSettings.juliaCommand self._simulationRunner.outputDaily = self._applicationSettings.outputDaily self._simulationRunner.outputSummary = self._applicationSettings.outputSummary @@ -287,3 +313,21 @@ def runSimulation(self): @pyqtSlot() def stopSimulation(self): self._simulationRunner.stop() + + @pyqtSlot() + def prepareDailyInfectionsData(self): + self._chart_preparer.start_preparing_data() + + @pyqtProperty(bool, notify=dailyInfectionsDataAvailableChanged) + def isDailyInfectionsDataAvailable(self): + dailypath = self._applicationSettings.abs_path(ApplicationSettings.PropertyNames.OUTPUT_DAILY) + return charts.is_daily_infections_data_available(dailypath) + + @pyqtSlot() + def addNextDailyInfectionSeries(self): + self._chart_preparer.on_series_added() + + @pyqtSlot() + def beforeClosingMainWindow(self): + self.stopSimulation() + self._chart_preparer.stop() diff --git a/model/ProjectSettings.py b/model/ProjectSettings.py index f0c0837..6eb08d7 100644 --- a/model/ProjectSettings.py +++ b/model/ProjectSettings.py @@ -1,7 +1,7 @@ # This Python file uses the following encoding: utf-8 from enum import Enum from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, QVariant -from model.Utilities import formatPath +from model.Utilities import format_path class GeneralSettings(QObject): @@ -45,7 +45,7 @@ def numTrajectories(self, val): @populationPath.setter def populationPath(self, path): - path = formatPath(path) + path = format_path(path) if self._populationPath != path: self._populationPath = path self.populationPathChanged.emit() diff --git a/model/SimulationRunner.py b/model/SimulationRunner.py index d197b29..fa5d654 100644 --- a/model/SimulationRunner.py +++ b/model/SimulationRunner.py @@ -8,7 +8,7 @@ import queue import tempfile from shutil import which -from model.Utilities import formatPath, ABS_PATH_TO_ADVANCED_CLI +from model.Utilities import format_path, ABS_PATH_TO_ADVANCED_CLI def enqueueOutStream(output, q): @@ -94,16 +94,16 @@ def _createCommand(self): cmd.append(cliname) if self.outputParamsDump: cmd.append("--output-params-dump") - cmd.append(formatPath(self._getworkdir() + "\\" + self.outputParamsDump)) + cmd.append(format_path(self._getworkdir() + "\\" + self.outputParamsDump)) if self.outputDaily: cmd.append("--output-daily") - cmd.append(formatPath(self._getworkdir() + "\\" + self.outputDaily)) + cmd.append(format_path(self._getworkdir() + "\\" + self.outputDaily)) if self.outputSummary: cmd.append("--output-summary") - cmd.append(formatPath(self._getworkdir() + "\\" + self.outputSummary)) + cmd.append(format_path(self._getworkdir() + "\\" + self.outputSummary)) if self.outputRunDumpPrefix: cmd.append("--output-run-dump-prefix") - cmd.append(formatPath(self._getworkdir() + "\\" + self.outputRunDumpPrefix)) + cmd.append(format_path(self._getworkdir() + "\\" + self.outputRunDumpPrefix)) cmd.append(self.openedFilePath) return cmd @@ -218,5 +218,5 @@ def stop(self): self._process = None def clean(self): - if self.openedFilePath.find(formatPath(tempfile.gettempdir())) != -1: + if self.openedFilePath.find(format_path(tempfile.gettempdir())) != -1: os.remove(self.openedFilePath) diff --git a/model/Utilities.py b/model/Utilities.py index 9a7c7cf..8d6fefd 100644 --- a/model/Utilities.py +++ b/model/Utilities.py @@ -1,27 +1,24 @@ -import sys import os -def formatPath(path, isFile=True, makeRelativeTo=None): +def format_path(path, isFile=True, makeRelativeTo=None): result = path.replace("file:///", "") result = result.replace('\\', '/') - if sys.platform == "darwin" and path != "": - result = "/" + result if isFile and len(result) > 2 and result.endswith('/'): result = result[:-1] - if makeRelativeTo and os.path.isabs(result): - result = os.path.relpath(result, formatPath(makeRelativeTo)) + if os.path.isabs(result) and makeRelativeTo: + result = os.path.relpath(result, format_path(makeRelativeTo, isFile=False)) result = result.replace('\\', '/') return result -def getOrEmptyStr(data, key): +def get_or_empty_str(data, key): if data.get(key) is None: return "" return data[key] -def getOr(data, key, alternative): +def get_or(data, key, alternative): if data.get(key) is None: return alternative return data[key] diff --git a/model/charts.py b/model/charts.py new file mode 100644 index 0000000..7930623 --- /dev/null +++ b/model/charts.py @@ -0,0 +1,92 @@ +import os +import threading +import h5py +from PyQt5.QtCore import pyqtSignal, QObject +import time + + +class DailyInfectionsSeriesPreparer(QObject): + preparing_begin = pyqtSignal(str, arguments=['filename']) + preparing_done = pyqtSignal() + series_prepared = pyqtSignal(str, list, arguments=['name', 'series']) + log_debug = pyqtSignal(str) + + def __init__(self, get_daily_path): + super().__init__() + self._dailypath = get_daily_path + self._condition = threading.Condition() + self._is_stopped = True + self._trajectories = {} + self._PROGRESS_LOG = "Daily Infections Chart: {} / {} {}\n" + + def start_preparing_data(self): + self.data_thread = threading.Thread(target=self.start_preparing_data_in_thread) + self.data_thread.start() + + def start_preparing_data_in_thread(self): + self._is_stopped = False + dailypath = self._dailypath() + if is_daily_infections_data_available(dailypath): + self._trajectories = get_infections_daily(dailypath) + self.preparing_begin.emit(dailypath) + self._update_thread = threading.Thread(target=self._send_series) + self._update_thread.start() + + def on_series_added(self): + with self._condition: + self._condition.notify() + + def stop(self): + self._is_stopped = True + with self._condition: + self._condition.notify() + self._update_thread.join() + + def _log_progress(self, current, max, flag=""): + self.log_debug.emit(self._PROGRESS_LOG.format(current, max, flag)) + + def _send_series(self): + counter = 0 + self._log_progress(counter, len(self._trajectories)) + for key in self._trajectories: + self.series_prepared.emit(key, self._trajectories[key]) + counter += 1 + if counter % 50 == 0: + self._log_progress(counter, len(self._trajectories)) + with self._condition: + self._condition.wait() + time.sleep(0.1) + if self._is_stopped: + self._log_progress(counter, len(self._trajectories), "[Stopped]") + break + if not self._is_stopped: + self._log_progress(counter, len(self._trajectories), "[Done]") + self._trajectories.clear() + self.preparing_done.emit() + + +def is_daily_infections_data_available(dailyfilepath): + if not dailyfilepath: + return False + if not os.access(dailyfilepath, os.R_OK): + return False + fh = h5py.File(dailyfilepath, 'r') + for tname in fh: + if list(fh[tname].keys()).index('daily_infections') == -1: + return False + return True + + +def get_infections_daily(inputfilepath): + result = {} + if not inputfilepath: + return result + trajectories = h5py.File(inputfilepath, 'r') + for t in trajectories: + series = [] + ds = trajectories[t]['daily_infections'] + for i in range(0, ds.len()): + series.append(int(ds[i])) + result[t] = series + trajectories.close() + return result diff --git a/requirements.txt b/requirements.txt index f7ec279..abbff58 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,5 @@ # external requirements PyQt5>=5.15 +PyQtChart>=5.15 jsonschema>=3.2 +h5py>=2.10.0 \ No newline at end of file diff --git a/views/ApplicationSettingsWindow.qml b/views/ApplicationSettingsWindow.qml index 10bd401..8d9f202 100644 --- a/views/ApplicationSettingsWindow.qml +++ b/views/ApplicationSettingsWindow.qml @@ -11,6 +11,13 @@ Window { height: 360 flags: Qt.Window | Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowCloseButtonHint | Qt.WindowStaysOnTopHint | Qt.MSWindowsFixedSizeDialogHint + function adjustPathToOS(path) { + if (Qt.platform.os != "windows") { + return "/" + path + } + return path + } + Item { anchors.fill: parent anchors.margins: 5 @@ -54,13 +61,17 @@ Window { id: outputDailyField text: applicationSettings.outputDaily onAccepted: applicationSettings.outputDaily = text - onActiveFocusChanged: if (!activeFocus) applicationSettings.outputDaily = text + onActiveFocusChanged: { + if (!activeFocus && applicationSettings.outputDaily !== text) { + applicationSettings.outputDaily = text + } + } color: applicationSettings.outputDailyAcceptable ? "black" : "red" KeyNavigation.tab: outputSummaryField } Button { function setOutputDaily() { - applicationSettings.outputDaily = jld2FileSelectDialog.fileUrl + applicationSettings.outputDaily = adjustPathToOS(jld2FileSelectDialog.fileUrl) jld2FileSelectDialog.accepted.disconnect(setOutputDaily) } @@ -82,13 +93,17 @@ Window { id: outputSummaryField text: applicationSettings.outputSummary onAccepted: applicationSettings.outputSummary = text - onActiveFocusChanged: if (!activeFocus) applicationSettings.outputSummary = text + onActiveFocusChanged: { + if (!activeFocus && applicationSettings.outputSummary !== text) { + applicationSettings.outputSummary = text + } + } color: applicationSettings.outputSummaryAcceptable ? "black" : "red" KeyNavigation.tab: outputParamsDumpField } Button { function setOutputSummary() { - applicationSettings.outputSummary = jld2FileSelectDialog.fileUrl + applicationSettings.outputSummary = adjustPathToOS(jld2FileSelectDialog.fileUrl) jld2FileSelectDialog.accepted.disconnect(setOutputSummary) } @@ -109,16 +124,21 @@ Window { TextField { id: outputParamsDumpField text: applicationSettings.outputParamsDump - onFocusChanged: applicationSettings.outputParamsDump = text onAccepted: applicationSettings.outputParamsDump = text + onActiveFocusChanged: { + if (!activeFocus && applicationSettings.outputParamsDump !== text) { + applicationSettings.outputParamsDump = text + } + } color: applicationSettings.outputParamsDumpAcceptable ? "black" : "red" KeyNavigation.tab: outputRunDumpPrefixField } Button { function setOutputParamsDump() { - applicationSettings.outputParamsDump = jld2FileSelectDialog.fileUrl + applicationSettings.outputParamsDump = adjustPathToOS(jld2FileSelectDialog.fileUrl) jld2FileSelectDialog.accepted.disconnect(setOutputParamsDump) } + text: "Select" onClicked: { jld2FileSelectDialog.folder = "file:///" + projectHandler.workdir() @@ -137,7 +157,11 @@ Window { id: outputRunDumpPrefixField text: applicationSettings.outputRunDumpPrefix onAccepted: applicationSettings.outputRunDumpPrefix = text - onActiveFocusChanged: if (!activeFocus) applicationSettings.outputRunDumpPrefix = text + onActiveFocusChanged: { + if (!activeFocus && applicationSettings.outputRunDumpPrefix !== text) { + applicationSettings.outputRunDumpPrefix = text + } + } color: applicationSettings.outputRunDumpPrefixAcceptable ? "black" : "red" KeyNavigation.tab: numOfThreadsField } @@ -157,7 +181,11 @@ Window { text: applicationSettings.numOfThreads validator: IntValidator{} onAccepted: applicationSettings.numOfThreads = text - onActiveFocusChanged: if (!activeFocus) applicationSettings.numOfThreads = text + onActiveFocusChanged: { + if (!activeFocus && applicationSettings.numOfThreads !== text) { + applicationSettings.numOfThreads = text + } + } KeyNavigation.tab: juliaCommandField } Slider { @@ -187,7 +215,7 @@ Window { sidebarVisible: true nameFilters: [ "JULIA executable (*)" ] onAccepted: { - applicationSettings.juliaCommand = juliaSelectDialog.fileUrl + applicationSettings.juliaCommand = adjustPathToOS(juliaSelectDialog.fileUrl) } } @@ -205,7 +233,7 @@ Window { selectExisting: false sidebarVisible: true onAccepted: { - applicationSettings.outputRunDumpPrefix = runDumpPrefixSelectDialog.fileUrl + applicationSettings.outputRunDumpPrefix = adjustPathToOS(runDumpPrefixSelectDialog.fileUrl) } } } diff --git a/views/ContactTrackingSettingsView.qml b/views/ContactTrackingSettingsView.qml index 57596a7..1c13492 100644 --- a/views/ContactTrackingSettingsView.qml +++ b/views/ContactTrackingSettingsView.qml @@ -39,16 +39,16 @@ GridLayout { Connections { target: contactTracking - onProbabilityChanged: { + function onProbabilityChanged() { probabilityInputField.text = contactTracking.probability } - onBackwardDetectionDelayChanged: { + function onBackwardDetectionDelayChanged() { backwardDetectionDelayInputField.text = contactTracking.backwardDetectionDelay } - onForwardDetectionDelayChanged: { + function onForwardDetectionDelayChanged() { forwardDetectionDelayInputField.text = contactTracking.forwardDetectionDelay } - onTestingTimeChanged: { + function onTestingTimeChanged() { testingTimeInputField.text = contactTracking.testingTime } } diff --git a/views/DailyInfectionsChartWindow.qml b/views/DailyInfectionsChartWindow.qml new file mode 100644 index 0000000..d0b327d --- /dev/null +++ b/views/DailyInfectionsChartWindow.qml @@ -0,0 +1,134 @@ +import QtQuick 2.0 +import QtQuick.Window 2.0 +import QtQuick.Controls 2.4 +import QtQuick.Dialogs 1.1 +import QtQuick.Layouts 1.0 +import QtCharts 2.2 + +Window { + id: dailyInfectionsWindow + title: "Daily Infected" + width: 640 + height: 360 + + function saveChart(filename) { + chartView.grabToImage(function(result){ + result.saveToFile(filename) + }, + Qt.size(widthField.targetValue, heightField.targetValue)) + } + + FileDialog { + id: chartSaveDialog + folder: shortcuts.home + selectExisting: false + sidebarVisible: true + nameFilters: [ "PNG files (*.png)" ] + onAccepted: { + var FILE_PREFIX = "file://" + let path = fileUrl.toString() + if (path.startsWith(FILE_PREFIX)) { + path = path.substr(FILE_PREFIX.length, path.length) + } + dailyInfectionsWindow.saveChart(path) + } + } + + Row { + id: savePanel + Button { + id: saveButton + text: "Save" + enabled: false + onClicked: { + chartSaveDialog.visible = true + } + } + Label { + text: "Width:" + anchors.verticalCenter: widthField.verticalCenter + } + IntNumField { + id: widthField + bottomValue: 1 + targetValue: chartView.width + readOnly: maintainOutputSizeCheckBox.checked + width: 100 + } + Label { + text: "Height:" + anchors.verticalCenter: heightField.verticalCenter + } + IntNumField { + id: heightField + bottomValue: 1 + targetValue: chartView.height + readOnly: maintainOutputSizeCheckBox.checked + width: 100 + } + CheckBox { + id: maintainOutputSizeCheckBox + checked: true + onCheckedChanged: { + if (checked) { + widthField.targetValue = Qt.binding(function(){ return chartView.width }) + heightField.targetValue = Qt.binding(function(){ return chartView.height }) + } else { + widthField.targetValue = chartView.width + heightField.targetValue = chartView.height + } + } + text: "Maintain Chart Size" + } + } + + ChartView { + id: chartView + anchors.top: savePanel.bottom + width: parent.width + height: 300 + anchors.bottom: parent.bottom + antialiasing: true + + ValueAxis { + id: axisY + gridVisible: true + tickCount: 5 + min: 0 + max: 1 + } + + ValueAxis { + id: axisX + gridVisible: true + tickCount: 1 + min: 0 + max: 1 + } + } + + Connections { + target: projectHandler + function onUpdateDailyInfectionsChartBegin(filename) { + var DEFAULT_TITLE = "Daily Infected " + dailyInfectionsWindow.title = DEFAULT_TITLE + filename + axisY.max = 1 + axisX.max = 1 + chartView.removeAllSeries() + } + + function onAddDailyInfectionSeries(name, series) { + axisX.max = Math.max(axisX.max, series.length-1) + var line = chartView.createSeries(ChartView.SeriesTypeLine, name, axisX, axisY) + for (let i=0; i 0 + } + } +} diff --git a/views/GeneralSettingsView.qml b/views/GeneralSettingsView.qml index ffe4c88..7d6f249 100644 --- a/views/GeneralSettingsView.qml +++ b/views/GeneralSettingsView.qml @@ -14,7 +14,13 @@ GridLayout { sidebarVisible: true nameFilters: [ "JLD2 files (*.jld2)" ] onAccepted: { - projectHandler.setPopulationFilePath(populationFileSelectDialog.fileUrl) + let path = "" + if (Qt.platform.os != "windows") { + path = "/" + fileUrl + } else { + path = fileUrl + } + generalSettings.populationPath = path } } @@ -59,13 +65,13 @@ GridLayout { Connections { target: generalSettings - onNumTrajectoriesChanged: { + function onNumTrajectoriesChanged() { numTrajectoriesInputField.text = generalSettings.numTrajectories } - onDetectionMildProbabilityChanged: { + function onDetectionMildProbabilityChanged() { detectionMildProbabilityInputField.text = generalSettings.detectionMildProbability } - onStopSimulationThresholdChanged: { + function onStopSimulationThresholdChanged() { stopSimulationThresholdInputField.text = generalSettings.stopSimulationThreshold } } diff --git a/views/InitialConditionsView.qml b/views/InitialConditionsView.qml index 7d72fcb..5afc339 100644 --- a/views/InitialConditionsView.qml +++ b/views/InitialConditionsView.qml @@ -18,6 +18,8 @@ GridLayout { Connections { target: initialConditions.cardinalities - onInfectiousChanged: infectiousNumField.text = initialConditions.cardinalities.infectious + function onInfectiousChanged() { + infectiousNumField.text = initialConditions.cardinalities.infectious + } } } diff --git a/views/LogWindow.qml b/views/LogWindow.qml index 941ad77..7d1cee2 100644 --- a/views/LogWindow.qml +++ b/views/LogWindow.qml @@ -51,14 +51,23 @@ Window { Connections { target: simulationRunner - onPrintSimulationMsg: { + function onPrintSimulationMsg(msg) { let newText = logViewer.text + msg logViewer.text = newText } - onClearLog: logViewer.text = "" - onNotifyStateAndProgress: { + function onClearLog() { + logViewer.text = "" + } + function onNotifyStateAndProgress(state, progress) { progressLabel.text = state progressBar.value = progress / 100 } } + Connections { + target: projectHandler + function onLogDebug(msg) { + let newText = logViewer.text + msg + logViewer.text = newText + } + } } diff --git a/views/MainWindow.qml b/views/MainWindow.qml index 4c07b03..2302ad9 100644 --- a/views/MainWindow.qml +++ b/views/MainWindow.qml @@ -9,9 +9,14 @@ ApplicationWindow { objectName: "mainWindow" title: createMainWindowTitle() height: 500 - visible: true + signal dailyInfectionSeriesAdded + + onClosing: { + projectHandler.beforeClosingMainWindow() + } + property variant contentSources: [ "InitialConditionsView.qml", "GeneralSettingsView.qml", @@ -70,14 +75,14 @@ ApplicationWindow { Connections { target: projectHandler - onShowErrorMsg: { + function onShowErrorMsg(msg) { errorMessageDialog.text = msg errorMessageDialog.visible = true } - onOpenedNewConf: { + function onOpenedNewConf() { mainWindow.title = createMainWindowTitle() } - onOpenedConfModified: { + function onOpenedConfModified() { mainWindow.title = createMainWindowTitle() } } @@ -92,6 +97,7 @@ ApplicationWindow { + spreadingSettingsButton.width + 50; recentFilesMenu.createMenuItems(applicationSettings.recentFiles) + projectHandler.prepareDailyInfectionsData() } FileDialog { @@ -209,13 +215,22 @@ ApplicationWindow { shortcut: "Ctrl+L" } } + Menu { + title: "&Postprocessing" + Action { + text: "&Daily Infections Chart" + enabled: projectHandler.isDailyInfectionsDataAvailable + onTriggered: dailyInfectionsChartWindow.visible = true + shortcut: "Ctrl+D" + } + } } footer: Label { id: statusBar Connections { target: simulationRunner - onNotifyStateAndProgress: { + function onNotifyStateAndProgress(state, progress) { let status = state if (progress >= 0) { status += " " + progress + "%" @@ -318,6 +333,14 @@ ApplicationWindow { console.assert(false) } + property var dailyInfectionsChartWindow: { + var component = Qt.createComponent("DailyInfectionsChartWindow.qml") + if (component.status === Component.Ready) { + return component.createObject(mainWindow) + } + console.assert(false) + } + Item { anchors.fill: parent @@ -337,14 +360,14 @@ ApplicationWindow { Connections { ignoreUnknownSignals: true target: contentLoader.item - onShowLogWindow: { + function onShowLogWindow() { logWindow.visible = true } } Connections { target: applicationSettings - onRecentFilesChanged: { + function onRecentFilesChanged() { recentFilesMenu.clear() recentFilesMenu.createMenuItems(applicationSettings.recentFiles) } diff --git a/views/ModulationSettingsView.qml b/views/ModulationSettingsView.qml index 8c2ad73..2e6af70 100644 --- a/views/ModulationSettingsView.qml +++ b/views/ModulationSettingsView.qml @@ -17,7 +17,7 @@ Item { Connections { target: projectHandler - onModulationFunctionChanged: { + function onModulationFunctionChanged() { let newIndex = projectHandler.getModulationFunctionTypes().indexOf(projectHandler.getActiveModulationFunction()) if (modulationFuncComboBox.currentIndex === newIndex) { modulationSettingsView.loadNewParams(false) diff --git a/views/PhoneTrackingSettingsView.qml b/views/PhoneTrackingSettingsView.qml index cf1437f..baa040c 100644 --- a/views/PhoneTrackingSettingsView.qml +++ b/views/PhoneTrackingSettingsView.qml @@ -35,9 +35,13 @@ GridLayout { Connections { target: phoneTracking - onUsageChanged: usageInputField.text = phoneTracking.usage - onDetectionDelayChanged: detectionDelayInputField.text = phoneTracking.detectionDelay - onUsageByHouseholdChanged: { + function onUsageChanged() { + usageInputField.text = phoneTracking.usage + } + function onDetectionDelayChanged() { + detectionDelayInputField.text = phoneTracking.detectionDelay + } + function onUsageByHouseholdChanged() { usageByHouseholdInputField.checkState = phoneTracking.usageByHousehold ? Qt.Checked : Qt.Unchecked } } diff --git a/views/TransmissionProbabilitiesView.qml b/views/TransmissionProbabilitiesView.qml index beea7d4..515796e 100644 --- a/views/TransmissionProbabilitiesView.qml +++ b/views/TransmissionProbabilitiesView.qml @@ -85,19 +85,19 @@ GridLayout { Connections { target: transmissionProbabilities - onHouseholdChanged: { + function onHouseholdChanged() { householdFactorField.targetValue = transmissionProbabilities.household householdFactorField.text = transmissionProbabilities.household } - onConstantChanged: { + function onConstantChanged() { constantFactorField.targetValue = transmissionProbabilities.constant constantFactorField.text = transmissionProbabilities.constant } - onHospitalChanged: { + function onHospitalChanged() { hospitalFactorField.targetValue = transmissionProbabilities.hospital hospitalFactorField.text = transmissionProbabilities.hospital } - onFriendshipChanged: { + function onFriendshipChanged() { friendshipKernelField.targetValue = transmissionProbabilities.friendship friendshipKernelField.text = transmissionProbabilities.friendship }