CMAC needs, per radar, a set of field-name mappings, processing thresholds,
plot ranges, and output metadata. The built-in values for every radar CMAC
already knows about live in cmac/default_config.py. To process a radar
that isn't in there — or to tweak a couple of values for one that is —
write a YAML file and pass its path as config_file= instead of editing
the Python defaults.
Two worked examples ship in the repository:
documents/example_config.yaml— a minimal file showing how to override one existing radar and add a brand-new one.configs/bnfcsapr2.yaml— a complete, real configuration (both PPI and RHI scan strategies) for the BNF C-SAPR2 radar.
Every function that reads per-radar configuration takes an optional
config_file argument:
cmac.cmac(radar, sonde, config, config_file=...)cmac.quicklooks_ppi(radar, config, config_file=...)/cmac.quicklooks_rhi(radar, config, config_file=...)cmac.area_coverage(radar, config=..., config_file=...)cmac.tall_clutter(..., config_file=...)- The lower-level getters in
cmac.config:get_metadata,get_field_names,get_cmac_values,get_plot_values,get_zs_relationships,get_default_metadata
as well as the cmac command line script, via -c / --config-file.
config (the other required argument, e.g. "bnf_csapr2_ppi") selects
which radar's entry to use out of the file/defaults. It must match a key
under the relevant section, either in cmac/default_config.py or in your
YAML file.
Loading is per-radar-key and per-section, not whole-file:
- Start from
cmac/default_config.py's dict forconfigin that section (empty dict ifconfigisn't a built-in radar). - If a YAML file was given and it defines that same section +
configkey, its keys are merged on top, overriding matching keys and adding new ones. - Everything you don't mention keeps its default value.
This means you only ever need to list the handful of values you actually
want to change — you never have to restate an entire radar's configuration
to override one threshold. The one exception is mbfs (see below): setting
it in YAML replaces the whole fuzzy-logic membership-function dict, not
just the classes you list.
Requires PyYAML (pip install pyyaml).
All five sections are optional — include only what you need.
metadata:
<config_name>:
<key>: <value>
...
field_names:
<config_name>:
<key>: <value>
...
cmac_values:
<config_name>:
<key>: <value>
...
plot_values:
<config_name>:
<key>: <value>
...
zs_relationships:
<relationship name>:
A: <float>
B: <float>
abbreviation: <str>
default_metadata:
<key>: <value>
...<config_name> is the same string passed as config everywhere else,
e.g. bnf_csapr2_ppi. default_metadata is not per-radar — it's the
single global fallback used by cmac() when meta_append is None or
"default" (see Metadata below).
If config_name isn't already a key in cmac/default_config.py, you must
supply every section the functions you call actually read:
| You call... | Sections required for that config_name |
|---|---|
cmac() |
field_names, cmac_values, metadata |
quicklooks_ppi() / quicklooks_rhi() |
field_names, plot_values |
area_coverage(config=...) |
cmac_values (only for its two threshold keys; both have hard-coded fallbacks otherwise) |
Missing a required section for a config name not covered by the defaults
raises KeyError.
bnf_csapr2_ppi and bnf_csapr2_rhi describe the same physical radar with
two scan strategies, so configs/bnfcsapr2.yaml defines metadata,
field_names, and cmac_values once with a YAML anchor (&name) and
reuses them for the second key with a merge key (<<: *name), overriding
only what actually differs (typically save_name):
cmac_values:
bnf_csapr2_ppi: &bnf_csapr2_cmac_values
save_name: bnfcsapr2cmacppiS3.c1
...
bnf_csapr2_rhi:
<<: *bnf_csapr2_cmac_values
save_name: bnfcsapr2cmacrhiS3.c1plot_values usually differs more between PPI and RHI (PPI has lat/lon map
bounds, RHI doesn't), so it's typically spelled out separately for each.
Maps the logical field names CMAC's processing code uses internally to the
actual field names present in your radar object / sonde dataset. All keys
below are required for a new radar entry used with cmac(); the last four
(zdr_field, pia_field, phidp_field, refl_field) name fields that
cmac() itself creates during processing, so they're typically pointed at
CMAC's own output field names rather than anything in the raw input file.
| Key | Meaning | Typical value |
|---|---|---|
reflectivity |
Input (raw) reflectivity field | reflectivity |
velocity |
Input radial (Doppler) velocity field | mean_doppler_velocity |
input_zdr |
Input differential reflectivity field (offset gets added to this one) | differential_reflectivity |
differential_reflectivity |
Differential reflectivity field name used downstream | differential_reflectivity |
input_phidp_field |
Input differential phase field | differential_phase |
input_clutter_corrected_reflectivity |
Reflectivity field used as input when gen_clutter_from_refl is enabled |
reflectivity |
clutter |
Ground-clutter flag field name (read if present, or written to if gen_clutter_from_refl generates one) |
ground_clutter |
normalized_coherent_power |
Normalized coherent power (NCP) field | normalized_coherent_power |
cross_correlation_ratio |
Copolar correlation coefficient field | copol_correlation_coeff |
signal_to_noise_ratio |
Existing SNR field name, or null/absent to have cmac() compute one from reflectivity |
signal_to_noise_ratio_copolar_h or null |
altitude |
Sonde variable: geopotential/altitude | alt |
temperature |
Sonde variable: dry-bulb temperature | tdry |
u_wind |
Sonde variable: zonal wind component | u_wind |
v_wind |
Sonde variable: meridional wind component | v_wind |
zdr_field |
ZDR field name passed into pyart.correct.calculate_attenuation_zphi |
corrected_differential_reflectivity |
pia_field |
Output name for path-integrated attenuation | path_integrated_attenuation |
phidp_field |
Differential phase field passed into attenuation correction | corrected_differential_phase |
refl_field |
Reflectivity field passed into attenuation correction | corrected_reflectivity |
Setting signal_to_noise_ratio: null (YAML) / None (JSON via
meta_append) tells cmac() to derive SNR from reflectivity with
pyart.retrieve.calculate_snr_from_reflectivity instead of reading an
existing field.
Processing thresholds and coefficients for one radar. Every key has a
built-in fallback (listed below), applied via cmac_config.get(key, default), except the ones marked (required) — those are read with
plain dict indexing (cmac_config[key]) and a new radar entry omitting
them will raise KeyError the first time cmac() runs.
| Key | Meaning |
|---|---|
save_name |
Base filename cmac, the CLI script, uses when writing the output CfRadial file |
sonde_name |
Informational: the ARM sonde datastream this radar is normally paired with |
site_alt |
Overrides radar.altitude['data'][0] (meters) before processing |
| Key | Meaning |
|---|---|
ref_offset |
dB added to the raw reflectivity field before processing, and passed to calculate_attenuation_zphi as its offset |
zdr_offset |
dB added to input_zdr (or every field listed in offset_zdrs, if present) |
offset_zdrs |
(optional list) Field names to apply zdr_offset to, instead of just input_zdr |
flip_phidp |
true/false — multiply PhiDP by -1 before processing |
phidp_flipped |
(optional list) Field names to flip when flip_phidp is true, instead of just input_phidp_field |
gen_clutter_from_refl |
true/false — derive a clutter field from reflectivity instead of reading clutter from the input |
gen_clutter_from_refl_diff |
dBZ difference threshold used by that derivation |
gen_clutter_from_refl_alt |
Altitude ceiling (m) used by that derivation |
clutter_mask_z_for_texture |
true/false — mask velocity by ground_clutter before computing velocity texture |
| Key | Meaning |
|---|---|
attenuation_a_coef |
a coefficient, specific attenuation (Z-PHI) |
c_coef |
c coefficient, specific differential attenuation |
d_coef |
d coefficient, specific differential attenuation |
beta_coef |
beta coefficient, Z-PHI |
self_const |
self_const passed to phase_proc_lp_gf (LP KDP method only) |
| Key | Meaning |
|---|---|
kdp_method |
"bringi" (CSU KDP, default if unset) or "lp" (linear-programming, needs COIN-OR/CyLP) |
(for the _Z variant, moment is corrected_reflectivity converted to
linear units, i.e. 10^(dBZ / 10), before the exponent is applied)
| Key |
|---|
rain_rate_a_coef_A, rain_rate_b_coef_A — from specific attenuation |
rain_rate_a_coef_Z, rain_rate_b_coef_Z — from corrected reflectivity |
rain_rate_a_coef_Kdp, rain_rate_b_coef_Kdp — from specific differential phase |
| Key | Meaning |
|---|---|
beam_width |
Radar beam width in degrees |
radar_height_offset |
Height offset (m) for the beam-blockage calculation |
cbb_blockage_threshold |
(has default 0.80) Cumulative beam-blockage fraction above which a gate is flagged terrain_blockage |
| Key | Default | Meaning |
|---|---|---|
gate_id_method |
cmac_fuzzy |
Which classifier fills gate_id. cmac_fuzzy is CMAC's own five-class fuzzy scheme; radar_palette delegates to radar_palette.gateid. See Gate-ID backends. |
Read only when gate_id_method is cmac_fuzzy.
| Key | Meaning |
|---|---|
mbfs |
Custom membership functions. Replaces the entire default dict if set — list every class you want active. |
hard_const |
Custom hard constraints, replacing the default list. |
fuzzy_tex_start (default 2.0), fuzzy_tex_end (default 2.1) |
Velocity-texture range used by the fuzzy scorer |
fuzzy_score_median_size (default (3, 4)) |
Median-filter kernel applied to the fuzzy score |
Read only when gate_id_method is radar_palette. Every key left null
takes that classifier's own documented default, so pin one only where your
radar needs to differ.
| Key | Default | Meaning |
|---|---|---|
gate_id_class_map |
null |
Class name -> CMAC category, overriding the default fold for the classes named. Partial maps are merged over the default. Targets must be one of multi_trip, rain, snow, no_scatter, melting. |
gate_id_freezing_level |
null |
Freezing level in m MSL for the melting-layer constraints. null derives it from the mapped sounding, which is what a site with a sounding should use. |
gate_id_apply_offsets |
true |
Add ref_offset / zdr_offset to the uncorrected moments the classifier sees. See Gate-ID backends. |
gate_id_unclassified_policy |
warn |
What to do when the classifier declines a volume. warn reports and continues; error fails the run. See Gate-ID backends. |
gate_id_snr_min |
3.0 |
Gates below this SNR are forced to no_scatter before scoring. null disables the test. |
gate_id_min_run |
3 |
Class runs shorter than this many gates along a ray are dissolved. |
gate_id_despeckle_keep_dbz |
30.0 |
Reflectivity at or above which a gate is never despeckled or gated out. |
gate_id_incoherent_frac |
null |
Velocity texture, as a fraction of the uniform-random-phase limit, above which a gate cannot be first-trip weather. Instrument-specific in practice — see below. 0 disables the test. |
gate_id_texture_window |
4 |
Footprint of the texture operator, in gates and rays. |
gate_id_publish_margin |
false |
Also write scatterer_classification_margin, the winning minus runner-up score. Doubles the classification storage in the output file. |
gate_id_incoherent_frac is exposed per radar because it was measured not to
transfer between instruments: the fraction of precipitation gates
(Z >= 25 dBZ, RhoHV > 0.95) it rejects ranges from under 1% on weak-echo
C-band RHIs to 24.6% on an S-band NEXRAD volume. Treat it as a per-instrument
calibration, not a universal constant.
The gate_id field is the mask behind every masked product in the file, and
each step includes a different set of categories:
| Step | Categories included |
|---|---|
| Velocity dealiasing, velocity texture | rain, melting, snow |
Z-PHI attenuation correction, corrected_reflectivity, specific_attenuation, KDP filtering |
rain, melting, snow |
rain_rate_A, rain_rate_Kdp, rain_rate_Z |
rain |
snow_rate_* |
snow |
The melting layer is a valid radar return and is included in the Z-PHI
correction, so corrected_reflectivity covers melting-layer gates. (Before
this branch that filter admitted gate_id 1 and 2 as bare literals, which
excluded melting and also assumed the fuzzy classifier's category order; the
codes now come from the field's own notes string.)
Both backends publish the same gate_id field with the same five categories
in the same order — 0:multi_trip,1:rain,2:snow,3:no_scatter,4:melting, with
clutter (5) and terrain_blockage (6) appended by cmac() when it has a
clutter field, a vendor classification mask or a geotiff. That ordering is a
contract: cmac.gate_id.get_gate_id_categories derives each code from its
position in the field's notes string, and the gate filter used before the
Z-PHI attenuation correction reads codes 1 and 2 as literals.
The radar_palette backend classifies into eleven classes and folds them
onto those five. The fold is lossy, so the eleven-class result is published
in full alongside it as scatterer_classification with CF
flag_values/flag_meanings:
| radar-palette class | CMAC category |
|---|---|
light_rain, moderate_rain, heavy_rain |
rain |
ice_snow, graupel_hail |
snow |
melting_wet |
melting |
multi_trip |
multi_trip |
no_scatter, unclassified, clutter, biological |
no_scatter |
clutter and biological fold to no_scatter rather than to CMAC's own
clutter category because that category is code 5 and exists only when
cmac() was given the inputs to add it; claiming it here would either leave
a gap in the positional notes contract or duplicate a label. Both classes
are excluded from meteorological gates either way, and both remain readable
in scatterer_classification.
radar_palette is not a CMAC dependency — it is imported only when a
configuration selects this backend. The classifier lives in the package's
gateid module, which is not yet in radar-palette's released main branch;
install it from the development branch:
pip install 'radar-palette @ git+https://github.com/scollis/radar-palette@gateid'
Selecting the backend without it installed raises an ImportError naming
both that install and the cmac_fuzzy fallback.
Two behaviours of this backend are CMAC's own and worth knowing about:
- Classification runs on the uncorrected moments. For each moment the
backend takes the
uncorrected_*field where the volume publishes one —uncorrected_reflectivity_h,uncorrected_differential_reflectivity,uncorrected_copol_correlation_coeff,uncorrected_differential_phase,uncorrected_mean_doppler_velocity_h,uncorrected_spectral_width_h, and the uncorrected NCP and SNR where they exist — and falls back to the radar'sfield_namesentry otherwise. Deciding what the instrument saw is a judgement about the measurement, and the unprefixed fields on an ARMa1volume already carry vendor clutter filtering, thresholding and (on the CACTI and TRACER generations) attenuation correction, whose definition varies by site and instrument generation. Legacy platforms (MDV-era X-SAPR, NEXRAD) publish no uncorrected moments, so there the config remains the source of truth. Exactly one candidate per moment is handed to the classifier, so its own internal preference order cannot pick a different field. The field each moment resolved to is printed whenverbose=True. - Because
cmac()appliesref_offsetandzdr_offsetin place to the configured reflectivity and ZDR, and an uncorrected moment has not been through that, the backend adds those offsets to its own view of them — calibration is a property of the instrument, not a correction. Setgate_id_apply_offsets: falseto classify on the raw values instead. A moment that fell back to the configured field is never offset twice. - A volume the classifier declines is reported, not folded to clear air.
radar_paletteskips sweeps it judges unsuitable — a narrow-elevation-span RHI, for example — and returnsunclassifiedfor every gate in them, which folds ontono_scatterand is then indistinguishable from empty sky: every masked product in the file would be empty with nothing saying why. When any sweep is skipped, or more than half the gates come backunclassified, the backend warns with the sweep numbers and elevation spans, and writesgate_id_unclassified_gatesandgate_id_skipped_sweepsto the file. Setgate_id_unclassified_policy: errorto make it a hard failure instead. Four TRACER C-SAPR2 cell-tracking RHIs (16-76 rays, elevation spans of 3.7 to 18.8 degrees) hit this; the fuzzy classifier handles them, labelling 1,186 to 15,434 gates rain on three of the four. - Spectrum width has no
field_nameskey (CMAC's own classifier does not use it,radar_palettedoes), so it is probed for by name —spectrum_widthon MDV and NEXRAD volumes,spectral_widthon modern ARM CfRadial. Add aspectral_widthkey tofield_namesto pin it. - A gate with no finite measured moment cannot carry a hydrometeor label.
The underlying classifier tests whether a feature is present rather than
whether its value at a gate is finite, and gate temperature is finite
everywhere, so empty-sky gates can otherwise score above zero on
temperature alone. The number of gates demoted is written to the output
file as the
gate_id_no_evidence_gatesglobal attribute, alongsidegate_id_methodandgate_id_temperature_source.
| Key | Default | Meaning |
|---|---|---|
snow_density |
0.073 |
1 / SWE ratio used for snowfall rate |
phidp_nowrap |
50 |
nowrap passed to phase_proc_lp_gf (LP method) |
kdp_phase_proc_max |
10.0 |
KDP ceiling for the phase-processing gate filter used before Z-PHI attenuation correction |
phidp_despeckle_size |
49 |
Despeckle filter size (gates) applied to differential phase before attenuation correction |
corrected_velocity_valid_min |
-100.0 |
valid_min metadata for corrected_velocity (only set if not already present) |
corrected_velocity_valid_max |
100.0 |
valid_max metadata for corrected_velocity |
melt_fzl_ceiling |
5000.0 |
Freezing-level ceiling (m) |
melt_fzl_replacement |
3500.0 |
Freezing level used when the sounding value is unrealistic |
melt_fzl_floor |
1000.0 |
Freezing-level floor (m) |
max_kdp |
15.0 |
KDP ceiling applied by fix_phase_fields |
velocity_texture_window |
4 |
Window size for velocity-texture calculation |
velocity_texture_median_size |
(4, 4) |
Median-filter kernel for velocity texture |
area_coverage_precip_threshold |
10.0 |
dBZ threshold used by area_coverage() |
area_coverage_convection_threshold |
40.0 |
dBZ threshold used by area_coverage() |
rain_rate_valid_max |
400 |
valid_max clamp applied to every rain_rate_* field |
snow_rate_valid_max |
500 |
valid_max clamp applied to every snow_rate_* field |
mbfs is a mapping of classification name -> field name -> [[x1, x2, x3, x4], weight], where [x1, x2, x3, x4] is a trapezoidal membership
function over that field's values and weight scales that field's
contribution to the class's fuzzy score. The classification names CMAC
expects are rain, snow, melting, multi_trip, and no_scatter:
cmac_values:
my_radar_ppi:
mbfs:
rain:
velocity_texture: [[0, 0, 2.0, 2.1], 0.0]
copol_correlation_coeff: [[0.97, 0.98, 1, 1], 2.0]
normalized_coherent_power: [[0.4, 0.5, 1, 1], 0.0]
height: [[0, 0, 5000, 6000], 0.0]
sounding_temperature: [[2.0, 5.0, 100, 100], 2.0]
signal_to_noise_ratio: [[-2, 2, 1000, 1000], 1.0]
snow: {...}
melting: {...}
multi_trip: {...}
no_scatter: {...}Setting mbfs overrides the built-in dict wholesale — a partial override
(e.g. redefining only rain) will leave the other classes undefined and
break classification, so always list all five classes together.
A list of [class, field, [lower, upper]] triples. Any gate already
classified as class gets forced back to no_scatter if field's value
at that gate falls outside [lower, upper]:
cmac_values:
my_radar_ppi:
hard_const:
- [melting, sounding_temperature, [-10000, -2]]
- [rain, sounding_temperature, [-1000, -5]]
- [snow, sounding_temperature, [3, 100]]Consumed by quicklooks_ppi/quicklooks_rhi. Every key has a default
from _DEFAULT_PLOT_FIELD_RANGES / _DEFAULT_PLOT_LAYOUT, so a new radar
entry only strictly needs sweep and, for PPI, the map bounds.
| Key | Default | Meaning |
|---|---|---|
sweep |
— | Sweep index used for single-sweep quicklooks |
min_lat, max_lat, min_lon, max_lon |
— | Map bounds (PPI only — omit for RHI configs) |
reflectivity_raw_vmin / _vmax |
-8 / 64 |
Color range, raw reflectivity |
reflectivity_vmin / _vmax |
-8 / 40 |
Color range, reflectivity panel |
corrected_reflectivity_vmin / _vmax |
0 / 40 |
Color range, attenuation-corrected reflectivity |
corrected_velocity_vmin / _vmax |
-60 / 60 |
Color range, dealiased velocity |
velocity_texture_vmin / _vmax |
0 / 14 |
Color range, velocity texture |
cross_correlation_ratio_vmin / _vmax |
0.5 / 1.0 |
Color range, correlation coefficient |
specific_attenuation_vmin / _vmax |
0 / 1.0 |
Color range, specific attenuation |
corrected_specific_diff_phase_vmin / _vmax |
0 / 6 |
Color range, KDP |
filtered_corrected_differential_phase_vmin / _vmax |
0 / 360 |
Color range, filtered PhiDP |
filtered_corrected_specific_diff_phase_vmin / _vmax |
-2 / 10 |
Color range, filtered KDP |
rain_rate_vmin / _vmax |
0 / 120 |
Color range, rain rate panels |
snow_rate_vmin / _vmax |
0 / 50 |
Color range, snow rate panels |
figsize_single |
[12, 8] |
Figure size, single-panel plots |
figsize_panel |
[15, 10] |
Figure size, the four-panel plot |
lat_lon_tick_spacing (PPI only) |
0.8 |
Degrees between map gridlines |
dd_lobe_grid_spacing (PPI only) |
0.01 |
Grid spacing (degrees) for the dual-Doppler lobe overlay (dd_lobes=True) |
dd_lobe_bca_levels (PPI only) |
[pi/6, 5*pi/6] |
Beam-crossing-angle contour levels for the DD lobe overlay |
sweep_fallback_nsweeps_lt (RHI only) |
4 |
If the radar has fewer sweeps than this... |
sweep_fallback (RHI only) |
2 |
...use this sweep index instead of sweep |
ymin, ymax (RHI only) |
0, 10 |
Height axis limits (km) for RHI cross-section panels |
cat_colors |
see below | gate_id category -> color, for the classification panel |
cat_colors default:
cat_colors:
rain: green
multi_trip: red
no_scatter: gray
snow: cyan
melting: yellow
clutter: black
terrain_blockage: brownmetadata.<config_name> supplies the global (file-level) NetCDF attributes
written to the output radar object when cmac(..., meta_append="config")
is used (this is also what the cmac CLI script defaults to). Keys are
free-form — they become attributes as-is — but the built-in radars
consistently use:
metadata:
my_radar_ppi:
site_id: bnf
facility_id: s3
comment: "..."
attributions: "..."
version: "2.0 lite"
vap_name: cmac
known_issues: "..."
developers: "Your Name, Your Institution."
translator: "..."
mentors: "..."
Conventions: "CF/Radial instrument_parameters ARM-1.3"
references: "..."
source: "..."
institution: "..."
doi: "..."default_metadata (top-level, not per-radar) is the fallback metadata used
when meta_append is None or "default" instead of "config" — it's a
single dict shared across all radars, meant for generic/no-config runs
rather than a specific instrument.
meta_append also accepts a path to a .json file as a third option, read
verbatim instead of anything in this YAML file.
Not per-radar — a flat mapping of relationship name -> {A, B, abbreviation}, used for S = (1 / snow_density) * (Z_linear / A)^(1 / B)
snowfall-rate fields, where Z_linear = 10^(corrected_reflectivity / 10)
and snow_density comes from cmac_values.<config_name>.snow_density
(default 0.073). Entries here add to (or override, by name) the built-in
set:
zs_relationships:
My Lab Relationship:
A: 95
B: 1.85
abbreviation: mylababbreviation becomes the field-name suffix: the field is added as
snow_rate_<abbreviation>. Built-in relationships: Wolf and Snider (2012) (ws2012), WSR 88D High Plains (ws88diw), Matrosov et al.(2009) Braham(1990) 1 (m2009_1), Matrosov et al.(2009) Braham(1990) 2 (m2009_2).
See configs/bnfcsapr2.yaml for a complete file covering metadata,
field_names, cmac_values (including custom mbfs/hard_const), and
plot_values for both a PPI and an RHI scan strategy of the same radar,
and documents/example_config.yaml for a shorter file that overrides one
existing radar and defines a minimal new one.