Introduction to xcmor

Introduction to xcmor#

This notebook is a brief introduction to xcmor’s current capabilities.

import xarray as xr

import xcmor

# For this notebooks, it's nicer if we don't show the array values by default
xr.set_options(display_expand_data=False)
<xarray.core.options.set_options at 0x7babb2c7d390>

xcmor works best when xarray keeps attributes by default.

xr.set_options(keep_attrs=True)
<xarray.core.options.set_options at 0x7bab8b996d40>

We use an example dataset with a 2D temperature field. Let’s load a regular gridded dataset:

from xcmor.datasets import reg_ds

reg_ds
<xarray.Dataset> Size: 248B
Dimensions:        (x: 2, y: 2, time: 3)
Coordinates:
    lon            (x) float64 16B 80.17 80.68
    lat            (y) float64 16B 42.25 42.21
  * time           (time) datetime64[ns] 24B 2014-09-06 2014-09-07 2014-09-08
Dimensions without coordinates: x, y
Data variables:
    temperature    (x, y, time) float64 96B 29.11 18.2 22.83 ... 16.15 26.63
    precipitation  (x, y, time) float64 96B 5.68 9.256 0.7104 ... 4.615 7.805

Also, let’s load some example cmor tables, e.g.

from xcmor.tests.tables import coords, dataset, mip_amon

These tables are just some subsets of the original CMIP6 CMOR tables. Now, we can use those tables to rewrite variable attributes acoording to CF conventions and the CMIP6 data request using

ds_cmor = xcmor.cmorize(
    reg_ds.rename({"temperature": "tas"}).tas,
    mip_table=mip_amon,
    coords_table=coords,
    dataset_table=dataset,
)
ds_cmor
Downloading data from 'https://raw.githubusercontent.com/cf-convention/cf-convention.github.io/master/Data/cf-standard-names/current/src/cf-standard-name-table.xml' to file '/home/docs/.cache/pooch/47dae451573e45041e8665efd76db5a9-cf-standard-name-table.xml'.
I think 'lon' is of type 'longitude'. It matched regex.Regex('x?(nav_lon|lon|glam)[a-z0-9]*', flags=regex.V0)
I think 'lat' is of type 'latitude'. It matched regex.Regex('y?(nav_lat|lat|gphi)[a-z0-9]*', flags=regex.V0)
I think 'time' is of type 'time'. It has a datetime-like type.
SHA256 hash of downloaded file: 3653c1e1a55cd0d3dd7b63c1c0cdf86b51681d672d8407cecccece2047ab6c94
Use this value as the 'known_hash' argument of 'pooch.retrieve' to ensure that the file hasn't changed if it is downloaded again in the future.
2026-06-03 09:54:55,478 - xcmor.rules - INFO - converting tas from float64 to float32 (rules.py:23)
2026-06-03 09:54:55,498 - xcmor.xcmor - DEBUG - curvilinear: False (xcmor.py:384)
2026-06-03 09:54:55,499 - xcmor.xcmor - DEBUG - has_grid_mapping: False (xcmor.py:385)
2026-06-03 09:54:55,516 - xcmor.xcmor - WARNING - "Dataset.cf does not understand the key 'X'. Use 'repr(Dataset.cf)' (or 'Dataset.cf' in a Jupyter environment) to see a list of key names that can be interpreted." (xcmor.py:110)
2026-06-03 09:54:55,537 - xcmor.xcmor - DEBUG - has lonlat: True (xcmor.py:398)
2026-06-03 09:54:55,538 - xcmor.xcmor - DEBUG - has xy: True (xcmor.py:399)
2026-06-03 09:54:55,538 - xcmor.xcmor - DEBUG - x-axis, y-axis: lon, lat (xcmor.py:400)
2026-06-03 09:54:55,539 - xcmor.xcmor - DEBUG - longitude, latitude: lon, lat (xcmor.py:401)
2026-06-03 09:54:55,540 - xcmor.xcmor - DEBUG - auxiliary coordinates: False (xcmor.py:407)
2026-06-03 09:54:55,540 - xcmor.xcmor - DEBUG - tas: ['longitude', 'latitude', 'time', 'height2m'] (xcmor.py:449)
2026-06-03 09:54:55,541 - xcmor.xcmor - DEBUG - longitude, {'standard_name': 'longitude', 'units': 'degrees_east', 'axis': 'X', 'long_name': 'Longitude', 'climatology': '', 'formula': '', 'must_have_bounds': 'yes', 'out_name': 'lon', 'positive': '', 'requested': '', 'requested_bounds': '', 'stored_direction': 'increasing', 'tolerance': '', 'type': 'double', 'valid_max': '360.0', 'valid_min': '0.0', 'value': '', 'z_bounds_factors': '', 'z_factors': '', 'bounds_values': '', 'generic_level_name': ''} (xcmor.py:313)
2026-06-03 09:54:55,541 - xcmor.xcmor - DEBUG - adding coordinate attribtes: lon (xcmor.py:272)
2026-06-03 09:54:55,542 - xcmor.xcmor - DEBUG - latitude, {'standard_name': 'latitude', 'units': 'degrees_north', 'axis': 'Y', 'long_name': 'Latitude', 'climatology': '', 'formula': '', 'must_have_bounds': 'yes', 'out_name': 'lat', 'positive': '', 'requested': '', 'requested_bounds': '', 'stored_direction': 'increasing', 'tolerance': '', 'type': 'double', 'valid_max': '90.0', 'valid_min': '-90.0', 'value': '', 'z_bounds_factors': '', 'z_factors': '', 'bounds_values': '', 'generic_level_name': ''} (xcmor.py:313)
2026-06-03 09:54:55,542 - xcmor.xcmor - DEBUG - adding coordinate attribtes: lat (xcmor.py:272)
2026-06-03 09:54:55,543 - xcmor.xcmor - DEBUG - time, {'standard_name': 'time', 'units': 'days since ?', 'axis': 'T', 'long_name': 'time', 'climatology': '', 'formula': '', 'must_have_bounds': 'yes', 'out_name': 'time', 'positive': '', 'requested': '', 'requested_bounds': '', 'stored_direction': 'increasing', 'tolerance': '', 'type': 'double', 'valid_max': '', 'valid_min': '', 'value': '', 'z_bounds_factors': '', 'z_factors': '', 'bounds_values': '', 'generic_level_name': ''} (xcmor.py:313)
2026-06-03 09:54:55,543 - xcmor.xcmor - DEBUG - adding coordinate attribtes: time (xcmor.py:272)
2026-06-03 09:54:55,545 - xcmor.xcmor - DEBUG - height2m, {'standard_name': 'height', 'units': 'm', 'axis': 'Z', 'long_name': 'height', 'climatology': '', 'formula': '', 'must_have_bounds': 'no', 'out_name': 'height', 'positive': 'up', 'requested': '', 'requested_bounds': '', 'stored_direction': 'increasing', 'tolerance': '', 'type': 'double', 'valid_max': '10.0', 'valid_min': '1.0', 'value': '2.', 'z_bounds_factors': '', 'z_factors': '', 'bounds_values': '', 'generic_level_name': ''} (xcmor.py:313)
2026-06-03 09:54:55,545 - xcmor.xcmor - DEBUG - adding coordinate attribtes: height (xcmor.py:272)
2026-06-03 09:54:55,580 - xcmor.xcmor - INFO - adding coordinate: height (xcmor.py:278)
2026-06-03 09:54:55,581 - xcmor.xcmor - DEBUG - added coordinates: ['lon', 'lat', 'time', 'height'] (xcmor.py:478)
2026-06-03 09:54:55,677 - xcmor.rules - DEBUG - checking bounds for lon: True (rules.py:89)
2026-06-03 09:54:55,680 - xcmor.rules - WARNING - lon must have bounds (rules.py:91)
2026-06-03 09:54:55,681 - xcmor.rules - INFO - adding bounds for lon (rules.py:93)
2026-06-03 09:54:55,737 - xcmor.rules - DEBUG - checking bounds for lat: True (rules.py:89)
2026-06-03 09:54:55,740 - xcmor.rules - WARNING - lat must have bounds (rules.py:91)
2026-06-03 09:54:55,741 - xcmor.rules - INFO - adding bounds for lat (rules.py:93)
2026-06-03 09:54:55,843 - xcmor.rules - DEBUG - checking bounds for time: True (rules.py:89)
2026-06-03 09:54:55,848 - xcmor.rules - WARNING - time must have bounds (rules.py:91)
2026-06-03 09:54:55,848 - xcmor.rules - INFO - adding bounds for time (rules.py:93)
2026-06-03 09:54:55,908 - xcmor.xcmor - DEBUG - setting time units: days since 2014-09-06T00:00:00 (xcmor.py:53)
2026-06-03 09:54:55,910 - xcmor.xcmor - DEBUG - coord: lon (xcmor.py:654)
2026-06-03 09:54:55,911 - xcmor.xcmor - DEBUG - coord: lat (xcmor.py:654)
2026-06-03 09:54:55,912 - xcmor.xcmor - INFO - swap dims: {'x': 'lon', 'y': 'lat'} (xcmor.py:656)
/home/docs/checkouts/readthedocs.org/user_builds/xcmor/conda/latest/lib/python3.10/site-packages/cf_xarray/accessor.py:718: FutureWarning: The return type of `Dataset.dims` will be changed to return a set of dimension names in future, in order to be more consistent with `DataArray.dims`. To access a mapping from dimension names to lengths, please use `Dataset.sizes`.
  unused_keys = set(attribute.keys()) - set(inverted)
