Skip to content

Implement new output api - #744

Draft
Whyborn wants to merge 10 commits into
mainfrom
implement-new-output-API
Draft

Whyborn wants to merge 10 commits into
mainfrom
implement-new-output-API

Conversation

@Whyborn

@Whyborn Whyborn commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

CABLE

Thank you for submitting a pull request to the CABLE Project.

Description

This development was driven by me, but the actual code was written by Claude.

This implements the new output API as specified in the forum post and the PDF
CABLE-output-config.pdf. In essence, it gives the user much more fine grained control over how variables are output from the model.

This includes a YAML parser, rewriting of the CABLE diagnostics into a catalogue of variables and integration into the cable driver, both in MPI and serial.

I have directed Claude to chunk the commits into logical parts, to make it easier to review.

Type of change

[x] Structural development

Checklist

  • The new content is accessible and located in the appropriate section
  • I have checked that links are valid and point to the intended content
  • I have checked my code/text and corrected any misspellings

Testing

  • Are the changes bitwise-compatible with the main branch? If working on an optional feature, are the results bitwise-compatible when this feature is off? If yes, copy benchcab output showing successful completion of the bitwise compatibility tests or equivalent results below this line.

  • Are the changes non bitwise-compatible with the main branch because of a bug fix or a feature being newly implemented or improved? If yes, add the link to the modelevaluation.org analysis versus the main branch or equivalent results below this line.

Please add a reviewer when ready for review.


📚 Documentation preview 📚: https://cable--744.org.readthedocs.build/en/744/

Whyborn and others added 10 commits October 1, 2026 10:56
The output configuration will be a YAML file, so CABLE needs a YAML parser.
yaFyaml is used (the other Fortran YAML library tried could not represent lists
of mappings). It is found with find_package if installed, and otherwise fetched
with FetchContent together with gFTL and gFTL-shared, pinned to exact tags.

cable_yaml_mod wraps the library: the parsed document is copied into a plain
tree of cable_yaml_node_t values (scalar, mapping or sequence), so no yaFyaml
types are visible outside the module and the library can be replaced later.
The node type has its own deep-copy assignment because gfortran 13 mishandles
intrinsic assignment of recursive allocatable components.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017hNmrg1iRG449BqCN25afu
def_var gains two optional arguments, shuffle and deflate_level. The nf90
implementation applies them to variables with dimensions (scalars cannot be
compressed) when the level is above zero; this needs a NetCDF-4 file. The stub
ignores them. The ParallelIO implementation accepts them but stops with a clear
message if compression is requested, because its deflate call could not be
checked here.

The test writes a NetCDF-4 file and reads the settings back with the raw
netCDF library.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017hNmrg1iRG449BqCN25afu
The design settles on a single name for each concept, with no mapping between
internal and user-facing names:

  frequency:    timestep (was all), 3hrly, daily, monthly, yearly
  aggregation:  instant (was point), mean, max, min, sum
  reduction:    none, grid_cell_average, first_tile_on_cell (was
                first_patch_in_grid_cell), dominant_tile

cable_timing_mod now owns the frequency list and a rank function (finest to
coarsest), supports 3hrly and yearly, drops the custom userNNN intervals, and
has a date helper that turns a start date and a number of days into
YYYY-MM-DD. The old is_greater_than comparison is replaced by comparing ranks.

dominant_tile is a new tile reduction (the largest area fraction in a cell; the
first tile wins ties) for every type and rank, wired into the write dispatch.

The original output module and diagnostics are updated for the renames so the
code stays consistent. The old output%averaging values all, daily and monthly are
still accepted and translated in one place.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017hNmrg1iRG449BqCN25afu
…tions

What an output variable is, and where its data comes from, used to be one
hand-written Fortran call per variable in cable_diagnostics, mixed with the
logic that decided whether it was written. It is now split in two:

* output_catalogue.yaml describes each variable: name, dimensions, type, units,
  long name, group, module, restart name and the like. The text is embedded into
  the executable at build time by cmake/EmbedText.cmake, so there is no file to
  find at run time.
* cable_output_bindings*.F90 supply what only Fortran can: the model variable
  (through an aggregator), the unit conversion, the valid range and whether the
  variable exists in the current model configuration. Model state and settings
  are passed in as arguments, not read from module variables.

