From b90eb3c78a9e2a8bd930b40589a4bc63e68a24a1 Mon Sep 17 00:00:00 2001 From: Kamil Galant Date: Tue, 8 Sep 2020 15:04:23 +0200 Subject: [PATCH 01/10] drawing and saving chart + cleaning repo --- mocos-gui.py | 8 +- mocos-gui.pyproject | 16 +-- model/ApplicationSettings.py | 28 ++--- model/DailyInfectionsChartGenerator.py | 25 +++++ model/ProjectHandler.py | 43 ++++++-- model/ProjectSettings.py | 4 +- model/SimulationRunner.py | 12 +-- model/Utilities.py | 8 +- views/ContactTrackingSettingsView.qml | 8 +- views/DailyInfectionsChartWindow.qml | 129 ++++++++++++++++++++++++ views/GeneralSettingsView.qml | 6 +- views/InitialConditionsView.qml | 4 +- views/LogWindow.qml | 8 +- views/MainWindow.qml | 28 +++-- views/ModulationSettingsView.qml | 2 +- views/PhoneTrackingSettingsView.qml | 10 +- views/TransmissionProbabilitiesView.qml | 8 +- 17 files changed, 279 insertions(+), 68 deletions(-) create mode 100644 model/DailyInfectionsChartGenerator.py create mode 100644 views/DailyInfectionsChartWindow.qml diff --git a/mocos-gui.py b/mocos-gui.py index a96c3f2..ce2134c 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,10 +19,10 @@ 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) diff --git a/mocos-gui.pyproject b/mocos-gui.pyproject index 96b8767..48cf9bd 100644 --- a/mocos-gui.pyproject +++ b/mocos-gui.pyproject @@ -2,24 +2,26 @@ "files": [ "mocos-gui.py", "model/ApplicationSettings.py", + "model/ConfigurationValidator.py", + "model/DailyInfectionsChartWindow.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..39a8ebf 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() diff --git a/model/DailyInfectionsChartGenerator.py b/model/DailyInfectionsChartGenerator.py new file mode 100644 index 0000000..3476975 --- /dev/null +++ b/model/DailyInfectionsChartGenerator.py @@ -0,0 +1,25 @@ +import h5py +import os + + +def get_infections_daily(inputfilepath): + trajectories = h5py.File(inputfilepath, 'r') + result = {} + for t in trajectories: + data_per_t = [] + ds = trajectories[t]['daily_infections'] + for i in range(0, ds.len()): + data_per_t.append(int(ds[i])) + result[t] = data_per_t + trajectories.close() + return result + + +def is_daily_infections_chart_available(dailyfilepath): + 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 diff --git a/model/ProjectHandler.py b/model/ProjectHandler.py index 95b85a6..d76fe9a 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 +from model.DailyInfectionsChartGenerator import get_infections_daily, is_daily_infections_chart_available class FunctionParametersModel(QAbstractTableModel): @@ -123,10 +124,14 @@ class ProjectHandler(QObject): _openedFilePath = None _isOpenedConfModified = False _isModifyingConfOngoing = True + _infectionTrajectories = None showErrorMsg = pyqtSignal(str, arguments=['msg']) modulationFunctionChanged = pyqtSignal() openedNewConf = pyqtSignal() openedConfModified = pyqtSignal() + infectionTrajectoriesChanged = pyqtSignal() + updateDailyInfectionsChart = pyqtSignal() + dailyInfectionsDataAvailableChanged = pyqtSignal() def __init__(self): super().__init__() @@ -158,6 +163,10 @@ def setModifiedToTrue(): return self.setOpenedConfModifiedIfModificationOngoing( 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.dailyInfectionsDataAvailableChanged) + self._applicationSettings.outputDailyChanged.connect(self.dailyInfectionsDataAvailableChanged) + self.dailyInfectionsDataAvailableChanged.connect(self.prepareDailyInfectionsData) + self._simulationRunner.isRunningChanged.connect(self.dailyInfectionsDataAvailableChanged) def setOpenedConfModifiedIfModificationOngoing(self): if self._isModifyingConfOngoing: @@ -166,7 +175,7 @@ def setOpenedConfModifiedIfModificationOngoing(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 +186,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 +202,7 @@ def quickSave(self): @pyqtSlot(str) def open(self, path): try: - path = formatPath(path) + path = format_path(path) inputFileHandle = open(path, 'r', encoding='utf-8') data = json.loads(inputFileHandle.read()) ConfigurationValidator.validateAgainstSchema(data) @@ -256,7 +265,7 @@ def loadParamsForFunction(self, funcType, isModifyingConf): @pyqtSlot(str) def setPopulationFilePath(self, path): - self._settings.generalSettings.populationPath = formatPath(path) + self._settings.generalSettings.populationPath = format_path(path) @pyqtSlot() def runSimulation(self): @@ -287,3 +296,25 @@ def runSimulation(self): @pyqtSlot() def stopSimulation(self): self._simulationRunner.stop() + + @pyqtSlot() + def prepareDailyInfectionsData(self): + if self.isDailyInfectionsDataAvailable: + dailypath = format_path(self.workdir() + "\\" + self._applicationSettings.outputDaily) + self._infectionTrajectories = get_infections_daily(dailypath) + else: + self._infectionTrajectories = {} + self.updateDailyInfectionsChart.emit() + + @pyqtSlot(result=QVariant) + def infectionTrajectories(self): + return list(self._infectionTrajectories.keys()) + + @pyqtSlot(str, result=QVariant) + def infectionTrajectoryValues(self, trajectory_name): + return self._infectionTrajectories[trajectory_name] + + @pyqtProperty(bool, notify=dailyInfectionsDataAvailableChanged) + def isDailyInfectionsDataAvailable(self): + dailypath = format_path(self.workdir() + "\\" + self._applicationSettings.outputDaily) + return is_daily_infections_chart_available(dailypath) 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..1d58b41 100644 --- a/model/Utilities.py +++ b/model/Utilities.py @@ -2,7 +2,7 @@ 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 != "": @@ -10,18 +10,18 @@ def formatPath(path, isFile=True, makeRelativeTo=None): 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)) + result = os.path.relpath(result, format_path(makeRelativeTo)) 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/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..1e1d240 --- /dev/null +++ b/views/DailyInfectionsChartWindow.qml @@ -0,0 +1,129 @@ +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: appSettingsWindow + 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) + } + appSettingsWindow.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 onUpdateDailyInfectionsChart() { + axisY.max = 1 + axisX.max = 1 + chartView.removeAllSeries() + let infections = projectHandler.infectionTrajectories() + for (var i=0; i 0 + } + } +} diff --git a/views/GeneralSettingsView.qml b/views/GeneralSettingsView.qml index ffe4c88..3a6776c 100644 --- a/views/GeneralSettingsView.qml +++ b/views/GeneralSettingsView.qml @@ -59,13 +59,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..affbbf6 100644 --- a/views/LogWindow.qml +++ b/views/LogWindow.qml @@ -51,12 +51,14 @@ 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 } diff --git a/views/MainWindow.qml b/views/MainWindow.qml index 4c07b03..562a820 100644 --- a/views/MainWindow.qml +++ b/views/MainWindow.qml @@ -70,14 +70,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() } } @@ -209,13 +209,21 @@ ApplicationWindow { shortcut: "Ctrl+L" } } + Menu { + title: "Postprocessing" + Action { + text: "Daily Infections Chart" + enabled: projectHandler.isDailyInfectionsDataAvailable + onTriggered: dailyInfectedChartWindow.visible = true + } + } } footer: Label { id: statusBar Connections { target: simulationRunner - onNotifyStateAndProgress: { + function onNotifyStateAndProgress(state, progress) { let status = state if (progress >= 0) { status += " " + progress + "%" @@ -318,6 +326,14 @@ ApplicationWindow { console.assert(false) } + property var dailyInfectedChartWindow: { + var component = Qt.createComponent("DailyInfectedChartWindow.qml") + if (component.status === Component.Ready) { + return component.createObject(mainWindow) + } + console.assert(false) + } + Item { anchors.fill: parent @@ -337,14 +353,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 } From cec870a823ae16b89af5499ad8f484c901c5b2e1 Mon Sep 17 00:00:00 2001 From: Kamil Galant Date: Fri, 11 Sep 2020 15:49:02 +0200 Subject: [PATCH 02/10] added h5py to requirements --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index f7ec279..ff1f894 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ # external requirements PyQt5>=5.15 jsonschema>=3.2 +h5py>=2.10.0 \ No newline at end of file From a94fc4b63577acd3a9d894fce3ca2232866b1d21 Mon Sep 17 00:00:00 2001 From: Kamil Galant Date: Fri, 11 Sep 2020 16:23:02 +0200 Subject: [PATCH 03/10] fix for empty path to output daily --- model/ProjectHandler.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/model/ProjectHandler.py b/model/ProjectHandler.py index d76fe9a..32c3929 100644 --- a/model/ProjectHandler.py +++ b/model/ProjectHandler.py @@ -316,5 +316,7 @@ def infectionTrajectoryValues(self, trajectory_name): @pyqtProperty(bool, notify=dailyInfectionsDataAvailableChanged) def isDailyInfectionsDataAvailable(self): + if not self._applicationSettings.outputDaily: + return False dailypath = format_path(self.workdir() + "\\" + self._applicationSettings.outputDaily) return is_daily_infections_chart_available(dailypath) From 59df83747366df487b7920036f29fe9e8746a8a5 Mon Sep 17 00:00:00 2001 From: Kamil Galant Date: Fri, 11 Sep 2020 18:57:23 +0200 Subject: [PATCH 04/10] fixed name of qml file creating daily infections chart window; fixed bug of not drawing chart when output-daily file is available on application open --- views/DailyInfectionsChartWindow.qml | 9 +++++++++ views/MainWindow.qml | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/views/DailyInfectionsChartWindow.qml b/views/DailyInfectionsChartWindow.qml index 1e1d240..0edad37 100644 --- a/views/DailyInfectionsChartWindow.qml +++ b/views/DailyInfectionsChartWindow.qml @@ -18,6 +18,15 @@ Window { Qt.size(widthField.targetValue, heightField.targetValue)) } + + property bool isFirstOpen: true + onVisibleChanged: { + if (visible === true && isFirstOpen) { + isFirstOpen = false + projectHandler.prepareDailyInfectionsData() + } + } + FileDialog { id: chartSaveDialog folder: shortcuts.home diff --git a/views/MainWindow.qml b/views/MainWindow.qml index 562a820..06ddea8 100644 --- a/views/MainWindow.qml +++ b/views/MainWindow.qml @@ -327,7 +327,7 @@ ApplicationWindow { } property var dailyInfectedChartWindow: { - var component = Qt.createComponent("DailyInfectedChartWindow.qml") + var component = Qt.createComponent("DailyInfectionsChartWindow.qml") if (component.status === Component.Ready) { return component.createObject(mainWindow) } From 94ec246846c1f5276cb8e8a933969bf4fda753ef Mon Sep 17 00:00:00 2001 From: Kamil Galant Date: Tue, 15 Sep 2020 17:55:49 +0200 Subject: [PATCH 05/10] format path on posix OS --- model/Utilities.py | 7 ++----- views/ApplicationSettingsWindow.qml | 18 +++++++++++++----- views/GeneralSettingsView.qml | 8 +++++++- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/model/Utilities.py b/model/Utilities.py index 1d58b41..8d6fefd 100644 --- a/model/Utilities.py +++ b/model/Utilities.py @@ -1,16 +1,13 @@ -import sys import os 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, format_path(makeRelativeTo)) + if os.path.isabs(result) and makeRelativeTo: + result = os.path.relpath(result, format_path(makeRelativeTo, isFile=False)) result = result.replace('\\', '/') return result diff --git a/views/ApplicationSettingsWindow.qml b/views/ApplicationSettingsWindow.qml index 10bd401..9bef9db 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 @@ -60,7 +67,7 @@ Window { } Button { function setOutputDaily() { - applicationSettings.outputDaily = jld2FileSelectDialog.fileUrl + applicationSettings.outputDaily = adjustPathToOS(jld2FileSelectDialog.fileUrl) jld2FileSelectDialog.accepted.disconnect(setOutputDaily) } @@ -88,7 +95,7 @@ Window { } Button { function setOutputSummary() { - applicationSettings.outputSummary = jld2FileSelectDialog.fileUrl + applicationSettings.outputSummary = adjustPathToOS(jld2FileSelectDialog.fileUrl) jld2FileSelectDialog.accepted.disconnect(setOutputSummary) } @@ -116,9 +123,10 @@ Window { } Button { function setOutputParamsDump() { - applicationSettings.outputParamsDump = jld2FileSelectDialog.fileUrl + applicationSettings.outputParamsDump = adjustPathToOS(jld2FileSelectDialog.fileUrl) jld2FileSelectDialog.accepted.disconnect(setOutputParamsDump) } + text: "Select" onClicked: { jld2FileSelectDialog.folder = "file:///" + projectHandler.workdir() @@ -187,7 +195,7 @@ Window { sidebarVisible: true nameFilters: [ "JULIA executable (*)" ] onAccepted: { - applicationSettings.juliaCommand = juliaSelectDialog.fileUrl + applicationSettings.juliaCommand = adjustPathToOS(juliaSelectDialog.fileUrl) } } @@ -205,7 +213,7 @@ Window { selectExisting: false sidebarVisible: true onAccepted: { - applicationSettings.outputRunDumpPrefix = runDumpPrefixSelectDialog.fileUrl + applicationSettings.outputRunDumpPrefix = adjustPathToOS(runDumpPrefixSelectDialog.fileUrl) } } } diff --git a/views/GeneralSettingsView.qml b/views/GeneralSettingsView.qml index 3a6776c..947f718 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) + path = "" + if (Qt.platform.os != "windows") { + path = "/" + fileUrl + } else { + path = fileUrl + } + projectHandler.setPopulationFilePath(path) } } From e12144274b95a23d9f1fc639e626b945b2075bc4 Mon Sep 17 00:00:00 2001 From: Kamil Galant Date: Wed, 16 Sep 2020 11:49:05 +0200 Subject: [PATCH 06/10] fixed several bugs: * loading daily infections chart on application start * removed lag on setting existing daily output file * removed double update of application settings * added missing 'let' to qml's script --- model/ProjectHandler.py | 22 +++++++++++++------- views/ApplicationSettingsWindow.qml | 30 +++++++++++++++++++++++----- views/DailyInfectionsChartWindow.qml | 9 --------- views/GeneralSettingsView.qml | 2 +- views/MainWindow.qml | 1 + 5 files changed, 42 insertions(+), 22 deletions(-) diff --git a/model/ProjectHandler.py b/model/ProjectHandler.py index 32c3929..d849cb8 100644 --- a/model/ProjectHandler.py +++ b/model/ProjectHandler.py @@ -10,6 +10,7 @@ from jsonschema import ValidationError import tempfile from model.DailyInfectionsChartGenerator import get_infections_daily, is_daily_infections_chart_available +import threading class FunctionParametersModel(QAbstractTableModel): @@ -162,11 +163,14 @@ 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.dailyInfectionsDataAvailableChanged) - self._applicationSettings.outputDailyChanged.connect(self.dailyInfectionsDataAvailableChanged) - self.dailyInfectionsDataAvailableChanged.connect(self.prepareDailyInfectionsData) - self._simulationRunner.isRunningChanged.connect(self.dailyInfectionsDataAvailableChanged) + 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) def setOpenedConfModifiedIfModificationOngoing(self): if self._isModifyingConfOngoing: @@ -297,8 +301,7 @@ def runSimulation(self): def stopSimulation(self): self._simulationRunner.stop() - @pyqtSlot() - def prepareDailyInfectionsData(self): + def prepareDailyInfectionsDataThreaded(self): if self.isDailyInfectionsDataAvailable: dailypath = format_path(self.workdir() + "\\" + self._applicationSettings.outputDaily) self._infectionTrajectories = get_infections_daily(dailypath) @@ -306,6 +309,11 @@ def prepareDailyInfectionsData(self): self._infectionTrajectories = {} self.updateDailyInfectionsChart.emit() + @pyqtSlot() + def prepareDailyInfectionsData(self): + self._dataPreparingThread = threading.Thread(target=self.prepareDailyInfectionsDataThreaded) + self._dataPreparingThread.start() + @pyqtSlot(result=QVariant) def infectionTrajectories(self): return list(self._infectionTrajectories.keys()) diff --git a/views/ApplicationSettingsWindow.qml b/views/ApplicationSettingsWindow.qml index 9bef9db..8d9f202 100644 --- a/views/ApplicationSettingsWindow.qml +++ b/views/ApplicationSettingsWindow.qml @@ -61,7 +61,11 @@ 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 } @@ -89,7 +93,11 @@ 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 } @@ -116,8 +124,12 @@ 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 } @@ -145,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 } @@ -165,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 { diff --git a/views/DailyInfectionsChartWindow.qml b/views/DailyInfectionsChartWindow.qml index 0edad37..1e1d240 100644 --- a/views/DailyInfectionsChartWindow.qml +++ b/views/DailyInfectionsChartWindow.qml @@ -18,15 +18,6 @@ Window { Qt.size(widthField.targetValue, heightField.targetValue)) } - - property bool isFirstOpen: true - onVisibleChanged: { - if (visible === true && isFirstOpen) { - isFirstOpen = false - projectHandler.prepareDailyInfectionsData() - } - } - FileDialog { id: chartSaveDialog folder: shortcuts.home diff --git a/views/GeneralSettingsView.qml b/views/GeneralSettingsView.qml index 947f718..af9e967 100644 --- a/views/GeneralSettingsView.qml +++ b/views/GeneralSettingsView.qml @@ -14,7 +14,7 @@ GridLayout { sidebarVisible: true nameFilters: [ "JLD2 files (*.jld2)" ] onAccepted: { - path = "" + let path = "" if (Qt.platform.os != "windows") { path = "/" + fileUrl } else { diff --git a/views/MainWindow.qml b/views/MainWindow.qml index 06ddea8..39066b3 100644 --- a/views/MainWindow.qml +++ b/views/MainWindow.qml @@ -92,6 +92,7 @@ ApplicationWindow { + spreadingSettingsButton.width + 50; recentFilesMenu.createMenuItems(applicationSettings.recentFiles) + projectHandler.prepareDailyInfectionsData() } FileDialog { From a3ebac3dda4b991f9ad4ae6afd5aebe74c6507b0 Mon Sep 17 00:00:00 2001 From: Kamil Galant Date: Thu, 17 Sep 2020 10:26:12 +0200 Subject: [PATCH 07/10] simplification of setting population path --- model/ProjectHandler.py | 4 ---- views/GeneralSettingsView.qml | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/model/ProjectHandler.py b/model/ProjectHandler.py index d849cb8..9377581 100644 --- a/model/ProjectHandler.py +++ b/model/ProjectHandler.py @@ -267,10 +267,6 @@ def loadParamsForFunction(self, funcType, isModifyingConf): else: self._isModifyingConfOngoing = True - @pyqtSlot(str) - def setPopulationFilePath(self, path): - self._settings.generalSettings.populationPath = format_path(path) - @pyqtSlot() def runSimulation(self): if not self._settings.generalSettings._populationPath: diff --git a/views/GeneralSettingsView.qml b/views/GeneralSettingsView.qml index af9e967..7d6f249 100644 --- a/views/GeneralSettingsView.qml +++ b/views/GeneralSettingsView.qml @@ -20,7 +20,7 @@ GridLayout { } else { path = fileUrl } - projectHandler.setPopulationFilePath(path) + generalSettings.populationPath = path } } From eb4bcc38fd510a5970b5d31bba16dca317b955bb Mon Sep 17 00:00:00 2001 From: Kamil Galant Date: Wed, 23 Sep 2020 13:36:02 +0200 Subject: [PATCH 08/10] improved generatiorn of daily infections charts --- mocos-gui.pyproject | 4 +- model/ApplicationSettings.py | 13 ++++ model/DailyInfectionsChartGenerator.py | 25 ------- model/ProjectHandler.py | 69 ++++++++++--------- model/charts.py | 92 ++++++++++++++++++++++++++ views/DailyInfectionsChartWindow.qml | 31 +++++---- views/LogWindow.qml | 7 ++ views/MainWindow.qml | 13 ++-- 8 files changed, 179 insertions(+), 75 deletions(-) delete mode 100644 model/DailyInfectionsChartGenerator.py create mode 100644 model/charts.py diff --git a/mocos-gui.pyproject b/mocos-gui.pyproject index 48cf9bd..65b55e3 100644 --- a/mocos-gui.pyproject +++ b/mocos-gui.pyproject @@ -2,8 +2,8 @@ "files": [ "mocos-gui.py", "model/ApplicationSettings.py", + "model/charts.py", "model/ConfigurationValidator.py", - "model/DailyInfectionsChartWindow.py", "model/ProjectHandler.py", "model/ProjectSettings.py", "model/SimulationRunner.py", @@ -24,5 +24,5 @@ "views/TransmissionProbabilitiesView.qml", "model/Configuration.schema", "model/TanhParameters.schema" - ] + ] } diff --git a/model/ApplicationSettings.py b/model/ApplicationSettings.py index 39a8ebf..2579621 100644 --- a/model/ApplicationSettings.py +++ b/model/ApplicationSettings.py @@ -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/DailyInfectionsChartGenerator.py b/model/DailyInfectionsChartGenerator.py deleted file mode 100644 index 3476975..0000000 --- a/model/DailyInfectionsChartGenerator.py +++ /dev/null @@ -1,25 +0,0 @@ -import h5py -import os - - -def get_infections_daily(inputfilepath): - trajectories = h5py.File(inputfilepath, 'r') - result = {} - for t in trajectories: - data_per_t = [] - ds = trajectories[t]['daily_infections'] - for i in range(0, ds.len()): - data_per_t.append(int(ds[i])) - result[t] = data_per_t - trajectories.close() - return result - - -def is_daily_infections_chart_available(dailyfilepath): - 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 diff --git a/model/ProjectHandler.py b/model/ProjectHandler.py index 9377581..9c75ed2 100644 --- a/model/ProjectHandler.py +++ b/model/ProjectHandler.py @@ -9,8 +9,7 @@ from model.SimulationRunner import SimulationRunner from jsonschema import ValidationError import tempfile -from model.DailyInfectionsChartGenerator import get_infections_daily, is_daily_infections_chart_available -import threading +import model.charts as charts class FunctionParametersModel(QAbstractTableModel): @@ -118,10 +117,6 @@ def notifyDataChanged(self, topLeft, bottomRight): class ProjectHandler(QObject): - _settings = ProjectSettings() - _modulationModel = FunctionParametersModel() - _applicationSettings = None - _simulationRunner = None _openedFilePath = None _isOpenedConfModified = False _isModifyingConfOngoing = True @@ -131,13 +126,25 @@ class ProjectHandler(QObject): openedNewConf = pyqtSignal() openedConfModified = pyqtSignal() infectionTrajectoriesChanged = pyqtSignal() - updateDailyInfectionsChart = 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.daily_infections_series_preparer( + 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) @@ -171,12 +178,18 @@ def setModifiedToTrue(): return self.setOpenedConfModifiedIfModificationOngoing( 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 = format_path(fh.name) @@ -206,6 +219,7 @@ def quickSave(self): @pyqtSlot(str) def open(self, path): try: + self._chart_preparer.stop() path = format_path(path) inputFileHandle = open(path, 'r', encoding='utf-8') data = json.loads(inputFileHandle.read()) @@ -223,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): @@ -282,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 @@ -297,30 +314,20 @@ def runSimulation(self): def stopSimulation(self): self._simulationRunner.stop() - def prepareDailyInfectionsDataThreaded(self): - if self.isDailyInfectionsDataAvailable: - dailypath = format_path(self.workdir() + "\\" + self._applicationSettings.outputDaily) - self._infectionTrajectories = get_infections_daily(dailypath) - else: - self._infectionTrajectories = {} - self.updateDailyInfectionsChart.emit() - @pyqtSlot() def prepareDailyInfectionsData(self): - self._dataPreparingThread = threading.Thread(target=self.prepareDailyInfectionsDataThreaded) - self._dataPreparingThread.start() - - @pyqtSlot(result=QVariant) - def infectionTrajectories(self): - return list(self._infectionTrajectories.keys()) - - @pyqtSlot(str, result=QVariant) - def infectionTrajectoryValues(self, trajectory_name): - return self._infectionTrajectories[trajectory_name] + self._chart_preparer.start_preparing_data() @pyqtProperty(bool, notify=dailyInfectionsDataAvailableChanged) def isDailyInfectionsDataAvailable(self): - if not self._applicationSettings.outputDaily: - return False - dailypath = format_path(self.workdir() + "\\" + self._applicationSettings.outputDaily) - return is_daily_infections_chart_available(dailypath) + 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/charts.py b/model/charts.py new file mode 100644 index 0000000..6585948 --- /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 daily_infections_series_preparer(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/views/DailyInfectionsChartWindow.qml b/views/DailyInfectionsChartWindow.qml index 1e1d240..8bb0a74 100644 --- a/views/DailyInfectionsChartWindow.qml +++ b/views/DailyInfectionsChartWindow.qml @@ -6,7 +6,7 @@ import QtQuick.Layouts 1.0 import QtCharts 2.2 Window { - id: appSettingsWindow + id: dailyInfectionsWindow title: "Daily Infected" width: 640 height: 360 @@ -30,7 +30,7 @@ Window { if (path.startsWith(FILE_PREFIX)) { path = path.substr(FILE_PREFIX.length, path.length) } - appSettingsWindow.saveChart(path) + dailyInfectionsWindow.saveChart(path) } } @@ -109,21 +109,26 @@ Window { Connections { target: projectHandler - function onUpdateDailyInfectionsChart() { + function onUpdateDailyInfectionsChartBegin(filename) { + var DEFAULT_TITLE = "Daily Infected " + dailyInfectionsWindow.title = DEFAULT_TITLE + filename axisY.max = 1 axisX.max = 1 chartView.removeAllSeries() - let infections = projectHandler.infectionTrajectories() - for (var i=0; i 0 + projectHandler.addNextDailyInfectionSeries() + } + + function onUpdateDailyInfectionsChartDone() { + saveButton.enabled = chartView.count > 0 } } } diff --git a/views/LogWindow.qml b/views/LogWindow.qml index affbbf6..7d1cee2 100644 --- a/views/LogWindow.qml +++ b/views/LogWindow.qml @@ -63,4 +63,11 @@ Window { 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 39066b3..4143ff8 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", @@ -215,7 +220,7 @@ ApplicationWindow { Action { text: "Daily Infections Chart" enabled: projectHandler.isDailyInfectionsDataAvailable - onTriggered: dailyInfectedChartWindow.visible = true + onTriggered: dailyInfectionsChartWindow.visible = true } } } @@ -327,13 +332,13 @@ ApplicationWindow { console.assert(false) } - property var dailyInfectedChartWindow: { + 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 From e6e12e897f618936f90183454014deac6b3321b5 Mon Sep 17 00:00:00 2001 From: Marcin Bodych Date: Sun, 18 Oct 2020 19:45:39 +0200 Subject: [PATCH 09/10] small modifications to make the charts running --- mocos-gui.py | 4 ++-- requirements.txt | 1 + views/DailyInfectionsChartWindow.qml | 2 +- views/MainWindow.qml | 5 +++-- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/mocos-gui.py b/mocos-gui.py index ce2134c..c2238d1 100644 --- a/mocos-gui.py +++ b/mocos-gui.py @@ -26,8 +26,8 @@ def shutdown(): 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/requirements.txt b/requirements.txt index ff1f894..abbff58 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +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/DailyInfectionsChartWindow.qml b/views/DailyInfectionsChartWindow.qml index 8bb0a74..d0b327d 100644 --- a/views/DailyInfectionsChartWindow.qml +++ b/views/DailyInfectionsChartWindow.qml @@ -25,7 +25,7 @@ Window { sidebarVisible: true nameFilters: [ "PNG files (*.png)" ] onAccepted: { - var FILE_PREFIX = "file:///" + var FILE_PREFIX = "file://" let path = fileUrl.toString() if (path.startsWith(FILE_PREFIX)) { path = path.substr(FILE_PREFIX.length, path.length) diff --git a/views/MainWindow.qml b/views/MainWindow.qml index 4143ff8..2302ad9 100644 --- a/views/MainWindow.qml +++ b/views/MainWindow.qml @@ -216,11 +216,12 @@ ApplicationWindow { } } Menu { - title: "Postprocessing" + title: "&Postprocessing" Action { - text: "Daily Infections Chart" + text: "&Daily Infections Chart" enabled: projectHandler.isDailyInfectionsDataAvailable onTriggered: dailyInfectionsChartWindow.visible = true + shortcut: "Ctrl+D" } } } From 1a072854f8ecd1af5bed97e0fe2f45277a5222db Mon Sep 17 00:00:00 2001 From: Kamil Galant Date: Mon, 19 Oct 2020 11:57:41 +0200 Subject: [PATCH 10/10] adjusted class name to camel case --- model/ProjectHandler.py | 2 +- model/charts.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/model/ProjectHandler.py b/model/ProjectHandler.py index 9c75ed2..013a4eb 100644 --- a/model/ProjectHandler.py +++ b/model/ProjectHandler.py @@ -139,7 +139,7 @@ def __init__(self): self._modulationModel = FunctionParametersModel() self._applicationSettings = ApplicationSettings(lambda: self.workdir()) self._simulationRunner = SimulationRunner(lambda: self.workdir()) - self._chart_preparer = charts.daily_infections_series_preparer( + self._chart_preparer = charts.DailyInfectionsSeriesPreparer( lambda: self._applicationSettings.abs_path( ApplicationSettings.PropertyNames.OUTPUT_DAILY)) self.connect_signals() diff --git a/model/charts.py b/model/charts.py index 6585948..7930623 100644 --- a/model/charts.py +++ b/model/charts.py @@ -5,7 +5,7 @@ import time -class daily_infections_series_preparer(QObject): +class DailyInfectionsSeriesPreparer(QObject): preparing_begin = pyqtSignal(str, arguments=['filename']) preparing_done = pyqtSignal() series_prepared = pyqtSignal(str, list, arguments=['name', 'series'])