Skip to content

Commit 3be7677

Browse files
committed
refactor(com1DFA): simplify resTypes handling and dataframe population
- Removed redundant condition for `resT` by excluding `FTDet` checks. - Simplified logic to populate `resultsDF` unconditionally when appending new rows. fix(com1DFA): correct timestep export condition for t=0 - Updated logic to use `np.any(dtSave <= 1.0e-8)` for handling initial timestep export. - Removed redundant contour fetching logic for dummy fields test(com1DFA): add unit test; squash refactor(tests): fix `test_tSteps_output_behavior` - Updated `com1DFA` to ensure particle directory initialization is handled conditionally during initial export. refactor(com1DFA): ensure valid `resTypes` and improve contour fetching logic - Added logic to append `pfv` to `resTypes` when it only contains invalid or insufficient field types (e.g., `particles` or empty). - Adjusted contour line computation to skip when the target field is a dummy array. refactor(com1DFA): simplify `resTypes` handling and remove redundant `resTypesLast` - Replaced all instances of `resTypesLast` with `resTypes` - Removed unused variable `resTypesLast` across the codebase. - Ensured consistent use of `resTypes` for initializing fields and max value computations.
1 parent 7ca099b commit 3be7677

File tree

3 files changed

+78
-69
lines changed

3 files changed

+78
-69
lines changed

avaframe/com1DFA/com1DFA.py

