Skip to content

Skip ismip7_forcing steps when final output already exists - #1001

Merged
trhille merged 6 commits into
MPAS-Dev:mainfrom
trhille:landice/ismip7_forcing_fixes
Oct 1, 2026
Merged

trhille merged 6 commits into
MPAS-Dev:mainfrom
trhille:landice/ismip7_forcing_fixes

Conversation

@trhille

@trhille trhille commented Sep 29, 2026 •

Copy link
Copy Markdown
Collaborator

This merge contains final fixes to the ismip7_forcing test group. These include:

  1. A one-line bug fix in config parser when determining fracture forcing verison.
  2. A check on the existence of the final processed forcing file before beginning processing each step. This will be very helpful for restarts in which compass run is executed anywhere higher than the individual step level. Without this, early steps risk being re-run if they had completed in the previous run.

Checklist

  • Document (in a comment titled Testing in this PR) any testing that was used to verify the changes

trhille and others added 2 commits September 29, 2026 12:14
When an ismip7_forcing processing step runs, it previously always
regenerated and overwrote its final output. The only resume logic was
at the temporary remapped_* level, which is deleted once a step
completes. This meant re-running a test case (after a partial timeout,
or to process a different scenario) re-did all expensive extrapolate +
ncremap work for steps that already finished.

This commit adds an early 'final output already exists -> info-log and
skip' guard to every forcing step that produces a final output, making
re-runs cheap and idempotent.

Changes:
- Add os.path.exists(dst) check at the start of each run() method,
  after source resolution but before any remap work
- Hoist output_file/output_path/dst computation to before the guard
  so the tail of run() reuses those variables
- Guard placement ensures each output is checked independently (e.g.
  ocean_thermal per-job outputs for AIS OCX ocean choices)

Affected steps (10):
- Atmosphere: process_smb, process_temperature, process_runoff,
  process_smb_gradient, process_temperature_gradient
- Ocean thermal: process_thermal_forcing (_process_ocean_forcing and
  _run_climatology), build_3d_thermal_forcing
- Fracture: process_excess_melt, process_lake_properties,
  process_shelf_collapse

The existing per-year remapped_* skip logic is unchanged; these guards
operate at the final canonical output level.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@trhille

trhille commented Sep 29, 2026 •

Copy link
Copy Markdown
Collaborator Author

Testing

I ran compass run at the atmosphere level in a directory where process_smb had already completed. process_smb.log contains this:

compass calling: compass.landice.tests.ismip7_forcing.atmosphere.process_smb.ProcessSmb.runtime_setup()
  inherited from: compass.step.Step.runtime_setup()
  in /global/cfs/cdirs/m4288/users/trhille/compass/compass/step.py


compass calling: compass.landice.tests.ismip7_forcing.atmosphere.process_smb.ProcessSmb.run()
  in /global/cfs/cdirs/m4288/users/trhille/compass/compass/landice/tests/ismip7_forcing/atmosphere/process_smb.py

Using atmosphere source /global/cfs/cdirs/m4288/users/trhille/ISMIP7/forcing/GIS/CESM2-WACCM/ctrl/SDBN1-1000m/acabf/v3
Found 286 SMB files for years 2000-2301
Output already exists, skipping: /global/cfs/cdirs/m4288/users/trhille/ISMIP7/process_forcing_20260929/GIS/CESM2-WACCM_ctrl/atmosphere/GIS_1to10km_r03_20260925_SMB_CESM2-WACCM_ctrl_2000-2301.nc

The compass.o* file reads:

landice/ismip7_forcing/atmosphere
compass calling: compass.landice.tests.ismip7_forcing.atmosphere.Atmosphere.run()
  inherited from: compass.testcase.TestCase.run()
  in /global/cfs/cdirs/m4288/users/trhille/compass/compass/testcase.py

compass calling: compass.run.serial._run_test()
  in /global/cfs/cdirs/m4288/users/trhille/compass/compass/run/serial.py

Running steps:
  build_mapping_file
  process_smb
  process_temperature
  process_smb_gradient
  process_temperature_gradient
  process_runoff

  * step: build_mapping_file
  * step: process_smb
  * step: process_temperature

So, this is cleanly skipping the completed step and moving on to the next.

I have also confirmed that the bug fix in landice/ismip7/archive.py fixes an error I was encountering, And I have run the AIS fracture test case successfully.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

