Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
a4570fe
Update cmip7-cmor-tables submodule to tag DR-1.2.2.3-v1.0.4
ilaflott Mar 31, 2026
ebcadac
bump cmor version
ilaflott Mar 31, 2026
0719388
move sole unit test in file by itself to be with the other tests of s…
ilaflott Apr 1, 2026
5e2499c
remove file
ilaflott Apr 1, 2026
1b056c3
test environment resolution without cdo packages first to confirm my …
ilaflott Apr 1, 2026
19f88df
nccmp likely not needed and also clashes
ilaflott Apr 1, 2026
f9f4eee
hail mary, maybe older cdo versions could be acceptable...
ilaflott Apr 1, 2026
c858950
Initial plan
Copilot Apr 1, 2026
30ebacd
Initial plan
Copilot Apr 1, 2026
8ebfbde
Initial plan
Copilot Apr 1, 2026
baf90f1
replace nccmp subprocess calls with netCDF4 comparisons in cmor tests
Copilot Apr 1, 2026
a45e53a
Replace nccmp subprocess calls with xarray comparisons in test files
Copilot Apr 1, 2026
852afe9
Use context managers for xarray datasets to ensure proper cleanup
Copilot Apr 1, 2026
8ca138a
Replace nccmp with xarray/numpy in test_split_netcdf.py
Copilot Apr 1, 2026
a79a46d
Address code review: add None guards for dataset close in finally blocks
Copilot Apr 1, 2026
276ba82
plan: enhance cmor test assertions with specific data and metadata ch…
Copilot Apr 1, 2026
08c7952
enhance cmor test assertions with specific data and metadata checks u…
Copilot Apr 1, 2026
c307cea
Correct file comparison logic in test_split_netcdf
ilaflott Apr 1, 2026
f7fe253
undo changes to fre/pp/tests/test_split_netcdf.py which is being tac…
ilaflott Apr 1, 2026
3a8d61c
Add variable dtype check to test_split_file_metadata
Copilot Apr 1, 2026
0b6400f
Initial plan
Copilot Apr 1, 2026
b44a30e
deprecate cdo/python-cdo: replace CDO usage with xarray, add deprecat…
Copilot Apr 1, 2026
128525f
revert unrelated JSON file changes
Copilot Apr 1, 2026
6e9ae74
address code review: use xarray .dt accessor and context managers
Copilot Apr 1, 2026
facdfba
remove CDO, add xarray averager, rename frepytools to NumpyTimeAverag…
Copilot Apr 3, 2026
a77657e
fix: handle non-numeric vars and cftime time_bnds in xarrayTimeAverag…
Copilot Apr 3, 2026
7fbe33f
test: add comprehensive unit tests for xarrayTimeAverager and add wei…
Copilot Apr 3, 2026
8250249
test: address code review — rename bnds_dtype → time_bnds_encoding, f…
Copilot Apr 3, 2026
c02d0b5
Initial plan
Copilot Apr 3, 2026
86c2419
fre.cmor: be more flexible with CF compliant calendar input (noleap/3…
Copilot Apr 3, 2026
c141a95
fre.cmor: remove self-referential entries from CF_CALENDAR_ALIASES
Copilot Apr 3, 2026
ded4840
lets get in position for when the conflicts are removed
ilaflott Apr 3, 2026
da5b290
fre.cmor: detect and raise informative error when mip_era config mism…
Copilot Apr 3, 2026
7d5e65b
Simplify calendar comparison logic
ilaflott Apr 6, 2026
3dbfdc6
Add fre cmor init command for experiment config templates and MIP tab…
Copilot Apr 3, 2026
c18232d
Fix tarball URL construction in cmor_init to avoid redundant assignment
Copilot Apr 3, 2026
135b992
Update docs to cover fre cmor init and fre cmor config steps
Copilot Apr 3, 2026
3aa1520
Update cmor_init.py
ilaflott Apr 6, 2026
568c5bc
Fix output path and file template in JSON example
ilaflott Apr 6, 2026
fa3ac3f
Update CMIP7 test output dir to remove version suffix
Copilot Apr 6, 2026
b089353
Update CMIP7 test output filename: variant_idtime_range -> variant_label
Copilot Apr 6, 2026
be17124
tweak outputfile name
ilaflott Apr 6, 2026
33210ee
Merge remote-tracking branch 'origin/copilot/remove-replace-nccmp-fro…
ilaflott Apr 7, 2026
6aa0c62
Merge remote-tracking branch 'origin/copilot/remove-replace-nccmp-tes…
ilaflott Apr 7, 2026
813bd7b
Merge remote-tracking branch 'origin/copilot/remove-replace-nccmp-in-…
ilaflott Apr 7, 2026
e7216cf
Merge remote-tracking branch 'origin/copilot/deprecate-cdo-python-cdo…
ilaflott Apr 7, 2026
91a5c09
Merge remote-tracking branch 'origin/copilot/add-empty-user-experimen…
ilaflott Apr 7, 2026
6b82abd
Merge remote-tracking branch 'origin/copilot/fix-cmor-error-cmip6-cmi…
ilaflott Apr 7, 2026
71d6bfa
Merge remote-tracking branch 'origin/copilot/fix-flexibility-calendar…
ilaflott Apr 7, 2026
584e9f5
Merge remote-tracking branch 'fork/frecmor-updates-for-cmip7' into pr…
ilaflott Apr 7, 2026
156a1d1
add script tracking which branches we have merged in, to QA the piece…
ilaflott Apr 7, 2026
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
28 changes: 28 additions & 0 deletions docs/tools/cmor.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,40 @@ CMOR Subcommands Overview
``fre cmor`` rewrites climate model output files with CMIP-compliant metadata. Both CMIP6 and CMIP7
workflows are supported. Available subcommands:

* ``fre cmor init`` - Initialise CMOR resources: generate experiment-config templates and/or fetch MIP tables
* ``fre cmor run`` - Rewrite individual directories of netCDF files
* ``fre cmor yaml`` - Process multiple directories/tables using YAML configuration
* ``fre cmor find`` - Search MIP tables for variable definitions
* ``fre cmor varlist`` - Generate variable lists from netCDF files
* ``fre cmor config`` - Generate a CMOR YAML configuration from a post-processing directory tree

``init``
--------

* Initialise CMOR resources for a new experiment: generate an empty experiment-config JSON template and/or fetch official MIP tables
* Tables are fetched via ``git clone --depth 1`` by default; pass ``--fast`` to download a tarball via ``curl`` instead
* Trusted sources: `pcmdi/cmip6-cmor-tables <https://github.com/pcmdi/cmip6-cmor-tables>`_, `WCRP-CMIP/cmip7-cmor-tables <https://github.com/WCRP-CMIP/cmip7-cmor-tables>`_
* Minimal Syntax: ``fre cmor init -m [mip_era] [options]``
* Required Options:
- ``-m, --mip_era [cmip6|cmip7]`` - MIP era for the template
* Optional:
- ``-e, --exp_config TEXT`` - Output path for the template experiment-config JSON file (default name used when omitted)
- ``-t, --tables_dir TEXT`` - Directory into which MIP tables will be fetched
- ``--tag TEXT`` - Specific git tag or release for the MIP tables repository
- ``--fast`` - Use ``curl`` to download a tarball instead of ``git clone``
* Examples:

.. code-block:: bash

# Generate an empty CMIP6 experiment config template
fre cmor init -m cmip6 -e my_experiment.json

# Generate a CMIP7 template and fetch the latest MIP tables via git
fre cmor init -m cmip7 -e my_cmip7_exp.json -t ./cmip7-tables

# Fetch CMIP6 tables at a specific tag using curl (fast mode)
fre cmor init -m cmip6 -t ./cmip6-tables --tag v6.2.7.18 --fast

``run``
-------

Expand Down
2 changes: 2 additions & 0 deletions docs/usage/cmor.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Getting Started

``fre cmor`` provides several subcommands:

* ``fre cmor init`` - Initialise CMOR resources: generate experiment-config templates and/or fetch MIP tables from trusted sources
* ``fre cmor run`` - Core engine for rewriting individual directories of netCDF files according to a MIP table
* ``fre cmor yaml`` - Higher-level tool for processing multiple directories / MIP tables using YAML configuration
* ``fre cmor find`` - Helper for exploring MIP table configurations for information on a specific variable
Expand All @@ -34,6 +35,7 @@ Additional Resources
--------------------

* `CMIP6 Tables <https://github.com/pcmdi/cmip6-cmor-tables>`_
* `CMIP7 Tables <https://github.com/WCRP-CMIP/cmip7-cmor-tables>`_
* `CMIP6 Controlled Vocabulary <https://github.com/WCRP-CMIP/CMIP6_CVs>`_
* `PCMDI CMOR User Guide <http://cmor.llnl.gov/>`_
* `fre cmor README <https://github.com/NOAA-GFDL/fre-cli/blob/main/fre/cmor/README.md>`_
Expand Down
87 changes: 83 additions & 4 deletions docs/usage/cmor_cookbook.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,54 @@ Overview

The ``fre cmor`` process typically follows this pattern:

1. Setup and Configuration - Identify your experiment parameters, create variable lists, and prepare experiment configuration
2. CMORization - Use ``fre cmor run`` to process individual directories or ``fre cmor yaml`` for bulk processing
3. Troubleshooting - Diagnose issues as needed (note: ``fre yamltools combine-yamls --use cmor`` can help debug YAML configurations)
1. Initialisation - Use ``fre cmor init`` to generate an experiment-config template and fetch MIP tables
2. Setup and Configuration - Fill in experiment parameters, create variable lists, and prepare experiment configuration
3. Auto-generate YAML (optional) - Use ``fre cmor config`` to scan a post-processing directory tree and generate the YAML that ``fre cmor yaml`` expects
4. CMORization - Use ``fre cmor run`` to process individual directories or ``fre cmor yaml`` for bulk processing
5. Troubleshooting - Diagnose issues as needed (note: ``fre yamltools combine-yamls --use cmor`` can help debug YAML configurations)

Initialisation
--------------

Use ``fre cmor init`` to bootstrap your CMORization setup. This command generates an empty experiment-config
JSON template for the target MIP era and can optionally fetch the official MIP tables from their trusted
GitHub repositories.

Generating an Experiment Config Template
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. code-block:: bash

# CMIP6 template
fre cmor init --mip_era cmip6 --exp_config my_cmip6_experiment.json

# CMIP7 template
fre cmor init --mip_era cmip7 --exp_config my_cmip7_experiment.json

The generated JSON file contains all fields expected by ``fre cmor run`` and the underlying CMOR library,
with empty placeholder values that you fill in for your specific experiment (e.g. ``experiment_id``,
``source_id``, ``calendar``, ``grid_label``, output path templates, etc.).

Fetching MIP Tables
~~~~~~~~~~~~~~~~~~~

``fre cmor init`` can also fetch the official MIP tables for you:

.. code-block:: bash

# Fetch CMIP6 tables via git (default, shallow clone)
fre cmor init --mip_era cmip6 --tables_dir ./cmip6-cmor-tables

# Fetch CMIP7 tables at a specific release tag using curl (fast mode)
fre cmor init --mip_era cmip7 --tables_dir ./cmip7-cmor-tables --tag v1.0.0 --fast

# Generate a template AND fetch tables in one call
fre cmor init --mip_era cmip7 --exp_config my_exp.json --tables_dir ./cmip7-tables

Trusted sources:

* CMIP6: `pcmdi/cmip6-cmor-tables <https://github.com/pcmdi/cmip6-cmor-tables>`_
* CMIP7: `WCRP-CMIP/cmip7-cmor-tables <https://github.com/WCRP-CMIP/cmip7-cmor-tables>`_

Setup and Configuration
-----------------------
Expand Down Expand Up @@ -86,6 +131,39 @@ This file should include:
* Calendar type

Refer to CMIP6 controlled vocabularies and your project's requirements when filling in these fields.
You can use ``fre cmor init`` to generate a template with all of these fields pre-populated with empty
placeholder values.

Auto-Generating CMOR YAML Configuration
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

If you have an existing post-processing directory tree from FRE workflows, ``fre cmor config`` can scan it
to auto-generate the CMOR YAML configuration file that ``fre cmor yaml`` expects. It cross-references found
variables against MIP tables, creates per-component variable list JSON files, and writes the structured YAML.

.. code-block:: bash

fre cmor config \
--pp_dir /archive/user/experiment/pp \
--mip_tables_dir /path/to/cmip7-cmor-tables/tables \
--mip_era cmip7 \
--exp_config /path/to/CMOR_input.json \
--output_yaml cmor_config.yaml \
--output_dir /path/to/cmor_output \
--varlist_dir /path/to/varlists \
--freq monthly --chunk 5yr --grid gn \
--calendar noleap

This command:

* Scans ``--pp_dir`` for component directories (e.g. ``ocean_monthly_1x1deg``)
* Looks for time-series data under each component at ``ts/<freq>/<chunk>/``
* Matches variable names from netCDF files against all MIP tables in ``--mip_tables_dir``
* Writes per-component variable list JSON files to ``--varlist_dir``
* Produces a YAML file at ``--output_yaml`` that can be fed directly to ``fre cmor yaml``

Pass ``--overwrite`` to regenerate existing variable list files. Adjust ``--freq``, ``--chunk``, and
``--grid`` to match your post-processing directory structure.

Running Your CMORization
------------------------
Expand Down Expand Up @@ -342,12 +420,13 @@ Full processing:
Tips
----

* Use ``fre cmor init`` to bootstrap your setup — it generates an experiment-config template and can fetch MIP tables in one step
* Use ``fre cmor config`` to auto-generate a CMOR YAML configuration from a post-processing directory tree — it scans components, cross-references against MIP tables, and writes both variable lists and the YAML that ``fre cmor yaml`` expects
* Use ``fre yamltools combine-yamls`` before attempting CMORization to help figure out YAML issues
* Use ``--dry_run`` with ``fre cmor yaml`` to preview the equivalent ``fre cmor run`` calls before execution
* Use ``--no-print_cli_call`` with ``--dry_run`` to see the Python ``cmor_run_subtool(...)`` call instead of the CLI invocation — useful for debugging
* Use ``--run_one`` with ``fre cmor run`` for testing to only process a single file and catch issues early
* Use ``--run_one`` with ``fre cmor yaml`` to process a single file per ``fre cmor run`` call for quicker debugging
* Use ``fre cmor config`` to auto-generate a CMOR YAML configuration from a post-processing directory tree — it scans components, cross-references against MIP tables, and writes both variable lists and the YAML that ``fre cmor yaml`` expects
* Increase verbosity when debugging - Use ``-v`` to see ``INFO`` logging, and ``-vv`` (or ``-v -v``) for ``DEBUG`` logging
* Version control your YAML files - Track changes to your CMORization configuration and commit them to git!
* Check controlled vocabulary - Verify grid labels and nominal resolutions are CV-compliant
Expand Down
7 changes: 1 addition & 6 deletions environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,18 @@ dependencies:
- python>=3.11
- noaa-gfdl::analysis_scripts==0.0.1
- noaa-gfdl::catalogbuilder==2025.01.01
# - noaa-gfdl::fre-nctools==2022.02.01
- conda-forge::cdo>=2
- conda-forge::cftime
- conda-forge::click>=8.2
- conda-forge::cmor>=3.14
- conda-forge::cmor>=3.14.1
- conda-forge::cylc-flow>=8.2
- conda-forge::cylc-rose
- conda-forge::jinja2>=3
- conda-forge::jsonschema
- conda-forge::metomi-rose
- conda-forge::nccmp
# - conda-forge::numpy==1.26.4
- conda-forge::numpy>=2
- conda-forge::pytest
- conda-forge::pytest-cov
- conda-forge::pylint
- conda-forge::python-cdo
- conda-forge::pyyaml
- conda-forge::xarray>=2024.*
- conda-forge::netcdf4>=1.7.*
8 changes: 4 additions & 4 deletions fre/app/freapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,8 @@ def mask_atmos_plevel(infile, psfile, outfile, warn_no_ps):
required = True,
help = "Output file name")
@click.option("-p", "--pkg",
type = click.Choice(["cdo","fre-nctools","fre-python-tools"]),
default = "cdo",
type = click.Choice(["cdo","fre-nctools","fre-python-tools","xarray"]),
default = "fre-python-tools",
help = "Time average approach")
@click.option("-v", "--var",
type = str,
Expand Down Expand Up @@ -192,8 +192,8 @@ def gen_time_averages(inf, outf, pkg, var, unwgt, avg_type):
required = True,
help = "Frequency of desired climatology: 'mon' or 'yr'")
@click.option("-p", "--pkg",
type = click.Choice(["cdo","fre-nctools","fre-python-tools"]),
default = "cdo",
type = click.Choice(["cdo","fre-nctools","fre-python-tools","xarray"]),
default = "fre-python-tools",
help = "Time average approach")
def gen_time_averages_wrapper(cycle_point, dir_, sources, output_interval, input_interval, grid, frequency, pkg):
"""
Expand Down
3 changes: 2 additions & 1 deletion fre/app/generate_time_averages/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
'''required for generate_time_averages module import functionality'''
__all__ = ['generate_time_averages', 'timeAverager', 'wrapper', 'combine',
'frenctoolsTimeAverager', 'cdoTimeAverager', 'frepytoolsTimeAverager']
'frenctoolsTimeAverager', 'cdoTimeAverager', 'frepytoolsTimeAverager',
'xarrayTimeAverager']
96 changes: 21 additions & 75 deletions fre/app/generate_time_averages/cdoTimeAverager.py
Original file line number Diff line number Diff line change
@@ -1,89 +1,35 @@
''' class using (mostly) cdo functions for time-averages '''
''' stub that redirects pkg='cdo' requests to the xarray time averager '''

import logging
import warnings

from netCDF4 import Dataset
import numpy as np

import cdo
from cdo import Cdo

from .timeAverager import timeAverager
from .xarrayTimeAverager import xarrayTimeAverager

fre_logger = logging.getLogger(__name__)

class cdoTimeAverager(timeAverager):

class cdoTimeAverager(xarrayTimeAverager): # pylint: disable=invalid-name
'''
class inheriting from abstract base class timeAverager
generates time-averages using cdo (mostly, see weighted approach)
Legacy entry-point kept for backward compatibility.
CDO/python-cdo has been removed. All work is now done by xarrayTimeAverager.
'''

def generate_timavg(self, infile = None, outfile = None):
def generate_timavg(self, infile=None, outfile=None):
"""
use cdo package routines via python bindings
Emit a loud warning then delegate to the xarray implementation.

