Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/post_process/m_data_output.fpp
Original file line number Diff line number Diff line change
Expand Up @@ -725,8 +725,8 @@ contains

if (p > 0) then
err = DBMKOPTLIST(2, optlist)
err = DBADDIOPT(optlist, DBOPT_LO_OFFSET, lo_offset)
err = DBADDIOPT(optlist, DBOPT_HI_OFFSET, hi_offset)
err = DBADDIAOPT(optlist, DBOPT_LO_OFFSET, 3, lo_offset)
err = DBADDIAOPT(optlist, DBOPT_HI_OFFSET, 3, hi_offset)
if (grid_geometry == 3) then
err = DBPUTQM(dbfile, 'rectilinear_grid', 16, &
'x', 1, 'y', 1, 'z', 1, &
Expand All @@ -743,8 +743,8 @@ contains
err = DBFREEOPTLIST(optlist)
elseif (n > 0) then
err = DBMKOPTLIST(2, optlist)
err = DBADDIOPT(optlist, DBOPT_LO_OFFSET, lo_offset)
err = DBADDIOPT(optlist, DBOPT_HI_OFFSET, hi_offset)
err = DBADDIAOPT(optlist, DBOPT_LO_OFFSET, 2, lo_offset)
err = DBADDIAOPT(optlist, DBOPT_HI_OFFSET, 2, hi_offset)
err = DBPUTQM(dbfile, 'rectilinear_grid', 16, &
'x', 1, 'y', 1, 'z', 1, &
x_cb, y_cb, DB_F77NULL, dims, 2, &
Expand Down
37 changes: 26 additions & 11 deletions src/simulation/m_data_output.fpp
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,7 @@ contains
write (3, *) ! new line

if (.not. f_approx_equal(icfl_max_glb, icfl_max_glb)) then
print *, 'icfl', icfl_max_glb
call s_mpi_abort('ICFL is NaN. Exiting.')
elseif (icfl_max_glb > 1._wp) then
print *, 'icfl', icfl_max_glb
Expand Down Expand Up @@ -1129,6 +1130,8 @@ contains
real(wp) :: G_local
real(wp) :: dyn_p, T
real(wp) :: damage_state
character(LEN=15) :: FMT_glb
character(len=30) :: FMT

integer :: i, j, k, l, s, d !< Generic loop iterator

Expand All @@ -1145,6 +1148,13 @@ contains

T = dflt_T_guess

! Set format string based on precision
if (precision == 1) then
FMT_glb = 'F28.7'
else
FMT_glb = 'F28.16'
end if

! Non-dimensional time calculation
if (time_stepper == 23) then
nondim_time = mytime
Expand Down Expand Up @@ -1578,28 +1588,35 @@ contains
R(1), &
Rdot(1)
else if (elasticity) then
write (i + 30, '(6X,F12.6,F24.8,F24.8,F24.8,F24.8,'// &
'F24.8,F24.8,F24.8)') &
FMT = '(6X,F12.6,8'//FMT_glb//')'
write (i + 30, FMT) &
nondim_time, &
rho, &
vel(1), &
vel(2), &
vel(3), &
pres, &
tau_e(1), &
tau_e(2), &
tau_e(3)
else
write (i + 30, '(6X,F12.6,F24.8,F24.8,F24.8)') &
FMT = '(6X,F12.6,11'//FMT_glb//')'
write (i + 30, FMT) &
nondim_time, &
rho, &
vel(1), &

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 2D probe format expects 11 values but only 10 supplied

The same p == 0 block now builds FMT = '(6X,F12.6,11'//FMT_glb//')', which requires eleven Fxx descriptors after the time column, yet the following write only passes ten scalars (ρ, u, v, w, p, γ, π∞, qv, c, accel). Fortran stops execution when the I/O list is shorter than the format, so every non‑bubble 2‑D case with probes enabled will abort while writing. Either lower the repeat count or provide the missing field.

Useful? React with 👍 / 👎.

pres
print *, 'time =', nondim_time, 'rho =', rho, 'pres =', pres
vel(2), &
vel(3), &

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid 3rd velocity component in 2D probe writer

When p == 0 the code falls into the 2‑D branch, where vel is declared with num_vels = num_dims = 2 (see m_global_parameters). The new write statement still dereferences vel(3) (and the elasticity block a few lines above does the same), so every 2‑D test that now has probes enabled will read past the end of the array, producing garbage or a bounds error as soon as probes are written. Only access vel(3) when num_vels ≥ 3.

Useful? React with 👍 / 👎.

pres, & ! Out of tolerance
gamma, &
pi_inf, &
qv, &
c, &
accel
Copy link
Contributor

@qodo-code-review qodo-code-review bot Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Correct the Fortran format string to use 10 instead of 11 for the number of floating-point fields to match the 11 variables being written. [possible issue, importance: 9]

Suggested change
FMT = '(6X,F12.6,11'//FMT_glb//')'
write (i + 30, FMT) &
nondim_time, &
rho, &
vel(1), &
pres
print *, 'time =', nondim_time, 'rho =', rho, 'pres =', pres
vel(2), &
vel(3), &
pres, & ! Out of tolerance
gamma, &
pi_inf, &
qv, &
c, &
accel
FMT = '(6X,F12.6,10'//FMT_glb//')'
write (i + 30, FMT) &
nondim_time, &
rho, &
vel(1), &
vel(2), &
vel(3), &
pres, & ! Out of tolerance
gamma, &
pi_inf, &
qv, &
c, &
accel

end if
else
write (i + 30, '(6X,F12.6,F24.8,F24.8,F24.8,F24.8,'// &
'F24.8,F24.8,F24.8,F24.8,F24.8,'// &
'F24.8)') &
FMT = '(6X,F12.6,11'//FMT_glb//')'
write (i + 30, FMT) &
nondim_time, &
rho, &
vel(1), &

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 3D probe format mismatch and dropped c/accel columns

The 3‑D branch now reuses the same '(6X,F12.6,11'//FMT_glb//')' format but only supplies eight scalars (ρ, the three velocities, p, γ, π∞ and qv). That causes the runtime to raise an I/O list length error, and even if the program survived, the sound speed and acceleration columns that existed before this commit are no longer emitted so probe goldens cannot check them. Restore the missing values (c and accel) or reduce the format width so the number of descriptors matches the output list.

Useful? React with 👍 / 👎.

Expand All @@ -1608,9 +1625,7 @@ contains
pres, &
gamma, &
pi_inf, &
qv, &
c, &
accel
qv
end if
end if
end do
Expand Down
2 changes: 0 additions & 2 deletions src/simulation/m_derived_variables.fpp
Original file line number Diff line number Diff line change
Expand Up @@ -168,9 +168,7 @@ contains
$:GPU_UPDATE(host='[accel_mag]')

call s_derive_center_of_mass(q_prim_ts2(2)%vf, c_mass)

call s_write_probe_files(t_step, q_cons_ts(1)%vf, accel_mag)

call s_write_com_files(t_step, c_mass)
end if

Expand Down
4 changes: 4 additions & 0 deletions toolchain/mfc/packer/pack.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@ def _extract_doubles(s: str) -> list:
lines = content.splitlines()
content = "\n".join(lines[1:]) # Skip the first line
doubles = _extract_doubles(content)
elif 'probe' in short_filepath:
# Probes are constructed as <time> <value1> <value2> ...
# Just discard the first column
doubles = [float(e) for line in content.strip().splitlines() if line.strip() for e in _extract_doubles(line)[1:]]
else:
# Every line is <x> <y> <z> <value> (<y> and <z> are optional). So the
# number of dimensions is the number of doubles in the first line minus 1.
Expand Down
30 changes: 29 additions & 1 deletion toolchain/mfc/test/case.py
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

High-level Suggestion

The automatic probe placement at 1/3 and 2/3 of the domain is a generic heuristic that might miss key physical phenomena at boundaries or interfaces. Consider making this feature opt-in or providing easier override mechanisms to ensure probes monitor relevant regions. [High-level, importance: 8]

Solution Walkthrough:

Before:

# In toolchain/mfc/test/case.py
# ...
# This logic is applied to all test cases by default (opt-out)
if 'probe_wrt' not in case:
    mods['probe_wrt'] = 'T'
    if case_is_1D:
        mods['num_probes'] = 2
        # Place 2 probes at 1/3 and 2/3 of the domain
    elif case_is_2D:
        mods['num_probes'] = 4
        # Place 4 probes in a 2x2 grid at 1/3 and 2/3 of domain
    else: # 3D
        mods['num_probes'] = 8
        # Place 8 probes in a 2x2x2 grid at 1/3 and 2/3 of domain

After:

# In toolchain/mfc/test/case.py
# ...
# Suggestion: Make this an opt-in feature
if case.get('auto_probes', False):
    mods['probe_wrt'] = 'T'
    if case_is_1D:
        mods['num_probes'] = 2
        # Place 2 probes at 1/3 and 2/3 of the domain
    elif case_is_2D:
        mods['num_probes'] = 4
        # Place 4 probes in a 2x2 grid at 1/3 and 2/3 of domain
    else: # 3D
        mods['num_probes'] = 8
        # Place 8 probes in a 2x2x2 grid at 1/3 and 2/3 of domain
# Individual tests would need to set `auto_probes: True` to enable this.

Original file line number Diff line number Diff line change
Expand Up @@ -230,8 +230,36 @@ def create_directory(self):
mods['omega_wrt(2)'] = 'T'
mods['omega_wrt(3)'] = 'T'
else:
mods['parallel_io'] = 'F'
mods['parallel_io'] = 'F'
mods['prim_vars_wrt'] = 'F'
if ('fd_order' not in case) and ('fd_order' not in mods):
mods['fd_order'] = 1
if ('probe_wrt' not in case):
mods['probe_wrt'] = 'T'
if case['n'] == 0:
mods['num_probes'] = 2
dx = (case['x_domain%end'] - case['x_domain%beg']) / 3
for i in range(2):
mods[f'probe({{i+1}})%x'] = case['x_domain%beg'] + (i+1) * dx
elif case['p'] == 0:
mods['num_probes'] = 4
dx = (case['x_domain%end'] - case['x_domain%beg']) / 3
dy = (case['y_domain%end'] - case['y_domain%beg']) / 3
for i in range(2):
for j in range(2):
mods[f'probe({{i*2+j+1}})%x'] = case['x_domain%beg'] + (i+1) * dx
mods[f'probe({{i*2+j+1}})%y'] = case['y_domain%beg'] + (j+1) * dy
else:
mods['num_probes'] = 8
dx = (case['x_domain%end'] - case['x_domain%beg']) / 3
dy = (case['y_domain%end'] - case['y_domain%beg']) / 3
dz = (case['z_domain%end'] - case['z_domain%beg']) / 3
for i in range(2):
for j in range(2):
for k in range(2):
mods[f'probe({{i*4+j*2+k+1}})%x'] = case['x_domain%beg'] + (i+1) * dx
mods[f'probe({{i*4+j*2+k+1}})%y'] = case['y_domain%beg'] + (j+1) * dy
mods[f'probe({{i*4+j*2+k+1}})%z'] = case['z_domain%beg'] + (k+1) * dz

Comment on lines +233 to 263
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

Auto probe defaults are reasonable but rely on domain keys being present

The new else-branch behavior does a few important things for tests:

  • Forces serial IO (parallel_io = 'F') and disables primitive-output (prim_vars_wrt = 'F') for non‑post_process runs.
  • Sets fd_order = 1 only if it isn’t already present in the base case or prior mods, which is a safe default for tests.
  • Enables probes by default (probe_wrt = 'T' if missing) and places them at 1/3 and 2/3 of each domain extent (2 probes in 1D, 4 in 2D, 8 in 3D), giving consistent coverage across all test cases.

Two things to double‑check:

  1. Domain-key assumptions: This logic assumes case always contains x_domain%beg/end (and y_domain%..., z_domain%... where needed). If any legacy or corner-case test lacks these keys, the generated case.py will crash when computing dx/dy/dz. You may want to either:

    • Add a small guard (e.g., fall back to a unit interval if keys are missing), or
    • Explicitly document that all test cases must define these domain keys.
  2. Default probe_wrt behavior: All non‑post_process runs will now emit probes unless the base case explicitly sets probe_wrt. That’s consistent with the new probe testing infrastructure, but if any existing tests were relying on no probes, they should be updated to set probe_wrt = 'F' explicitly.

Overall the structure is sound; just ensure the domain-key assumption holds across the full test suite.


I'll verify the concerns raised in the review comment by examining the context around the code snippet and checking for domain key initialization patterns.


---

🏁 Script executed:

```shell
#!/bin/bash
# Get broader context around lines 233-263 in case.py
cat toolchain/mfc/test/case.py | head -300 | tail -100

Length of output: 4017


🏁 Script executed:

#!/bin/bash
# Search for domain key initialization patterns in case.py
rg -n "x_domain|y_domain|z_domain" toolchain/mfc/test/case.py -B2 -A2 | head -50

Length of output: 1453


🏁 Script executed:

#!/bin/bash
# Look for any guards or error handling around domain key access
rg -n "KeyError|try:|except|in case" toolchain/mfc/test/case.py | head -30

Length of output: 103


🏁 Script executed:

#!/bin/bash
# Check the function signature and initial case dict population
rg -n "def |case\s*=" toolchain/mfc/test/case.py | head -20

Length of output: 1037



---

🏁 Script executed:

```shell
#!/bin/bash
# Find the gen_json_dict_str method definition
rg -n "def gen_json_dict_str|gen_json_dict_str" toolchain/mfc/test/case.py -B3 -A10 | head -40

Length of output: 526


🏁 Script executed:

#!/bin/bash
# Search for test case definitions and domain key initialization
rg -n "x_domain|domain" toolchain/mfc/test/ -l | head -10

Length of output: 54


🏁 Script executed:

#!/bin/bash
# Look for where TestCase instances are created with mods
rg -n "define_case_f|mods\s*=" toolchain/mfc/test/ --type=py -B2 -A5 | head -60

Length of output: 3759


🏁 Script executed:

#!/bin/bash
# Check if there are any test cases in the test directory
find toolchain/mfc/test -name "*.py" -type f | grep -v __pycache__

Length of output: 112



---

🏁 Script executed:

```shell
#!/bin/bash
# Search for TestCaseBuilder class and gen_json_dict_str method
rg -n "class TestCaseBuilder|def gen_json_dict_str" toolchain/mfc/test/case.py -A15 | head -80

Length of output: 523


🏁 Script executed:

#!/bin/bash
# Look for actual test case definitions with domain info in cases.py
rg -n "x_domain|y_domain|z_domain|define_case" toolchain/mfc/test/cases.py -B2 -A2 | head -100

Length of output: 5227


🏁 Script executed:

#!/bin/bash
# Search for examples dir to see actual case.py files that might lack domain keys
find examples -name "case.py" -type f 2>/dev/null | head -5

Length of output: 209


🏁 Script executed:

#!/bin/bash
# Examine one example case file to see domain key patterns
find examples -name "case.py" -type f 2>/dev/null | head -1 | xargs cat | head -80

Length of output: 1605



---

🏁 Script executed:

```shell
#!/bin/bash
# Search for gen_json_dict_str in the entire codebase
rg -n "gen_json_dict_str" toolchain/mfc/test/ -B2 -A8

Length of output: 700


🏁 Script executed:

#!/bin/bash
# Look for TestCase class definition and __init__ method
rg -n "class TestCase|def __init__" toolchain/mfc/test/case.py -A12 | head -60

Length of output: 1534


🏁 Script executed:

#!/bin/bash
# Check a few more example case files to see if they all have domain keys
find examples -name "case.py" -type f 2>/dev/null | head -3 | xargs -I {} sh -c 'echo "=== {} ===" && grep -E "x_domain|y_domain|z_domain" {} || echo "NO DOMAIN KEYS"'

Length of output: 351


🏁 Script executed:

#!/bin/bash
# Look at cases.py to see if there's any test that passes custom mods with domain keys
rg -n "x_domain|y_domain|z_domain" toolchain/mfc/test/cases.py -B3 -A3

Length of output: 1979



---

🏁 Script executed:

```shell
#!/bin/bash
# Search for BASE_CFG definition
rg -n "BASE_CFG" toolchain/mfc/test/case.py -B5 -A10

Length of output: 1307


🏁 Script executed:

#!/bin/bash
# Look for case.Case import and find where it's defined
rg -n "^import|^from.*case|class Case" toolchain/mfc/test/case.py | head -20

Length of output: 181


🏁 Script executed:

#!/bin/bash
# Search for BASE_CFG and gen_json_dict_str in the input module (likely case.Case parent)
find toolchain -name "*.py" -type f | xargs rg -l "BASE_CFG|gen_json_dict_str" | head -5

Length of output: 48


🏁 Script executed:

#!/bin/bash
# Check if any test case in cases.py is defined without domain keys
rg -n "define_case_d|define_case_f" toolchain/mfc/test/cases.py -A3 | grep -v "x_domain\|y_domain\|z_domain" | head -50

Length of output: 1874



---

🏁 Script executed:

```shell
#!/bin/bash
# Get full BASE_CFG definition
rg -n "BASE_CFG = {" toolchain/mfc/test/case.py -A100 | head -120

Length of output: 87


🏁 Script executed:

#!/bin/bash
# Search for gen_json_dict_str in parent case.Case
rg -n "def gen_json_dict_str" toolchain/mfc/case.py -B3 -A15

Length of output: 631


🏁 Script executed:

#!/bin/bash
# Check for any test that might trigger probe placement without domain keys
# Look for cases with n=0 or p=0 and empty or minimal mods
rg -n "define_case_d.*n.*0|define_case_f" toolchain/mfc/test/cases.py -B2 -A5 | head -80

Length of output: 4077


🏁 Script executed:

#!/bin/bash
# Search for any get_parameters or similar that might show what goes into case dict
rg -n "get_parameters|params" toolchain/mfc/test/case.py -B2 -A5 | head -40

Length of output: 1842



---

🏁 Script executed:

```shell
#!/bin/bash
# Get BASE_CFG using cat instead of rg regex
sed -n '14,150p' toolchain/mfc/test/case.py

Length of output: 5523


🏁 Script executed:

#!/bin/bash
# Search for x_domain in BASE_CFG section
rg -n "x_domain" toolchain/mfc/test/case.py -B3 -A3

Length of output: 1447


🏁 Script executed:

#!/bin/bash
# Check TestCaseBuilder.to_case() to see how it loads params
rg -n "def to_case" toolchain/mfc/test/case.py -A30

Length of output: 1038


🏁 Script executed:

#!/bin/bash
# Look for test cases that might have n=0 or p=0 (1D/2D) without explicit domain mods
rg -n "stack.push.*1D|n.*:.*0|p.*:.*0" toolchain/mfc/test/cases.py -B2 -A8 | head -100

Length of output: 6890


Add guards for missing domain keys or document requirement

The code at lines 241, 246–256, 260–262 accesses domain keys (x_domain%beg/end, y_domain%beg/end, z_domain%beg/end) without validation. While most test cases populate these keys via stack setup, BASE_CFG lacks domain keys. If a test case is created without proper domain initialization (e.g., via custom define_case_f with a path or mods that omit domain keys), accessing these keys will raise KeyError at runtime.

Recommendations:

  1. Add defensive checks before accessing domain keys (use .get() with fallback values or explicit key validation), or
  2. Explicitly document that all test case definitions must include domain keys for their respective dimensions (1D→x_domain, 2D→x/y_domain, 3D→x/y/z_domain), and validate this invariant early in case setup.

print(json.dumps({{**case, **mods}}))
""")
Expand Down
Loading