From f25cf11972a4387c1265186163322cc83f26cf34 Mon Sep 17 00:00:00 2001 From: Felix Oesterle Date: Thu, 20 Mar 2025 12:41:01 +0100 Subject: [PATCH 1/2] feat(com9): add `com9MoTVoellmy` algorithm and related functionalities feat(com8): Add com8MoTPSA algorithm (addInitialcom8) refactor(com8): Update parameter descriptions and remove unused friction size option - Introduced `runCom9MoTVoellmy_algorithm` for dense flow simulations. - Added support for processing DEM, release layers, entrainment, resistance, and secondary release files. - Integrated the new algorithm into `pb_tool.cfg` and `avaframeConnector_provider.py`. feat(com9) Mock up input data for DI does not work feat(com9): enhance `com9MoTVoellmy` algorithm with raster support - Added `copyRaster` to handle raster copying with suffixes. - Updated parameter names and descriptions for clarity (e.g., `mu`, `k`, `b0`). - Added support for raster release layers and handling of raster input parameters (`mu`, `k`, `b0`, `tau_c`). - Improved error handling to ensure either shapefile or raster release layers are provided, not both. fix(com9): adjust `b0` raster handling in `com9MoTVoellmy` algorithm - Updated `copyRaster` call for `b0` to use the correct target directory and suffix. fix(com9): improve parameter handling and validation in `com9MoTVoellmy` algorithm - parameter description for readability and clarity. - Add validation to ensure `b0` and `tau_c` are both provided or omitted together. - Update algorithm version string to `NGI_Experimental`. feat(com9): add new parameter options and add validation in `com9MoTVoellmy` algorithm - Introduce new friction, entrainment, and forest parameter options. - Add validation for friction, entrainment, and forest-related parameters. - Update parameter descriptions - Refactor simulation command to handle new parameter-related configurations. --- avaframeConnector_commonFunc.py | 62 +++++ avaframeConnector_provider.py | 7 +- pb_tool.cfg | 2 + runCom8MoTPSA_algorithm.py | 293 ++++++++++++++++++++ runCom9MoTVoellmy_algorithm.py | 467 ++++++++++++++++++++++++++++++++ 5 files changed, 827 insertions(+), 4 deletions(-) create mode 100644 runCom8MoTPSA_algorithm.py create mode 100644 runCom9MoTVoellmy_algorithm.py diff --git a/avaframeConnector_commonFunc.py b/avaframeConnector_commonFunc.py index fae1dd7..68d1124 100644 --- a/avaframeConnector_commonFunc.py +++ b/avaframeConnector_commonFunc.py @@ -29,6 +29,30 @@ def copyDEM(dem, targetDir): pass +def copyRaster(raster, targetDir, suffix): + """copies raster file to targetDir with suffix added to filename + + Parameters + ----------- + raster: + qgis raster layer + targetDir: pathlib.Path + target directory + suffix: string + suffix to add to filename (e.g., "_mu", "_k") + """ + sourceRasterPath = pathlib.Path(raster.source()) + + # Add suffix before file extension + newFileName = sourceRasterPath.stem + suffix + sourceRasterPath.suffix + targetRasterPath = targetDir / newFileName + + try: + shutil.copy(sourceRasterPath, targetRasterPath) + except shutil.SameFileError: + pass + + def copyMultipleShp(sourceDict, targetPath, addToName=""): """copies multiple shapefile parts to targetPath @@ -87,6 +111,43 @@ def getSHPParts(base): return globbed +# TODO: maybe combine this with getLatestPeak +def getLatestPeakCom8(targetDir): + """Get latest peakFiles of com8MoTPSA results + + Parameters + ----------- + targetDir: pathlib path + to avalanche directory + Returns + ------- + rasterResults: dataframe + dataframe with info about simulations, including path + """ + avaDir = pathlib.Path(str(targetDir)) + inputDirPeak = avaDir / "Outputs" / "com8MoTPSA" / "peakFiles" + allRasterResults = fU.makeSimDF(inputDirPeak, avaDir=avaDir) + + return allRasterResults + +def getLatestPeakCom9(targetDir): + """Get latest peakFiles of com9MoTVoellmy results + + Parameters + ----------- + targetDir: pathlib path + to avalanche directory + Returns + ------- + rasterResults: dataframe + dataframe with info about simulations, including path + """ + avaDir = pathlib.Path(str(targetDir)) + inputDirPeak = avaDir / "Outputs" / "com9MoTVoellmy" / "peakFiles" + allRasterResults = fU.makeSimDF(inputDirPeak, avaDir=avaDir) + + return allRasterResults + def getLatestPeak(targetDir): """Get latest peakFiles of com1DFA results @@ -259,6 +320,7 @@ def addStyleToCom1DFAResults(rasterResults): qmls = dict() qmls["ppr"] = str(scriptDir / "QGisStyles" / "ppr.qml") qmls["pft"] = str(scriptDir / "QGisStyles" / "pft.qml") + qmls["pfd"] = str(scriptDir / "QGisStyles" / "pft.qml") qmls["pfv"] = str(scriptDir / "QGisStyles" / "pfv.qml") qmls["PR"] = str(scriptDir / "QGisStyles" / "ppr.qml") qmls["FV"] = str(scriptDir / "QGisStyles" / "pfv.qml") diff --git a/avaframeConnector_provider.py b/avaframeConnector_provider.py index 69bb771..5472d02 100644 --- a/avaframeConnector_provider.py +++ b/avaframeConnector_provider.py @@ -38,10 +38,6 @@ from qgis.core import QgsProcessingProvider from qgis.PyQt.QtGui import QIcon -from qgis.core import ( - QgsMessageLog, - QgsGeometry, -) from qgis.PyQt.QtWidgets import ( QMessageBox, @@ -91,6 +87,7 @@ def find_python(): from .getVersion_algorithm import getVersionAlgorithm from .runCom1DFA_algorithm import runCom1DFAAlgorithm from .runCom2AB_algorithm import runCom2ABAlgorithm +from .runCom9MoTVoellmy_algorithm import runCom9MoTVoellmyAlgorithm from .runAna4ProbAna_algorithm import runAna4ProbAnaAlgorithm from .runAna4ProbDirOnly_algorithm import runAna4ProbDirOnlyAlgorithm from .runAna5DFAPathGeneration_algorithm import runAna5DFAPathGenerationAlgorithm @@ -132,6 +129,8 @@ def loadAlgorithms(self): self.addAlgorithm(runCom5SnowSlideAlgorithm()) self.addAlgorithm(runCom6RockAvalancheAlgorithm()) self.addAlgorithm(runCom6ScarpAlgorithm()) + # self.addAlgorithm(runCom8MoTPSAAlgorithm()) + self.addAlgorithm(runCom9MoTVoellmyAlgorithm()) self.addAlgorithm(runCom7RegionalSplittingAlgorithm()) self.addAlgorithm(runCom7RegionalComputationAlgorithm()) self.addAlgorithm(runAna4ProbAnaAlgorithm()) diff --git a/pb_tool.cfg b/pb_tool.cfg index 59939d2..cd28888 100644 --- a/pb_tool.cfg +++ b/pb_tool.cfg @@ -57,6 +57,8 @@ python_files: __init__.py avaframeConnector.py avaframeConnector_provider.py runCom2AB_algorithm.py runCom5SnowSlide_algorithm.py runCom6RockAvalanche_algorithm.py + runCom8MoTPSA_algorithm.py + runCom9MoTVoellmy_algorithm.py runCom6Scarp_algorithm.py runAna4ProbAna_algorithm.py runAna4ProbDirOnly_algorithm.py diff --git a/runCom8MoTPSA_algorithm.py b/runCom8MoTPSA_algorithm.py new file mode 100644 index 0000000..2f36458 --- /dev/null +++ b/runCom8MoTPSA_algorithm.py @@ -0,0 +1,293 @@ +# -*- coding: utf-8 -*- + +""" +/*************************************************************************** + AvaFrameRunCom1DFA + A QGIS plugin + Connects to AvaFrame + Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/ + ------------------- + begin : 2021-08-26 + copyright : (C) 2021 by AvaFrame Team + email : felix@avaframe.org + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +""" + +__author__ = 'AvaFrame Team' +__date__ = '2022' +__copyright__ = '(C) 2022 by AvaFrame Team' + +# This will get replaced with a git SHA1 when you do a git archive + +__revision__ = '$Format:%H$' + +import pathlib +import subprocess +import shutil +from pathlib import Path + +from qgis.PyQt.QtCore import QCoreApplication +from qgis.core import (QgsProcessing, + QgsRasterLayer, + QgsProcessingException, + QgsProcessingAlgorithm, + QgsProcessingContext, + QgsProcessingParameterFeatureSource, + QgsProcessingParameterRasterLayer, + QgsProcessingParameterEnum, + QgsProcessingParameterMultipleLayers, + QgsProcessingParameterFolderDestination, + QgsProcessingOutputVectorLayer, + QgsProcessingParameterDefinition, + QgsProcessingOutputMultipleLayers) + + +class runCom8MoTPSAAlgorithm(QgsProcessingAlgorithm): + """ + This is the AvaFrame Connection, i.e. the part running with QGis. For this + connector to work, more installation is needed. See instructions at docs.avaframe.org + """ + + DEM = 'DEM' + REL = 'REL' + RELTH = 'RELTH' + SECREL = 'SECREL' + ENT = 'ENT' + RES = 'RES' + FRICTSIZE = 'FRICTSIZE' + OUTPUT = 'OUTPUT' + OUTPPR = 'OUTPPR' + FOLDEST = 'FOLDEST' + ADDTONAME = "ADDTONAME" + SMALLAVA = 'SMALLAVA' + DATA_TYPE = 'DATA_TYPE' + + def initAlgorithm(self, config): + """ + Here we define the inputs and output of the algorithm, along + with some other properties. + """ + + self.addParameter(QgsProcessingParameterRasterLayer( + self.DEM, + self.tr("DEM layer"))) + + self.addParameter(QgsProcessingParameterMultipleLayers( + self.REL, + self.tr('Release layer(s)'), + layerType=QgsProcessing.TypeVectorAnyGeometry + )) + + # self.addParameter(QgsProcessingParameterRasterLayer( + # self.RELTH, + # self.tr("Release thickness layer"))) + + # self.addParameter(QgsProcessingParameterString( + # self.ADDTONAME, + # self.tr('Add to simName (default empty)'), + # '', + # optional=True, + # )) + + # self.addParameter(QgsProcessingParameterMultipleLayers( + self.addParameter(QgsProcessingParameterFeatureSource( + self.SECREL, + self.tr('Bed shear strength'), + optional=True, + defaultValue="", + types=[QgsProcessing.TypeVectorAnyGeometry] + )) + + self.addParameter(QgsProcessingParameterFeatureSource( + self.ENT, + self.tr('Entrainment layer (only one is allowed)'), + optional=True, + defaultValue="", + types=[QgsProcessing.TypeVectorAnyGeometry] + )) + + self.addParameter(QgsProcessingParameterFeatureSource( + self.RES, + self.tr('Resistance layer (only one is allowed)'), + optional=True, + defaultValue="", + types=[QgsProcessing.TypeVectorAnyGeometry] + )) + + # dataType_param = QgsProcessingParameterEnum(self.DATA_TYPE, + # self.tr('Output data type'), + # allowMultiple=False, + # defaultValue=0) + # dataType_param.setFlags(dataType_param.flags() | QgsProcessingParameterDefinition.FlagAdvanced) + # self.addParameter(dataType_param) + + self.addParameter(QgsProcessingParameterFolderDestination( + self.FOLDEST, + self.tr('Destination folder') + )) + + self.addOutput(QgsProcessingOutputVectorLayer( + self.OUTPUT, + self.tr("Output layer"), + QgsProcessing.TypeVectorAnyGeometry)) + + self.addOutput(QgsProcessingOutputMultipleLayers( + self.OUTPPR, + )) + + def flags(self): + return super().flags() + # return super().flags() | QgsProcessingAlgorithm.FlagNoThreading + + def processAlgorithm(self, parameters, context, feedback): + """ + Here is where the processing itself takes place. + """ + + import avaframe.version as gv + from . import avaframeConnector_commonFunc as cF + + feedback.pushInfo('AvaFrame Version: ' + gv.getVersion()) + + # targetADDTONAME = self.parameterAsString(parameters, self.ADDTONAME, context) + targetADDTONAME = '' + + sourceDEM = self.parameterAsRasterLayer(parameters, self.DEM, context) + if sourceDEM is None: + raise QgsProcessingException(self.invalidSourceError(parameters, self.DEM)) + + # Release files + allREL = self.parameterAsLayerList(parameters, self.REL, context) + if allREL is None: + raise QgsProcessingException(self.invalidSourceError(parameters, self.REL)) + + relDict = {} + if allREL: + relDict = {lyr.source(): lyr for lyr in allREL} + + # Secondary release files + sourceSecREL = self.parameterAsVectorLayer(parameters, self.SECREL, context) + if sourceSecREL is not None: + srInfo = '_sec' + Path(sourceSecREL.source()).stem + targetADDTONAME = targetADDTONAME + srInfo + + sourceENT = self.parameterAsVectorLayer(parameters, self.ENT, context) + + sourceRES = self.parameterAsVectorLayer(parameters, self.RES, context) + + sourceFOLDEST = self.parameterAsFile(parameters, self.FOLDEST, context) + + # create folder structure (targetDir is the tmp one) + finalTargetDir, targetDir = cF.createFolderStructure(sourceFOLDEST) + + feedback.pushInfo(sourceDEM.source()) + + # copy DEM + cF.copyDEM(sourceDEM, targetDir) + + # copy all release shapefile parts + cF.copyMultipleShp(relDict, targetDir / 'Inputs' / 'REL', targetADDTONAME) + + # copy all secondary release shapefile parts + if sourceSecREL is not None: + cF.copyShp(sourceSecREL.source(), targetDir / 'Inputs' / 'SECREL') + + # copy all entrainment shapefile parts + if sourceENT is not None: + cF.copyShp(sourceENT.source(), targetDir / 'Inputs' / 'ENT') + + # copy all resistance shapefile parts + if sourceRES is not None: + cF.copyShp(sourceRES.source(), targetDir / 'Inputs' / 'RES') + + feedback.pushInfo('Starting the simulations') + feedback.pushInfo('This might take a while') + feedback.pushInfo('See console for progress') + + # Generate command and run via subprocess.run + command = ['python', '-m', 'avaframe.com8MoTPSA.runCom8MoTPSA', str(targetDir)] + cF.runAndCheck(command, self, feedback) + + feedback.pushInfo('Done, start loading the results') + + # Move input, log and output folders to finalTargetDir + cF.moveInputAndOutputFoldersToFinal(targetDir, finalTargetDir) + + # Get peakfiles to return to QGIS + try: + rasterResults = cF.getLatestPeakCom8(finalTargetDir) + except: + raise QgsProcessingException(self.tr('Something went wrong with com8MoTPSA, please check log files')) + + allRasterLayers = cF.addStyleToCom1DFAResults(rasterResults) + + context = cF.addLayersToContext(context, allRasterLayers, self.OUTPPR) + + feedback.pushInfo('\n---------------------------------') + feedback.pushInfo('Done, find results and logs here:') + feedback.pushInfo(str(finalTargetDir.resolve())) + feedback.pushInfo('---------------------------------\n') + + return {self.OUTPPR: allRasterLayers} + + def name(self): + """ + Returns the algorithm name, used for identifying the algorithm. This + string should be fixed for the algorithm, and must not be localised. + The name should be unique within each provider. Names should contain + lowercase alphanumeric characters only and no spaces or other + formatting characters. + """ + return 'com8powder' + + def displayName(self): + """ + Returns the translated algorithm name, which should be used for any + user-visible display of the algorithm name. + """ + return self.tr('PSA (com8)') + + def group(self): + """ + Returns the name of the group this algorithm belongs to. This string + should be localised. + """ + return self.tr(self.groupId()) + + def groupId(self): + """ + Returns the unique ID of the group this algorithm belongs to. This + string should be fixed for the algorithm, and must not be localised. + The group id should be unique within each provider. Group id should + contain lowercase alphanumeric characters only and no spaces or other + formatting characters. + """ + return 'Experimental' + + def tr(self, string): + return QCoreApplication.translate('Processing', string) + + def shortHelpString(self) -> str: + hstring = 'Runs powder snow simulations via module com8MoTPSA. \n\ + For more information go to (or use the help button below): \n\ + AvaFrame Documentation: https://docs.avaframe.org\n\ + Homepage: https://avaframe.org\n\ + Praxisleitfaden: https://avaframe.org/reports\n' + + return self.tr(hstring) + # Praxisleitfaden: https://info.bml.gv.at/dam/jcr:edebd872-2a86-4edf-ac5e-635ef11e35fe/Praxisleitfaden%20LawSim%20WLV%202022%20Gr%C3%BCn.pdf\n' + + def helpUrl(self): + return "https://docs.avaframe.org/en/latest/connector.html" + + def createInstance(self): + return runCom8MoTPSAAlgorithm() diff --git a/runCom9MoTVoellmy_algorithm.py b/runCom9MoTVoellmy_algorithm.py new file mode 100644 index 0000000..5820416 --- /dev/null +++ b/runCom9MoTVoellmy_algorithm.py @@ -0,0 +1,467 @@ +# -*- coding: utf-8 -*- + +""" +/*************************************************************************** + AvaFrameRunCom1DFA + A QGIS plugin + Connects to AvaFrame + Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/ + ------------------- + begin : 2021-08-26 + copyright : (C) 2021 by AvaFrame Team + email : felix@avaframe.org + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +""" + +__author__ = "AvaFrame Team" +__date__ = "2022" +__copyright__ = "(C) 2022 by AvaFrame Team" + +# This will get replaced with a git SHA1 when you do a git archive + +__revision__ = "$Format:%H$" + + +from qgis.PyQt.QtCore import QCoreApplication +from qgis.core import ( + QgsProcessing, + QgsProcessingException, + QgsProcessingAlgorithm, + QgsProcessingParameterRasterLayer, + QgsProcessingParameterMultipleLayers, + QgsProcessingParameterFolderDestination, + QgsProcessingParameterEnum, + QgsProcessingOutputVectorLayer, + QgsProcessingOutputMultipleLayers, +) + + +class runCom9MoTVoellmyAlgorithm(QgsProcessingAlgorithm): + """ + This is the AvaFrame Connection, i.e. the part running with QGis. For this + connector to work, more installation is needed. See instructions at docs.avaframe.org + """ + + DEM = "DEM" + RELSHP = "RELSHP" + RELRAS = "RELRAS" + FRICTION = "FRICTION" + MU = "MU" + K = "K" + ENTRAINMENT = "ENTRAINMENT" + B0 = "B0" + TAUC = "TAUC" + FOREST = "FOREST" + ND = "ND" + BHD = "BHD" + OUTPUT = "OUTPUT" + OUTPPR = "OUTPPR" + FOLDEST = "FOLDEST" + + ADDTONAME = "ADDTONAME" + SMALLAVA = "SMALLAVA" + DATA_TYPE = "DATA_TYPE" + + def initAlgorithm(self, config): + """ + Here we define the inputs and output of the algorithm, along + with some other properties. + """ + + self.addParameter( + QgsProcessingParameterRasterLayer(self.DEM, self.tr("DEM layer (.asc raster)")) + ) + + self.addParameter( + QgsProcessingParameterMultipleLayers( + self.RELSHP, + self.tr( + "EITHER Release layer(s) as shape (with thickness attribute for release height)" + ), + layerType=QgsProcessing.TypeVectorAnyGeometry, + optional=True, + defaultValue="", + ) + ) + + self.addParameter( + QgsProcessingParameterMultipleLayers( + self.RELRAS, + self.tr("OR Release layer(s) as .asc raster"), + layerType=QgsProcessing.TypeRaster, + optional=True, + defaultValue="", + ) + ) + + self.addParameter( + QgsProcessingParameterEnum( + self.FRICTION, + self.tr("-----------Friction-----------"), + options=[ + self.tr("Constant"), + self.tr("Variable (needs mu and k)"), + ], + defaultValue=0, + allowMultiple=False, + ) + ) + + self.addParameter( + QgsProcessingParameterRasterLayer( + self.MU, + self.tr("mu / Dry friction coeff (.asc raster)"), + optional=True, + defaultValue="", + ) + ) + + self.addParameter( + QgsProcessingParameterRasterLayer( + self.K, + self.tr("k / Turbulent friction coeff (.asc raster)"), + optional=True, + defaultValue="", + ) + ) + + self.addParameter( + QgsProcessingParameterEnum( + self.ENTRAINMENT, + self.tr("---------Entrainment----------"), + options=[ + self.tr("No Entrainment"), + self.tr("TJEM (needs b0 and tau_c)"), + ], + defaultValue=0, + allowMultiple=False, + ) + ) + + self.addParameter( + QgsProcessingParameterRasterLayer( + self.B0, + self.tr("b0 / Erodible snow depth (.asc raster)"), + optional=True, + defaultValue="", + ) + ) + + self.addParameter( + QgsProcessingParameterRasterLayer( + self.TAUC, + self.tr("tau_c / Snow shear strength (.asc raster)"), + optional=True, + defaultValue="", + ) + ) + + self.addParameter( + QgsProcessingParameterEnum( + self.FOREST, + self.tr("------------Forest------------"), + options=[ + self.tr("No Forest"), + self.tr("Forest (needs nd and bhd)"), + ], + defaultValue=0, + allowMultiple=False, + ) + ) + + self.addParameter( + QgsProcessingParameterRasterLayer( + self.ND, + self.tr("nd / Forest density (.asc raster)"), + optional=True, + defaultValue="", + ) + ) + + self.addParameter( + QgsProcessingParameterRasterLayer( + self.BHD, + self.tr("bhd / Tree diameter (.asc raster)"), + optional=True, + defaultValue="", + ) + ) + + self.addParameter( + QgsProcessingParameterFolderDestination( + self.FOLDEST, self.tr("------Destination folder (Should be empty)------") + ) + ) + + self.addOutput( + QgsProcessingOutputVectorLayer( + self.OUTPUT, + self.tr("Output layer"), + QgsProcessing.TypeVectorAnyGeometry, + ) + ) + + self.addOutput( + QgsProcessingOutputMultipleLayers( + self.OUTPPR, + ) + ) + + def flags(self): + return super().flags() + # return super().flags() | QgsProcessingAlgorithm.FlagNoThreading + + def processAlgorithm(self, parameters, context, feedback): + """ + Here is where the processing itself takes place. + """ + + import avaframe.version as gv + from . import avaframeConnector_commonFunc as cF + + feedback.pushInfo("AvaFrame Version: " + gv.getVersion()) + + targetADDTONAME = "" + + sourceDEM = self.parameterAsRasterLayer(parameters, self.DEM, context) + if sourceDEM is None: + raise QgsProcessingException(self.invalidSourceError(parameters, self.DEM)) + + # Release files - check both shapefile and raster options + allRELSHP = self.parameterAsLayerList(parameters, self.RELSHP, context) + allRELRAS = self.parameterAsLayerList(parameters, self.RELRAS, context) + + # Check that only one release type is provided + if allRELSHP and allRELRAS: + raise QgsProcessingException( + self.tr( + "Error: Please provide EITHER release layers as shapefile OR as raster, not both" + ) + ) + + relShpDict = {} + if allRELSHP: + relShpDict = {lyr.source(): lyr for lyr in allRELSHP} + + relRasDict = {} + if allRELRAS: + relRasDict = {lyr.source(): lyr for lyr in allRELRAS} + + # Get friction option + frictionOption = self.parameterAsInt(parameters, self.FRICTION, context) + frictionOptions = ["constant", "variable"] + frictionString = frictionOptions[frictionOption] + + # Extract raster parameters + sourceMU = self.parameterAsRasterLayer(parameters, self.MU, context) + sourceK = self.parameterAsRasterLayer(parameters, self.K, context) + + # Validate friction-related parameters + if frictionString == "variable": + # Variable friction requires MU and K + if sourceMU is None: + raise QgsProcessingException( + self.tr("Error: Variable friction requires mu (dry friction coeff) to be provided") + ) + if sourceK is None: + raise QgsProcessingException( + self.tr("Error: Variable friction requires k (turbulent friction coeff) to be provided") + ) + + # Get entrainment option + entrainmentOption = self.parameterAsInt(parameters, self.ENTRAINMENT, context) + entrainmentOptions = ["noEntrainment", "tjem"] + entrainmentString = entrainmentOptions[entrainmentOption] + + sourceB0 = self.parameterAsRasterLayer(parameters, self.B0, context) + sourceTAUC = self.parameterAsRasterLayer(parameters, self.TAUC, context) + + # Validate entrainment-related parameters + hasEntrainment = 0 + if entrainmentString == "tjem": + # TJEM requires K and B0 + if sourceTAUC is None: + raise QgsProcessingException( + self.tr("Error: TJEM entrainment requires tau_c (snow shear strength ) to be provided") + ) + if sourceB0 is None: + raise QgsProcessingException( + self.tr("Error: TJEM entrainment requires b0 (erodible snow depth) to be provided") + ) + hasEntrainment = 1 + + # Get forest option + forestOption = self.parameterAsInt(parameters, self.FOREST, context) + forestOptions = ["noForest", "forest"] + forestString = forestOptions[forestOption] + + sourceND = self.parameterAsRasterLayer(parameters, self.ND, context) + sourceBHD = self.parameterAsRasterLayer(parameters, self.BHD, context) + + # Validate forest-related parameters + hasForest = 0 + if forestString == "forest": + # Forest requires ND and BHD + if sourceND is None: + raise QgsProcessingException( + self.tr("Error: Forest requires nd (forest density) to be provided") + ) + if sourceBHD is None: + raise QgsProcessingException( + self.tr("Error: Forest requires bhd (tree diameter) to be provided") + ) + hasForest = 1 + + sourceFOLDEST = self.parameterAsFile(parameters, self.FOLDEST, context) + + # create folder structure (targetDir is the tmp one) + finalTargetDir, targetDir = cF.createFolderStructure(sourceFOLDEST) + + feedback.pushInfo(sourceDEM.source()) + + # copy DEM + cF.copyDEM(sourceDEM, targetDir) + + # copy all release shapefile parts + if relShpDict: + cF.copyMultipleShp( + relShpDict, targetDir / "Inputs" / "REL", targetADDTONAME + ) + + # copy all release raster parts + if relRasDict: + for source in relRasDict: + cF.copyRaster(relRasDict[source], targetDir / "Inputs" / "REL", "") + + # copy raster parameter files to RASTERS folder with suffixes + if sourceMU is not None: + cF.copyRaster(sourceMU, targetDir / "Inputs" / "RASTERS", "_mu") + + if sourceK is not None: + cF.copyRaster(sourceK, targetDir / "Inputs" / "RASTERS", "_k") + + if sourceB0 is not None: + cF.copyRaster(sourceB0, targetDir / "Inputs" / "ENT", "") + + if sourceTAUC is not None: + cF.copyRaster(sourceTAUC, targetDir / "Inputs" / "RASTERS", "_tauc") + + if sourceND is not None: + cF.copyRaster(sourceND, targetDir / "Inputs" / "RASTERS", "_nd") + + if sourceBHD is not None: + cF.copyRaster(sourceBHD, targetDir / "Inputs" / "RASTERS", "_bhd") + + feedback.pushInfo("Starting the simulations") + feedback.pushInfo("This might take a while") + feedback.pushInfo("See console for progress") + + # Determine -st argument based on FOREST and ENTRAINMENT parameters + if hasForest and hasEntrainment: + stValue = "entres" + elif hasForest: + stValue = "res" + elif hasEntrainment: + stValue = "ent" + else: + stValue = "null" + + # Generate command and run via subprocess.run + command = [ + "python", + "-m", + "avaframe.com9MoTVoellmy.runCom9MoTVoellmy", + str(targetDir), + "-st", + stValue, + ] + cF.runAndCheck(command, self, feedback) + + feedback.pushInfo("Done, start loading the results") + + # Move input, log and output folders to finalTargetDir + cF.moveInputAndOutputFoldersToFinal(targetDir, finalTargetDir) + + # Get peakfiles to return to QGIS + try: + rasterResults = cF.getLatestPeakCom9(finalTargetDir) + except: + raise QgsProcessingException( + self.tr( + "Something went wrong with com9MoTVoellmy, please check log files" + ) + ) + + allRasterLayers = cF.addStyleToCom1DFAResults(rasterResults) + + context = cF.addLayersToContext(context, allRasterLayers, self.OUTPPR) + + feedback.pushInfo("\n---------------------------------") + feedback.pushInfo("Done, find results and logs here:") + feedback.pushInfo(str(finalTargetDir.resolve())) + feedback.pushInfo("---------------------------------\n") + + return {self.OUTPPR: allRasterLayers} + + def name(self): + """ + Returns the algorithm name, used for identifying the algorithm. This + string should be fixed for the algorithm, and must not be localised. + The name should be unique within each provider. Names should contain + lowercase alphanumeric characters only and no spaces or other + formatting characters. + """ + return "com9motvoellmy" + + def displayName(self): + """ + Returns the translated algorithm name, which should be used for any + user-visible display of the algorithm name. + """ + return self.tr("MoTVoellmy (com9)") + + def group(self): + """ + Returns the name of the group this algorithm belongs to. This string + should be localised. + """ + return self.tr(self.groupId()) + + def groupId(self): + """ + Returns the unique ID of the group this algorithm belongs to. This + string should be fixed for the algorithm, and must not be localised. + The group id should be unique within each provider. Group id should + contain lowercase alphanumeric characters only and no spaces or other + formatting characters. + """ + return "NGI_Experimental" + + def tr(self, string): + return QCoreApplication.translate("Processing", string) + + def shortHelpString(self) -> str: + hstring = "Runs dense flow simulations via module com9MoTVoellmy. \n\ + All raster files need to have the same extend as the DEM. \n\ + For more information go to (or use the help button below): \n\ + AvaFrame Documentation: https://docs.avaframe.org\n\ + Homepage: https://avaframe.org\n\ + Praxisleitfaden: https://avaframe.org/reports\n" + + return self.tr(hstring) + # Praxisleitfaden: https://info.bml.gv.at/dam/jcr:edebd872-2a86-4edf-ac5e-635ef11e35fe/Praxisleitfaden%20LawSim%20WLV%202022%20Gr%C3%BCn.pdf\n' + + def helpUrl(self): + return "https://docs.avaframe.org/en/latest/connector.html" + + def createInstance(self): + return runCom9MoTVoellmyAlgorithm() From bd1907427cea713a46de0f1d7cb13fada464947c Mon Sep 17 00:00:00 2001 From: Felix Oesterle <6945681+fso42@users.noreply.github.com> Date: Tue, 21 Oct 2025 14:18:13 +0200 Subject: [PATCH 2/2] chore(metadata): bump version to 1.14 and update changelog --- metadata.txt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/metadata.txt b/metadata.txt index 27dfaa3..ec6274c 100644 --- a/metadata.txt +++ b/metadata.txt @@ -6,7 +6,7 @@ name=AvaFrameConnector qgisMinimumVersion=3.22 description=Connects to AvaFrame -version=1.13 +version=1.14 author=AvaFrame Team email=felix@avaframe.org @@ -24,7 +24,8 @@ repository=https://github.com/OpenNHM/QGisAF hasProcessingProvider=yes # Uncomment the following line and add your changelog: -changelog= 1.13 Add Scarp (com6) analysis tool +changelog= 1.14 Add experimental MoT Voellmy (com9); com7 Regional + 1.13 Add Scarp (com6) analysis tool 1.12.1 Remove numpy update on installation 1.12 On Release area statics run, open parent folder on completion 1.10 Add path generation tool, accept geotiff as raster input