From aefc2ce7bc65615db338a1e5dc03b0c275bad5ba Mon Sep 17 00:00:00 2001 From: MunsMan Date: Fri, 14 Aug 2026 09:56:08 +0200 Subject: [PATCH] check input parameters to fixing issue #1327 - improved roved input parameter/data checks for com4FlowPy - fix(com4FlowPy): defining default as fallback - add(com4FlowPy) raster value validation - refactor array validation - add (test) array validation - fixing pra detection - moving limit into cfg - updating to python conventions - releasePath to releasePathWork - fixes pytest issue with .shp input re-structuring input paramter check and raising ValueError instead of sys.exit(1) f strings in check function consistently import suggestions Paula for added com4FlowPy pytest expose valid parameter limits in .ini minor formatting fix add tests --- avaframe/com4FlowPy/com4FlowPy.py | 283 +++++++++++++++---- avaframe/com4FlowPy/com4FlowPyCfg.ini | 30 +++ avaframe/tests/test_com4FlowPy.py | 374 +++++++++++++++++++++++++- 3 files changed, 633 insertions(+), 54 deletions(-) diff --git a/avaframe/com4FlowPy/com4FlowPy.py b/avaframe/com4FlowPy/com4FlowPy.py index 42b088331..a572b9f24 100755 --- a/avaframe/com4FlowPy/com4FlowPy.py +++ b/avaframe/com4FlowPy/com4FlowPy.py @@ -204,7 +204,8 @@ def com4FlowPyMain(cfgPath, cfgSetup): checkInputLayerDimensions(modelParameters, modelPaths) # check if input parameters are within physically sensible ranges - checkInputParameterValues(modelParameters, modelPaths) + validParamRanges = getValidParameterRanges(cfgSetup) + checkInputParameterValues(modelParameters, modelPaths, validParamRanges) # get information on cellsize and nodata value from demHeader rasterAttributes = {} @@ -390,9 +391,8 @@ def checkInputLayerDimensions(modelParameters, modelPaths): # return sys.exit(1) - -def checkInputParameterValues(modelParameters, modelPaths): - """check if the input parameters +def checkInputParameterValues(modelParameters, modelPaths, validParamRanges): + """check if the input parameters are valid are valid and within physically sensible limits alpha, uMaxLimit/ zDeltaMaxLimit, exponent are within a physically sensible range @@ -404,7 +404,9 @@ def checkInputParameterValues(modelParameters, modelPaths): modelPaths: dict contains paths to input files """ - + + log.info("checking input data validity") + # check if engine parameter provided in the .ini matches available options engine = modelParameters["engine"] validEngines = {"python","numba"} @@ -413,57 +415,35 @@ def checkInputParameterValues(modelParameters, modelPaths): f"Invalid engine '{engine}'. " f"Valid engine options are {sorted(validEngines)}" ) + + # checking value ranges of (global) input parameters + # alpha, max_z, exp, flux_threshold + checkGlobalParameters(modelParameters, modelPaths, validParamRanges) + # check spatially varying input parameters for validity and completeness + checkVariableInputParameters(modelParameters, modelPaths, validParamRanges) - alpha = modelParameters["alpha"] - if alpha < 0 or alpha > 90: - log.error("Error: Alpha value is not within a physically sensible range ([0,90]).") - sys.exit(1) - - zDelta = modelParameters["max_z"] - if zDelta < 0 or zDelta > 8848: - log.error("Error: zDeltaMaxLimit value is not within a physically sensible range ([0,8848]).") - sys.exit(1) - - exp = modelParameters["exp"] - if exp < 0: - log.error("Error: Exponent value is not within a physically sensible range (> 0).") - sys.exit(1) - - _checkVarParams = True - if modelParameters["varAlphaBool"]: - data = IOf.readRaster(modelPaths["varAlphaPath"]) - rasterValues = data["rasterData"] - rasterValues[rasterValues < 0] = np.nan # handle different noData values - if np.any(rasterValues > 90, where=~np.isnan(rasterValues)): - log.error( - "Error: Not all Alpha-raster values are within a physically sensible range ([0,90]),\ - in respective startcells the general alpha angle is used." - ) - _checkVarParams = False - - if modelParameters["varUmaxBool"]: - data = IOf.readRaster(modelPaths["varUmaxPath"]) - rasterValues = data["rasterData"] - rasterValues[rasterValues < 0] = np.nan - if modelParameters["varUmaxType"].lower() == "umax": - _maxVal = 1500 # ~sqrt(8848*2*9.81) - else: - _maxVal = 8848 - if np.any(rasterValues > _maxVal, where=~np.isnan(rasterValues)): - log.error( - "Error: Not all zDeltaMaxLimit-raster values are within a physically sensible range \ - ([0, 8848 m] or [0, 1500 m/s]), in respective startcells the general zDeltaMax value is used." - ) - _checkVarParams = False +def validateInputArray(praMask, data, maxValue, minValue=0): + """validates the input array, by checking the required valued based on the provided pra + + Parameters + ----------- + praMask: np.ndarray + pra input array + data: np.ndarray + data array which will be tested + maxValue: int | float + max value, for range validation of data entires + minValue: int | float, optional + min value, for range validation of data entires, by default 0 - if _checkVarParams: - log.info("All input parameters are within a physically sensible range.") - log.info("========================") - else: - log.info("NOT ALL variable input parameter rasters are within physically sensible ranges.") - log.info("!!PLEASE RE-CHECK Input Rasters and handle Results with Care!!") - log.info("========================") + Returns + -------- + bool + if input is valid + """ + dataMask = data[praMask] + return (dataMask.min() >= minValue) and (dataMask.max() <= maxValue) def tileInputLayers(modelParameters, modelPaths, rasterAttributes, tilingParameters): @@ -933,3 +913,200 @@ def defineNotAffectedCells(raster, affectedCells, noDataValue=-9999): """ raster[affectedCells <= 0] = noDataValue return raster + +def getValidParameterRanges(cfgSetup): + """ + read valid parameter ranges from cfgSetup + and provide default fallbacks if they are missing. + + Parameters + ----------- + cfgSetup: configparser.SectionProxy Object + "GENERAL" model configs (from .ini file) + + Returns + ----------- + validParamRanges: dict + dictionary with the parameter limits used by the input parameter + checker functions + """ + validParamRanges = {} + + # get valid data ranges from cfg - if they are not provided use default fallbacks + validParamRanges['minAlphaValid'] = cfgSetup.getfloat("minAlphaValid", 0) + validParamRanges['maxAlphaValid'] = cfgSetup.getfloat("maxAlphaValid", 90) + validParamRanges['maxUMaxValid'] = cfgSetup.getfloat("maxUMaxValid", 630) + validParamRanges['maxZDeltaValid'] = cfgSetup.getfloat("maxZDeltaValid", 20000) + validParamRanges['maxExpValid'] = cfgSetup.getfloat("maxExpValid", 600) + validParamRanges['minFluxThreshValid'] = cfgSetup.getfloat("minFluxThreshValid", 1e-6) + validParamRanges['maxFluxThreshValid'] = cfgSetup.getfloat("maxFluxThreshValid", 1) + + log.info("retreiving valid parameter ranges:") + log.info(24*"-") + for key, val in validParamRanges.items(): + log.info(f"{key}: {val}") + log.info("========================") + + return validParamRanges + +def checkGlobalParameters(modelParameters, modelPaths, validParamRanges): + """ + checks global input parameters for valid input ranges + + Parameters + ----------- + modelParameters: dict + model input parameters (from .ini - file) + modelPaths: dict + contains paths to input files + validParameterRanges: dict + contains valid parameter ranges to be checked against + + Returns + ----------- + raises a ValueError if any check is unsuccessful + """ + + minAlphaValid = validParamRanges['minAlphaValid'] + maxAlphaValid = validParamRanges['maxAlphaValid'] + maxZDeltaValid = validParamRanges['maxZDeltaValid'] + maxExpValid = validParamRanges['maxExpValid'] + minFluxThreshValid = validParamRanges['minFluxThreshValid'] + maxFluxThreshValid = validParamRanges['maxFluxThreshValid'] + + # checking value ranges of (global) input parameters + # alpha, max_z, exp, flux_threshold + alpha = modelParameters["alpha"] + if alpha < minAlphaValid or alpha > maxAlphaValid: + log.error( + f"Error: Alpha value is not within a physically sensible range ([{minAlphaValid}, {maxAlphaValid}])." + ) + raise ValueError( + f"Invalid (global) model parameter 'alpha' provided: {alpha}\n" + f"please provide values for 'alpha' between {minAlphaValid} and {maxAlphaValid}.\n" + f"HINT: if you insist on using values outside of [{minAlphaValid}, {maxAlphaValid}] you can adapt " + "the values in 'getValidParameterRanges()' in com4FlowPy/com4FlowPy.py" + ) + + zDelta = modelParameters["max_z"] + if zDelta < 0 or zDelta > maxZDeltaValid: + log.error( + f"Error: zDeltaMaxLimit value is not within a physically sensible range ([0,{maxZDeltaValid}])." + ) + raise ValueError( + f"Invalid (global) model parameter 'max_z' provided: {zDelta} meters\n" + f"please provide values for 'max_z' between 0 and {maxZDeltaValid} meters.\n" + f"HINT: if you insist on using values outside of [0, {maxZDeltaValid}] you can adapt " + "the values in 'getValidParameterRanges()' in com4FlowPy/com4FlowPy.py" + ) + + exp = modelParameters["exp"] + if exp < 0 or exp > maxExpValid: + log.error( + f"Error: Exponent value is not within a physically sensible range (> 0 and <={maxExpValid})." + ) + raise ValueError( + f"Invalid (global) model parameter 'exp' provided: ({exp})\n" + f"please provide values for 'exp' between 0 and {maxExpValid}.\n" + f"HINT: if you insist on using values outside of [>0, <={maxExpValid}] you can adapt " + "the values in 'getValidParameterRanges()' in com4FlowPy/com4FlowPy.py" + ) + + fluxTh = modelParameters["flux_threshold"] + if fluxTh < minFluxThreshValid or fluxTh > maxFluxThreshValid: + log.error( + f"Error: flux_threshold value is not within a physically sensible range ([{minFluxThreshValid}, {maxFluxThreshValid}])." + ) + raise ValueError( + f"Invalid (global) model parameter 'flux_threshold' provided: {fluxTh}\n" + f"please provide values for 'flux_threshold' between {minFluxThreshValid} and {maxFluxThreshValid}.\n" + f"HINT: if you insist on using values outside of [{minFluxThreshValid}, {maxFluxThreshValid}] you can adapt " + "the values in 'getValidParameterRanges()' in com4FlowPy/com4FlowPy.py" + ) + log.info("global input parameters are complete and within the valid range.") + + +def checkVariableInputParameters(modelParameters, modelPaths, validParamRanges): + """ + checks spatially variable input parameters for valid input ranges and completness + + Parameters + ----------- + modelParameters: dict + model input parameters (from .ini - file) + modelPaths: dict + contains paths to input files + validParameterRanges: dict + contains valid parameter ranges to be checked against + + Returns + ----------- + raises a ValueError if any check is unsuccessful + """ + minAlphaValid = validParamRanges['minAlphaValid'] + maxAlphaValid = validParamRanges['maxAlphaValid'] + maxZDeltaValid = validParamRanges['maxZDeltaValid'] + maxUMaxValid = validParamRanges['maxUMaxValid'] + maxExpValid = validParamRanges['maxExpValid'] + + # read the release cells raster + pras = IOf.readRaster(modelPaths["releasePathWork"])["rasterData"] + # convert to binary following the > 0 convention used + praMask = (pras > 0) + + if modelParameters["varAlphaBool"]: + data = IOf.readRaster(modelPaths["varAlphaPath"]) + rasterValues = data["rasterData"] + isValid = validateInputArray(praMask, rasterValues, maxAlphaValid, minAlphaValid) + + if not isValid: + log.error( + f"Error: Some Alpha-raster values are incomplete or outside a physically sensible range ([0,{maxAlphaValid}])" + ) + raise ValueError( + f"Missing or invalid values for spatially varying model parameter 'alpha' found in {modelPaths['varAlphaPath']}\n" + f"please provide values for 'alpha' between {minAlphaValid} and {maxAlphaValid} for all release cells.\n" + f"HINT: if you insist on using values outside of [{minAlphaValid}, {maxAlphaValid}] you can adapt " + "the values in 'getValidParameterRanges()' in com4FlowPy/com4FlowPy.py" + ) + + if modelParameters["varUmaxBool"]: + data = IOf.readRaster(modelPaths["varUmaxPath"]) + rasterValues = data["rasterData"] + _type = modelParameters["varUmaxType"].lower() + if _type == "umax": + _maxVal = maxUMaxValid + else: + _maxVal = maxZDeltaValid + isValid = validateInputArray(praMask, rasterValues, _maxVal) + + if not isValid: + log.error( + f"Error: Some raster values of type '{_type}' are incomplete or outside a physically sensible range." + ) + raise ValueError( + f"Missing or invalid values for spatially varying model parameter '{_type}' found in {modelPaths['varUmaxPath']}\n" + f"please provide values for '{_type}' between 0 and {_maxVal} for all release cells.\n" + f"HINT: if you insist on using values outside of [0, {_maxVal}] you can adapt " + "the values in 'getValidParameterRanges()' in com4FlowPy/com4FlowPy.py" + ) + + if modelParameters["varExponentBool"]: + data = IOf.readRaster(modelPaths["varExponentPath"]) + rasterValues = data["rasterData"] + + isValid = validateInputArray(praMask, rasterValues, maxExpValid) + + if not isValid: + log.error( + f"Error: Some raster values of type 'exp' are incomplete or outside the allowed range [0, {maxExpValid}]." + ) + raise ValueError( + f"Missing or invalid values for spatially varying model parameter 'exp' found in {modelPaths['varExponentPath']}\n" + f"please provide values for 'exp' between 0 and {maxExpValid} for all release cells.\n" + f"HINT: if you insist on using values outside of [0, {maxExpValid}] you can adapt " + "the values in 'getValidParameterRanges()' in com4FlowPy/com4FlowPy.py" + ) + + log.info("spatially variable input parameters are complete and within the valid range.") + log.info(24*"=") \ No newline at end of file diff --git a/avaframe/com4FlowPy/com4FlowPyCfg.ini b/avaframe/com4FlowPy/com4FlowPyCfg.ini index ca6da7c07..6c3e86e9b 100644 --- a/avaframe/com4FlowPy/com4FlowPyCfg.ini +++ b/avaframe/com4FlowPy/com4FlowPyCfg.ini @@ -208,6 +208,36 @@ procPerCPUCore = 1 chunkSize = 50 maxChunks = 500 +#++++++++++++ Limits for checking input data and parameter validity+++++++++++ +# These limits are used to check, if selected model parameters and provided +# input data are within physically and numerically valid ranges +#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# only change these values if you want to operate the model explicitly outside +# the specified ranges, otherwise leave this section unchanged! +#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# DISCLAIMER: +# CHANGING THESE LIMITS MAY LEAD TO NUMERICAL ISSUES, +# UNEXPECTED RESULTS OR RUNTIME ERRORS. +#--------------------- +# alpha - only sensible between 0 degrees (no friction) and 90 degrees (infinite friction) +minAlphaValid = 0 +maxAlphaValid= 90 +# max Velocity or zDelta limits +# 20000m - max vertical relief on Earth +# 630 m/s - corresponding max velocity via: +# maxUMaxValidValid = np.sqrt(2*9.81 * maxZDeltaValidValid) +# These are physical limits for applications to GMFs on Earth +maxZDeltaValid= 20000 +maxUMaxValid = 630 +# exp is expected to be > 0 and +# <=600 above we get numerical errors and flow behavior is already +# single flow at 10m resolution - +maxExpValid = 600 +# flux_threshold +minFluxThreshValid = 1e-6 +maxFluxThreshValid = 1 +#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + # Optional Custom Paths [PATHS] diff --git a/avaframe/tests/test_com4FlowPy.py b/avaframe/tests/test_com4FlowPy.py index 6e59720f4..a154db42c 100644 --- a/avaframe/tests/test_com4FlowPy.py +++ b/avaframe/tests/test_com4FlowPy.py @@ -19,6 +19,7 @@ import avaframe.com4FlowPy.splitAndMerge as SPAM import avaframe.in2Trans.rasterUtils as IOf import avaframe.runCom4FlowPy as runCom4FlowPy +from avaframe.com4FlowPy import com4FlowPy import avaframe.runStandardTestsCom4FlowPy as runStandardTestsCom4 @@ -859,6 +860,372 @@ def _fakeReadRaster(requestedPath, noDataToNan): (referenceRasterPath, False), ] +def test_validateInputArray(): + praMask = (np.array([[1, 0], [1, 0]]) == 1) + dataValid = np.array([[255, 0], [128, -1]]) + dataNan = np.array([[255.0, 0], [np.nan, -1]]) + dataHigh = np.array([[255.0, 0], [10000.0, -1]]) + + assert com4FlowPy.validateInputArray(praMask, dataValid, maxValue=300) + assert not com4FlowPy.validateInputArray(praMask, dataNan, maxValue=300.0) + assert not com4FlowPy.validateInputArray(praMask, dataHigh, maxValue=300.0) + + +def makeValidParamRanges(minAlphaValid=0.0, maxAlphaValid=90.0, maxZDeltaValid=20000.0, + maxExpValid=600.0, minFluxThreshValid=1e-6, maxFluxThreshValid=1.0, + maxUMaxValid=100.0): + """ build a valid parameter-ranges dict manually, with sensible defaults, + overridable per test via explicit keyword arguments. + """ + return { + "minAlphaValid": minAlphaValid, + "maxAlphaValid": maxAlphaValid, + "maxZDeltaValid": maxZDeltaValid, + "maxExpValid": maxExpValid, + "minFluxThreshValid": minFluxThreshValid, + "maxFluxThreshValid": maxFluxThreshValid, + "maxUMaxValid": maxUMaxValid, + } + + +def test_getValidParameterRanges(): + """ test that getValidParameterRanges falls back to its documented defaults + when the .ini config section provides no overrides + """ + cfgSetup = configparser.ConfigParser() + cfgSetup["GENERAL"] = {} + + result = com4FlowPy.getValidParameterRanges(cfgSetup["GENERAL"]) + + assert result["maxZDeltaValid"] == pytest.approx(20000.0) + assert result["maxExpValid"] == pytest.approx(600.0) + assert result["minFluxThreshValid"] == pytest.approx(1e-6) + assert result["maxFluxThreshValid"] == pytest.approx(1.0) + + cfgSetup = configparser.ConfigParser() + cfgSetup["GENERAL"] = { + "maxZDeltaValid": "15000", + "maxExpValid": "450", + "minFluxThreshValid": "1e-5", + "maxFluxThreshValid": "0.5", + } + + result = com4FlowPy.getValidParameterRanges(cfgSetup["GENERAL"]) + + assert result["maxZDeltaValid"] == pytest.approx(15000.0) + assert result["maxExpValid"] == pytest.approx(450.0) + assert result["minFluxThreshValid"] == pytest.approx(1e-5) + assert result["maxFluxThreshValid"] == pytest.approx(0.5) + + +# --------------------------------------------------------------------------- +# helpers +# --------------------------------------------------------------------------- + +def makeValidModelParameters(alpha=25.0, max_z=8000.0, exp=8, flux_threshold=0.003): + """ build a set of global model parameters that pass all checkGlobalParameters checks """ + return { + "alpha": alpha, + "max_z": max_z, + "exp": exp, + "flux_threshold": flux_threshold, + } + + +def makeModelPaths(): + return { + "releasePathWork": "dummy_release.tif", + "varAlphaPath": "dummy_alpha.tif", + "varUmaxPath": "dummy_umax.tif", + "varExponentPath": "dummy_exp.tif", + } + + +# --------------------------------------------------------------------------- +# checkGlobalParameters +# --------------------------------------------------------------------------- + +def test_checkGlobalParameters_alpha(): + """ test that an alpha value below minAlphaValid raises ValueError """ + modelParameters = makeValidModelParameters(alpha=-5.0) + validParamRanges = makeValidParamRanges() + + with pytest.raises(ValueError, match="alpha"): + com4FlowPy.checkGlobalParameters(modelParameters, makeModelPaths(), validParamRanges) + + """ test that an alpha value above maxAlphaValid raises ValueError """ + modelParameters = makeValidModelParameters(alpha=95.0) + + with pytest.raises(ValueError, match="alpha"): + com4FlowPy.checkGlobalParameters(modelParameters, makeModelPaths(), validParamRanges) + + """ test that an alpha value raises no ValueError """ + modelParameters = makeValidModelParameters(alpha=45.0) + com4FlowPy.checkGlobalParameters(modelParameters, makeModelPaths(), validParamRanges) + + +def test_checkGlobalParameters_maxZ(): + """ test that max_z values outside [0, maxZDeltaValid] raise ValueError """ + modelParameters = makeValidModelParameters(max_z=-5) + validParamRanges = makeValidParamRanges() + + with pytest.raises(ValueError, match="max_z"): + com4FlowPy.checkGlobalParameters(modelParameters, makeModelPaths(), validParamRanges) + + modelParameters = makeValidModelParameters(max_z=25000.0) + + with pytest.raises(ValueError, match="max_z"): + com4FlowPy.checkGlobalParameters(modelParameters, makeModelPaths(), validParamRanges) + + modelParameters = makeValidModelParameters(max_z=200.0) + + com4FlowPy.checkGlobalParameters(modelParameters, makeModelPaths(), validParamRanges) + + +def test_checkGlobalParameters_exp(): + """ test that exp values outside [0, maxExpValid] raise ValueError """ + modelParameters = makeValidModelParameters(exp=-1) + validParamRanges = makeValidParamRanges() + + with pytest.raises(ValueError, match="exp"): + com4FlowPy.checkGlobalParameters(modelParameters, makeModelPaths(), validParamRanges) + + modelParameters = makeValidModelParameters(exp=700) + + with pytest.raises(ValueError, match="exp"): + com4FlowPy.checkGlobalParameters(modelParameters, makeModelPaths(), validParamRanges) + + """ test that exp at exactly 0 and at maxExpValid is accepted """ + + com4FlowPy.checkGlobalParameters(makeValidModelParameters(exp=8), makeModelPaths(), validParamRanges) + + +def test_checkGlobalParameters_fluxThreshold(): + """ test that flux_threshold values outside [minFluxThreshValid, maxFluxThreshValid] raise ValueError """ + modelParameters = makeValidModelParameters(flux_threshold=0) + validParamRanges = makeValidParamRanges() + + with pytest.raises(ValueError, match="flux_threshold"): + com4FlowPy.checkGlobalParameters(modelParameters, makeModelPaths(), validParamRanges) + + modelParameters = makeValidModelParameters(flux_threshold=5.0) + + with pytest.raises(ValueError, match="flux_threshold"): + com4FlowPy.checkGlobalParameters(modelParameters, makeModelPaths(), validParamRanges) + + """ test that flux_threshold at exactly the min and max valid values is accepted """ + + com4FlowPy.checkGlobalParameters( + makeValidModelParameters(flux_threshold=0.003), + makeModelPaths(), validParamRanges + ) + + +def test_checkGlobalParameters_errorMessageMentionsValidRange(): + """ test that the raised error message includes the configured min/max bounds """ + validParamRanges = makeValidParamRanges(minAlphaValid=5.0, maxAlphaValid=60.0) + modelParameters = makeValidModelParameters(alpha=100.0) + + with pytest.raises(ValueError, match=r"5\.0 and 60\.0"): + com4FlowPy.checkGlobalParameters(modelParameters, makeModelPaths(), validParamRanges) + + +# --------------------------------------------------------------------------- +# checkVariableInputParameters +# --------------------------------------------------------------------------- + +def test_checkVariableInputParameters_allDisabled_doesNotRaise(monkeypatch): + """ test that when all 'var*Bool' flags are False, no raster is validated and no error is raised """ + praRaster = np.array([[0, 1], [1, 0]]) + monkeypatch.setattr(IOf, "readRaster", lambda path: {"rasterData": praRaster}) + + modelParameters = { + "varAlphaBool": False, + "varUmaxBool": False, + "varExponentBool": False, + } + validParamRanges = makeValidParamRanges() + + com4FlowPy.checkVariableInputParameters(modelParameters, makeModelPaths(), validParamRanges) + + +def test_checkVariableInputParameters_varAlpha(monkeypatch): + """ test that a valid alpha raster passes validation without raising """ + praRaster = np.array([[0, 1], [1, 0]]) + alphaRaster = np.array([[np.nan, 25.0], [30.0, np.nan]]) + + def fakeReadRaster(path): + if "alpha" in path: + return {"rasterData": alphaRaster} + return {"rasterData": praRaster} + + monkeypatch.setattr(IOf, "readRaster", fakeReadRaster) + + modelParameters = { + "varAlphaBool": True, + "varUmaxBool": False, + "varExponentBool": False, + } + validParamRanges = makeValidParamRanges() + + com4FlowPy.checkVariableInputParameters(modelParameters, makeModelPaths(), validParamRanges) + + """ test that an invalid alpha raster (per validateInputArray) raises ValueError """ + alphaRaster = np.array([[np.nan, 200.0], [30.0, np.nan]]) + + monkeypatch.setattr(IOf, "readRaster", fakeReadRaster) + + modelParameters = { + "varAlphaBool": True, + "varUmaxBool": False, + "varExponentBool": False, + } + validParamRanges = makeValidParamRanges() + + with pytest.raises(ValueError, match="alpha"): + com4FlowPy.checkVariableInputParameters(modelParameters, makeModelPaths(), validParamRanges) + + +def test_checkVariableInputParameters_inPRA(monkeypatch): + """check invalid value in PRA""" + pra2Raster = np.array([[1, 1], [1, 0]]) + alpha2Raster = np.array([[np.nan, 25.0], [30.0, np.nan]]) + + def fakeReadRaster(path): + if "alpha" in path: + return {"rasterData": alpha2Raster} + return {"rasterData": pra2Raster} + + monkeypatch.setattr(IOf, "readRaster", fakeReadRaster) + + modelParameters = { + "varAlphaBool": True, + "varUmaxBool": False, + "varExponentBool": False, + } + validParamRanges = makeValidParamRanges() + + with pytest.raises(ValueError): + com4FlowPy.checkVariableInputParameters(modelParameters, makeModelPaths(), validParamRanges) + + +def test_checkVariableInputParameters_varUmax_typeUmax(monkeypatch): + """ test that when varUmaxType is 'umax', validateInputArray is called with maxUMaxValid + as the upper bound rather than maxZDeltaValid + """ + praRaster = np.array([[1, 0]]) + umaxRaster = np.array([[50.0, 50.0]]) + + monkeypatch.setattr(IOf, "readRaster", lambda path: {"rasterData": ( + praRaster if "release" in path else umaxRaster + )}) + + modelParameters = { + "varAlphaBool": False, + "varUmaxBool": True, + "varUmaxType": "umax", + "varExponentBool": False, + } + validParamRanges = makeValidParamRanges(maxUMaxValid=77.0, maxZDeltaValid=999.0) + + com4FlowPy.checkVariableInputParameters(modelParameters, makeModelPaths(), validParamRanges) + + """ test that an invalid u-max/zdelta raster raises ValueError mentioning its type """ + praRaster = np.array([[1]]) + umaxRaster = np.array([[999999.0]]) + + monkeypatch.setattr(IOf, "readRaster", lambda path: {"rasterData": ( + praRaster if "release" in path else umaxRaster + )}) + + modelParameters = { + "varAlphaBool": False, + "varUmaxBool": True, + "varUmaxType": "umax", + "varExponentBool": False, + } + validParamRanges = makeValidParamRanges() + + with pytest.raises(ValueError, match="umax"): + com4FlowPy.checkVariableInputParameters(modelParameters, makeModelPaths(), validParamRanges) + + """test np.nan value in PRA""" + praRaster = np.array([[1, 0]]) + umaxRaster = np.array([[np.nan, 100]]) + + monkeypatch.setattr(IOf, "readRaster", lambda path: {"rasterData": ( + praRaster if "release" in path else umaxRaster + )}) + + modelParameters = { + "varAlphaBool": False, + "varUmaxBool": True, + "varUmaxType": "umax", + "varExponentBool": False, + } + validParamRanges = makeValidParamRanges() + + with pytest.raises(ValueError, match="umax"): + com4FlowPy.checkVariableInputParameters(modelParameters, makeModelPaths(), validParamRanges) + + +def test_checkVariableInputParameters_varUmax_typeZDelta_usesMaxZDeltaValid(monkeypatch): + """ test that when varUmaxType is anything other than 'umax' (e.g. 'zdelta'), + validateInputArray is called with maxZDeltaValid as the upper bound + """ + praRaster = np.array([[1, 0]]) + umaxRaster = np.array([[500.0, np.nan]]) + + monkeypatch.setattr(IOf, "readRaster", lambda path: {"rasterData": ( + praRaster if "release" in path else umaxRaster + )}) + + modelParameters = { + "varAlphaBool": False, + "varUmaxBool": True, + "varUmaxType": "ZDelta", # mixed case; function lowercases it internally + "varExponentBool": False, + } + validParamRanges = makeValidParamRanges(maxUMaxValid=77.0, maxZDeltaValid=999.0) + + com4FlowPy.checkVariableInputParameters(modelParameters, makeModelPaths(), validParamRanges) + + +def test_checkVariableInputParameters_varExponent(monkeypatch): + """ test that a valid exponent raster passes validation without raising """ + praRaster = np.array([[1, 0]]) + expRaster = np.array([[8.0, np.nan]]) + + monkeypatch.setattr(IOf, "readRaster", lambda path: {"rasterData": ( + praRaster if "release" in path else expRaster + )}) + + modelParameters = { + "varAlphaBool": False, + "varUmaxBool": False, + "varExponentBool": True, + } + validParamRanges = makeValidParamRanges() + + com4FlowPy.checkVariableInputParameters(modelParameters, makeModelPaths(), validParamRanges) + + """ test that an invalid exponent raster raises ValueError """ + praRaster = np.array([[1, 0]]) + expRaster = np.array([[900.0, np.nan]]) + + monkeypatch.setattr(IOf, "readRaster", lambda path: {"rasterData": ( + praRaster if "release" in path else expRaster + )}) + + modelParameters = { + "varAlphaBool": False, + "varUmaxBool": False, + "varExponentBool": True, + } + + with pytest.raises(ValueError, match="exp"): + com4FlowPy.checkVariableInputParameters(modelParameters, makeModelPaths(), validParamRanges) + if __name__ == "__main__": test_add_os() @@ -870,4 +1237,9 @@ def _fakeReadRaster(requestedPath, noDataToNan): test_mergeDict(tmpDir) test_mergeDictToRaster(tmpDir) test_mergeDictToPolygon(tmpDir) - test_runCom4FlowPy() + test_runCom4FlowPy(tmpDir) + test_checkGlobalParameters_alpha() + test_checkGlobalParameters_maxZ() + test_checkGlobalParameters_exp() + test_checkGlobalParameters_fluxThreshold() + test_checkGlobalParameters_errorMessageMentionsValidRange()