cable_output_catalogue_mod joins the two by name into
cable_output_variable_definition_t values and stops, listing every problem, if a
name has no partner on the other side. The field name users see is the NetCDF name
the variable always had; restart files keep their existing variable names through
a separate restart name. Variables the model cannot provide stay in the catalogue
as unavailable.

Nothing uses the new code yet; cable_diagnostics is still the source of output
variables.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017hNmrg1iRG449BqCN25afu
cable_output_config_mod reads the YAML output configuration: streams (a file and
a write frequency), and variables, groups and modules directed to them. It
expands modules, then groups, then variables into a flat list of variable uses,
fills in the {field_name}, {frequency}, {aggregation}, {reduction}, {start_date}
and {end_date} templates, and applies the design's two rules (no two variables
with the same NetCDF name in a stream, and no two identical variables in a
stream).

Everything is a function of its arguments and every problem found is collected, so
one run reports all the mistakes in a file. It also rejects unknown keys and names,
reductions that make no sense for a variable (integers, scalars, variables with no
tile dimension), writing a variable more often than the model updates it, two
streams writing one file, and attributes that clash with the automatic ones.
Variables that are unavailable are skipped inside a group or module, and are an
error only when requested by name.

The stream and variable settings types live in cable_output_mod so that the
engine can take them. The parser is not connected to the drivers yet.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017hNmrg1iRG449BqCN25afu
The serial and MPI master drivers now set output up in five steps: build the
variable definitions from the catalogue and bindings, register them, work out the
run's first and last dates, read the output configuration file named by
filename%output_config, and create the files. The output engine is rewritten to
write several streams: output_streams replaces the single global stream, every
variable use owns its own aggregator, and each stream has its own file, frequency
and compression. update, write, write_parameters and write_restart loop over the
streams. A stream with compression above zero is a NetCDF-4 file, otherwise
classic. The restart file is written from the definitions that have a restart
name, as before and independent of the configuration.

Breaking changes:

* filename%output_config is required. A run without it stops at start-up.
* The output%met, output%flux, ... group switches, the per-variable output%<name>
  switches, output%averaging and all patchout% switches are removed.
  cable_iovars keeps output%restart and output%grid. A namelist that still sets a
  removed switch stops with a message that says what to do.
* cable_diagnostics and cable_diagnostics_casa are removed; the catalogue and
  bindings replace them.
* output%balances no longer controls the balance printout at the end of a run,
  which now depends on verbose only.
* Files written for a separate file per variable go in the directory part of the
  stream's file_name.

The example cable.nml in src/offline is updated, with a matching output_config.yaml.

Output of the original code was reproduced exactly (apart from the new
cell_methods attributes) for all four test sites, at monthly, daily and every
time step, in serial and under MPI, using YAML equivalent to the old switches.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017hNmrg1iRG449BqCN25afu
cable_output_build_streams works out which files a configuration produces and
which variables go in each, without creating any. The tests check the file and
variable counts for several streams, that each use takes its settings from the
definition and the configuration, the order of attributes (definition attributes,
then cell_methods, then the user's), that stream compression and global attributes
are carried over, one file per variable with the directory of file_name, and that
a stream with no variables makes no file.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017hNmrg1iRG449BqCN25afu
The documentation already uses the mkdocs macros plugin. main.py is its hook and
registers a macro that builds the catalogue page every time the documentation is
built, so the page cannot go out of date. catalogue_docs.py reads three source
files: output_catalogue.yaml (the metadata of each variable), the Fortran bindings
(which model variable each name is bound to, the unit conversion and when it
exists) and the valid ranges in cable_checks.F90.

For each variable the page shows its name, long name, units, dimensions and group,
the model variable it is bound to with the type of the structure it lives in, and
notes on conversion, valid range, availability and restart name. Restart-only
variables have their own table. A catalogue entry with no binding is shown as a
warning rather than hidden.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017hNmrg1iRG449BqCN25afu
The Output files section replaces its placeholder with an overview (summary first,
the namelist settings that concern output, and a table for converting the removed
output% and patchout% switches), a reference for every setting and rule of the
configuration file, and ten worked examples. Every example was run through the
model. The cable.nml reference loses the removed switches and gains
filename%output_config.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017hNmrg1iRG449BqCN25afu
Flow charts (as Mermaid diagrams) of how a definition, a use and a file relate,
what happens at start-up, on every time step and at the end of a run, with a
table of where each part lives and the things worth knowing when changing it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017hNmrg1iRG449BqCN25afu

This branch has not been deployed

No deployments
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.

1 participant