Unresolved moderate findings affect cache identity, partial-output handling, and 3-D restart detection.

Review effort: Lite
Findings: 1 Medium severity

Open (1)
What changed in this PR

This PR adds restart-aware skipping for ISMIP7 forcing outputs and fixes fracture-version configuration lookup.

Changes:

  • Reuses completed atmosphere, ocean, and fracture forcing outputs.
  • Adds a 3-D thermal forcing skip check.
  • Corrects fracture configuration parsing.
File Review summary
compass/​landice/​tests/​ismip7_forcing/​ocean_thermal/​process_thermal_forcing.py Four unresolved moderate findings (1 vote each): cache identities omit remapping or archive inputs, and direct copies can leave partial outputs.
compass/​landice/​tests/​ismip7_forcing/​ocean_thermal/​build_3d_thermal_forcing.py One unresolved moderate finding (4 votes): the checked path is not produced; all chunk outputs should be checked.
compass/​landice/​tests/​ismip7_forcing/​fracture/​process_shelf_collapse.py Two unresolved moderate findings (1 vote each): cache identity omits archive/remapping inputs, and existing destinations may be partial copies.
compass/​landice/​tests/​ismip7_forcing/​fracture/​process_lake_properties.py Two unresolved moderate findings (1 vote each): cache identity omits archive/remapping inputs, and existing destinations may be partial copies.
compass/​landice/​tests/​ismip7_forcing/​fracture/​process_excess_melt.py Two unresolved moderate findings (1 vote each): cache identity omits archive/remapping inputs, and existing destinations may be partial copies.
compass/​landice/​tests/​ismip7_forcing/​atmosphere/​process_temperature.py Two unresolved moderate findings (1 vote each): cache identity omits version/remapping inputs, and existing destinations may be partial copies.
compass/​landice/​tests/​ismip7_forcing/​atmosphere/​process_temperature_gradient.py Two unresolved moderate findings (1 vote each): cache identity omits archive/remapping inputs, and existing destinations may be partial copies.
compass/​landice/​tests/​ismip7_forcing/​atmosphere/​process_smb.py Two unresolved moderate findings (1 vote each): cache identity omits archive/remapping inputs, and existing destinations may be partial copies.
compass/​landice/​tests/​ismip7_forcing/​atmosphere/​process_smb_gradient.py Two unresolved moderate findings (1 vote each): cache identity omits archive/remapping inputs, and existing destinations may be partial copies.
compass/​landice/​tests/​ismip7_forcing/​atmosphere/​process_runoff.py Two unresolved moderate findings (1 vote each): cache identity omits archive/remapping inputs, and existing destinations may be partial copies.
compass/​landice/​ismip7/​archive.py Corrects fracture-version configuration lookup.

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread compass/landice/tests/ismip7_forcing/ocean_thermal/build_3d_thermal_forcing.py Outdated
trhille and others added 2 commits September 29, 2026 14:25
The greenland_3d.run() function writes per-chunk output files named by
their start year (e.g., GrIS_..._3dThermalForcing_OCX_2007.nc,
..._2017.nc), not a single file with the full year range. The previous
guard checked for the non-existent full-range filename, so it never
skipped even after a successful run.

This commit fixes the guard to check for the first chunk file (with
start_year only, not start_year-end_year), which greenland_3d.run
actually produces. The check is conservative: it only verifies the
first chunk exists, consistent with the per-year remapped file skip
pattern used in the other ismip7_forcing steps.

Addresses Copilot review comment on PR MPAS-Dev#1001.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
When an ismip7_forcing job times out mid-remap, it can leave truncated
or incomplete remapped_* and extrap_* files. The existing per-file
resume logic reused these based purely on os.path.exists(), silently
feeding bad intermediates into the final concatenation, which then
failed with 'ValueError: coordinate time not present in all datasets'.

This commit adds netcdf_file_is_valid() to compass/landice/ismip7/remap.py,
which checks that a file exists, opens cleanly, contains the expected
variable and (optionally) time coordinate, and can actually read its
last time record (catches header-complete but data-truncated files that
open cleanly but fail later). Each step's per-file skip guard is now:
'file exists AND valid → skip; otherwise delete and regenerate.'