Lines changed: 38 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1658,7 +1658,11 @@ def initializeFields(cfg, dem, particles, releaseLine):
16581658
cfgGen = cfg["GENERAL"]
16591659
# what result types are desired as output (we need this to decide which fields we actually need to compute)
16601660
resTypes = fU.splitIniValueToArraySteps(cfgGen["resType"])
1661-
resTypesLast = resTypes
1661+
# ensure at least one field type is present for internal computations
1662+
# if resTypes only contains particles add pfv
1663+
validFieldTypes = [rt for rt in resTypes if rt not in ["particles"]]
1664+
if len(validFieldTypes) == 0:
1665+
resTypes.append("pfv")
16621666
# read dem header
16631667
header = dem["header"]
16641668
ncols = header["ncols"]
@@ -1683,7 +1687,7 @@ def initializeFields(cfg, dem, particles, releaseLine):
16831687
fields["timeInfo"] = np.zeros((nrows, ncols)) # first time
16841688
# for optional fields, initialize with dummys (minimum size array). The cython functions then need something
16851689
# even if it is empty to run properly
1686-
if ("TA" in resTypesLast) or ("pta" in resTypesLast):
1690+
if ("TA" in resTypes) or ("pta" in resTypes):
16871691
fields["pta"] = np.zeros((nrows, ncols)) # peak travel angle [°]
16881692
fields["TA"] = np.zeros((nrows, ncols)) # travel angle [°]
16891693
fields["computeTA"] = True
@@ -1692,14 +1696,14 @@ def initializeFields(cfg, dem, particles, releaseLine):
16921696
fields["pta"] = np.zeros((1, 1))
16931697
fields["TA"] = np.zeros((1, 1))
16941698
fields["computeTA"] = False
1695-
if "pke" in resTypesLast:
1699+
if "pke" in resTypes:
16961700
fields["pke"] = np.zeros((nrows, ncols)) # peak kinetic energy [kJ/m²]
16971701
fields["computeKE"] = True
16981702
log.debug("Computing Kinetic energy")
16991703
else:
17001704
fields["pke"] = np.zeros((1, 1))
17011705
fields["computeKE"] = False
1702-
if ("P" in resTypesLast) or ("ppr" in resTypesLast):
1706+
if ("P" in resTypes) or ("ppr" in resTypes):
17031707
fields["P"] = np.zeros((nrows, ncols)) # pressure [kPa]
17041708
fields["ppr"] = np.zeros((nrows, ncols)) # peak pressure [kPa]
17051709
fields["computeP"] = True
@@ -2037,8 +2041,11 @@ def DFAIterate(cfg, particles, fields, dem, inputSimLines, outDir, cuSimName, si
20372041
# add particles to the results type if trackParticles option is activated
20382042
if cfg.getboolean("TRACKPARTICLES", "trackParticles"):
20392043
resTypes = list(set(resTypes + ["particles"]))
2040-
# use resTypes for all time steps
2041-
resTypesLast = resTypes
2044+
# ensure at least one field type is present for internal computations
2045+
# if resTypes only contains particles, add pfv
2046+
validFieldTypes = [rt for rt in resTypes if rt not in ["particles"]]
2047+
if len(validFieldTypes) == 0:
2048+
resTypes.append("pfv")
20422049
# derive friction type
20432050
# turn friction model into integer
20442051
frictModelsList = [
@@ -2077,7 +2084,7 @@ def DFAIterate(cfg, particles, fields, dem, inputSimLines, outDir, cuSimName, si
20772084
pfvTimeMax = []
20782085

20792086
# setup a result fields info data frame to save max values of fields and avalanche front
2080-
resultsDF = setupresultsDF(resTypesLast, cfg["VISUALISATION"].getboolean("createRangeTimeDiagram"))
2087+
resultsDF = setupresultsDF(resTypes, cfg["VISUALISATION"].getboolean("createRangeTimeDiagram"))
20812088

20822089
# Add different time stepping options here
20832090
log.debug("Use standard time stepping")
@@ -2091,13 +2098,16 @@ def DFAIterate(cfg, particles, fields, dem, inputSimLines, outDir, cuSimName, si
20912098
t = particles["t"]
20922099
log.debug("Saving results for time step t = %f s", t)
20932100

2101+
# Initialize particles output directory if needed
2102+
if "particles" in resTypes:
2103+
outDirData = outDir / "particles"
2104+
fU.makeADir(outDirData)
2105+
20942106
# export initial time step only if t=0 is explicitly in dtSave
2095-
if cfg["EXPORTS"].getboolean("exportData") and (dtSave.size > 0 and dtSave[0] <= 1.0e-8):
2107+
if cfg["EXPORTS"].getboolean("exportData") and (dtSave.size > 0 and np.any(dtSave <= 1.0e-8)):
20962108
exportFields(cfg, t, fields, dem, outDir, cuSimName, TSave="initial")
20972109

20982110
if "particles" in resTypes:
2099-
outDirData = outDir / "particles"
2100-
fU.makeADir(outDirData)
21012111
savePartToPickle(particles, outDirData, cuSimName)
21022112

21032113
# Update dtSave to remove the initial timestep we just saved
@@ -2129,7 +2139,7 @@ def DFAIterate(cfg, particles, fields, dem, inputSimLines, outDir, cuSimName, si
21292139
cfgRangeTime["GENERAL"]["simHash"] = simHash
21302140

21312141
# add initial time step to Tsave array only if it was exported
2132-
if dtSave.size > 0 and dtSave[0] <= 1.0e-8:
2142+
if dtSave.size > 0 and np.any(dtSave <= 1.0e-8):
21332143
Tsave = [0]
21342144
else:
21352145
Tsave = []
@@ -2169,7 +2179,7 @@ def DFAIterate(cfg, particles, fields, dem, inputSimLines, outDir, cuSimName, si
21692179
rangeValue = mtiInfo["rangeList"][-1]
21702180
else:
21712181
rangeValue = ""
2172-
resultsDF = addMaxValuesToDF(resultsDF, fields, t, resTypesLast, rangeValue=rangeValue)
2182+
resultsDF = addMaxValuesToDF(resultsDF, fields, t, resTypes, rangeValue=rangeValue)
21732183

21742184
tCPU["nSave"] = nSave
21752185
particles["t"] = t
@@ -2351,25 +2361,19 @@ def DFAIterate(cfg, particles, fields, dem, inputSimLines, outDir, cuSimName, si
23512361
# export particles dictionaries of saving time steps
23522362
if "particles" in resTypes:
23532363
savePartToPickle(particles, outDirData, cuSimName)
2354-
else:
2355-
# fetch contourline info
2364+
2365+
# save contour line for each sim only if the field is properly computed (not a dummy array)
2366+
contourResType = cfg["VISUALISATION"]["contourResType"]
2367+
if fields[contourResType].shape != (1, 1):
23562368
contourDictXY = outCom1DFA.fetchContCoors(
23572369
dem["header"],
2358-
fields[cfg["VISUALISATION"]["contourResType"]],
2370+
fields[contourResType],
23592371
cfg["VISUALISATION"],
23602372
cuSimName,
23612373
)
2362-
2363-
# save contour line for each sim
2364-
contourDictXY = outCom1DFA.fetchContCoors(
2365-
dem["header"],
2366-
fields[cfg["VISUALISATION"]["contourResType"]],
2367-
cfg["VISUALISATION"],
2368-
cuSimName,
2369-
)
2370-
outDirDataCont = outDir / "contours"
2371-
fU.makeADir(outDirDataCont)
2372-
saveContToPickle(contourDictXY, outDirDataCont, cuSimName)
2374+
outDirDataCont = outDir / "contours"
2375+
fU.makeADir(outDirDataCont)
2376+
saveContToPickle(contourDictXY, outDirDataCont, cuSimName)
23732377

23742378
# export particles properties for visulation
23752379
if cfg["VISUALISATION"].getboolean("writePartToCSV"):
@@ -2409,7 +2413,7 @@ def setupresultsDF(resTypes, cfgRangeTime):
24092413
# TODO catch empty resTypes
24102414
resDict = {"timeStep": [0.0]}
24112415
for resT in resTypes:
2412-
if resT != "particles" and resT != "FTDet":
2416+
if resT != "particles":
24132417
resDict["max" + resT] = [0.0]
24142418
if cfgRangeTime:
24152419
resDict["rangeList"] = [0.0]
@@ -2443,11 +2447,12 @@ def addMaxValuesToDF(resultsDF, fields, timeStep, resTypes, rangeValue=""):
24432447

24442448
newLine = []
24452449
for resT in resTypes:
2446-
if resT != "particles" and resT != "FTDet":
2450+
if resT != "particles":
24472451
newLine.append(np.nanmax(fields[resT]))
24482452

24492453
if rangeValue != "":
24502454
newLine.append(rangeValue)
2455+
24512456
resultsDF.loc[timeStep] = newLine
24522457

24532458
return resultsDF
@@ -2980,6 +2985,11 @@ def exportFields(
29802985
resTypesGen.remove("particles")
29812986

29822987
resTypes = resTypesGen
2988+
# ensure at least one field type is present for export
2989+
# if resTypes only contains FTDet or is empty, add pfv
2990+
validFieldTypes = [rt for rt in resTypes if rt != "FTDet"]
2991+
if len(validFieldTypes) == 0:
2992+
resTypes.append("pfv")
29832993

29842994
if resTypesForced != []:
29852995
resTypes = resTypesForced

avaframe/log2Report/generateReport.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ def writeReport(outDir, reportDictList, reportOneFile, plotDict='', standaloneRe
203203
# Loop through all simulations
204204
for reportD in reportDictList:
205205

206-
if plotDict != '' and ('simName' in reportD):
206+
if plotDict != '' and ('simName' in reportD) and (reportD['simName']['name'] in plotDict):
207207
# add plot info to general report Dict
208208
reportD['Simulation Results'] = plotDict[reportD['simName']['name']]
209209
reportD['Simulation Results'].update({'type': 'image'})
@@ -222,7 +222,7 @@ def writeReport(outDir, reportDictList, reportOneFile, plotDict='', standaloneRe
222222
# Loop through all simulations
223223
for reportD in reportDictList:
224224

225-
if plotDict != '':
225+
if plotDict != '' and (reportD['simName']['name'] in plotDict):
226226
# add plot info to general report Dict
227227
reportD['Simulation Results'] = plotDict[reportD['simName']['name']]
228228
reportD['Simulation Results'].update({'type': 'image'})

avaframe/tests/test_com1DFA.py

Lines changed: 38 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2926,10 +2926,9 @@ def test_adaptDEM():
29262926
assert np.all(fieldsAdapted["sfcChangeTotal"] == fields["FTDet"] / NzNormed)
29272927

29282928

2929-
def test_tSteps_output_behavior(tmp_path):
2929+
def test_tSteps_output_behavior(tmp_path, caplog):
29302930
"""Test that tSteps controls which timesteps are exported correctly.
29312931
2932-
New behavior:
29332932
- Empty tSteps (default): only final timestep is exported
29342933
- Explicit tSteps with t=0: t=0 timestep is exported
29352934
"""
@@ -2941,31 +2940,19 @@ def test_tSteps_output_behavior(tmp_path):
29412940
shutil.copytree(inputDir, avaDir1)
29422941
cfgFile1 = avaDir1 / "test_com1DFACfg.ini"
29432942

2943+
# Get main configuration
2944+
cfgMain = cfgUtils.getGeneralConfig()
2945+
cfgMain['MAIN']['avalancheDir'] = str(avaDir1)
29442946
# Modify config to have empty tSteps and NO parameter variations
2945-
cfg = configparser.ConfigParser()
2946-
cfg.read(cfgFile1)
2947+
cfg = cfgUtils.getModuleConfig(com1DFA, cfgFile1)
29472948
cfg["GENERAL"]["tSteps"] = ""
29482949
cfg["GENERAL"]["tEnd"] = "10" # Short simulation
29492950
cfg["GENERAL"]["dt"] = "0.1" # Single value, no variations
29502951
cfg["GENERAL"]["simTypeList"] = "null" # Simple simulation, no entrainment/resistance
2951-
# Ensure exports are enabled
2952-
if "EXPORTS" not in cfg:
2953-
cfg["EXPORTS"] = {}
2954-
cfg["EXPORTS"]["exportData"] = "True"
29552952
with open(cfgFile1, "w") as f:
29562953
cfg.write(f)
29572954

2958-
cfgMain1 = configparser.ConfigParser()
2959-
cfgMain1["MAIN"] = {"avalancheDir": str(avaDir1), "nCPU": "1"}
2960-
cfgMain1["FLAGS"] = {
2961-
"showPlot": "False",
2962-
"savePlot": "False",
2963-
"ReportDir": "False",
2964-
"reportOneFile": "False",
2965-
"debugPlot": "False",
2966-
}
2967-
2968-
dem, plotDict, reportDictList, simDF = com1DFA.com1DFAMain(cfgMain1, cfgInfo=cfgFile1)
2955+
dem, plotDict, reportDictList, simDF = com1DFA.com1DFAMain(cfgMain, cfgInfo=cfgFile1)
29692956

29702957
# Check that only final timestep files exist in timeSteps directory
29712958
timeStepsDir1 = avaDir1 / "Outputs" / "com1DFA" / "peakFiles" / "timeSteps"
@@ -2981,37 +2968,49 @@ def test_tSteps_output_behavior(tmp_path):
29812968
shutil.copytree(inputDir, avaDir2)
29822969
cfgFile2 = avaDir2 / "test_com1DFACfg.ini"
29832970

2971+
cfgMain['MAIN']['avalancheDir'] = str(avaDir2)
2972+
29842973
# Modify config to have explicit tSteps including t=0 and NO parameter variations
2985-
cfg2 = configparser.ConfigParser()
2986-
cfg2.read(cfgFile2)
2974+
cfg2 = cfgUtils.getModuleConfig(com1DFA, cfgFile2)
29872975
cfg2["GENERAL"]["tSteps"] = "0|5"
29882976
cfg2["GENERAL"]["tEnd"] = "10" # Short simulation
29892977
cfg2["GENERAL"]["dt"] = "0.1" # Single value, no variations
29902978
cfg2["GENERAL"]["simTypeList"] = "null" # Simple simulation, no entrainment/resistance
2991-
# Ensure exports are enabled
2992-
if "EXPORTS" not in cfg2:
2993-
cfg2["EXPORTS"] = {}
2994-
cfg2["EXPORTS"]["exportData"] = "True"
29952979
with open(cfgFile2, "w") as f:
29962980
cfg2.write(f)
29972981

2998-
cfgMain2 = configparser.ConfigParser()
2999-
cfgMain2["MAIN"] = {"avalancheDir": str(avaDir2), "nCPU": "1"}
3000-
cfgMain2["FLAGS"] = {
3001-
"showPlot": "False",
3002-
"savePlot": "False",
3003-
"ReportDir": "False",
3004-
"reportOneFile": "False",
3005-
"debugPlot": "False",
3006-
}
3007-
3008-
dem2, plotDict2, reportDictList2, simDF2 = com1DFA.com1DFAMain(cfgMain2, cfgInfo=cfgFile2)
2982+
dem2, plotDict2, reportDictList2, simDF2 = com1DFA.com1DFAMain(cfgMain, cfgInfo=cfgFile2)
30092983

30102984
# Check that t=0 timestep files exist
30112985
timeStepsDir2 = avaDir2 / "Outputs" / "com1DFA" / "peakFiles" / "timeSteps"
30122986
assert timeStepsDir2.exists(), "timeSteps directory should exist"
3013-
allFiles2 = list(timeStepsDir2.glob("*.asc"))
3014-
print(f"\nAll timestep files: {[f.name for f in allFiles2]}")
30152987
tStepFiles2 = list(timeStepsDir2.glob("*_t0.0*.asc"))
3016-
print(f"Files matching t0.0: {[f.name for f in tStepFiles2]}")
30172988
assert len(tStepFiles2) > 0, "Should have initial timestep files at t=0 when tSteps includes 0"
2989+
2990+
# Test 3: exportData = False should trigger contour fetching in else block
2991+
avaDir3 = pathlib.Path(tmp_path, "testExportDataFalse")
2992+
shutil.copytree(inputDir, avaDir3)
2993+
cfgFile3 = avaDir3 / "test_com1DFACfg.ini"
2994+
2995+
cfgMain['MAIN']['avalancheDir'] = str(avaDir3)
2996+
2997+
# Modify config to have exportData = False
2998+
cfg3 = cfgUtils.getModuleConfig(com1DFA, cfgFile3)
2999+
cfg3["GENERAL"]["tSteps"] = ""
3000+
cfg3["GENERAL"]["tEnd"] = "5" # Very short simulation
3001+
cfg3["GENERAL"]["dt"] = "0.1"
3002+
cfg3["GENERAL"]["simTypeList"] = "null"
3003+
cfg3["EXPORTS"]["exportData"] = "False" # Key setting to test else block
3004+
with open(cfgFile3, "w") as f:
3005+
cfg3.write(f)
3006+
3007+
dem3, plotDict3, reportDictList3, simDF3 = com1DFA.com1DFAMain(cfgMain, cfgInfo=cfgFile3)
3008+
3009+
# Check that contour data was generated (stored in reportDict) instead of exported files
3010+
assert len(reportDictList3) > 0, "Should have report dict even with exportData=False"
3011+
# Verify that timeSteps directory doesn't exist (no data exported)
3012+
timeStepsDir3 = avaDir3 / "Outputs" / "com1DFA" / "peakFiles" / "timeSteps"
3013+
if timeStepsDir3.exists():
3014+
tStepFiles3 = list(timeStepsDir3.glob("*.asc"))
3015+
# With exportData=False, intermediate timesteps should not be exported
3016+
assert len(tStepFiles3) == 0, "No timestep files should be exported when exportData=False"

0 commit comments

Comments
 (0)