Skip to content

cmaq

CMAQ File Reader

CMAQReader

Bases: GriddedReader

Reader for CMAQ (Community Multiscale Air Quality) model output files.

Source code in monetio/readers/cmaq.py
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
@register_reader("cmaq")
class CMAQReader(GriddedReader):
    """
    Reader for CMAQ (Community Multiscale Air Quality) model output files.
    """

    def open_dataset(
        self,
        files: str | list[str],
        earth_radius: float = 6370000,
        convert_to_ppb: bool = True,
        drop_duplicates: bool = False,
        **kwargs: Any,
    ) -> xr.Dataset:
        """
        Reads CMAQ netCDF files.

        Parameters
        ----------
        files : Union[str, List[str]]
            File path, list of paths, or glob pattern.
        earth_radius : float, optional
            Earth radius in meters, by default 6370000.
        convert_to_ppb : bool, optional
            Convert gas species from ppmV to ppbV, by default True.
        drop_duplicates : bool, optional
            Drop duplicate time steps within each file, by default False.
        **kwargs : Any
            Additional arguments passed to xarray.open_mfdataset or the driver.

        Returns
        -------
        xarray.Dataset
            The processed CMAQ dataset.
        """
        # 1. Setup preprocessing
        if "preprocess" not in kwargs:
            kwargs["preprocess"] = partial(
                cmaq_preprocess,
                earth_radius=earth_radius,
                convert_to_ppb=convert_to_ppb,
            )

        # 2. Open the dataset using standard xarray (via XarrayDriver)
        if "combine" not in kwargs:
            kwargs["combine"] = "nested"
        if "concat_dim" not in kwargs:
            # If we rename in preprocess, we should use 'time'
            # But to be safe with existing logic, we let it be 'TSTEP' if not renamed yet,
            # or 'time' if it is.
            # Actually, preprocess runs BEFORE concatenation.
            kwargs["concat_dim"] = "time"

        ds = self.driver.open(files, **kwargs)

        # 3. Finalize
        if drop_duplicates:
            ds = ds.drop_duplicates("time")
            ds = update_history(ds, "Dropped duplicate time steps.")

        ds = self.harmonize(ds)

        # Update history
        ds = update_history(ds, "Read CMAQ data.")

        return ds

    def harmonize(self, ds: xr.Dataset) -> xr.Dataset:
        """
        Standardize variable names and metadata.

        Parameters
        ----------
        ds : xr.Dataset
            CMAQ dataset.

        Returns
        -------
        xr.Dataset
            Harmonized dataset.
        """
        # 1. Standardize variable names and drop redundant ones
        ds = _harmonize_ioapi_vars(ds)

        # 2. Clean up attributes
        ds = _scientific_hygiene(ds)

        # Update history
        ds = update_history(ds, "Harmonized CMAQ dataset.")

        return ds

harmonize(ds)

Standardize variable names and metadata.

Parameters:

Name Type Description Default
ds Dataset

CMAQ dataset.

required

Returns:

Type Description
Dataset

Harmonized dataset.

Source code in monetio/readers/cmaq.py
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
def harmonize(self, ds: xr.Dataset) -> xr.Dataset:
    """
    Standardize variable names and metadata.

    Parameters
    ----------
    ds : xr.Dataset
        CMAQ dataset.

    Returns
    -------
    xr.Dataset
        Harmonized dataset.
    """
    # 1. Standardize variable names and drop redundant ones
    ds = _harmonize_ioapi_vars(ds)

    # 2. Clean up attributes
    ds = _scientific_hygiene(ds)

    # Update history
    ds = update_history(ds, "Harmonized CMAQ dataset.")

    return ds

open_dataset(files, earth_radius=6370000, convert_to_ppb=True, drop_duplicates=False, **kwargs)

Reads CMAQ netCDF files.

Parameters:

Name Type Description Default
files Union[str, List[str]]

File path, list of paths, or glob pattern.

required
earth_radius float

Earth radius in meters, by default 6370000.

6370000
convert_to_ppb bool

Convert gas species from ppmV to ppbV, by default True.

True
drop_duplicates bool

Drop duplicate time steps within each file, by default False.

False
**kwargs Any

Additional arguments passed to xarray.open_mfdataset or the driver.

{}

Returns:

Type Description
Dataset

The processed CMAQ dataset.