/home/docs/checkouts/readthedocs.org/user_builds/xcmor/conda/latest/lib/python3.10/site-packages/cf_xarray/accessor.py:719: FutureWarning: The return type of `Dataset.dims` will be changed to return a set of dimension names in future, in order to be more consistent with `DataArray.dims`. To access a mapping from dimension names to lengths, please use `Dataset.sizes`.
  for key, value in attribute.items():
/home/docs/checkouts/readthedocs.org/user_builds/xcmor/conda/latest/lib/python3.10/site-packages/cf_xarray/accessor.py:727: FutureWarning: The return type of `Dataset.dims` will be changed to return a set of dimension names in future, in order to be more consistent with `DataArray.dims`. To access a mapping from dimension names to lengths, please use `Dataset.sizes`.
  newmap.update({key: attribute[key] for key in unused_keys})
2026-06-03 09:54:55,916 - xcmor.xcmor - DEBUG - transposing order: ['T', 'Y', 'X'] (xcmor.py:31)
<xarray.Dataset> Size: 224B
Dimensions:      (lon: 2, lat: 2, time: 3, bounds: 2)
Coordinates:
  * lon          (lon) float64 16B 80.17 80.68
  * lat          (lat) float64 16B 42.25 42.21
    height       float64 8B ...
    lon_bounds   (lon, bounds) float64 32B ...
    lat_bounds   (lat, bounds) float64 32B ...
    time_bounds  (time, bounds) datetime64[ns] 48B 2014-09-05T12:00:00 ... 20...
  * time         (time) datetime64[ns] 24B 2014-09-06 2014-09-07 2014-09-08