:param self: This is an instance of the class cdoTimeAverager
:param infile: path to history file, or list of paths, default is None
:type infile: str, list
:param outfile: path to where output file should be stored, default is None
:param infile: path to input NetCDF file
:type infile: str
:param outfile: path to output file
:type outfile: str
:return: 1 if the instance variable self.avg_typ is unsupported, 0 if function has a clean exit
:return: 0 on success
:rtype: int
"""

if self.avg_type not in ['all', 'seas', 'month']:
fre_logger.error('requested unknown avg_type %s.', self.avg_type)
raise ValueError(f'requested unknown avg_type {self.avg_type}')

if self.var is not None:
fre_logger.warning('WARNING: variable specification not twr supported for cdo time averaging. ignoring!')

fre_logger.info('python-cdo version is %s', cdo.__version__)

_cdo = Cdo()

wgts_sum = 0
if not self.unwgt: #weighted case, cdo ops alone don't support a weighted time-average.

nc_fin = Dataset(infile, 'r')

time_bnds = nc_fin['time_bnds'][:].copy()
# Ensure float64 precision for consistent results across numpy versions
# NumPy 2.0 changed type promotion rules (NEP 50), so explicit casting
# is needed to avoid precision differences
time_bnds = np.asarray(time_bnds, dtype=np.float64)
# Transpose once to avoid redundant operations
time_bnds_transposed = np.moveaxis(time_bnds, 0, -1)
wgts = time_bnds_transposed[1] - time_bnds_transposed[0]
# Use numpy.sum for consistent dtype handling across numpy versions
wgts_sum = np.sum(wgts, dtype=np.float64)

fre_logger.debug('wgts_sum = %s', wgts_sum)

if self.avg_type == 'all':
fre_logger.info('time average over all time requested.')
if self.unwgt:
_cdo.timmean(input = infile, output = outfile, returnCdf = True)
else:
_cdo.divc( str(wgts_sum), input = "-timsum -muldpm "+infile, output = outfile)
fre_logger.info('done averaging over all time.')

elif self.avg_type == 'seas':
fre_logger.info('seasonal time-averages requested.')
_cdo.yseasmean(input = infile, output = outfile, returnCdf = True)
fre_logger.info('done averaging over seasons.')

elif self.avg_type == 'month':
fre_logger.info('monthly time-averages requested.')
outfile_str = str(outfile)
_cdo.ymonmean(input = infile, output = outfile_str, returnCdf = True)
fre_logger.info('done averaging over months.')

fre_logger.warning(" splitting by month")
outfile_root = outfile_str.removesuffix(".nc") + '.'
_cdo.splitmon(input = outfile_str, output = outfile_root)
fre_logger.debug('Done with splitting by month, outfile_root = %s', outfile_root)

fre_logger.info('done averaging')
fre_logger.info('output file created: %s', outfile)
return 0
msg = (
"WARNING *** CDO/python-cdo has been REMOVED from fre-cli. "
"pkg='cdo' now uses the xarray time-averager under the hood. "
"Please switch to pkg='xarray' or pkg='fre-python-tools'. ***"
)
warnings.warn(msg, FutureWarning, stacklevel=2)
fre_logger.warning(msg)
return super().generate_timavg(infile=infile, outfile=outfile)
Loading
Loading