Source code in monetio/readers/cmaq.py
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
def open_dataset(
    self,
    files: str | list[str],
    earth_radius: float = 6370000,
    convert_to_ppb: bool = True,
    drop_duplicates: bool = False,
    **kwargs: Any,
) -> xr.Dataset:
    """
    Reads CMAQ netCDF files.

    Parameters
    ----------
    files : Union[str, List[str]]
        File path, list of paths, or glob pattern.
    earth_radius : float, optional
        Earth radius in meters, by default 6370000.
    convert_to_ppb : bool, optional
        Convert gas species from ppmV to ppbV, by default True.
    drop_duplicates : bool, optional
        Drop duplicate time steps within each file, by default False.
    **kwargs : Any
        Additional arguments passed to xarray.open_mfdataset or the driver.

    Returns
    -------
    xarray.Dataset
        The processed CMAQ dataset.
    """
    # 1. Setup preprocessing
    if "preprocess" not in kwargs:
        kwargs["preprocess"] = partial(
            cmaq_preprocess,
            earth_radius=earth_radius,
            convert_to_ppb=convert_to_ppb,
        )

    # 2. Open the dataset using standard xarray (via XarrayDriver)
    if "combine" not in kwargs:
        kwargs["combine"] = "nested"
    if "concat_dim" not in kwargs:
        # If we rename in preprocess, we should use 'time'
        # But to be safe with existing logic, we let it be 'TSTEP' if not renamed yet,
        # or 'time' if it is.
        # Actually, preprocess runs BEFORE concatenation.
        kwargs["concat_dim"] = "time"

    ds = self.driver.open(files, **kwargs)

    # 3. Finalize
    if drop_duplicates:
        ds = ds.drop_duplicates("time")
        ds = update_history(ds, "Dropped duplicate time steps.")

    ds = self.harmonize(ds)

    # Update history
    ds = update_history(ds, "Read CMAQ data.")

    return ds

cmaq_preprocess(ds, *, earth_radius=6370000, convert_to_ppb=True)

Preprocess function for a single CMAQ file.

Parameters:

Name Type Description Default
ds Dataset

Input CMAQ dataset.

required
earth_radius float

Earth radius in meters, by default 6370000.

6370000
convert_to_ppb bool

Convert gas species to ppbV, by default True.

True

Returns:

Type Description
Dataset

Processed dataset.

Examples:

>>> ds = cmaq_preprocess(ds, convert_to_ppb=True)
Source code in monetio/readers/cmaq.py
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
def cmaq_preprocess(
    ds: xr.Dataset,
    *,
    earth_radius: float = 6370000,
    convert_to_ppb: bool = True,
) -> xr.Dataset:
    """
    Preprocess function for a single CMAQ file.

    Parameters
    ----------
    ds : xarray.Dataset
        Input CMAQ dataset.
    earth_radius : float, optional
        Earth radius in meters, by default 6370000.
    convert_to_ppb : bool, optional
        Convert gas species to ppbV, by default True.

    Returns
    -------
    xarray.Dataset
        Processed dataset.

    Examples
    --------
    >>> ds = cmaq_preprocess(ds, convert_to_ppb=True)
    """
    # 1. Add lazy diagnostic variables
    for name, spec in DIAGNOSTICS.items():
        ds = add_lazy_diagnostic(ds, name, spec)

    # 2. Grid and Coordinates
    grid = grid_from_dataset(ds, earth_radius=earth_radius)
    if grid:
        ds = ds.assign_attrs({"proj4_srs": grid})
        ds = _add_ioapi_latlon(ds, grid)

        # Also assign proj4_srs to all data variables for compatibility
        for var in ds.data_vars:
            ds[var].attrs["proj4_srs"] = grid

    # 3. Time
    if "TFLAG" in ds.variables:
        from .base import _get_ioapi_times

        ds = _get_ioapi_times(ds)

    # 4. Units and Formatting
    if convert_to_ppb:
        ds = _convert_to_ppb(ds)
    ds = _format_units(ds)

    # 5. Rename dimensions
    ds = _harmonize_ioapi_dims(ds)

    # 6. Harmonize variables (lowercase and drop redundant)
    ds = _harmonize_ioapi_vars(ds)

    # 7. Scientific Hygiene
    ds = _scientific_hygiene(ds)

    # Update history
    ds = update_history(ds, "Preprocessed CMAQ data.")

    return ds