CrocoDash.forcing package#
Submodules#
CrocoDash.forcing.atm module#
Align data-model (DATM/DROF) forcing streams with the case’s run dates.
CDEPS hardcodes year_align=1 and the full year span for every JRA stream in
stream_definition_{datm,drof}.xml, and the DATM_YR_ALIGN/_START/
_END xml variables are only read by the CLM/GSWP3/NEON/CPLHIST stream sets
– no JRA, NYF or ERA5 entry references them. So a JRA case silently reads an
arbitrary data year, and xmlchange cannot fix it. The only lever CDEPS
offers is user_nl_<comp>_streams, which this configurator writes.
Narrowing the file list matters as much as the alignment: the <datafiles>
list is expanded from the stream definition’s own year tags before user mods
are merged, so retagging the years alone still leaves all 59 JRA files
enumerated, and CDEPS opens each one to build its time axis.
- class CrocoDash.forcing.atm.StreamYearConfigurator(case_cesmroot, date_range=None, start_date=None, end_date=None, stream_year_padding=1, narrow_stream_files=True)#
Bases:
BaseConfigurator- allowed_compsets: List[str] = ['DATM']#
- configure()#
Bind input values to parameters and files.
- input_params: List[Param] = [InputValueParam(name='case_cesmroot'), InputValueParam(name='start_date'), InputValueParam(name='end_date'), InputValueParam(name='stream_year_padding'), InputValueParam(name='narrow_stream_files')]#
- name: str = 'StreamYears'#
- output_params: List[OutputParam] = [ConfigOutputParam(name='datm_streams'), ConfigOutputParam(name='drof_streams')]#
- required_for_compsets: List[str] = ['DATM%JRA']#
CrocoDash.forcing.base module#
- class CrocoDash.forcing.base.BaseConfigurator(**kwargs)#
Bases:
ABCBase class for all CrocoDash configurators.
- allowed_compsets: List[str] = []#
- classmethod check_input_params_synced()#
Make sure the init args exactly match the input param names. This check is only run in testing
- classmethod check_output_params_exist()#
- abstractmethod configure()#
Bind input values to parameters and files.
- depends_on_outputs: Dict[str, List[str]] = {}#
- classmethod deserialize(data: Dict[str, Any])#
Rebuild a configurator from its config.json entry.
Tolerates entries an older or newer CrocoDash wrote: an input missing from the entry takes its constructor default, and keys the class no longer declares are ignored. A missing input with no default, or an entry the constructor rejects, raises ReconfigureRequired.
A missing output, or a missing input left None that isn’t optional (a placeholder configure_forcings() fills in, see InputParam), can’t be known without configuring again. The entry still loads, so steps that don’t need it still run, and reading it raises ReconfigureRequired.
- forbidden_compsets: List[str] = []#
- get_input_param(name: str) Any#
- get_input_param_object(name: str) InputParam#
- get_output_filepaths(ocn_ice_directory)#
Get output files from the output parameters
- get_output_param(name: str) Any#
- get_output_param_object(name: str) OutputParam#
- classmethod inspect(caseroot)#
Return an instance of the configurator with placeholder values for input and correct output params from case
- property is_non_local: bool#
Whether this configurator’s case is non-local (CIME’s –non-local).
Sourced from the live Case via the registry rather than a declared input param, so an XMLConfigParam output doesn’t require its configurator to accept/thread a case_is_non_local ctor arg just to reach apply() – it’s always False when there’s no live Case (e.g. direct construction in tests, or deserialize()).
- classmethod is_required(compset: str) bool#
- make_serializable(obj)#
- name: str = ''#
- output_params: List[OutputParam]#
- process_components: Dict[str, str] = {}#
- registry: ForcingConfigRegistry | None = None#
- required_for_compsets: List[str] = []#
- serialize() Dict[str, Any]#
- set_input_param(name: str, value)#
- set_output_param(name: str, value)#
- stale_outputs(previous_config, current_config, inputdir)#
Files an earlier process step left that the current configuration must not reuse, for configure_forcings() to remove.
previous_config/current_config are the whole config.json contents from the earlier configure_forcings() call ({} if there is no record) and from this one, since a process step can read a sibling’s entry (the conditions dates, say). Only a process step that skips outputs it finds on disk needs to override this; every other one rewrites its files anyway.
- validate_args(**kwargs)#
Validate provided inputs against declared input_params.
- classmethod validate_compset_compatibility(compset: str) bool#
- validate_output_filepaths(ocn_ice_directory)#
- class CrocoDash.forcing.base.ConfigOutputParam(name: str, comment: str | None = None, is_file: bool = False)#
Bases:
OutputParamDerived value with no case-side effect (not written to user_nl or xmlchange’d).
Exists purely so set_output_param() + the generic serialize() pick it up under a configurator’s outputs, for values consumed only via config.json (e.g. by a sibling configurator’s process_* method).
- apply()#
- inspect(caseroot)#
Inspect the current value of this parameter in the case located at caseroot.
- class CrocoDash.forcing.base.ForcingConfigRegistry(compset, inputs: dict, case=None)#
Bases:
object- classmethod all_process_flags()#
Every process-component flag name any registered configurator can answer to, regardless of whether it’s active for a given case – used to build cli.py’s argparse flags generically.
- classmethod available_process_flags(config: dict)#
Every process-component flag name available given which forcing types are present in a config.json dict – looked up from each entry’s declared class only (no full deserialize), so this works even when an entry’s inputs/outputs aren’t fully valid yet. Used by resolve_components/cli.py to answer “does this flag exist” without needing working configurator instances the way actual dispatch does.
- find_active_configurators(compset, inputs: dict)#
- classmethod find_required_configurators(compset)#
Returns the required configurations based on the compset in a list
- classmethod find_valid_configurators(compset)#
Returns the valid configurations based on the compset in a list
- get_active_configurators()#
- classmethod get_configurator(obj_dict)#
- classmethod get_configurator_from_name(name)#
- classmethod get_configurator_output(config: dict, configurator_name: str, output_name: str)#
Look up another configurator’s serialized output value from config.json – for a process_*() method that depends on a sibling’s output but (unlike configure()) can’t rely on a live registry/Case, since process() may run in a different process. Raises a clear, named error instead of a bare KeyError several dict levels deep if the dependency isn’t there (e.g. the sibling hasn’t run yet, or never configured this case).
- classmethod get_ctor_signature(configurator_cls)#
- classmethod get_user_args(configurator_cls)#
- classmethod instantiate_configurator(configurator_cls, inputs)#
- is_active(name: str) bool#
Return True if a configurator with this name is active.
- classmethod register(configurator_cls: type)#
- registered_types: List[type] = [<class 'CrocoDash.forcing.atm.StreamYearConfigurator'>, <class 'CrocoDash.forcing.bgc.BGCConfigurator'>, <class 'CrocoDash.forcing.bgc.BGCICConfigurator'>, <class 'CrocoDash.forcing.bgc.BGCIronForcingConfigurator'>, <class 'CrocoDash.forcing.bgc.BGCRiverNutrientsConfigurator'>, <class 'CrocoDash.forcing.chl.ChlConfigurator'>, <class 'CrocoDash.forcing.cice.CICEConfigurator'>, <class 'CrocoDash.forcing.mom6.ConditionsConfigurator'>, <class 'CrocoDash.forcing.runoff.RunoffConfigurator'>, <class 'CrocoDash.forcing.tides.TidesConfigurator'>, <class 'CrocoDash.forcing.ww3.WW3Configurator'>]#
- classmethod resolve_process_targets(config: dict)#
Deserialize every configurator present in a config.json dict and return {flag_name: (configurator_instance, method_name)} covering every process component any of them declare. Used by driver.py to dispatch generically instead of a hand-maintained if-ladder.
- classmethod return_missing_inputs(configurator_cls, inputs)#
- run_configurators()#
- stale_outputs(previous_config, current_config, inputdir)#
Every active configurator’s stale_outputs().
- class CrocoDash.forcing.base.InputFileParam(name: str, comment: str | None = None, optional=False)#
Bases:
InputParamBase class for a single file parameter in our forcing configurations.
- set_item(filepath: str)#
Bind a runtime value to this parameter.
- class CrocoDash.forcing.base.InputParam(name: str, comment: str | None = None, optional=False)#
Bases:
ParamAn input of a configurator.
optional: the configurator works with this input left None. Any other input whose constructor default is None is a placeholder that configure_forcings() fills in (from the case, the run dates or the product), so a config.json entry without it can’t be processed until the forcings are configured again (see BaseConfigurator.deserialize).
- class CrocoDash.forcing.base.InputValueParam(name: str, comment: str | None = None, optional=False)#
Bases:
InputParamBase class for a single value parameter in our forcing configurations.
- set_item(item)#
Bind a runtime value to this parameter.
- class CrocoDash.forcing.base.OutputParam(name: str, comment: str | None = None, is_file: bool = False)#
Bases:
ParamBase class for a single configuration parameter applied to a CESM/MOM6 case.
Subclasses apply themselves (
apply()), except UserNLConfigParam, which BaseConfigurator.configure() writes through user_nl_blocks.- abstractmethod inspect(caseroot)#
Inspect the current value of this parameter in the case located at caseroot.
- set_item(value: Any)#
Bind a runtime value to this parameter.
- class CrocoDash.forcing.base.Param(name: str, comment: str | None = None)#
Bases:
ABCBase class for a single parameter in our forcing configurations.
- abstractmethod set_item(item: Any)#
Bind a runtime value to this parameter.
- exception CrocoDash.forcing.base.ReconfigureRequired#
Bases:
ValueErrorconfig.json lacks something this version of CrocoDash needs, so the case’s forcings must be configured again before they can be processed.
- exception CrocoDash.forcing.base.UndeclaredParamError#
Bases:
KeyErrorCode referenced an input/output param name that isn’t declared on the class (a schema bug, e.g. a stale name after a rename). Kept as a KeyError subclass so existing except KeyError call sites are unaffected, but a distinct type so a param-consistency check can catch exactly this failure and not any other incidental KeyError raised by a configurator’s own business logic.
- class CrocoDash.forcing.base.UserNLConfigParam(name: str, user_nl_name: str = 'mom', comment: str | None = None, is_file: bool = False)#
Bases:
OutputParamParameter written to a user_nl_<component> file (default: user_nl_mom), inside its configurator’s CrocoDash block (see BaseConfigurator.configure).
- inspect(caseroot)#
Inspect the current value of this parameter in the case located at caseroot.
- pair()#
The (name, value) this parameter writes.
- class CrocoDash.forcing.base.WorkflowContext(inputdir: Path, supergrid_path: Path, vgrid_path: Path, topo_path: Path, raw_data_dir: Path, regridded_data_dir: Path, output_path: Path, config: dict, preview: bool = False)#
Bases:
objectWorkflow-level paths shared by every configurator’s process step.
Built once by driver.run_workflow() from config.json + case_state, and passed into every
process_*(ctx)call. These are paths no single configurator owns (they’re not part of any configurator’s own inputs/ outputs) but that most process steps need – grid/topo/vgrid files and the raw/regridded/output directories for this case.- property grid#
- property ocn_topo#
- class CrocoDash.forcing.base.XMLConfigParam(name: str, is_non_local: bool = False, comment: str | None = None, is_file: bool = False)#
Bases:
OutputParamParameter applied via xmlchange.
XML changes are permanent and do not save previous state.
- apply()#
- inspect(caseroot)#
Inspect the current value of this parameter in the case located at caseroot.
- CrocoDash.forcing.base.calendar_as_dict(calendar)#
Store a Calendar in config.json as a plain dict, leaving anything else be.
A configurator is normally constructed from a live Calendar, but also from the serialized dict (BaseConfigurator.deserialize) and from empty placeholders (BaseConfigurator.inspect), so only the dataclass is converted here.
- CrocoDash.forcing.base.is_serializable(v)#
- CrocoDash.forcing.base.iter_configurator_entries(config)#
(key, entry) for each configurator entry in a config.json dict, skipping the top-level CONFIG_META_KEYS.
- CrocoDash.forcing.base.read_config(path)#
Read the config.json at path, upgrade it if a migration applies, validate its schema version, and return it.
- CrocoDash.forcing.base.register(cls)#
Decorator: register a BaseConfigurator subclass with ForcingConfigRegistry.
Every module under CrocoDash.forcing that defines a configurator class applies this at class-definition time; CrocoDash.forcing/__init__.py’s auto-discovery ensures those modules (and hence this decorator) run for every file in the package, so adding a new forcing type is just adding a new file here – nothing else needs to import it by name.
- CrocoDash.forcing.base.write_config(path, config)#
Write config to path, injecting schema_version as the first key.
CrocoDash.forcing.bgc module#
- class CrocoDash.forcing.bgc.BGCConfigurator#
Bases:
BaseConfiguratorToggles the MARBL-tracer namelist default – no forcing data of its own to extract. BGCIC/BGCIronForcing/BGCRiverNutrients below are the configurators that actually produce BGC forcing files.
- allowed_compsets: List[str] = ['MARBL']#
- configure()#
Bind input values to parameters and files.
- name: str = 'BGC'#
- output_params: List[OutputParam] = [UserNLConfigParam(name='MAX_FIELDS')]#
- required_for_compsets: List[str] = ['MARBL']#
- class CrocoDash.forcing.bgc.BGCICConfigurator(marbl_ic_filepath)#
Bases:
BaseConfigurator- allowed_compsets: List[str] = ['MARBL']#
- configure()#
Bind input values to parameters and files.
- name: str = 'BGCIC'#
- output_params: List[OutputParam] = [UserNLConfigParam(name='MARBL_TRACERS_IC_FILE')]#
- process(ctx)#
Copy the MARBL initial condition file into place.
- process_components: Dict[str, str] = {'bgcic': 'process'}#
- required_for_compsets: List[str] = ['MARBL']#
- class CrocoDash.forcing.bgc.BGCIronForcingConfigurator(case_session_id, case_grid_name)#
Bases:
BaseConfigurator- allowed_compsets: List[str] = ['MARBL']#
- configure()#
Bind input values to parameters and files.
- input_params: List[Param] = [InputValueParam(name='case_session_id'), InputValueParam(name='case_grid_name')]#
- name: str = 'BGCIronForcing'#
- output_params: List[OutputParam] = [UserNLConfigParam(name='MARBL_FESEDFLUX_FILE'), UserNLConfigParam(name='MARBL_FEVENTFLUX_FILE'), UserNLConfigParam(name='MARBL_FESEDFLUXRED_FILE')]#
- process(ctx)#
Create dummy iron forcing files for MARBL.
- process_components: Dict[str, str] = {'bgcironforcing': 'process'}#
- required_for_compsets: List[str] = ['MARBL']#
- class CrocoDash.forcing.bgc.BGCRiverNutrientsConfigurator(global_river_nutrients_filepath, case_session_id, case_grid_name, case_forcing_product=None, calendar=None)#
Bases:
BaseConfigurator- allowed_compsets: List[str] = ['MARBL', 'DROF']#
- configure()#
Bind input values to parameters and files.
- depends_on_outputs: Dict[str, List[str]] = {'runoff': ['ROF2OCN_LIQ_RMAPNAME']}#
- input_params: List[Param] = [InputFileParam(name='global_river_nutrients_filepath'), InputValueParam(name='case_session_id'), InputValueParam(name='case_grid_name'), InputValueParam(name='calendar')]#
- name: str = 'BGCRiverNutrients'#
- output_params: List[OutputParam] = [UserNLConfigParam(name='READ_RIV_FLUXES'), UserNLConfigParam(name='RIV_FLUX_FILE')]#
- process(ctx)#
Regrid global river nutrients onto the ocean grid via the runoff mapping file – requires RunoffConfigurator’s process step to have already produced that mapping file. See depends_on_outputs above: driver.py derives the required run-before ordering from it automatically.
- process_components: Dict[str, str] = {'bgcrivernutrients': 'process'}#
CrocoDash.forcing.cdeps_streams module#
Read CDEPS stream definitions through CIME’s own parsers.
These are thin wrappers over StreamCDEPS and NamelistDefinition rather
than XML parsing of our own, so that the stream list and the expanded file
paths stay identical to what the CDEPS buildnml would generate. That
matters for streams whose files carry more than one datestamp: JRA v1.5 splits
1958-2023 across four <file> entries stamped 210504/220505/230718/240531,
and a hand-rolled expansion would silently emit paths that do not exist.
- CrocoDash.forcing.cdeps_streams.coverage(cesmroot, component, stream_name, cime_case)#
The (year_first, year_last) the stream definition itself declares.
- CrocoDash.forcing.cdeps_streams.datafiles(cesmroot, component, stream_name, year_first, year_last, cime_case)#
Resolved file paths covering [year_first, year_last].
Mirrors the
stream_datafilesbranch ofStreamCDEPS.create_stream_xml: each<file>contributes only the years its own first_year/last_year attributes cover, so multi-datestamp streams resolve correctly.
- CrocoDash.forcing.cdeps_streams.stream_names(cesmroot, component, mode)#
The stream names active for a DATM_MODE/DROF_MODE, in buildnml’s order.
The mode-to-streams mapping is matched by regex on the mode name, so this cannot be replaced by prefixing: CORE_IAF_JRA_1p5_2023 yields streams named CORE_IAF_JRA_1p5_2023.GCGCS.PREC, and the DROF mode IAF_JRA yields rof.iaf_jra.
CrocoDash.forcing.chl module#
- class CrocoDash.forcing.chl.ChlConfigurator(chl_processed_filepath, case_grid_name, case_session_id, case_forcing_product=None, calendar=None)#
Bases:
BaseConfigurator- configure()#
Bind input values to parameters and files.
- forbidden_compsets: List[str] = ['MARBL']#
- input_params: List[Param] = [InputFileParam(name='chl_processed_filepath'), InputValueParam(name='case_grid_name'), InputValueParam(name='case_session_id'), InputValueParam(name='calendar')]#
- name: str = 'Chl'#
- output_params: List[OutputParam] = [UserNLConfigParam(name='CHL_FILE'), UserNLConfigParam(name='CHL_FROM_FILE'), UserNLConfigParam(name='VAR_PEN_SW'), UserNLConfigParam(name='PEN_SW_NBANDS')]#
- process(ctx)#
- process_components: Dict[str, str] = {'chl': 'process'}#
CrocoDash.forcing.cice module#
CICE forcing for CrocoDash: one expanded-grid restart serving as both the initial condition and the restoring target.
CICE’s restoring mechanism (ice_restoring.F90) relaxes the
boundary-adjacent ghost cells toward a target ice state over time.
CICEConfigurator.process produces the file that supplies it: the case’s
regional domain plus a one-T-cell halo on every side, built from a
CICE-shaped forcing product (a real global restart, or a fast synthetic
stand-in – see cice_product_name/cice_function_name below) regridded
onto every point of that expanded grid. configure then points CICE’s
ice_ic at that file and sets restart_ext = .true., so CICE reads it as
a restart including its ghost ring (ni = nx_global + 2*nghost,
nj = ny_global + 2*nghost – see ice_restart.F90).
The ghost-ring values that land this way are what gets restored toward, given
the restoring-for-OBCs support in Dave Bailey’s CICE checkout, which sits on
top of this. Note that stock ice_restoring.F90 does not consume that
ring: its active restore_ic = 'initial' path extrapolates the innermost
physical row/column outward into the ghost cells instead (the commented-out
“easy way”, aicen_rest = aicen, is what would use it).
nghost is 1 in CICE (ice_blocks.F90), hence the halo is fixed at one
T-cell: the generated file has to match that extended-restart shape exactly
or CICE won’t read it, so there’s no user-facing knob for it.
Restoring is off by default and takes all three of restore_ice=True,
cice_product_name and cice_function_name. Leave any of them unset and
process generates nothing, ice_ic stays 'default' (CICE’s own
latitude/SST initialization, needing no restart file on disk at all), and CICE
runs with zero-gradient boundaries and no restoring – a valid configuration,
with ice still free to advect out of the domain. A partial combination raises
in validate_args rather than quietly generating nothing.
Like a real CICE restart/initial-condition file, the output carries no
time dimension at all – just the single static snapshot, regridded once
– on CICE’s own nj/ni dimension names, which is what its restart
reader expects.
Unlike MOM6/WW3’s OBC, there’s no boundary-only regrid and no date-chunking
need (one static snapshot, no real time evolution to fetch incrementally),
so this doesn’t route through obc.py’s shared GET->chunk->REGRID->MERGE
engine at all – just resolves the requested product/function via the same
ProductRegistry lookup MOM6/WW3 use (utils.get_data_access_function)
instead of hardcoding a single product.
Every variable in the source restart passes through unfiltered (T-point and
U-point alike) – a superset of the aicen/vicen/vsnon/trcrn
(category-indexed, ncat=5) that ice_restoring.F90’s restoring arrays
actually read. configure sets restore_flds = 'state', which restores
exactly those four; uvel/vvel are not restored, since the NUOPC
driver’s ice_restoring_interior('velocity') call (CICE_RunMod.F90) only
acts when restore_flds lists velocity.
- class CrocoDash.forcing.cice.CICEConfigurator(cice_product_name=None, cice_function_name=None, cice_function_args=None, restore_ice=False, case_inputdir=None)#
Bases:
BaseConfigurator- allowed_compsets: List[str] = ['CICE']#
- configure()#
Bind input values to parameters and files.
- classmethod deserialize(data)#
Rebuild the restoring params too, when the saved case restored.
- get_output_filepaths(ocn_directory)#
CICE’s forcing file, which lives beside ocn/ rather than in it.
The base implementation walks output_params for is_file entries, but all of CICE’s are namelist settings – the forcing file’s location is fixed by process() rather than carried in a parameter. Without this override the base returns nothing, so CaseBundle.bundle() copies no CICE file at all and validate_output_filepaths() passes vacuously.
ocn_directory is <inputdir>/ocn; process() writes to <inputdir>/ice, hence the sibling lookup.
- input_params: List[Param] = [InputValueParam(name='cice_product_name'), InputValueParam(name='cice_function_name'), InputValueParam(name='cice_function_args'), InputValueParam(name='restore_ice'), InputValueParam(name='case_inputdir')]#
- name: str = 'CICE'#
- output_params: List[OutputParam] = [UserNLConfigParam(name='ice_ic'), UserNLConfigParam(name='ns_boundary_type'), UserNLConfigParam(name='ew_boundary_type'), UserNLConfigParam(name='advection'), UserNLConfigParam(name='restart_ext'), UserNLConfigParam(name='restart_aero'), UserNLConfigParam(name='restart_age'), UserNLConfigParam(name='restart_fy'), UserNLConfigParam(name='restart_fsd'), UserNLConfigParam(name='restart_lvl'), UserNLConfigParam(name='restart_pond_sealvl'), UserNLConfigParam(name='restart_snow'), UserNLConfigParam(name='histfreq'), UserNLConfigParam(name='f_afsd'), UserNLConfigParam(name='f_afsdn'), UserNLConfigParam(name='f_dafsd_latg'), UserNLConfigParam(name='f_dafsd_latm'), UserNLConfigParam(name='f_dafsd_newi'), UserNLConfigParam(name='f_dafsd_wave'), UserNLConfigParam(name='f_dafsd_weld'), UserNLConfigParam(name='f_diam_ww'), UserNLConfigParam(name='f_fsdperim'), UserNLConfigParam(name='f_fsdrad')]#
- process(ctx)#
Generate CICE’s single restoring forcing file into <inputdir>/ice/cice_forcing.nc.
Does nothing unless the caller named both
cice_product_nameandcice_function_name– restoring is opt-in, see_resolve_forcing_source.When they are given, covers the case’s domain plus a one-T-cell halo on every side (grown via
SupergridBase.expand), windowed from that CICE forcing product (resolved via the sameProductRegistrylookup MOM6/WW3 use –restart_path/grid_pathfor the realcice_restartproduct go incice_function_args) and regridded onto that expanded grid. Like a real CICE restart/initial-condition file, the output has notimedimension – a single static snapshot (forcice_restart), not a time series.
- process_components: Dict[str, str] = {'cice': 'process'}#
- required_for_compsets: List[str] = ['CICE']#
- validate_args(**kwargs)#
Validate provided inputs against declared input_params.
- CrocoDash.forcing.cice.dynamics_substeps(ice_dt, min_dx)#
ndtd for an ice time step of ice_dt seconds on a grid whose smallest cell is min_dx meters.
CrocoDash.forcing.driver module#
CrocoDash Forcing Driver
Orchestrates both stages of the forcing workflow for a CrocoDash case:
configuration (Case.configure_forcings(), which writes config.json)
and extraction (Case.process_forcings(), which reads it back).
Extraction dispatches generically: for every forcing type present in
config.json, the matching BaseConfigurator subclass is deserialized
and asked which process flags it answers to (its process_components
manifest) – there’s no hand-maintained per-type if branch to update
when a new forcing type is added. The CLI entry point is crocodash
process (see CrocoDash.cli).
Typical Python usage:
from CrocoDash.forcing.driver import run_workflow
run_workflow(config_path="~/croc_input/mycase/extract_forcings/config.json", bc=True, ic=True)
- CrocoDash.forcing.driver.resolve_components(args, config)#
Resolve which components to run based on CLI args and config availability.
- CrocoDash.forcing.driver.run_workflow(config_path, preview=False, **flags)#
Execute the forcing extraction workflow.
- Parameters:
config_path (str or Path) – Path to the
config.jsonwritten byCase.configure_forcings.preview (bool) – Preview task graph without executing.
**flags (bool) – Which process components to run, e.g.
ic=True, bc=True, tides=True. Valid names are whateverprocess_componentskeys the forcing types present inconfig.jsondeclare – seeForcingConfigRegistry.resolve_process_targets.
CrocoDash.forcing.ic module#
IC (Initial Condition) forcing extraction engine for CrocoDash.
Model-agnostic: this module gets the raw t=0 snapshot, but knows nothing
about how to regrid it. Callers (mom6.py, cice.py, ww3.py)
supply a regrid_fn that turns the raw snapshot into that target’s own
initial-condition file(s) – the GET step is shared.
- CrocoDash.forcing.ic.process_initial_condition(product_name: str, function_name: str, variables: list, extra_args: dict, dataset_varnames: dict, start_date: str | datetime, hgrid_path: str | Path, raw_data_dir: str | Path, output_data_dir: str | Path, regrid_fn, preview: bool = False)#
Process the initial condition (t=0) through the GET → REGRID pipeline.
- Parameters:
product_name – The name of the data product to retrieve.
function_name – The function to call for retrieving data.
variables – Variable names to request from the download function (already resolved by the caller from its own product metadata).
extra_args – Extra kwargs for the download function (already resolved by the caller).
dataset_varnames – Opaque metadata dict forwarded to
regrid_fn– this module never reads its keys itself.start_date – The start date (any pandas-parseable string or datetime).
hgrid_path – Path to the hgrid supergrid file.
raw_data_dir – Directory for raw downloaded data.
output_data_dir – Directory for final output files.
regrid_fn – Target-specific regrid step, called as
regrid_fn(raw_file, hgrid, start_date, output_dir, dataset_varnames)once the raw snapshot has been downloaded. Owns its own idempotency (this engine has no per-chunk state to check – IC is a single snapshot, not a date-chunked series).preview – Return metadata dict without executing, default False.
CrocoDash.forcing.mom6 module#
MOM6 initial + open boundary condition configuration and processing.
ConditionsConfigurator is always active for regional MOM6 cases – it
builds the user_nl_mom initial condition and OBC_SEGMENT_* parameters at
configure time, and (one-to-many: this one configurator answers for both
the “ic” and “bc” process flags) drives IC/OBC extraction at process time via
forcing.ic/forcing.obc’s model-agnostic engines, supplying MOM6’s own
regrid step – built on regional_mom6’s segment/experiment
classes and mom6_forge’s fill utilities – and the MOM6-specific piece of
the GET step (turning a forcing product’s own u/v/eta/tracer var-name
metadata into a download request).
- class CrocoDash.forcing.mom6.ConditionsConfigurator(boundaries, product_name, function_name, compset, date_range=None, start_date=None, end_date=None, function_args=None)#
Bases:
BaseConfiguratorInitial condition + open boundary condition (OBC) setup for MOM6.
Always active for regional MOM6 cases. Builds the user_nl_mom initial condition and OBC_SEGMENT_* parameters, and the derived values consumed by process_ic/process_bc (dates, forcing product metadata, boundary numbering).
- configure()#
Bind input values to parameters and files.
- classmethod deserialize(data)#
Reconstruct dynamic per-boundary output params alongside the static ones.
- input_params: List[Param] = [InputValueParam(name='start_date'), InputValueParam(name='end_date'), InputValueParam(name='boundaries'), InputValueParam(name='product_name'), InputValueParam(name='function_name'), InputValueParam(name='compset'), InputValueParam(name='function_args')]#
- name: str = 'conditions'#
- output_params: List[OutputParam] = [UserNLConfigParam(name='INIT_LAYERS_FROM_Z_FILE'), UserNLConfigParam(name='Z_INIT_ALE_REMAPPING'), UserNLConfigParam(name='TEMP_SALT_INIT_VERTICAL_REMAP_ONLY'), UserNLConfigParam(name='DEPRESS_INITIAL_SURFACE'), UserNLConfigParam(name='VELOCITY_CONFIG'), UserNLConfigParam(name='TEMP_SALT_Z_INIT_FILE'), UserNLConfigParam(name='SURFACE_HEIGHT_IC_FILE'), UserNLConfigParam(name='VELOCITY_FILE'), UserNLConfigParam(name='Z_INIT_FILE_PTEMP_VAR'), UserNLConfigParam(name='Z_INIT_FILE_SALT_VAR'), UserNLConfigParam(name='SURFACE_HEIGHT_IC_VAR'), UserNLConfigParam(name='U_IC_VAR'), UserNLConfigParam(name='V_IC_VAR'), UserNLConfigParam(name='OBC_NUMBER_OF_SEGMENTS'), UserNLConfigParam(name='OBC_FREESLIP_VORTICITY'), UserNLConfigParam(name='OBC_FREESLIP_STRAIN'), UserNLConfigParam(name='OBC_COMPUTED_VORTICITY'), UserNLConfigParam(name='OBC_COMPUTED_STRAIN'), UserNLConfigParam(name='OBC_ZERO_BIHARMONIC'), UserNLConfigParam(name='OBC_TRACER_RESERVOIR_LENGTH_SCALE_OUT'), UserNLConfigParam(name='OBC_TRACER_RESERVOIR_LENGTH_SCALE_IN'), UserNLConfigParam(name='BRUSHCUTTER_MODE'), UserNLConfigParam(name='ENABLE_BUGS_BY_DEFAULT'), ConfigOutputParam(name='date_format'), ConfigOutputParam(name='information'), ConfigOutputParam(name='get_step_days'), ConfigOutputParam(name='regrid_step_days'), ConfigOutputParam(name='boundary_number_conversion'), ConfigOutputParam(name='preview'), ConfigOutputParam(name='function_args')]#
- process_bc(ctx)#
Process MOM6 boundary conditions through forcing.obc’s GET → REGRID → MERGE engine, using regional_mom6’s Segment as the regrid step.
- process_components: Dict[str, str] = {'bc': 'process_bc', 'ic': 'process_ic'}#
- process_ic(ctx)#
Process the MOM6 initial condition (t=0) through forcing.ic’s GET → REGRID engine, using regional_mom6’s experiment + mom6_forge’s fill utilities as the regrid step.
- required_for_compsets: List[str] = ['MOM6']#
- stale_outputs(previous_config, current_config, inputdir)#
process_ic/process_bc skip every file they find already written – that is what makes resuming a crashed run cheap – and nothing in the file names ties the files to the configuration. So once what they are made from changed, or there is no record of it, these go:
in <inputdir>/ocn, the IC (init_*.nc, init_*_filled.nc), the merged OBCs (forcing_obc_segment_NNN.nc) and the BGC per-tracer OBCs (<tracer>_obc_segment.nc);
extract_forcings/raw_data and regridded_data, which only these two steps use: raw_data/ic_unprocessed.nc has no date in its name, no chunk names the product or its arguments, the regridded chunks are named by segment number rather than boundary, and chunks of another date range overlap the new ones, which the OBC coverage check rejects.
- validate_args(**kwargs)#
Validate provided inputs against declared input_params.
- CrocoDash.forcing.mom6.build_forcing_request(product_info: dict, function_args: dict = None) tuple[list, dict]#
Build the (variables, extra_args) an access function needs from a MOM6 forcing product_info dict (u/v/eta/tracer var names).
function_args: user overrides (or access-function defaults) for the access function’s non-required arguments, as written to config.json’s forcing.function_args by configure_forcings()’s function_overrides. Merged into extra_args last so they take precedence over product_info-derived keys.
- CrocoDash.forcing.mom6.final_cleanliness_fill(var, x_dim, y_dim, z_dim=None)#
CrocoDash.forcing.obc module#
OBC (Open Boundary Condition) forcing extraction engine for CrocoDash.
Model-agnostic: this module gets raw data and chunks/merges it, but knows
nothing about how to regrid it. Callers (mom6.py, cice.py, ww3.py)
supply a regrid_chunk_fn that turns one raw chunk into that target’s own
per-segment output file – everything else (GET, date-chunking, idempotency,
MERGE) is shared.
Three-phase pipeline per boundary:
- GET — download raw data, chunked by
get_step(default: full range in one request). Chunk size is driven by data-provider constraints (API limits, download size). Each chunk is written as
{boundary}_unprocessed.{start}_{end}.nc.
- GET — download raw data, chunked by
- REGRID — validate raw coverage from filenames, then open all raw files
lazily and regrid (via the caller-supplied
regrid_chunk_fn) inregrid_step-sized slices. Chunk size is driven by memory and xESMF performance. GET and REGRID chunks are fully independent.
MERGE — concatenate regridded chunks into
forcing_obc_segment_NNN.nc.
GET and REGRID phase automatically switch to multiprocessing when chunking and multiple cpus are available.
Each phase is idempotent: existing output files are detected and skipped, so a failed run can be safely re-started.
- CrocoDash.forcing.obc.available_cpus()#
Get the number of worker processes to spawn.
CROCODASH_MAX_WORKERS, if set, wins outright. Otherwise, sched_getaffinity(0) reports the whole node’s core count – correct inside a PBS batch job, since PBS cpusets bind it to the actual allocation, but badly wrong on a login node (e.g. 128 on Derecho, where a single user is really capped to a handful of cores and ~10GB). PBS_JOBID is only set inside a batch job, so it’s used here to tell the two apart.
- CrocoDash.forcing.obc.process_obc_conditions(start_date, end_date, boundary_number_conversion: dict, product_name: str, function_name: str, variables: list, extra_args: dict, dataset_varnames: dict, hgrid_path, raw_dataset_path, regridded_dataset_path, output_path, regrid_chunk_fn, get_step_days=None, regrid_step_days: int = 30, bathymetry_path=None, preview: bool = False)#
Process boundary conditions through the GET → REGRID → MERGE pipeline.
Each phase is idempotent. Re-running after a partial failure resumes from the last completed file.
GET and REGRID chunk sizes are independent. GET defaults to the full date range in one request; REGRID defaults to 30-day slices for memory efficiency.
- Parameters:
start_date – Forcing start date (datetime or any pandas-parseable string).
end_date – Forcing end date (datetime or any pandas-parseable string).
boundary_number_conversion – Boundary name -> target-model segment number.
product_name – Forcing data product name.
function_name – Download function name for the product.
variables – Variable names to request from the download function (already resolved by the caller from its own product metadata).
extra_args – Extra kwargs for the download function (already resolved by the caller).
dataset_varnames – Opaque metadata dict forwarded to
regrid_chunk_fn– this module never reads its keys itself.hgrid_path – Path to the hgrid supergrid file.
raw_dataset_path – Directory for raw downloaded data.
regridded_dataset_path – Directory for per-chunk regridded data.
output_path – Directory for final, merged output files.
regrid_chunk_fn – Target-specific regrid step – see
_regrid_boundary.get_step_days – GET chunk size in days; None = full range in one request.
regrid_step_days – REGRID chunk size in days.
bathymetry_path – Optional path to the case’s bathymetry file. When given, download bounding boxes are computed from the bathymetry ocean tmask (tighter than the full supergrid edge extent). When omitted, falls back to the full supergrid bounding box per boundary.
preview – If True, return a dict of expected date pairs without executing any downloads or regridding.
CrocoDash.forcing.runoff module#
- class CrocoDash.forcing.runoff.RunoffConfigurator(case_grid_name, case_session_id, case_compset_lname, case_inputdir, case_esmf_mesh_path, case_cime=None, rmax=None, fold=None, rof_grid_name=None, rof_esmf_mesh_filepath=None)#
Bases:
BaseConfigurator- allowed_compsets: List[str] = {'DROF'}#
- configure()#
Bind input values to parameters and files.
- get_output_filepaths(ocn_ice_directory)#
Get output files from the output parameters
- input_params: List[Param] = [InputValueParam(name='case_grid_name'), InputValueParam(name='case_session_id'), InputValueParam(name='case_compset_lname'), InputValueParam(name='case_inputdir'), InputValueParam(name='rmax'), InputValueParam(name='rof_grid_name'), InputValueParam(name='fold'), InputFileParam(name='rof_esmf_mesh_filepath'), InputFileParam(name='case_esmf_mesh_path')]#
- name: str = 'Runoff'#
- output_params: List[OutputParam] = [XMLConfigParam(name='ROF2OCN_LIQ_RMAPNAME'), XMLConfigParam(name='ROF2OCN_ICE_RMAPNAME')]#
- process(ctx)#
Generate runoff-to-ocean mapping files if runoff is active in the compset.
- process_components: Dict[str, str] = {'runoff': 'process'}#
- required_for_compsets: List[str] = {'DROF'}#
- validate_args(**kwargs)#
Validate provided inputs against declared input_params.
CrocoDash.forcing.tides module#
- class CrocoDash.forcing.tides.TidesConfigurator(tpxo_elevation_filepath, tpxo_velocity_filepath, tidal_constituents, boundaries, date_range=None, start_date=None)#
Bases:
BaseConfigurator- configure()#
Bind input values to parameters and files.
- get_output_filepaths(ocn_ice_directory)#
Get output files from the output parameters
- input_params: List[Param] = [InputFileParam(name='tpxo_elevation_filepath'), InputFileParam(name='tpxo_velocity_filepath'), InputValueParam(name='tidal_constituents'), InputValueParam(name='start_date'), InputValueParam(name='boundaries')]#
- name: str = 'tides'#
- output_params: List[OutputParam] = [UserNLConfigParam(name='TIDES'), UserNLConfigParam(name='TIDE_M2'), UserNLConfigParam(name='CD_TIDES'), UserNLConfigParam(name='TIDE_USE_EQ_PHASE'), UserNLConfigParam(name='TIDE_REF_DATE'), UserNLConfigParam(name='OBC_TIDE_ADD_EQ_PHASE'), UserNLConfigParam(name='OBC_TIDE_N_CONSTITUENTS'), UserNLConfigParam(name='OBC_TIDE_CONSTITUENTS'), UserNLConfigParam(name='OBC_TIDE_REF_DATE')]#
- process(ctx)#
- process_components: Dict[str, str] = {'tides': 'process'}#
CrocoDash.forcing.user_nl_blocks module#
CrocoDash-managed blocks in a case’s user_nl_* files.
Case.configure_forcings() may be called again on the same case (a notebook
cell rerun, or a new date range), so the user_nl entries it writes go inside
marked blocks, one per configurator and file:
! >>> CrocoDash configure_forcings: conditions [sha:1a2b3c4d] (rewritten ...)
...
! <<< CrocoDash configure_forcings: conditions
Every call first removes all such blocks (strip), then writes its own
(write), so the files end up as if only the latest call had happened.
Lines outside the blocks are left alone: what case creation wrote (INPUTDIR,
the WW3 time steps, CICE’s ndtd) and whatever the user added.
A variable the user sets outside the blocks takes precedence: write leaves
it out of CrocoDash’s block. Otherwise user_nl_mom would list it twice, which
stops the MOM6 build, and CICE, WW3 and CDEPS would take whichever line comes
last.
The marker lines are ! comments, which MOM_interface’s user_nl parser,
CIME’s namelist parser and CDEPS’s stream-mod parser all skip.
- CrocoDash.forcing.user_nl_blocks.report_edits(caseroot, edited)#
Warn about the hand edits
stripfound, once the blocks have been rewritten: the lines each edited block lost, and those it got back.
- CrocoDash.forcing.user_nl_blocks.restore(caseroot, contents)#
Put the user_nl files back as
snapshotfound them, so that a configure_forcings() call that fails leaves them as they were.
- CrocoDash.forcing.user_nl_blocks.snapshot(caseroot)#
The contents of every user_nl file in
caseroot, forrestore.
- CrocoDash.forcing.user_nl_blocks.strip(caseroot)#
Remove every CrocoDash block from the user_nl files in
caseroot.Returns
[(file name, owner, body lines)]for the blocks whose contents differ from what CrocoDash wrote, i.e. that were edited by hand, so thatreport_editscan say what was replaced.
- CrocoDash.forcing.user_nl_blocks.write(owner, model, groups)#
Write
owner’s block into user_nl_<model> (each instance’s file).groupsis[(comment or None, [(var, val), ...]), ...]; the block holds them as consecutive append_user_nl calls would write them, and the log shows them the same way. Variables the file already sets outside the blocks are left out (see the module docstring).
CrocoDash.forcing.utils module#
- CrocoDash.forcing.utils.check_date_continuity(boundary_file_list: dict)#
Check for overlaps or missing dates between consecutive files.
- CrocoDash.forcing.utils.fetch_raw_chunk(data_access_fn, dates: list, latlon: dict, output_folder: str | Path, output_filename: str, variables: list, extra_args: dict, name=None) Path#
Download one raw data chunk, skipping if a valid output file already exists.
Shared by obc.py and ic.py — both fetch a chunk of raw data for a given date range and bounding box, and both need to be idempotent across re-runs.
- CrocoDash.forcing.utils.get_data_access_function(product_name: str, function_name: str)#
Load the product registry and return the raw access function for (product_name, function_name).
- CrocoDash.forcing.utils.is_valid_netcdf(path: Path) bool#
Check a file’s magic bytes match a known NetCDF format (HDF5, classic, or 64-bit offset).
- CrocoDash.forcing.utils.parse_dataset_folder(folder: str | Path, input_dataset_regex: str, date_format: str)#
Parse a folder to find and extract dataset file information based on a regex pattern.
- Parameters:
folder (str or Path) – Path to the folder containing the dataset files.
input_dataset_regex (str) – Regular expression pattern to match dataset filenames, for example
(north|east|south|west)_unprocessed\.(\d{8})_(\d{8})\.ncdate_format (str) – Date format string used to parse dates in filenames (e.g., “%Y%m%d”).
- Returns:
Dictionary mapping boundaries to a list of tuples with:
Start date (
datetime)End date (
datetime)Full file path (
Path)
For example:
{ "north": [(datetime(2000, 1, 1), datetime(2000, 1, 2), Path("/path/to/north_20000101_20000102.nc"))], "east": [(datetime(2000, 1, 3), datetime(2000, 1, 4), Path("/path/to/east_20000103_20000104.nc"))], }
- Return type:
dict
- CrocoDash.forcing.utils.read_min_depth(bathymetry_path) float#
The
min_depthglobal attribute of a topog file.Topog files written by mom6_forge always carry it. A hand-made file may not; then 0.0 is assumed, so any cell shallower than MOM6’s MINIMUM_DEPTH is treated as wet, and a warning says so.
CrocoDash.forcing.ww3 module#
- class CrocoDash.forcing.ww3.WW3Configurator(case_inputdir, boundaries, ww3_obc_product_name=None, ww3_obc_function_name=None, ww3_obc_function_overrides=None, get_step_days=None, regrid_step_days=None)#
Bases:
BaseConfigurator- allowed_compsets: List[str] = ['WW3']#
- configure()#
Bind input values to parameters and files.
- get_output_filepaths(ocn_ice_directory)#
Everything WW3 generated, which is a whole directory rather than a fixed set of named files.
The base implementation walks output_params for is_file entries, and all of WW3’s are XML settings – WW3_GRID_INP_DIR even holds the directory rather than any single file. Without this override the base returns nothing, so CaseBundle.bundle() copies no WW3 input at all and validate_output_filepaths() passes vacuously.
Globbing rather than naming files is deliberate: process() writes one ww3.pointN_spec.nc per boundary station, so the count is not known up front, and spec.list references those paths while ww3_bounc.nml references spec.list. The directory is the unit that stays coherent, which is also why WW3_GRID_INP_DIR points at it.
ocn_ice_directory is <inputdir>/ocn; process() writes to <inputdir>/wav (WAVE_SUBDIR), hence the sibling lookup.
- input_params: List[Param] = [InputValueParam(name='case_inputdir'), InputValueParam(name='boundaries'), InputValueParam(name='ww3_obc_product_name'), InputValueParam(name='ww3_obc_function_name'), InputValueParam(name='ww3_obc_function_overrides'), InputValueParam(name='get_step_days'), InputValueParam(name='regrid_step_days')]#
- name: str = 'WW3'#
- output_params: List[OutputParam] = [XMLConfigParam(name='WW3_GRID_INP_DIR')]#
- process(ctx)#
Generate WW3 boundary spectra, spec.list, and ww3_bounc.nml into <inputdir>/wav (WAVE_SUBDIR).
get_step_days/regrid_step_days: passed straight through to obc.py’s GET/REGRID chunking (see forcing/mom6.py’s process_bc for the same pattern). None (default) fetches/regrids the whole date range in one request per boundary – for the real ERA5 product, a multi-day request expands into a very large number of GRIB messages (days * 24 hours * 24 directions * 30 frequencies each) that can take a long time to queue/process on CDS. Passing get_step_days=1 splits each boundary’s GET step into one request per calendar day instead.
ww3_obc_product_name is required: a WW3 forcing product together with ww3_obc_function_name, or ‘none’ to skip boundary spectra (the open boundary ring then stays calm).
Routes through obc.py’s shared GET -> chunk -> REGRID -> MERGE engine. The product must match a WW3ForcingProduct- derived class’s spectral contract (enforced in validate_args); e.g. ‘CESM-WW3-JRA’ / ‘get_cesm_ww3_jra_spectra’ (raw_data_access/datasets/cesm_ww3_jra.py).
Each boundary’s merged output carries a “station” dimension – one real point per boundary window, not reduced to a single value. Finalization below splits every boundary’s stations out into individual ww3.pointN_spec.nc files (one per real station, via write_ww3_boundary_spectrum – its per-point contract is unchanged), and pools ALL boundaries’ stations into one global spec.list (ww3_bounc’s list/interpolation is domain-wide, not scoped to a named boundary).
ww3_bounc.nml’s INTERP is set to 2 (linear): each boundary contributes several real, spatially-distributed stations worth actually interpolating between via ww3_bounc’s own machinery, rather than pre-collapsing to one hand-picked point ourselves.
The time axis must span the full run: WW3 interpolates linearly in time between whatever records exist in nest.ww3, but a time axis that runs out mid-run permanently disables boundary forcing (an EOF in w3iobcmd.F90 sets FLBPI=.FALSE. for the rest of the run).
- process_components: Dict[str, str] = {'ww3': 'process'}#
- required_for_compsets: List[str] = ['WW3']#
- stale_outputs(previous_config, current_config, inputdir)#
The spectra in <inputdir>/wav are rewritten on every process() run, but from extract_forcings/ww3, whose raw, regridded and merged files are skipped when they exist – and only the raw and regridded names carry a date. So that staging directory goes once this configurator’s inputs or the conditions dates it reads have changed, or there is no record.
- validate_args(**kwargs)#
Validate provided inputs against declared input_params.
- CrocoDash.forcing.ww3.to_direction_to(direction, convention)#
Rotate a spectrum’s direction axis into the “to” convention.
write_ww3_boundary_spectrumwrites the axis through unchanged, and ww3_bounc rebuilds WW3’s internal THETA from it asmod(2.5*pi - deg2rad(direction), 2*pi)(ww3_bounc.F90:581) – the exact inverse of what ww3_ounp writes (ww3_ounp.F90:3245). So the number in the file has to be the direction the waves are propagating TOWARDS. A product that reports “coming from” is the same spectrum rotated 180 degrees, and only the product knows which it is – henceWW3ForcingProduct.direction_convention.Only the axis labels move;
efthis indexed by that axis and is left alone. The resulting order can be non-monotonic, which ww3_bounc handles (it remaps via W3CSPC whenever THETA(1) != TH(1)).
- CrocoDash.forcing.ww3.write_spec_list(file_dir, spectra_paths, spec_list_filename='spec.list')#
Write the spec.list file that ww3_bounc.nml’s BOUND%FILE points at: one _spec.nc path per line.
- Parameters:
file_dir (str) – Directory to write spec_list_filename to.
spectra_paths (list[str]) – Paths to the per-boundary-point spectra netCDF files.
spec_list_filename (str) – Name of the file to write.
- CrocoDash.forcing.ww3.write_ww3_bounc_nml(file_dir, spec_list_filename='spec.list', mode='WRITE', interp=2, verbose=1, nml_filename='ww3_bounc.nml')#
Write ww3_bounc.nml, the BOUND_NML namelist that drives the ww3_bounc preprocessor. Points BOUND%FILE at spec_list_filename (see write_spec_list).
- Parameters:
file_dir (str) – Directory to write ww3_bounc.nml to.
spec_list_filename (str) – Name of the spec-list file BOUND%FILE should point at.
nml_filename (str) – Name of the file written, ww3_bounc.nml unless staged under another.
mode (str) – ‘WRITE’ to build nest.ww3 from spectra files, ‘READ’ to diagnose an existing nest.ww3 instead.
interp (int) – Interpolation method onto boundary points: 1 (nearest), 2 (linear).
verbose (int) – Verbosity level: 0, 1, or 2.
- CrocoDash.forcing.ww3.write_ww3_boundary_spectrum(file_path, lat, lon, freq, direction, efth, time=None)#
Write a single-point WW3 boundary spectrum file, in the minimal shape ww3_bounc.F90 actually reads.
This is deliberately narrower than a real ww3_ounp point-output file (the kind NOAA’s own hindcast archive ships): things like station_name, frequency1/frequency2 (band edges), depth, u10m/udir, curr/currdir exist in that format for CF/GlobWave metadata compliance, but grepping ww3_bounc.F90 shows none of them are ever read (no NF90_INQ_VARID call for any of them). Only these are:
dims/vars “time”, “frequency”, “direction” (names must match exactly, ww3_bounc looks them up by name via NF90_INQ_DIMID/NF90_INQ_VARID)
“latitude”/”longitude” (only the first value is ever read into a scalar, even if this were made time-varying – so it’s fine as a length-1 dim)
“efth” (or “Efth” – ww3_bounc falls back to that capitalization if “efth” isn’t found)
no “station_name” var: if present, ww3_bounc instead reads efth as (time, station, frequency, direction) – but that’s not a “multi station” capability, it’s just a different dimension order for the same one-point behavior: that branch’s NF90_GET_VAR call hardcodes start/count to always read station index 1 only, no matter how many stations the file actually declares. Omitting station_name simply keeps us on the branch whose dimension order matches what we write here, efth(time, frequency, direction, latitude, longitude).
“_FillValue” on efth is NOT optional the way scale_factor/add_offset are: ww3_bounc.F90 calls CHECK_ERR() (a hard EXTCDE(59) exit) right after reading it, whereas a missing scale_factor/add_offset is silently defaulted to 1./0. (FACTOR=1., OFFSET=0. if the NF90_GET_ATT call fails). We write efth as plain float (not packed short), so scale_factor=1./ add_offset=0. is the true identity transform here – we set them explicitly anyway rather than lean on that default-if-missing behavior.
Because this writes exactly one point, ww3_bounc’s own fallback (see W3BOUNC: when only one file is listed in spec.list, NBO2==1 and its interpolation block is skipped entirely) applies this single spectrum uniformly to every active boundary point on the grid – lat/lon here don’t need to sit exactly on a boundary cell for that reason; they’re mostly informational until you add more points.
Time is written as plain seconds-since-epoch floats, NOT as a datetime64 coordinate. This was found the hard way: when time is datetime64, xarray’s own CF encoder recomputes the “units” string at write time and ignores whatever we set in .encoding[“units”] – for a timestamp that lands exactly on midnight it collapses to a bare “seconds since 1990-01-01” with no “hh:mm:ss” at all. W3TIMEMD’s U2D parses that units string by fixed column position (verified by reading the source), so a truncated string means it reads past the real attribute text into whatever’s in the underlying buffer – not reliably a crash, just undefined. Keeping time as plain floats (not datetime64) means xarray never touches the units/calendar attributes we set, so the string we write is exactly the string in the file.
- Parameters:
file_path (str) – Output netCDF path.
lat (float) – Nominal point location, degrees.
lon (float) – Nominal point location, degrees.
freq (array-like, shape (NK,)) – Frequency bins, Hz. Doesn’t have to match the grid’s own NK/FR1/XFR – ww3_bounc auto-remaps (the SPCONV/W3CSPC path) onto the grid’s own discretization if these differ, so mismatching is not fatal.
direction (array-like, shape (NTH,)) – Direction bins, degrees, propagating-towards (“to”) convention, clockwise from true north – ww3_bounc converts this internally (THETA = mod(2.5*pi - deg2rad(direction), 2*pi), ww3_bounc.F90:581), which is the exact inverse of what ww3_ounp writes out (mod(450 - th_deg, 360), ww3_ounp.F90:3245). Callers hand this axis over already converted – see to_direction_to.
efth (array-like, shape (NT, NK, NTH)) – 2D variance density spectrum per timestep, m^2 s / rad.
time (array-like of np.datetime64, optional) – Timestamps for each efth slice. Defaults to a single arbitrary step. Converted to plain seconds-since-1990-01-01 floats before writing (see note above) – passed in as datetime64 just for a nicer API.
Module contents#
- CrocoDash.forcing.load_all_configurators()#
Dynamically import every module in this package so each configurator class’s @register decorator runs and populates ForcingConfigRegistry. Adding a new forcing type is just adding a new file here – nothing needs to import it by name.