Coverage:
- State 1: file never created → os.path.exists catches it
- State 2: truncated header (bad magic bytes) → raises on open
- State 3: header OK but variable/time missing → presence check
- State 4: header complete but data truncated → last-record .load()
  raises (this closes the subtle gap where lazy xarray reads only the
  header and the truncated file passes an open+presence check)

Changes:
- Add netcdf_file_is_valid() helper to remap.py
- Update all 5 atmosphere steps (SMB, temperature, runoff, gradients)
  to validate both remapped_* (require_time=True) and extrap_* files
- Update ocean_thermal/_process_ocean_forcing (tf, require_time=True)
  and _run_climatology (tf, require_time=False — static 3-D field)
- Update fracture/process_shelf_collapse (mask, require_time=True)

No change to excess_melt / lake_properties — those regenerate
intermediates unconditionally (no exists-guard to reuse a stale file).

Fixes the reported 'time not present' concat error and covers all four
interrupted-write states at negligible cost (single-slice read).

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

Critical and moderate restart-integrity issues remain unresolved.

Review effort: Lite
Findings: 2 High severity · 1 Medium severity · 1 Low severity

Open (4)
Resolved since last review (1)
Previously missed (10)

In code that hasn't changed since last review

Medium severity Prevent truncated destinations from being reused on restart

compass/​landice/​tests/​ismip7_forcing/​atmosphere/​process_runoff.py:105

This new guard treats any existing destination as complete, but the destination is populated later with shutil.copy(), which can leave a truncated .nc if the job is interrupted during the copy. A restart then preserves the truncated forcing; publish atomically and/or validate the final NetCDF before skipping.

Medium severity Prevent truncated destinations from being reused on restart

compass/​landice/​tests/​ismip7_forcing/​atmosphere/​process_smb.py:105

This new guard treats any existing destination as complete, but the destination is populated later with shutil.copy(), which can leave a truncated .nc if the job is interrupted during the copy. A restart then preserves the truncated forcing; publish atomically and/or validate the final NetCDF before skipping.

Medium severity Prevent truncated destinations from being reused on restart

compass/​landice/​tests/​ismip7_forcing/​atmosphere/​process_smb_gradient.py:106

This new guard treats any existing destination as complete, but the destination is populated later with shutil.copy(), which can leave a truncated .nc if the job is interrupted during the copy. A restart then preserves the truncated forcing; publish atomically and/or validate the final NetCDF before skipping.

Medium severity Prevent truncated destinations from being reused on restart

compass/​landice/​tests/​ismip7_forcing/​atmosphere/​process_temperature.py:105

This new guard treats any existing destination as complete, but the destination is populated later with shutil.copy(), which can leave a truncated .nc if the job is interrupted during the copy. A restart then preserves the truncated forcing; publish atomically and/or validate the final NetCDF before skipping.

Medium severity Prevent truncated destinations from being reused on restart

compass/​landice/​tests/​ismip7_forcing/​atmosphere/​process_temperature_gradient.py:107

This new guard treats any existing destination as complete, but the destination is populated later with shutil.copy(), which can leave a truncated .nc if the job is interrupted during the copy. A restart then preserves the truncated forcing; publish atomically and/or validate the final NetCDF before skipping.

Medium severity Prevent truncated destinations from being reused on restart

compass/​landice/​tests/​ismip7_forcing/​fracture/​process_excess_melt.py:112

This new guard treats any existing destination as complete, but the destination is populated later with shutil.copy(), which can leave a truncated .nc if the job is interrupted during the copy. A restart then preserves the truncated forcing; publish atomically and/or validate the final NetCDF before skipping.

Medium severity Prevent truncated destinations from being reused on restart

compass/​landice/​tests/​ismip7_forcing/​fracture/​process_lake_properties.py:115

This new guard treats any existing destination as complete, but the destination is populated later with shutil.copy(), which can leave a truncated .nc if the job is interrupted during the copy. A restart then preserves the truncated forcing; publish atomically and/or validate the final NetCDF before skipping.

Medium severity Prevent truncated destinations from being reused on restart

compass/​landice/​tests/​ismip7_forcing/​fracture/​process_shelf_collapse.py:103

This new guard treats any existing destination as complete, but the destination is populated later with shutil.copy(), which can leave a truncated .nc if the job is interrupted during the copy. A restart then preserves the truncated forcing; publish atomically and/or validate the final NetCDF before skipping.