Dimensions without coordinates: bounds
Data variables:
    tas          (time, lat, lon) float32 48B ...
Attributes: (12/42)
    activity_id:            ISMIP6
    branch_method:          standard
    branch_time_in_child:   59400.0
    branch_time_in_parent:  59400.0
    calendar:               360_day
    comment:                
    ...                     ...
    source_type:            AOGCM ISM AER
    sub_experiment:         none
    sub_experiment_id:      none
    tracking_prefix:        hdl:21.14100
    variable_id:            tas
    version:                20260603

The Cmorizer class#

xcmor comes with some pre-configures table options through the Cmorizer class. A simple example for CMIP6 would be:

from xcmor import Cmorizer

cmor = Cmorizer(project="CMIP6")
ds_out = cmor.cmorize(
    reg_ds.rename(temperature="tas").tas, "Amon", cmor.tables["input_example"]
)
ds_out
Downloading data from 'https://raw.githubusercontent.com/PCMDI/cmip6-cmor-tables/master/Tables/CMIP6_input_example.json' to file '/home/docs/.cache/pooch/b1e6a504651492874b7eb0c52a902532-CMIP6_input_example.json'.
SHA256 hash of downloaded file: fdaa955f10fe95bbe62d1c0cf08810338a97d92a553abcb73bb33456b098bc5b
Use this value as the 'known_hash' argument of 'pooch.retrieve' to ensure that the file hasn't changed if it is downloaded again in the future.
Downloading data from 'https://raw.githubusercontent.com/PCMDI/cmip6-cmor-tables/master/Tables/CMIP6_Amon.json' to file '/home/docs/.cache/pooch/69da11734d03d2070269b48e6322d094-CMIP6_Amon.json'.
SHA256 hash of downloaded file: d3ac72751f401e551ab60ae94a1c2a24441563ad8e5c0be84687775f5b75ca1d
Use this value as the 'known_hash' argument of 'pooch.retrieve' to ensure that the file hasn't changed if it is downloaded again in the future.
Downloading data from 'https://raw.githubusercontent.com/PCMDI/cmip6-cmor-tables/master/Tables/CMIP6_coordinate.json' to file '/home/docs/.cache/pooch/914887963ad537da427851a882fab071-CMIP6_coordinate.json'.
SHA256 hash of downloaded file: 0536448a0e85a4a57122839f2d0f58701beb89cf2b5245f2ff34456c348bf8f0
Use this value as the 'known_hash' argument of 'pooch.retrieve' to ensure that the file hasn't changed if it is downloaded again in the future.
Downloading data from 'https://raw.githubusercontent.com/PCMDI/cmip6-cmor-tables/master/Tables/CMIP6_CV.json' to file '/home/docs/.cache/pooch/1d0ce1bf51bbab9d775c345522ab3938-CMIP6_CV.json'.
SHA256 hash of downloaded file: 4554d289977a3c0c88f712ff5d03c6dfcb44a86a4cc434b4feb2e3ec0874e9ab
Use this value as the 'known_hash' argument of 'pooch.retrieve' to ensure that the file hasn't changed if it is downloaded again in the future.
Downloading data from 'https://raw.githubusercontent.com/PCMDI/cmip6-cmor-tables/master/Tables/CMIP6_grids.json' to file '/home/docs/.cache/pooch/21d259528b2aae0c41a9c08d8dc74a61-CMIP6_grids.json'.
SHA256 hash of downloaded file: 50477cedb65c6a28c2c484c4c6dd20479defb12937a7466a2f6121c22cfcd573
Use this value as the 'known_hash' argument of 'pooch.retrieve' to ensure that the file hasn't changed if it is downloaded again in the future.
2026-06-03 09:54:56,936 - xcmor.rules - INFO - converting tas from float64 to float32 (rules.py:23)
2026-06-03 09:54:56,956 - xcmor.xcmor - DEBUG - curvilinear: False (xcmor.py:384)
2026-06-03 09:54:56,957 - xcmor.xcmor - DEBUG - has_grid_mapping: False (xcmor.py:385)
2026-06-03 09:54:56,974 - xcmor.xcmor - WARNING - "Dataset.cf does not understand the key 'X'. Use 'repr(Dataset.cf)' (or 'Dataset.cf' in a Jupyter environment) to see a list of key names that can be interpreted." (xcmor.py:110)
2026-06-03 09:54:56,995 - xcmor.xcmor - DEBUG - has lonlat: True (xcmor.py:398)
2026-06-03 09:54:56,995 - xcmor.xcmor - DEBUG - has xy: True (xcmor.py:399)
2026-06-03 09:54:56,996 - xcmor.xcmor - DEBUG - x-axis, y-axis: lon, lat (xcmor.py:400)
2026-06-03 09:54:56,996 - xcmor.xcmor - DEBUG - longitude, latitude: lon, lat (xcmor.py:401)
2026-06-03 09:54:56,997 - xcmor.xcmor - DEBUG - auxiliary coordinates: False (xcmor.py:407)
2026-06-03 09:54:56,997 - xcmor.xcmor - DEBUG - tas: ['longitude', 'latitude', 'time', 'height2m'] (xcmor.py:449)
2026-06-03 09:54:56,998 - xcmor.xcmor - DEBUG - longitude, {'standard_name': 'longitude', 'units': 'degrees_east', 'axis': 'X', 'long_name': 'Longitude', 'climatology': '', 'formula': '', 'must_have_bounds': 'yes', 'out_name': 'lon', 'positive': '', 'requested': '', 'requested_bounds': '', 'stored_direction': 'increasing', 'tolerance': '', 'type': 'double', 'valid_max': '360.0', 'valid_min': '0.0', 'value': '', 'z_bounds_factors': '', 'z_factors': '', 'bounds_values': '', 'generic_level_name': ''} (xcmor.py:313)
2026-06-03 09:54:56,998 - xcmor.xcmor - DEBUG - adding coordinate attribtes: lon (xcmor.py:272)
2026-06-03 09:54:56,999 - xcmor.xcmor - DEBUG - latitude, {'standard_name': 'latitude', 'units': 'degrees_north', 'axis': 'Y', 'long_name': 'Latitude', 'climatology': '', 'formula': '', 'must_have_bounds': 'yes', 'out_name': 'lat', 'positive': '', 'requested': '', 'requested_bounds': '', 'stored_direction': 'increasing', 'tolerance': '', 'type': 'double', 'valid_max': '90.0', 'valid_min': '-90.0', 'value': '', 'z_bounds_factors': '', 'z_factors': '', 'bounds_values': '', 'generic_level_name': ''} (xcmor.py:313)
2026-06-03 09:54:56,999 - xcmor.xcmor - DEBUG - adding coordinate attribtes: lat (xcmor.py:272)
2026-06-03 09:54:57,000 - xcmor.xcmor - DEBUG - time, {'standard_name': 'time', 'units': 'days since ?', 'axis': 'T', 'long_name': 'time', 'climatology': '', 'formula': '', 'must_have_bounds': 'yes', 'out_name': 'time', 'positive': '', 'requested': '', 'requested_bounds': '', 'stored_direction': 'increasing', 'tolerance': '', 'type': 'double', 'valid_max': '', 'valid_min': '', 'value': '', 'z_bounds_factors': '', 'z_factors': '', 'bounds_values': '', 'generic_level_name': ''} (xcmor.py:313)
2026-06-03 09:54:57,000 - xcmor.xcmor - DEBUG - adding coordinate attribtes: time (xcmor.py:272)
2026-06-03 09:54:57,000 - xcmor.xcmor - DEBUG - height2m, {'standard_name': 'height', 'units': 'm', 'axis': 'Z', 'long_name': 'height', 'climatology': '', 'formula': '', 'must_have_bounds': 'no', 'out_name': 'height', 'positive': 'up', 'requested': '', 'requested_bounds': '', 'stored_direction': 'increasing', 'tolerance': '', 'type': 'double', 'valid_max': '10.0', 'valid_min': '1.0', 'value': '2.', 'z_bounds_factors': '', 'z_factors': '', 'bounds_values': '', 'generic_level_name': ''} (xcmor.py:313)
2026-06-03 09:54:57,001 - xcmor.xcmor - DEBUG - adding coordinate attribtes: height (xcmor.py:272)
2026-06-03 09:54:57,038 - xcmor.xcmor - INFO - adding coordinate: height (xcmor.py:278)
2026-06-03 09:54:57,039 - xcmor.xcmor - DEBUG - added coordinates: ['lon', 'lat', 'time', 'height'] (xcmor.py:478)
I think 'lon' is of type 'longitude'. It matched regex.Regex('x?(nav_lon|lon|glam)[a-z0-9]*', flags=regex.V0)
I think 'lat' is of type 'latitude'. It matched regex.Regex('y?(nav_lat|lat|gphi)[a-z0-9]*', flags=regex.V0)
I think 'time' is of type 'time'. It has a datetime-like type.
2026-06-03 09:54:57,086 - xcmor.rules - DEBUG - checking bounds for lon: True (rules.py:89)
2026-06-03 09:54:57,090 - xcmor.rules - WARNING - lon must have bounds (rules.py:91)
2026-06-03 09:54:57,091 - xcmor.rules - INFO - adding bounds for lon (rules.py:93)
2026-06-03 09:54:57,193 - xcmor.rules - DEBUG - checking bounds for lat: True (rules.py:89)
2026-06-03 09:54:57,197 - xcmor.rules - WARNING - lat must have bounds (rules.py:91)
2026-06-03 09:54:57,198 - xcmor.rules - INFO - adding bounds for lat (rules.py:93)
2026-06-03 09:54:57,251 - xcmor.rules - DEBUG - checking bounds for time: True (rules.py:89)
2026-06-03 09:54:57,256 - xcmor.rules - WARNING - time must have bounds (rules.py:91)
2026-06-03 09:54:57,256 - xcmor.rules - INFO - adding bounds for time (rules.py:93)
2026-06-03 09:54:57,373 - xcmor.xcmor - DEBUG - setting time units: days since 2014-09-06T00:00:00 (xcmor.py:53)
2026-06-03 09:54:57,374 - xcmor.xcmor - DEBUG - coord: lon (xcmor.py:654)
2026-06-03 09:54:57,376 - xcmor.xcmor - DEBUG - coord: lat (xcmor.py:654)
2026-06-03 09:54:57,376 - xcmor.xcmor - INFO - swap dims: {'x': 'lon', 'y': 'lat'} (xcmor.py:656)
2026-06-03 09:54:57,378 - xcmor.xcmor - DEBUG - for attribute 'activity' --> add value 'Ice Sheet Model Intercomparison Project for CMIP6' (xcmor.py:592)
2026-06-03 09:54:57,378 - xcmor.xcmor - INFO - attribute 'experiment_id' has value 'piControl-withism' and requires attribute 'experiment' to be set to 'preindustrial control with interactive ice sheet' (xcmor.py:616)
2026-06-03 09:54:57,379 - xcmor.xcmor - WARNING - attribute 'experiment_id' has value 'piControl-withism' but attribute 'parent_activity_id' has value 'CMIP' which is not in the list of expected values: ['no parent'] (xcmor.py:611)
2026-06-03 09:54:57,380 - xcmor.xcmor - WARNING - attribute 'experiment_id' has value 'piControl-withism' but attribute 'parent_experiment_id' has value 'historical' which is not in the list of expected values: ['no parent'] (xcmor.py:611)
2026-06-03 09:54:57,381 - xcmor.xcmor - DEBUG - for attribute 'frequency_info' --> add value 'monthly mean samples' (xcmor.py:602)
2026-06-03 09:54:57,382 - xcmor.xcmor - DEBUG - for attribute 'grid_label_info' --> add value 'data reported on a model's native grid' (xcmor.py:602)
2026-06-03 09:54:57,382 - xcmor.xcmor - DEBUG - for attribute 'institution' --> add value 'Program for Climate Model Diagnosis and Intercomparison, Lawrence Livermore National Laboratory, Livermore, CA 94550, USA' (xcmor.py:592)
2026-06-03 09:54:57,383 - xcmor.xcmor - WARNING - attribute 'source_id' has value 'PCMDI-test-1-0' but attribute 'source' is set to 'PCMDI-test 1.0 (1989)' but CV requires 'PCMDI-test 1.0 (1989): 
aerosol: none
atmos: Earth1.0-gettingHotter (360 x 180 longitude/latitude; 50 levels; top level 0.1 mb)
atmosChem: none
land: Earth1.0
landIce: none
ocean: BlueMarble1.0-warming (360 x 180 longitude/latitude; 50 levels; top grid cell 0-10 m)
ocnBgchem: none
seaIce: Declining1.0-warming (360 x 180 longitude/latitude)'! (xcmor.py:620)
2026-06-03 09:54:57,384 - xcmor.xcmor - DEBUG - for attribute 'sub_experiment' --> add value 'none' (xcmor.py:592)
2026-06-03 09:54:57,384 - xcmor.xcmor - DEBUG - Checking global attribute 'Conventions' with value 'CF-1.7 CMIP-6.2' (xcmor.py:540)
2026-06-03 09:54:57,386 - xcmor.xcmor - DEBUG - Checking global attribute 'activity_id' with value 'ISMIP6' (xcmor.py:540)
2026-06-03 09:54:57,387 - xcmor.xcmor - DEBUG - Found valid value 'ISMIP6...' for 'activity_id' (xcmor.py:548)
2026-06-03 09:54:57,387 - xcmor.xcmor - DEBUG - Checking global attribute 'creation_date' with value '2026-06-03T09:54:57UTC' (xcmor.py:540)
2026-06-03 09:54:57,387 - xcmor.xcmor - DEBUG - Found global attribute 'creation_date' with value '2026-06-03T09:54:57UTC...' which has no specific requirements (xcmor.py:544)
2026-06-03 09:54:57,388 - xcmor.xcmor - DEBUG - Checking global attribute 'data_specs_version' with value '01.00.33' (xcmor.py:540)
2026-06-03 09:54:57,388 - xcmor.xcmor - DEBUG - Checking global attribute 'experiment' with value 'preindustrial control with interactive ice sheet' (xcmor.py:540)
2026-06-03 09:54:57,389 - xcmor.xcmor - DEBUG - Found global attribute 'experiment' with value 'preindustrial control with interactive ice sheet...' which has no specific requirements (xcmor.py:544)
2026-06-03 09:54:57,389 - xcmor.xcmor - DEBUG - Checking global attribute 'experiment_id' with value 'piControl-withism' (xcmor.py:540)
2026-06-03 09:54:57,390 - xcmor.xcmor - DEBUG - Found valid value 'piControl-withism...' for 'experiment_id' (xcmor.py:548)
2026-06-03 09:54:57,390 - xcmor.xcmor - DEBUG - Checking global attribute 'forcing_index' with value '1' (xcmor.py:540)
2026-06-03 09:54:57,391 - xcmor.xcmor - DEBUG - Checking global attribute 'frequency' with value 'mon' (xcmor.py:540)
2026-06-03 09:54:57,391 - xcmor.xcmor - DEBUG - Found valid value 'mon...' for 'frequency' (xcmor.py:548)
2026-06-03 09:54:57,392 - xcmor.xcmor - DEBUG - Checking global attribute 'further_info_url' with value 'None' (xcmor.py:540)
2026-06-03 09:54:57,392 - xcmor.xcmor - ERROR - global further_info_url not found but required (xcmor.py:542)
2026-06-03 09:54:57,392 - xcmor.xcmor - DEBUG - Checking global attribute 'grid' with value 'native atmosphere regular grid (3x4 latxlon)' (xcmor.py:540)
2026-06-03 09:54:57,393 - xcmor.xcmor - DEBUG - Found global attribute 'grid' with value 'native atmosphere regular grid (3x4 latxlon)...' which has no specific requirements (xcmor.py:544)
2026-06-03 09:54:57,393 - xcmor.xcmor - DEBUG - Checking global attribute 'grid_label' with value 'gn' (xcmor.py:540)
2026-06-03 09:54:57,394 - xcmor.xcmor - DEBUG - Found valid value 'gn...' for 'grid_label' (xcmor.py:548)
2026-06-03 09:54:57,394 - xcmor.xcmor - DEBUG - Checking global attribute 'initialization_index' with value '1' (xcmor.py:540)
2026-06-03 09:54:57,395 - xcmor.xcmor - DEBUG - Checking global attribute 'institution' with value 'Program for Climate Model Diagnosis and Intercomparison, Lawrence Livermore National Laboratory, Livermore, CA 94550, USA' (xcmor.py:540)
2026-06-03 09:54:57,395 - xcmor.xcmor - DEBUG - Found global attribute 'institution' with value 'Program for Climate Model Diagnosis and Intercompa...' which has no specific requirements (xcmor.py:544)
2026-06-03 09:54:57,395 - xcmor.xcmor - DEBUG - Checking global attribute 'institution_id' with value 'PCMDI' (xcmor.py:540)
2026-06-03 09:54:57,396 - xcmor.xcmor - DEBUG - Found valid value 'PCMDI...' for 'institution_id' (xcmor.py:548)
2026-06-03 09:54:57,396 - xcmor.xcmor - DEBUG - Checking global attribute 'license' with value 'CMIP6 model data produced by Lawrence Livermore PCMDI is licensed under a Creative Commons Attribution ShareAlike 4.0 International License (https://creativecommons.org/licenses). Consult https://pcmdi.llnl.gov/CMIP6/TermsOfUse for terms of use governing CMIP6 output, including citation requirements and proper acknowledgment. Further information about this data, including some limitations, can be found via the further_info_url (recorded as a global attribute in this file) and at https:///pcmdi.llnl.gov/. The data producers and data providers make no warranty, either express or implied, including, but not limited to, warranties of merchantability and fitness for a particular purpose. All liabilities arising from the supply of the information (including any liability arising in negligence) are excluded to the fullest extent permitted by law.' (xcmor.py:540)
2026-06-03 09:54:57,398 - xcmor.xcmor - ERROR - global attribute 'license' has value 'CMIP6 model data produced by Lawrence Livermore PC...' which does not match expected regex '^CMIP6 model data produced by .* is licensed under a Creative Commons .* License (https://creativecommons\.org/.*)\. *Consult https://pcmdi\.llnl\.gov/CMIP6/TermsOfUse for terms of use governing CMIP6 output, including citation requirements and proper acknowledgment\. *Further information about this data, including some limitations, can be found via the further_info_url (recorded as a global attribute in this file).*\. *The data producers and data providers make no warranty, either express or implied, including, but not limited to, warranties of merchantability and fitness for a particular purpose\. *All liabilities arising from the supply of the information (including any liability arising in negligence) are excluded to the fullest extent permitted by law\.$' (xcmor.py:555)
2026-06-03 09:54:57,401 - xcmor.xcmor - DEBUG - Checking global attribute 'mip_era' with value 'CMIP6' (xcmor.py:540)
2026-06-03 09:54:57,401 - xcmor.xcmor - DEBUG - Found valid value 'CMIP6...' for 'mip_era' (xcmor.py:548)
2026-06-03 09:54:57,402 - xcmor.xcmor - DEBUG - Checking global attribute 'nominal_resolution' with value '10000 km' (xcmor.py:540)
2026-06-03 09:54:57,402 - xcmor.xcmor - DEBUG - Found valid value '10000 km...' for 'nominal_resolution' (xcmor.py:548)
2026-06-03 09:54:57,402 - xcmor.xcmor - DEBUG - Checking global attribute 'physics_index' with value '1' (xcmor.py:540)
2026-06-03 09:54:57,403 - xcmor.xcmor - DEBUG - Checking global attribute 'product' with value 'model-output' (xcmor.py:540)
2026-06-03 09:54:57,403 - xcmor.xcmor - DEBUG - Found valid value 'model-output...' for 'product' (xcmor.py:548)
2026-06-03 09:54:57,404 - xcmor.xcmor - DEBUG - Checking global attribute 'realization_index' with value '3' (xcmor.py:540)
2026-06-03 09:54:57,404 - xcmor.xcmor - DEBUG - Checking global attribute 'realm' with value 'atmos atmosChem' (xcmor.py:540)
2026-06-03 09:54:57,405 - xcmor.xcmor - ERROR - global attribute 'realm' has value 'atmos atmosChem...' which is not one of the valid values: ['aerosol', 'atmos', 'atmosChem', 'land', 'landIce... (xcmor.py:559)
2026-06-03 09:54:57,405 - xcmor.xcmor - DEBUG - Checking global attribute 'source' with value 'PCMDI-test 1.0 (1989): 
aerosol: none
atmos: Earth1.0-gettingHotter (360 x 180 longitude/latitude; 50 levels; top level 0.1 mb)
atmosChem: none
land: Earth1.0
landIce: none
ocean: BlueMarble1.0-warming (360 x 180 longitude/latitude; 50 levels; top grid cell 0-10 m)
ocnBgchem: none
seaIce: Declining1.0-warming (360 x 180 longitude/latitude)' (xcmor.py:540)
2026-06-03 09:54:57,405 - xcmor.xcmor - DEBUG - Found global attribute 'source' with value 'PCMDI-test 1.0 (1989): 
aerosol: none
atmos: Earth...' which has no specific requirements (xcmor.py:544)
2026-06-03 09:54:57,407 - xcmor.xcmor - DEBUG - Checking global attribute 'source_id' with value 'PCMDI-test-1-0' (xcmor.py:540)
2026-06-03 09:54:57,407 - xcmor.xcmor - DEBUG - Found valid value 'PCMDI-test-1-0...' for 'source_id' (xcmor.py:548)
2026-06-03 09:54:57,407 - xcmor.xcmor - DEBUG - Checking global attribute 'source_type' with value 'AOGCM ISM AER' (xcmor.py:540)
2026-06-03 09:54:57,408 - xcmor.xcmor - ERROR - global attribute 'source_type' has value 'AOGCM ISM AER...' which is not one of the valid values: ['AER', 'AGCM', 'AOGCM', 'BGC', 'CHEM', 'ISM', 'LA... (xcmor.py:559)
2026-06-03 09:54:57,408 - xcmor.xcmor - DEBUG - Checking global attribute 'sub_experiment' with value 'none' (xcmor.py:540)
2026-06-03 09:54:57,409 - xcmor.xcmor - DEBUG - Found global attribute 'sub_experiment' with value 'none...' which has no specific requirements (xcmor.py:544)
2026-06-03 09:54:57,409 - xcmor.xcmor - DEBUG - Checking global attribute 'sub_experiment_id' with value 'none' (xcmor.py:540)
2026-06-03 09:54:57,410 - xcmor.xcmor - DEBUG - Found valid value 'none...' for 'sub_experiment_id' (xcmor.py:548)
2026-06-03 09:54:57,410 - xcmor.xcmor - DEBUG - Checking global attribute 'table_id' with value 'Amon' (xcmor.py:540)
2026-06-03 09:54:57,410 - xcmor.xcmor - DEBUG - Found valid value 'Amon...' for 'table_id' (xcmor.py:548)
2026-06-03 09:54:57,411 - xcmor.xcmor - DEBUG - Checking global attribute 'tracking_id' with value 'None' (xcmor.py:540)
2026-06-03 09:54:57,411 - xcmor.xcmor - ERROR - global tracking_id not found but required (xcmor.py:542)
2026-06-03 09:54:57,412 - xcmor.xcmor - DEBUG - Checking global attribute 'variable_id' with value 'tas' (xcmor.py:540)
2026-06-03 09:54:57,412 - xcmor.xcmor - DEBUG - Found global attribute 'variable_id' with value 'tas...' which has no specific requirements (xcmor.py:544)
2026-06-03 09:54:57,412 - xcmor.xcmor - DEBUG - Checking global attribute 'variant_label' with value 'None' (xcmor.py:540)
2026-06-03 09:54:57,413 - xcmor.xcmor - ERROR - global variant_label not found but required (xcmor.py:542)
/home/docs/checkouts/readthedocs.org/user_builds/xcmor/conda/latest/lib/python3.10/site-packages/cf_xarray/accessor.py:718: FutureWarning: The return type of `Dataset.dims` will be changed to return a set of dimension names in future, in order to be more consistent with `DataArray.dims`. To access a mapping from dimension names to lengths, please use `Dataset.sizes`.
  unused_keys = set(attribute.keys()) - set(inverted)
/home/docs/checkouts/readthedocs.org/user_builds/xcmor/conda/latest/lib/python3.10/site-packages/cf_xarray/accessor.py:719: FutureWarning: The return type of `Dataset.dims` will be changed to return a set of dimension names in future, in order to be more consistent with `DataArray.dims`. To access a mapping from dimension names to lengths, please use `Dataset.sizes`.
  for key, value in attribute.items():
/home/docs/checkouts/readthedocs.org/user_builds/xcmor/conda/latest/lib/python3.10/site-packages/cf_xarray/accessor.py:727: FutureWarning: The return type of `Dataset.dims` will be changed to return a set of dimension names in future, in order to be more consistent with `DataArray.dims`. To access a mapping from dimension names to lengths, please use `Dataset.sizes`.
  newmap.update({key: attribute[key] for key in unused_keys})
2026-06-03 09:54:57,424 - xcmor.xcmor - DEBUG - transposing order: ['T', 'Y', 'X'] (xcmor.py:31)
<xarray.Dataset> Size: 224B
Dimensions:      (lon: 2, lat: 2, time: 3, bounds: 2)
Coordinates:
  * lon          (lon) float64 16B 80.17 80.68
  * lat          (lat) float64 16B 42.25 42.21
    height       float64 8B ...
    lon_bounds   (lon, bounds) float64 32B ...
    lat_bounds   (lat, bounds) float64 32B ...
    time_bounds  (time, bounds) datetime64[ns] 48B 2014-09-05T12:00:00 ... 20...
  * time         (time) datetime64[ns] 24B 2014-09-06 2014-09-07 2014-09-08
Dimensions without coordinates: bounds
Data variables:
    tas          (time, lat, lon) float32 48B ...
Attributes: (12/50)
    Conventions:            CF-1.7 CMIP-6.2
    activity:               Ice Sheet Model Intercomparison Project for CMIP6
    activity_id:            ISMIP6
    branch_method:          standard
    branch_time_in_child:   59400.0
    branch_time_in_parent:  59400.0
    ...                     ...
    sub_experiment:         none
    sub_experiment_id:      none
    table_id:               Amon
    tracking_prefix:        hdl:21.14100
    variable_id:            tas
    version:                20260603

Let’s write this to NetCDF and use the compliance checker to find issues:

ds_out.to_netcdf("tas.nc")
/tmp/ipykernel_2788/313901145.py:1: UserWarning: Times can't be serialized faithfully to int64 with requested units 'days since 2014-09-06'. Resolution of 'hours' needed. Serializing times to floating point instead. Set encoding['dtype'] to integer dtype to serialize to int64. Set encoding['dtype'] to floating point dtype to silence this warning.
  ds_out.to_netcdf("tas.nc")
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[7], line 1
----> 1 ds_out.to_netcdf("tas.nc")

File ~/checkouts/readthedocs.org/user_builds/xcmor/conda/latest/lib/python3.10/site-packages/xarray/core/dataset.py:2030, in Dataset.to_netcdf(self, path, mode, format, group, engine, encoding, unlimited_dims, compute, invalid_netcdf, auto_complex)
   2027     encoding = {}
   2028 from xarray.backends.api import to_netcdf
-> 2030 return to_netcdf(  # type: ignore[return-value]  # mypy cannot resolve the overloads:(
   2031     self,
   2032     path,
   2033     mode=mode,
   2034     format=format,
   2035     group=group,
   2036     engine=engine,
   2037     encoding=encoding,
   2038     unlimited_dims=unlimited_dims,
   2039     compute=compute,
   2040     multifile=False,
   2041     invalid_netcdf=invalid_netcdf,
   2042     auto_complex=auto_complex,
   2043 )

File ~/checkouts/readthedocs.org/user_builds/xcmor/conda/latest/lib/python3.10/site-packages/xarray/backends/api.py:1928, in to_netcdf(dataset, path_or_file, mode, format, group, engine, encoding, unlimited_dims, compute, multifile, invalid_netcdf, auto_complex)
   1923 # TODO: figure out how to refactor this logic (here and in save_mfdataset)
   1924 # to avoid this mess of conditionals
   1925 try:
   1926     # TODO: allow this work (setting up the file for writing array data)
   1927     # to be parallelized with dask
-> 1928     dump_to_store(
   1929         dataset, store, writer, encoding=encoding, unlimited_dims=unlimited_dims
   1930     )
   1931     if autoclose:
   1932         store.close()

File ~/checkouts/readthedocs.org/user_builds/xcmor/conda/latest/lib/python3.10/site-packages/xarray/backends/api.py:1975, in dump_to_store(dataset, store, writer, encoder, encoding, unlimited_dims)
   1972 if encoder:
   1973     variables, attrs = encoder(variables, attrs)
-> 1975 store.store(variables, attrs, check_encoding, writer, unlimited_dims=unlimited_dims)

File ~/checkouts/readthedocs.org/user_builds/xcmor/conda/latest/lib/python3.10/site-packages/xarray/backends/common.py:456, in AbstractWritableDataStore.store(self, variables, attributes, check_encoding_set, writer, unlimited_dims)
    453 if writer is None:
    454     writer = ArrayWriter()
--> 456 variables, attributes = self.encode(variables, attributes)
    458 self.set_attributes(attributes)
    459 self.set_dimensions(variables, unlimited_dims=unlimited_dims)

File ~/checkouts/readthedocs.org/user_builds/xcmor/conda/latest/lib/python3.10/site-packages/xarray/backends/common.py:640, in WritableCFDataStore.encode(self, variables, attributes)
    637 def encode(self, variables, attributes):
    638     # All NetCDF files get CF encoded by default, without this attempting
    639     # to write times, for example, would fail.
--> 640     variables, attributes = cf_encoder(variables, attributes)
    641     variables = {
    642         k: ensure_dtype_not_object(v, name=k) for k, v in variables.items()
    643     }
    644     variables = {k: self.encode_variable(v) for k, v in variables.items()}

File ~/checkouts/readthedocs.org/user_builds/xcmor/conda/latest/lib/python3.10/site-packages/xarray/conventions.py:787, in cf_encoder(variables, attributes)
    784 # add encoding for time bounds variables if present.
    785 _update_bounds_encoding(variables)
--> 787 new_vars = {k: encode_cf_variable(v, name=k) for k, v in variables.items()}
    789 # Remove attrs from bounds variables (issue #2921)
    790 for var in new_vars.values():

File ~/checkouts/readthedocs.org/user_builds/xcmor/conda/latest/lib/python3.10/site-packages/xarray/conventions.py:787, in <dictcomp>(.0)
    784 # add encoding for time bounds variables if present.
    785 _update_bounds_encoding(variables)
--> 787 new_vars = {k: encode_cf_variable(v, name=k) for k, v in variables.items()}
    789 # Remove attrs from bounds variables (issue #2921)
    790 for var in new_vars.values():

File ~/checkouts/readthedocs.org/user_builds/xcmor/conda/latest/lib/python3.10/site-packages/xarray/conventions.py:102, in encode_cf_variable(var, needs_copy, name)
     90 ensure_not_multiindex(var, name=name)
     92 for coder in [
     93     CFDatetimeCoder(),
     94     CFTimedeltaCoder(),
   (...)
    100     variables.BooleanCoder(),
    101 ]:
--> 102     var = coder.encode(var, name=name)
    104 for attr_name in CF_RELATED_DATA:
    105     pop_to(var.encoding, var.attrs, attr_name)

File ~/checkouts/readthedocs.org/user_builds/xcmor/conda/latest/lib/python3.10/site-packages/xarray/coding/times.py:1377, in CFDatetimeCoder.encode(self, variable, name)
   1374     dtype = data.dtype if data.dtype.kind == "f" else "float64"
   1375 (data, units, calendar) = encode_cf_datetime(data, units, calendar, dtype)
-> 1377 safe_setitem(attrs, "units", units, name=name)
   1378 safe_setitem(attrs, "calendar", calendar, name=name)
   1380 return Variable(dims, data, attrs, encoding, fastpath=True)

File ~/checkouts/readthedocs.org/user_builds/xcmor/conda/latest/lib/python3.10/site-packages/xarray/coding/common.py:108, in safe_setitem(dest, key, value, name)
    106 if key in dest:
    107     var_str = f" on variable {name!r}" if name else ""
--> 108     raise ValueError(
    109         f"failed to prevent overwriting existing key {key} in attrs{var_str}. "
    110         "This is probably an encoding field used by xarray to describe "
    111         "how a variable is serialized. To proceed, remove this key from "
    112         "the variable's attributes manually."
    113     )
    114 dest[key] = value

ValueError: failed to prevent overwriting existing key units in attrs on variable 'time_bounds'. This is probably an encoding field used by xarray to describe how a variable is serialized. To proceed, remove this key from the variable's attributes manually.
!compliance-checker -t cf:1.7 tas.nc