Medium severity Prevent truncated destinations from being reused on restart

compass/​landice/​tests/​ismip7_forcing/​ocean_thermal/​process_thermal_forcing.py:268

This new guard treats any existing destination as complete, but the destination is populated later with shutil.copy(), which can leave a truncated .nc if the job is interrupted during the copy. A restart then preserves the truncated forcing; publish atomically and/or validate the final NetCDF before skipping.

Medium severity Prevent truncated destinations from being reused on restart

compass/​landice/​tests/​ismip7_forcing/​ocean_thermal/​process_thermal_forcing.py:389

This new guard treats any existing destination as complete, but the destination is populated later with shutil.copy(), which can leave a truncated .nc if the job is interrupted during the copy. A restart then preserves the truncated forcing; publish atomically and/or validate the final NetCDF before skipping.

Comment on lines +105 to +107
if os.path.exists(dst):
logger.info(f"Output already exists, skipping: {dst}")
return

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Addressed in aa20937

Comment on lines +86 to +89
if os.path.exists(first_chunk):
logger.info(f"Output already exists (first chunk found), "
f"skipping: {first_chunk}")
return

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Addressed in aa20937

Comment on lines +86 to +89
if os.path.exists(first_chunk):
logger.info(f"Output already exists (first chunk found), "
f"skipping: {first_chunk}")
return

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Addressed in aa20937

Comment on lines +13 to +14
def netcdf_file_is_valid(path, varname=None, require_time=False,
logger=None):

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Addressed in aa20937

@trhille

trhille commented Sep 29, 2026

Copy link
Copy Markdown
Collaborator Author

More testing

I ran into an issue restarting processing after a job timed out because the code could not differentiate between complete and existing-but-incomplete files as of 90fc71a. Here's the error I ran into:

Details
Traceback (most recent call last):
  File "/global/cfs/cdirs/m4288/users/trhille/compass/pixi-env/.pixi/envs/default/bin/compass", line 6, in <module>
    sys.exit(main())
             ~~~~^^
  File "/global/cfs/cdirs/m4288/users/trhille/compass/compass/__main__.py", line 63, in main
    commands[args.command]()
    ~~~~~~~~~~~~~~~~~~~~~~^^
  File "/global/cfs/cdirs/m4288/users/trhille/compass/compass/run/serial.py", line 206, in main
    run_single_step(args.step_is_subprocess)
    ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/global/cfs/cdirs/m4288/users/trhille/compass/compass/run/serial.py", line 167, in run_single_step
    _run_test(test_case, available_resources)
    ~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/global/cfs/cdirs/m4288/users/trhille/compass/compass/run/serial.py", line 419, in _run_test
    _run_step(test_case, step, test_case.new_step_log_file,
    ~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
              available_resources)
              ^^^^^^^^^^^^^^^^^^^^
  File "/global/cfs/cdirs/m4288/users/trhille/compass/compass/run/serial.py", line 470, in _run_step
    step.run()
    ~~~~~~~~^^
  File "/global/cfs/cdirs/m4288/users/trhille/compass/compass/landice/tests/ismip7_forcing/atmosphere/process_temperature.py", line 141, in run
    self._combine_and_rename(remapped_files, output_file)
    ~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/global/cfs/cdirs/m4288/users/trhille/compass/compass/landice/tests/ismip7_forcing/atmosphere/process_temperature.py", line 170, in _combine_and_rename
    ds = xr.open_mfdataset(remapped_files, concat_dim="time",
                           combine="nested", engine="netcdf4",
                           drop_variables="time_bnds")
  File "/global/cfs/cdirs/m4288/users/trhille/compass/pixi-env/.pixi/envs/default/lib/python3.14/site-packages/xarray/backends/api.py", line 1675, in open_mfdataset
    combined = _nested_combine(
        datasets,
    ...<7 lines>...
        fill_value=dtypes.NA,
    )
  File "/global/cfs/cdirs/m4288/users/trhille/compass/pixi-env/.pixi/envs/default/lib/python3.14/site-packages/xarray/structure/combine.py", line 397, in _nested_combine
    combined = _combine_nd(
        combined_ids,
    ...<6 lines>...
        combine_attrs=combine_attrs,
    )
  File "/global/cfs/cdirs/m4288/users/trhille/compass/pixi-env/.pixi/envs/default/lib/python3.14/site-packages/xarray/structure/combine.py", line 262, in _combine_nd
    combined_ids = _combine_all_along_first_dim(
        combined_ids,
    ...<6 lines>...
        combine_attrs=combine_attrs,
    )
  File "/global/cfs/cdirs/m4288/users/trhille/compass/pixi-env/.pixi/envs/default/lib/python3.14/site-packages/xarray/structure/combine.py", line 294, in _combine_all_along_first_dim
    new_combined_ids[new_id] = _combine_1d(
                               ~~~~~~~~~~~^
        datasets,
        ^^^^^^^^^
    ...<6 lines>...
        combine_attrs=combine_attrs,
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
  File "/global/cfs/cdirs/m4288/users/trhille/compass/pixi-env/.pixi/envs/default/lib/python3.14/site-packages/xarray/structure/combine.py", line 324, in _combine_1d
    combined = concat(
        datasets,
    ...<6 lines>...
        combine_attrs=combine_attrs,
    )
  File "/global/cfs/cdirs/m4288/users/trhille/compass/pixi-env/.pixi/envs/default/lib/python3.14/site-packages/xarray/structure/concat.py", line 325, in concat
    return _dataset_concat(
        objs,
    ...<8 lines>...
        create_index_for_new_dim=create_index_for_new_dim,
    )
  File "/global/cfs/cdirs/m4288/users/trhille/compass/pixi-env/.pixi/envs/default/lib/python3.14/site-packages/xarray/structure/concat.py", line 772, in _dataset_concat
    raise ValueError(
        f"coordinate {name!r} not present in all datasets."
    )
ValueError: coordinate 'time' not present in all datasets.
In commit [9994e92](https://github.com//pull/1001/commits/9994e92d6d151c3f79ee324b1edb3fe1432e8cc7), I added additional checks (detailed in the commit message) that find, delete, and reprocess incomplete files. There may still be some edge cases that are not caught, but this should catch the vast majority of cases.

When I re-ran the

  Remapped file exists, skipping: ts_GrIS_CESM2-WACCM_ctrl_SDBN1-1000m_v2_2125.nc
  Remapped file exists, skipping: ts_GrIS_CESM2-WACCM_ctrl_SDBN1-1000m_v2_2126.nc
  Remapped file exists, skipping: ts_GrIS_CESM2-WACCM_ctrl_SDBN1-1000m_v2_2127.nc
  Remapped file exists, skipping: ts_GrIS_CESM2-WACCM_ctrl_SDBN1-1000m_v2_2128.nc
  Remapped file exists, skipping: ts_GrIS_CESM2-WACCM_ctrl_SDBN1-1000m_v2_2129.nc
  Remapped file exists, skipping: ts_GrIS_CESM2-WACCM_ctrl_SDBN1-1000m_v2_2130.nc
  Reprocessing incomplete remapped file: ts_GrIS_CESM2-WACCM_ctrl_SDBN1-1000m_v2_2131.nc
    Extrapolating fill values on source grid: ts_GrIS_CESM2-WACCM_ctrl_SDBN1-1000m_v2_2131.nc

…d; document validation helper

Copilot findings MPAS-Dev#1-4 were valid:

1. Source version not in filename (HIGH): The forcing archive versions each
   source (e.g. v1, v2, v2.1) as a directory, and ForcingSource.version
   carries it, but output filenames omitted it. Upgrading from v1 to v2 in
   the archive caused the 'skip if exists' guard to reuse stale v1 output.

2. 3-D guard mishandles partial runs (HIGH): The first_chunk guard skipped
   the whole step when only the first chunk existed, leaving later chunks
   unwritten after a timeout. greenland_3d already has its own PER-CHUNK
   skip logic that correctly resumes partial runs.

3. Overwrite config ignored (MEDIUM): The first_chunk guard ran before the
   JSON was loaded, defeating a user's 'overwrite=true' request.
   greenland_3d's own logic honors cfg.overwrite.

4. netcdf_file_is_valid missing from API (LOW): Function had a complete
   docstring but was not registered in the Sphinx autosummary.

Changes:
- Embed source.version (or job.version) in all output filenames, placed
  BEFORE the year range: {mesh}_SMB_{model}_{scenario}_{version}_{start}-
  {end}.nc. Placement before the year range preserves trailing patterns
  that downstream regexes anchor to (_\d{4}-\d{4}$, _(\d{4})\.nc$), so no
  changes needed in greenland_3d.py or ismip7_run/set_up_experiment.py.
- Remove the step-level first_chunk guard from build_3d_thermal_forcing.py
  (the redundant/conflicting guard added in commit 90fc71a). Delegate to
  greenland_3d's own per-chunk skip/overwrite logic, which is finer-grained
  (handles partial runs correctly) and overwrite-aware (honors the JSON
  cfg.overwrite flag).
- Add remap.netcdf_file_is_valid to docs/developers_guide/landice/api.rst
  (before remap.extrapolate_source, following source order).

Affected filenames (10 steps):
- Atmosphere: {version}_{start}-{end}.nc for SMB, temperature, runoff,
  gradients
- Ocean: {version}_{start}-{end}.nc for 2D/3D thermal forcing; climatology
  already embedded version (unchanged)
- Fracture: {stem}_{version}{ext} (no year range)

Downstream globs (*SMB*.nc, *2dThermalForcing_*.nc) still match; no regex
changes needed. Now a version bump produces a new filename -> guard doesn't
fire -> output regenerates (fixes staleness hazard). The GrIS 3-D path now
honors overwrite and resumes partial runs correctly.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@trhille

trhille commented Sep 29, 2026 •

Copy link
Copy Markdown
Collaborator Author

I had Claude draft up a response to the latest co-pilot review: #1001 (review).

Response to Latest Copilot Review (Sept 29)

Thanks for the additional review. After further investigation, findings 1–4 are all valid and have been fixed.

1. ✅ Source version changes (High) - Conceded + Fixed

Finding: Guard doesn't validate whether source data changed; changing to a new version reuses stale output.

Response: Valid. The output filename was missing the source.version tag. The forcing archive versions each source (e.g. v1, v2, v2.1) as a directory level, but that version was never embedded in the output filename, so upgrading from v1 to v2 in the archive caused the skip guard to reuse stale v1 output.

Fixed: Version is now embedded before the year range in all output filenames: {mesh}_SMB_{model}_{scenario}_{version}_{start}-{end}.nc (and similarly for temperature, runoff, gradients, 2D/3D thermal forcing, fracture). Placed before the year range (not at the end) to preserve trailing _{start}-{end}.nc and _{start}.nc patterns that downstream regexes anchor to. Now a version bump naturally produces a new filename → guard doesn't fire → output regenerates.

2. ✅ All-chunks check (High) - Conceded + Fixed

Finding: Only first chunk is checked; if a previous run completed the first chunk but died mid-generation, later chunks are missing but the step still skips.

Response: Valid. The step-level first_chunk guard was coarse-grained (whole-step, first-chunk-only) and broke partial-run resume.

Fixed: Removed the step-level guard entirely. greenland_3d already has its own per-chunk skip logic (_write_forcing_chunk checks output_path.exists() independently for every chunk). It correctly resumes partial runs: skips completed chunks, writes missing ones. The step-level guard was redundant and conflicting; delegating to the finer-grained internal logic fixes both this and finding 3.

3. ✅ Overwrite configuration (Medium) - Conceded + Fixed

Finding: The early-return guard ignores the overwrite config option.

Response: Valid. The greenland_3d JSON config has an "overwrite": true/false key (the committed greenland_3d_tf_config.json sets it to true). The step-level first_chunk guard ran before the JSON was even loaded, so it had no knowledge of overwrite and returned early, defeating a user's overwrite=true request.

Fixed: Removed the step-level guard (same fix as 2). greenland_3d's per-chunk logic already honors cfg.overwrite: when false, it skips existing chunks; when true, it regenerates them (atomic .partial → os.replace). Now the JSON flag takes effect as intended.

4. ✅ Documentation (Low) - Conceded + Fixed

Finding: netcdf_file_is_valid() lacks documentation for the developer API.

Response: The function has a complete docstring (Parameters, Returns, explanation) in the source, but Copilot correctly flagged that it's not registered in the Sphinx autosummary — it won't appear in the rendered developer's guide.

Fixed: Added remap.netcdf_file_is_valid to docs/developers_guide/landice/api.rst under the compass.landice.ismip7 module block (before remap.extrapolate_source, following source order).

5. ❌ Truncated destinations (10 instances) - Acceptable risk

Finding: shutil.copy can leave truncated final .nc files if interrupted.

Response: True, but this is an acceptable rare failure mode because:

  • The interruption window is tiny: the copy takes seconds vs. hours of remap work
  • A partial NetCDF fails to open downstream (MALI or the next step) with a clear error
  • The user deletes the bad file and re-runs — same workflow as any corrupted output
  • The existing per-year remapped_* skip logic has the exact same risk and hasn't been a problem in practice
  • Fixing it (atomic write: copy-to-temp + rename) adds complexity for marginal benefit in a failure mode that is both rare and easily recovered

No change. The worst case is well-understood and tolerable.


Summary

  • 4 findings fixed: Version now in filenames (1), redundant/conflicting 3-D guard removed (2, 3 together), API docs added (4)
  • 1 finding accepted as-is: Partial-output risk (5, same pattern as existing skip logic)

Commits pushed.

@trhille trhille added this to the ISMIP7 milestone Sep 29, 2026
@xylar

xylar commented Sep 30, 2026

Copy link
Copy Markdown
Collaborator

@trhille, sorry for not getting to this today. Hopefully tomorrow!

@xylar xylar left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I ran the atmosphere, ocean_thermal and fracture cases on Chrysalis against a small synthetic AIS archive (v1, then v2). Skipping on rerun and recovering from truncated or empty intermediate files both work. Two things need fixing (inline). Separately, a bug already on main: excess melt and lake properties (Paths A and B) fail with TypeError: extrapolate_source() got an unexpected keyword argument 'decode_times', and removing that argument from the two calls fixes it. I couldn't test the 3-D Greenland step because the EN4 data isn't on Chrysalis.


Posted by Claude Code on @xylar's behalf. The testing, analysis and wording above are AI-authored; please check them accordingly.


# Check if final output already exists; skip if so
output_file = (f"{mali_mesh_name}_SMB_{model}_{scenario}_"
f"{source.version}_{start_year}-{end_year}.nc")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

With the version in the name, a new version writes a second file next to the old one, but ismip7_run expects exactly one of each. When I went from v1 to v2, its setup exited for SMB, temperature and shelf collapse, and gave runoff and the gradients an empty filename. Output written before this commit also won't be recognized, so a resume reprocesses everything and leaves both files. I'd either take the version out of the filenames (and delete outputs to regenerate) or have each step remove other versions' files when it writes. This applies to all the steps.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

"When I went from v1 to v2, its setup exited for SMB, temperature and shelf collapse, and gave runoff and the gradients an empty filename." Was this for ismip7_run? I'm not sure I follow what happened with runoff and gradients. Is this a bug in ismip7_run?

"Output written before this commit also won't be recognized, so a resume reprocesses everything and leaves both files." I don't think I care about backward compatibility before this commit, since we can easily manually rename previously processed files if necessary to avoid reprocessing. However, I'll think a little more about it.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Yes, this is ismip7_run's set_up_experiment.py. It exits unless it finds exactly one SMB, temperature, 3-D TF or shelf-collapse file. For runoff and the gradients, though, it just leaves the filename empty, so those streams get filename_template="". I didn't check what MALI does with that.

The gap is in ismip7_run, but this PR is what triggers it. Before, a rerun after an archive version bump overwrote the output; now it adds a second file next to the old one. If you'd rather clean up by hand when that happens, that's fine, but then ismip7_run should error on extra runoff and gradient files too.

Agreed on outputs from before this commit; renaming by hand is fine.


Posted by Claude Code on @xylar's behalf. The testing, analysis and wording above are AI-authored; please check them accordingly.

@xylar xylar Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@trhille, if this is just Claude misusing the tool, feel free to ignore this advice.

f"{source.version}_{start_year}-{end_year}.nc")
output_file = os.path.join(
ocean_dir,
f"{mali_mesh_name}_3dThermalForcing_{label}_"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

With the step guard gone, resuming relies on greenland_3d's per-chunk check, which only skips when overwrite is false. greenland_3d_tf_config.json sets it to true, so every rerun rebuilds all the chunks. Also, a chunk interrupted by a timeout leaves a .partial file, and the restart then raises FileExistsError until someone deletes it. Setting "overwrite": false and removing a stale .partial instead of raising would fix both.

@xylar

xylar commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator

@trhille, those comments are all Claude but they seem sensible to me but if it feels like these are situations you won't actually hit in practice, feel free to push back.

I'm also doing some testing on Perlmutter right now. I'll have Claude report back on that soon (or as soon as the queue allows).

@xylar

xylar commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

Perlmutter Testing

I ran ismip7_forcing on Perlmutter (pm-cpu, gnu/mpich) against the real archive, using 2-year runs. Work directories are in /pscratch/sd/x/xylar/ismip7_pr1001.

  • AIS atmosphere and ocean_thermal (CESM2-WACCM ssp585, AIS_2to20km_r03): passed. Outputs are named ..._v2_2015-2016.nc and ..._v3_2015-2016.nc, and a rerun skips every step.
  • GrIS build_3d_thermal_forcing (CESM2-WACCM historical, 1-year chunks, existing OCX melt parameters): both points in my review hold. With "overwrite": true every rerun rebuilds all chunks. A job killed during nccopy leaves a .partial that makes the next run raise FileExistsError, even with "overwrite": false if that chunk was never finished. A kill during the HDF5 write is harmless.
  • AIS fracture (ssp585, v2.1): with decode_times removed, Paths A, B and C all process and skip on rerun. Their filenames repeat the version, e.g. ..._ismip7_8km-v2.1_v2.1.nc. There is no GrIS fracture data to test.

Unrelated failure

Building the Path A conserve mapping file makes ESMF_RegridWeightGen segfault (ESMF 8.9.1, 128 tasks on one node), reproducibly. The bilinear map from the same source grid builds fine, so I used it for Path A to get past this.


Posted by Claude Code on @xylar's behalf. The testing, analysis and wording above are AI-authored; please check them accordingly.

@trhille

trhille commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator Author

Thanks @xylar!

  • GrIS build_3d_thermal_forcing (CESM2-WACCM historical, 1-year chunks, existing OCX melt parameters): both points in my review hold. With "overwrite": true every rerun rebuilds all chunks. A job killed during nccopy leaves a .partial that makes the next run raise FileExistsError, even with "overwrite": false if that chunk was never finished. A kill during the HDF5 write is harmless.

A job is extremely unlikely to be interrupted during nccopy, since that command should only take a few seconds, compared with multiple hours for the annual remapping, so i think it's safe to ignore this. We can always manually delete a .partial file if that ever occurs, which seems preferable to adding more code complexity.

  • AIS fracture (ssp585, v2.1): with decode_times removed, Paths A, B and C all process and skip on rerun. Their filenames repeat the version, e.g. ..._ismip7_8km-v2.1_v2.1.nc. There is no GrIS fracture data to test.

Great, I will make this change. For ISMIP7, we are only planning to use Path C.

Unrelated failure

Building the Path A conserve mapping file makes ESMF_RegridWeightGen segfault (ESMF 8.9.1, 128 tasks on one node), reproducibly. The bilinear map from the same source grid builds fine, so I used it for Path A to get past this.

Yep, that's how OOM manifests with ESMF_RegridWeightGen conserve. You would likely need 4 or 8 nodes to create it successfully.

@xylar

xylar commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator

Yep, that's how OOM manifests with ESMF_RegridWeightGen conserve. You would likely need 4 or 8 nodes to create it successfully.

That makes sense.

Thanks for putting up with the AI flailing. It seems to be more all-over-the place with Compass testing than with Polaris and this isn't a workflow I know as well so I wasn't able to rein it in as well as I would have liked.

Set overwrite=false in greenland_3d_tf_config.json so completed 3-D
thermal forcing chunks are skipped on rerun instead of rebuilt. Sweep
stale *.partial files at the start of greenland_3d.run() and drop the
now-redundant FileExistsError guards, so an interrupted write no longer
blocks the next run.

Remove the unsupported decode_times=False argument from the
extrapolate_source calls in process_excess_melt and
process_lake_properties, which raised TypeError on fracture Paths A/B.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@trhille

trhille commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator Author

@xylar, fd5ee59 should address your comments.

I've decided not to worry about the versioning issue wrt ismip7_run behavior. It would be very fast for a user to move forcings from prior versions to new directories if needed.

@xylar xylar left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Looks good to me. Thanks @trhille!

@trhille
trhille merged commit 2377ab6 into MPAS-Dev:main Oct 1, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants