Configuration

Cocoa reads each run’s configuration from a YAML file.

Configuration File Structure

A configuration file has these top-level sections:

mesh:
  # Mesh file and projection settings

simulation:
  # Simulation time control

initial_conditions:
  # Initial state

physics:
  # Physical parameters

numeric:
  # Numerical solver parameters

forcing:
  # Forcing configuration (tides, meteorological, ramp)

output:
  # Output file configuration

diagnostics:
  # Logging and screen output

checkpoint:
  # Checkpoint/restart (optional)

Validation

Configuration files are validated against a schema at startup, before any other input is read. Unknown keys (with “did you mean” suggestions for near-misses), missing required fields, wrong value types, and out-of-range values are reported together in a single pass.

Boolean values must be written as true or false; the YAML 1.1 spellings yes/no/on/off are rejected. A key present with no value behaves as if the key were omitted, and the default applies.

A key that takes a number or mesh (manning_n, tau0 and the like) must hold one in full: nan, inf, a leading + or trailing text such as 0.025n fail at startup with the key named. Durations follow the same rule (see Duration Syntax).

The complete schema is reproduced in the Schema Reference at the end of this page.

Mesh Section

Specifies the mesh file, coordinate projection, and optional coordinate rotation for global meshes.

mesh:
  filename: "mesh.nc"              # Path to mesh file (required)
  projection:
    type: "EquidistantCylindrical" # Projection type (optional)
    center: [-76.5, 23.2]          # Projection center [lon, lat] (optional)
  rotation:                        # Coordinate rotation (optional)
    enabled: false
    pole_location: [0.0, 0.0]      # [longitude, latitude] of new North Pole
  partition:                       # MPI partition (optional)
    wet_balance: none              # none, initial_condition, or a level [m]

Parameters:

Parameter

Type

Default

Description

filename

string

(required)

Path to mesh file (NetCDF UGRID format)

projection.type

string

EquidistantCylindrical

Coordinate projection type: EquidistantCylindrical, EqualArea, Mercator, Miller or GallStereographic (case-sensitive)

projection.center

[lon, lat]

[0.0, 0.0]

Projection center coordinates

partition.wet_balance

string or number

none

Balance the nodes that start wet across MPI ranks as well as the elements: initial_condition takes the wet set the run starts from, a number takes ground below that level [m above datum] as wet, and none balances elements only. It changes only the partition; see Balancing Wet Work.

The mesh is checked as it is read: NaN or fill-value depths or coordinates, latitudes beyond 90 degrees, and zero-area triangles stop the run with an error naming the file and element.

Coordinate Rotation (Global Meshes)

For global ocean models, coordinate rotation moves the computational poles away from the geographic poles to avoid singularities.

mesh:
  rotation:
    enabled: true
    pole_location: [0.0, 0.0]  # [longitude, latitude] of new North Pole

Rotation Parameters:

Parameter

Type

Default

Description

rotation.enabled

bool

false

Enable coordinate rotation

rotation.pole_location

[lon, lat]

(required)

Geographic coordinates where the new North Pole is located [degrees]

When enabled: true, the pole_location parameter must be specified.

Common choices for pole_location:

  • [0.0, 0.0]: North Pole moves to the equator at the prime meridian

  • [180.0, 0.0]: North Pole moves to the equator at the date line

  • [-90.0, 0.0]: North Pole moves to the equator in the Atlantic

Effects of rotation:

  • Node coordinates are transformed to the rotated frame

  • Metric terms (spherical tan(phi) correction, projection scale factors) use the rotated latitude, since they belong to the frame the equations are discretized in

  • Coriolis parameter continues to use geographic latitude; the planetary rotation term is frame-independent

  • Tidal potential continues to use geographic latitude

  • Output velocities are transformed back to geographic (east, north) frame

See Coordinate Rotation in the Theory documentation for details and Global Simulations for a walkthrough.

Simulation Section

Controls simulation timing.

simulation:
  start_time: 2025-01-01           # Simulation start time (required)
  end_time: 2025-01-15             # Simulation end time (required)
  time_step: 10s                   # Time step (default 1s), e.g. 10s, 1m, 0.5s

Parameters:

Parameter

Type

Default

Description

start_time

datetime

(required)

Simulation start time. May be omitted when restarting (checkpoint.restart_file); the checkpoint supplies it.

end_time

datetime

(required)

Simulation end time

time_step

duration

1s

Computational time step. Accepts a duration string (e.g. 4s, 1m) or a bare number of seconds (e.g. 10.0).

The run from start_time to end_time must be a whole number of time steps; a remainder is a startup error.

DateTime formats supported:

  • 2025-01-01 (date only, midnight assumed)

  • 2025-01-01T12:00:00 (ISO 8601 format)

  • 2025-01-01 12:00:00 (space-separated)

Initial Conditions Section

Sets initial state for the simulation.

initial_conditions:
  water_level: 0.0                 # Initial water surface elevation [m]

Parameters:

Parameter

Type

Default

Description

water_level

float

0.0

Initial water surface elevation [m], and the open-boundary offset: added to the tidal boundary level, or held by the open boundaries when there is no tidal boundary forcing.

Physics Section

Physical model parameters.

physics:
  manning_n: 0.025                 # Manning's roughness coefficient (or "mesh")
  tau0: 0.05                       # GWCE weighting parameter (or "mesh")
  cf_lower_limit: 0.001            # Minimum friction coefficient
  internal_tide_friction: 0        # Internal tide friction (0, constant, or "mesh")
  smagorinsky_coefficient: 0.2     # Smagorinsky turbulence coefficient
  min_viscosity: 1.0e-6            # Minimum eddy viscosity [m^2/s]
  h0: 0.1                          # Minimum depth threshold [m]
  velmin: 0.01                     # Minimum wetting velocity [m/s]
  wet_dry_slope_limiter: 1.0e9     # Slope limiter coefficient
  wetting_velocity: linear         # Wetting test form (linear, quadratic, inertial)

Parameters:

Parameter

Type

Default

Description

manning_n

float or “mesh”

0.025

Manning’s roughness coefficient for bottom friction

tau0

float or “mesh”

0.05

GWCE weighting parameter

cf_lower_limit

float

0.001

Lower limit for the computed quadratic friction coefficient

min_depth_friction

float

0.1

Minimum total water depth (HBREAK) used in the bottom friction computation [m]

min_velocity_friction

float

0.0

Minimum velocity used in the bottom friction computation [m/s]

internal_tide_friction

float, “mesh”, or 0

0

Internal tide friction coefficient [1/s]. Set to 0 to disable, a constant value to apply uniformly, or mesh to read from the mesh file (see Spatially-Varying Parameters).

h0

float

0.1

Minimum depth threshold for wet elements [m]

velmin

float

0.01

Minimum velocity for node wetting [m/s]

smagorinsky_coefficient

float

0.2

Smagorinsky subgrid turbulence coefficient

min_viscosity

float

1.0e-6

Minimum eddy viscosity [m^2/s]

wind_drag_limit

float

0.0025

Maximum drag coefficient for wind stress

surface_directional_roughness

0 or “mesh”

0

Directional effective roughness lengths for wind reduction. Set to mesh to read the 12-directional roughness field from the mesh file, or 0 to disable (see Wind Reduction).

surface_canopy_coefficient

float or “mesh”

1.0

Binary canopy mask for wind reduction (0 or 1). Set to mesh to read per-node values from the mesh file, or 1 to disable (identity). A fractional value is a startup error. See Wind Reduction.

wet_dry_slope_limiter

float

1.0e9

Wet/dry shallow-water slope limiter coefficient (ADCIRC SLIM). Distinct from elemental_slope_limiter below.

wetting_velocity

“linear”, “quadratic” or “inertial”

linear

How the nodal wetting test estimates the flow onto a dry node, and with it the friction boost after wetting. linear is ADCIRC’s default; quadratic and inertial are its directvelWD and directvelWD + useHF settings, the latter the one Global STOFS uses. See Wetting Velocity.

elemental_slope_limiter

float or “mesh”

(disabled)

Optional selective post-continuity elevation-gradient limiter [m/m]. Absent/null disables it; a constant applies one threshold everywhere; mesh reads per-node thresholds from the mesh file. See Elemental Slope Limiter.

advection

bool, “off_at_open_boundaries” or “mesh”

off_at_open_boundaries

Where the advective terms are evaluated. off_at_open_boundaries (the default) disables advection along the open, flow and radiation boundary strings, true applies it everywhere, false disables it everywhere, and mesh reads a per-node binary mask (1 = on, 0 = off) from the mesh file’s advection_state nodal attribute. A fractional mesh value is a startup error; yes/on/1 are not accepted spellings. See Advection.

Advection

Every mode resolves to the same per-node binary mask; they differ only in where it comes from. The mask is reduced to one flag per element once at startup, and an element evaluates advection only when every one of its corner nodes does (ADCIRC’s ADVECTLOCAL). A node with advection off therefore switches it off in every element touching that node.

physics:
  advection: off_at_open_boundaries  # default: off where the domain ends
  # advection: true                  # everywhere
  # advection: false                 # nowhere
  # advection: mesh                  # per-node mask from the mesh file

off_at_open_boundaries clears the mask at every node of the boundary strings where the domain is truncated (elevation-specified or open, specified-flow and radiation), so the disabled region is a band one element thick just inside those strings. That is where the advective terms are least well behaved: they reach for an upstream state the mesh does not hold. Land walls and weirs keep advection, because the flow they bound is inside the mesh; clearing them too costs a channel bend its cross-bend balance one element in from each bank.

For anything finer, write the mask you want into the mesh file and use mesh: for example, to disable advection only on the open boundary and not on the inflow, set advection_state to 0 at those nodes and 1 everywhere else. See Mesh Preparation.

Note

off_at_open_boundaries became the default in place of “advection everywhere”. A run that needs the previous behavior must set advection: true explicitly.

Spatially-Varying Parameters

The manning_n, tau0, internal_tide_friction, surface_directional_roughness, and surface_canopy_coefficient parameters support spatially-varying values from the mesh file. To enable this, set the parameter value to mesh:

physics:
  manning_n: mesh                         # Read from "mannings_n_at_sea_floor"
  tau0: mesh                              # Read from "primitive_weighting_in_continuity_equation"
  internal_tide_friction: mesh            # Read from "internal_tide_friction"
  surface_directional_roughness: mesh     # Read from "surface_directional_effective_roughness_length"
  surface_canopy_coefficient: mesh        # Read from "surface_canopy_coefficient"

When set to mesh, Cocoa reads the corresponding nodal attribute from the NetCDF mesh file:

Parameter

Mesh Variable Name

Description

manning_n

mannings_n_at_sea_floor

Manning’s n at each node [s/m^(1/3)]

tau0

primitive_weighting_in_continuity_equation

GWCE tau0 at each node [1/s]

internal_tide_friction

internal_tide_friction

Internal tide friction coefficient at each node [1/s]. Supports both scalar (1 value per node, isotropic) and tensor (3 values per node: xx, yy, xy components) storage in the mesh file.

surface_directional_roughness

surface_directional_effective_roughness_length

Directional land roughness lengths at each node [m]. Stored as a 12-component vector (one per 30-degree directional bin).

surface_canopy_coefficient

surface_canopy_coefficient

Canopy coefficient at each node [dimensionless, 0–1]. Multiplicative reduction applied to wind velocity under forest canopy.

If the mesh file does not contain the required variable when mesh is specified, Cocoa exits with an error.

See Mesh Preparation for details on creating mesh files with nodal attributes.

Wind Reduction

When meteorological forcing is enabled, Cocoa can apply two types of overland wind reduction before computing wind stress:

  1. Directional roughness reduction (surface_directional_roughness: mesh) scales wind speed by the ratio of upwind land surface roughness to marine roughness. Uses 12 directional bins (30-degree spacing) with linear interpolation between bins.

  2. The canopy coefficient (surface_canopy_coefficient: mesh) multiplies the wind components to represent sheltering by forest canopy. A value of 1.0 means no reduction; 0.0 means complete sheltering.

The processing order within the meteorological forcing kernel is:

  1. Apply directional roughness reduction (if enabled)

  2. Apply canopy coefficient (if enabled)

  3. Apply meteorological ramp

  4. Compute kinematic wind stress from the reduced, ramped wind

Bottom Friction:

The friction coefficient is computed from Manning’s n:

\[C_f = \max\left(\frac{g n^2}{H^{1/3}}, C_{f,min}\right)\]

where \(C_{f,min}\) is cf_lower_limit.

Wetting and Drying Algorithm

Minimum Depth Threshold (H0):

The h0 parameter sets the minimum total water depth for a node to be considered wet. When the total depth falls below this threshold, the node transitions to dry. Typical values range from 0.01 to 0.5 meters.

Minimum Wetting Velocity (VELMIN):

The velmin parameter controls the minimum velocity required for water to advance onto a dry node during the wetting process. Lower values allow easier wetting but may cause numerical oscillations. Default is 0.01 m/s.

Wetting Velocity:

wetting_velocity selects the force balance behind the velocity that velmin is compared against (equations in Wetting and Drying):

  • linear (default): the surface slope against friction held at its velmin value. ADCIRC’s default.

  • quadratic: the surface slope against quadratic friction. ADCIRC directvelWD = T.

  • inertial: the frictionless shallow-water speed, with a 100-fold friction boost at the wet neighbors after wetting. ADCIRC directvelWD = T with useHF = T, the setting Global STOFS uses.

To reproduce an ADCIRC run, match its &wetDryControl namelist; a fort.15 without one uses linear.

Slope Limiter (SLIM):

The wet_dry_slope_limiter parameter limits water surface gradients in shallow water to prevent spurious oscillations. The limiter computes a scaling factor \(\alpha\) for the elevation gradient:

\[\alpha = \frac{\text{SLIM} \cdot |\nabla h|}{|\nabla \zeta|}\]

where \(h\) is bathymetric depth and \(\zeta\) is water surface elevation. The gradient is scaled by \(\min(\alpha, 1)\).

The default, 1.0e9, never limits in practice. A typical active value is 0.0004, which limits gradients in shallow areas.

Example enabling slope limiting:

physics:
  h0: 0.1
  velmin: 0.01
  wet_dry_slope_limiter: 0.0004

Numeric Section

Numerical solver parameters.

numeric:
  solver: explicit                       # Solver type (default: explicit)
  gwce_coefficients: [0.0, 1.0, 0.0]     # Temporal weights [a00, b00, c00]
  linear_solver:                          # CG solver settings (consistent only)
    type: belos_cg
    tolerance: 1.0e-5
    max_iterations: 50
    convergence_scaling: norm_of_initial_residual

Parameters:

Parameter

Type

Default

Description

solver

string

explicit

GWCE solver type (see below)

gwce_coefficients

[a00, b00, c00]

[0.0, 1.0, 0.0]

GWCE temporal integration weights

Solver Types:

The solver parameter accepts the following values:

Value

Description

explicit or lumped

Diagonal mass matrix with direct solve (faster, coefficients set to [0, 1, 0])

implicit, iterative, or consistent

Sparse matrix with iterative CG solver (more accurate, semi-implicit)

With the explicit/lumped solver, gwce_coefficients is forced to [0, 1, 0] whatever the configuration sets. For the implicit/consistent solver, typical values are [0.35, 0.30, 0.35].

GWCE Coefficients:

The coefficients control temporal weighting in the GWCE:

  • a00: Weight for time level n+1

  • b00: Weight for time level n

  • c00: Weight for time level n-1

Linear Solver Configuration

The linear_solver subsection controls the iterative Conjugate Gradient solver used by the consistent (implicit) GWCE solver. These settings are ignored when using the explicit/lumped solver.

Parameter

Type

Default

Description

type

string

belos_cg

CG solver implementation (see below)

tolerance

float

1.0e-5

Convergence tolerance for the iterative solver. The solver terminates when the (scaled) residual norm drops below this value.

max_iterations

int

50

Maximum number of CG iterations per solve. If the solver does not converge within this limit, it reports unconverged.

verbosity

int

0

Solver output verbosity: 0 = silent, 1 = summary, 2 = per-iteration.

convergence_scaling

string

norm_of_initial_residual

How the residual is scaled when testing convergence (see below)

Solver Implementation Types:

The type parameter selects the CG algorithm variant. All variants solve the same linear system with Jacobi preconditioning; they differ in communication pattern and internal implementation.

Value

Description

belos_cg

Standard Belos CG, with Belos convergence testing and configurable verbosity output.

tpetra_cg_pipeline

Tpetra-native pipelined CG. Overlaps communication with computation by pipelining inner products, which can reduce latency on distributed systems with many MPI ranks.

tpetra_single_reduce

Tpetra-native single-reduce CG. Combines two inner products per iteration into a single MPI allreduce, reducing synchronization points. Useful on high-latency interconnects.

Use the default belos_cg. The three are equivalent in exact arithmetic but not in iteration count: the single-reduce recurrence was measured to take more iterations per solve and to be slower overall.

Convergence Scaling:

The convergence_scaling parameter determines how the residual norm is normalized when checking the convergence criterion.

Value

Description

norm_of_initial_residual

Relative convergence: the solver converges when \(\|r_k\| / \|r_0\| < \text{tolerance}\), which makes the tolerance independent of problem scaling.

norm_of_prec_init_res

Like norm_of_initial_residual but scales by the preconditioned initial residual \(\|M^{-1} r_0\|\) instead, for when the preconditioner changes the residual norm substantially.

none

Absolute convergence: the solver converges when \(\|r_k\| < \text{tolerance}\). The tolerance must be chosen relative to the expected magnitude of the right-hand side.

Example: Consistent solver with custom linear solver settings:

numeric:
  solver: consistent
  gwce_coefficients: [0.35, 0.30, 0.35]
  linear_solver:
    type: belos_cg
    tolerance: 1.0e-6
    max_iterations: 100
    convergence_scaling: norm_of_initial_residual

Forcing Section

Configures tidal and meteorological forcing, along with the spinup ramp.

Ramp Configuration

Every ramp is the same block. See Ramps for its keys, its forms and where each ramp applies.

forcing:
  ramp:
    enabled: true                  # default true
    duration: 1d                   # time to rise from 0 to 1
    form: tanh                     # tanh (default), linear or smoothstep
    delay: 0                       # time held at 0 before it rises
  meteorological:
    ramp:                          # optional: a key it omits comes from forcing.ramp
      duration: 12h

Meteorological Forcing

Configures atmospheric wind and pressure forcing. See Meteorological Forcing for full details on file formats and usage.

forcing:
  meteorological:
    enabled: true
    format: cf_netcdf                    # cf_netcdf, owi_ascii, owi_netcdf, or grib
    filename: "path/to/meteo_data.nc"    # Single file (cf_netcdf, owi_netcdf)

This is the flat, single-source form. Two or more sources (mixed formats, nested grids, a reduced vs. unreduced field) are configured as a forcing.meteorological.domains list instead; see Meteorological Forcing for the composition rules.

Parameters:

Parameter

Type

Default

Description

meteorological.enabled

bool

false

Enable meteorological forcing

meteorological.format

string

cf_netcdf

File format: cf_netcdf (aliases: cf, netcdf), owi_ascii (alias: owi), owi_netcdf, or grib (alias: grib2, requires cocoa_ENABLE_GRIB)

meteorological.filename

string

(one of filename/filenames)

Path to meteorological data file (CF NetCDF, OWI NetCDF, or a single GRIB file)

meteorological.filenames

list

(one of filename/filenames)

List of pressure/wind file pairs (OWI ASCII), or a list of GRIB files (GRIB)

meteorological.product

string

(required for GRIB)

Preset weather product name (e.g. gfs, hrrr) selecting the numeric GRIB2 field identities, or omit and supply a variables override

meteorological.drag_law

string

garratt

Wind drag coefficient formulation

meteorological.pressure_scale

float

100.0

Scale factor for pressure, OWI formats only (default converts hPa to Pa). Rejected for cf_netcdf (units come from the file’s units attribute) and grib (GRIB2 fields are SI by definition).

meteorological.wind_scale

float

1.0

Scale factor for wind velocity

meteorological.wind_reduction

bool

true

Opt out of overland wind reduction for this source by setting false; the reduction applies only when the mesh carries the roughness/canopy attributes (see Wind Reduction)

meteorological.variables

map

(format defaults)

Variable names (CF NetCDF) or numeric GRIB2 field selectors to read in place of the defaults (see below)

meteorological.domains

list

(none)

Two or more self-contained sources in place of the flat form above (see Meteorological Forcing)

meteorological.ice

bool

false

Read sea-ice concentration, which attenuates the wind drag. Ice is opt-in: any other ice key set while this is false is a startup error (see Ice Concentration)

meteorological.ice_drag_law

string

lupkes

Ice drag attenuation law; lupkes is the only value

meteorological.ice_scale

float

1.0

Scale factor for ice concentration, CF NetCDF only (0.01 for percent-encoded ice)

meteorological.vortex

map

(none)

Parametric (GAHM) vortex from ATCF tracks, alone or blended with a gridded source (see Parametric Vortex (GAHM))

The cf_netcdf format reads its variables by name, and the default names can be overridden:

forcing:
  meteorological:
    enabled: true
    format: cf_netcdf
    filename: "met_data.nc"
    variables:
      pressure: "mslp"      # Default: "mslp"
      wind_u: "wind_u"      # Default: "wind_u"
      wind_v: "wind_v"      # Default: "wind_v"
      time: "time"          # Default: "time"
      lon: "lon"            # Default: "lon"
      lat: "lat"            # Default: "lat"

For OWI ASCII format, use filenames with paired pressure/wind files:

forcing:
  meteorological:
    enabled: true
    format: owi_ascii
    filenames:
      - pressure: "basin.pre"
        wind: "basin.wnd"

For GRIB2 (requires a build with cocoa_ENABLE_GRIB), name a weather product preset and the file list; see Meteorological Forcing for the available products, numeric variables overrides, and multi-source composition:

forcing:
  meteorological:
    enabled: true
    format: grib
    product: gfs
    filenames: ["gfs_f000.grib2", "gfs_f003.grib2"]

External File Inclusion

Two configuration blocks often grow long enough to keep in their own files: meteorological file lists (a multi-day forecast run’s GRIB list can span hundreds of files) and the open-ocean tide boundary block (per-node constituent data). At these locations a node’s content may be replaced with an include mapping naming a YAML file. That file’s root content is spliced in place of the node before validation runs, so the result is the same as writing the content inline:

forcing:
  meteorological:
    enabled: true
    format: grib
    product: gfs
    filenames:
      include: hafs_ida_files.yaml    # a YAML list of file paths
  tide:
    boundary:
      include: wnat_tide_boundary.yaml  # the whole boundary block

Rules:

  • include is accepted only at forcing.meteorological.filenames, a domains entry’s filenames, forcing.tide.boundary, and forcing.tide.boundary.constituents. Anywhere else it is a configuration error naming the allowed locations.

  • The include mapping must contain only the include key.

  • Relative paths resolve against the directory of the file containing the include, so a configuration and its included files can be moved together.

  • Included files may not themselves contain include nodes (one level only).

Flow Boundary Forcing

Configures normal-flow (discharge) boundary conditions driven by per-segment time series.

forcing:
  flow_boundary:
    enabled: true
    ramp:                          # Optional spinup ramp for flow boundaries
      enabled: true
      duration: 1d
    segments:
      - segment_index: 0
        time_series_file: "inflow_0.txt"
      - segment_index: 1
        time_series_file: "inflow_1.txt"

Parameters:

Parameter

Type

Default

Description

flow_boundary.enabled

bool

false

Enable flow boundary forcing

flow_boundary.ramp

ramp

enabled: true, duration: 1d

Optional spinup ramp applied to flow boundaries, a ramp block (Ramps). It does not inherit forcing.ramp: an omitted key takes the default shown.

flow_boundary.segments

list

(none)

List of flow boundary segments. Each entry maps a flow boundary to a time series file.

Each segments entry contains:

  • segment_index: Index of the flow boundary segment in the mesh

  • time_series_file: Path to the discharge time series file for that segment

Baroclinic Forcing

Applies a precomputed baroclinic nodal forcing file (baroclinic pressure gradient, dynamic internal-tide drag, dispersion as friction). The file is built with cocoa-ocean3d; see Baroclinic Coupling for the full key reference and the rules that tie the keys together.

forcing:
  baroclinic:
    enabled: true                     # default false
    filename: "baroclinic_forcing.nc" # required when enabled
    ramp:                             # default enabled, 1d; independent of forcing.ramp
      enabled: true
      duration: 12h
    internal_tide:
      enabled: false                  # default false
    dispersion:
      enabled: false                  # default false
      mode: mld                       # mld (default) or cdisp
      deep_water_depth: 1000.0        # [m], default 1000
      mld_coefficients: {a: 0.01, b: 0.5}  # no default; required for mode: mld

Data Assimilation

Applies a water level correction field from data assimilation as a pseudo atmospheric pressure, so that a slowly varying correction raises the water surface by its own value. The correction comes from one of two file types:

  • format: nodal (default): a netCDF time series on the mesh’s nodes, with a CF time axis, a node dimension and a (time, node) variable in metres.

  • format: cf_netcdf: a CF regular longitude/latitude grid, read and interpolated bilinearly onto the mesh like CF meteorological forcing (the coordinate variables are lon, lat and time). A node next to a fill value averages its remaining stencil corners; nodes outside the grid get no correction, and startup logs how many lie outside.

See Data Assimilation for the method, the file formats and a restart workflow.

forcing:
  water_level_correction:
    enabled: true                     # default false
    filename: "correction.nc"         # required when enabled
    format: nodal                     # nodal (default) or cf_netcdf
    variable: water_level_correction  # the variable's name, units "m"
    method: pressure_and_mass_adjust  # (default) or pressure
    multiplier: 1.0                   # applied to every value, default 1
    ramp:                             # default enabled, 1d; independent of forcing.ramp
      duration: 12h
      form: linear                    # ADCIRC's ramp shape; default tanh
      delay: 1d                       # held at 0 for the first day
  • method: pressure_and_mass_adjust also adds each step’s change in the correction to the water level of wet nodes, so the water surface follows the correction at once, at the cost of mass conservation. pressure applies the pressure term alone and conserves mass; water then reaches enclosed basins only through their inlets.

  • ramp: a ramp block (Ramps), measured from simulation.start_time, so a restart continues it. A sudden correction acts like a sudden pressure change and can excite spurious waves, so ramp it over hours. form: linear with a delay is ADCIRC’s ramp: its start is the delay and its end is the delay plus the duration.

  • The file’s snapshots are interpolated linearly in time; after the last one its values are held, and a file with one snapshot applies for the whole run. A file with more than one snapshot must span the run’s start time.

Tidal Potential Forcing

The tide potential can be computed using either harmonic constituents or the astronomical method. The type parameter selects the method:

  • constituent (or harmonic): Uses pre-defined tidal constituents

  • astronomical (or ephemeris, direct): Computes directly from Moon and Sun positions

  • auto (default): Selects automatically based on what is configured

Harmonic Constituent Method:

forcing:
  tide:
    potential:
      enabled: true
      type: constituent
      constituents:
        - name: M2
          frequency: 0.000140518902509  # rad/s
          amplitude: 0.242334           # m
          etrf: 0.693                   # Earth tide reduction factor
          nodal_factor: 0.96461         # Nodal modulation
          equilibrium_arg: 315.69       # degrees

Constituent Parameters:

Parameter

Units

Description

name

string

Constituent name (Q1, O1, P1, K1, N2, M2, S2, K2, etc.)

frequency

rad/s

Angular frequency

amplitude

m

Tidal potential constant K

etrf

Earth tide reduction factor

nodal_factor

Nodal modulation factor f

equilibrium_arg

degrees

Equilibrium argument V+u

Astronomical Method:

forcing:
  tide:
    potential:
      enabled: true
      type: astronomical
      astronomical:
        order: 3               # Expansion order: 2=P2, 3=P2+P3, 4+=untruncated
        include_nutation: true  # Include nutation corrections
        k2: 0.302              # Love number (optional)
        h2: 0.609              # Love number (optional)

Astronomical Parameters:

Parameter

Default

Description

order

3

Legendre polynomial expansion order (ADCIRC’s TIPOrder semantics). 2 is the degree-2 equilibrium tide; 3, the default, adds degree-3 corrections; 4 or higher carries every degree that matters rather than truncating. See Tidal Forcing for how cocoa evaluates the untruncated form and why it deviates from ADCIRC there.

include_nutation

true

Include nutation corrections in celestial position calculations.

k2

0.302

Second-degree Love number for body tide.

h2

0.609

Second-degree Love number for vertical displacement.

The astronomical method needs no constituent list. See Tidal Forcing for details.

Tidal Boundary Forcing

Elevation-specified tidal boundary conditions.

forcing:
  tide:
    boundary:
      enabled: true
      constituents:
        - name: M2
          frequency: 0.000140518902509  # rad/s
          nodal_factor: 0.96461          #
          equilibrium_arg: 315.69        # degrees
          boundary_values:               # Per-node amplitude/phase
            - { amplitude: 0.52, phase: 344.17 }
            - { amplitude: 0.51, phase: 345.15 }
            # ... one entry per open boundary node

Constituent Parameters:

Parameter

Units

Description

name

string

Constituent name

frequency

rad/s

Angular frequency

nodal_factor

Nodal modulation factor

equilibrium_arg

degrees

Equilibrium argument

boundary_values

list

Per-node amplitude [m] and phase [degrees]

Self-Attraction and Loading (SAL)

SAL corrections account for the ocean’s gravitational self-attraction and Earth deformation under tidal loads. SAL data must be pre-computed and embedded in the mesh file (see Tidal Forcing).

forcing:
  tide:
    sal:
      enabled: true
      parameter_source: "auto"    # or "manual"

Parameters:

Parameter

Default

Description

enabled

false

Enable SAL forcing

parameter_source

“auto”

How to determine constituent parameters. "auto" looks up frequencies and nodal factors from the built-in tidal database. "manual" uses values provided in the constituents list.

In manual mode, provide constituent parameters explicitly:

forcing:
  tide:
    sal:
      enabled: true
      parameter_source: "manual"
      constituents:
        - name: M2
          frequency: 1.405189028e-04   # rad/s
          nodal_factor: 1.026777
          equilibrium_arg: 26.668130   # degrees

Output Section

Controls simulation output. global configures the mesh-wide time-series file (cocoa_output.nc in the working directory by default) and its peak file; stations configures point time series in a file of their own.

output:
  compression:                     # Shared by every output file (optional)
    level: 1
  global:
    filename: "cocoa_output.nc"    # Output file name (optional, default: cocoa_output.nc)
    step_interval: 1h              # Output every hour (duration or integer step count)
    start_time: 2025-01-05         # Time-series window (optional, default: whole run)
    end_time: 2025-01-10
    mode: split                    # "split" (default) or "combined"
    variables:                     # Selectively disable time-series variables
      pressure: false
      wind: false
  stations:                        # Point time series (optional)
    step_interval: 6m
    locations: {include: stations.yaml}

Parameters:

Parameter

Type

Default

Description

compression

map

level: 1, shuffle: false

NetCDF deflate settings for every output file: level 0-9 (0 disables compression) and the shuffle byte-shuffle filter. Lower the level (or use 0) when frequent output stalls the run on the background writer. See Output Files.

global

map

(defaults below)

The global output file; keys in the next table.

stations

map

(none)

Point time series, written to cocoa_station_output.nc with their own step_interval (default: the global interval), start_time/end_time, variables and partially_wet. locations (required) holds name and location: [x, y] entries, inline or through include:. See Station Output.

output.global parameters:

Parameter

Type

Default

Description

filename

string

cocoa_output.nc

Output file name (NetCDF format)

step_interval

int or duration

100

Interval between output writes. Accepts a plain integer (number of time steps) or a duration string with suffix: s (seconds), m (minutes), h (hours), d (days). Fractional values are allowed (e.g., 0.5h). The duration must be evenly divisible by the simulation time step.

start_time

datetime

simulation start

First time-series slice; later slices follow at step_interval. Must be a whole number of time steps from simulation.start_time. Peak variables and checkpoints ignore the window. See Time Window.

end_time

datetime

simulation end

Last time a time-series slice may be written. Same rules as start_time.

mode

string

split

Controls where peak variables are written. In split mode (default), peak variables are written to a separate file named <filename>_peak.nc (e.g., cocoa_output_peak.nc). In combined mode, peak variables are included in the main output file. See Output Files for details.

variables

map

(all enabled except tide_potential, bpg_effective and water_level_correction )

Selectively disable time-series output variables. List variable names with false to disable them. Omitted variables remain enabled, except the forcing diagnostics tide_potential, bpg_effective and water_level_correction, which are off unless set to true. Peak variables are always written and cannot be disabled. Available names: zeta, velocity, pressure, wind, tide_potential, ice_concentration, bpg_effective, water_level_correction. See Output Files for the full variable reference.

Diagnostics Section

Controls logging and screen output.

diagnostics:
  screen_interval: 2h              # Screen output every 2 hours (duration or integer step count)
  log_file: "simulation.log"       # Structured log file path (optional)

Parameters:

Parameter

Type

Default

Description

screen_interval

int or duration

100

Interval between screen output. Accepts a plain integer (number of time steps) or a duration string (e.g., 30m, 1h). See Duration Syntax for the full syntax.

log_file

string

(none)

Path to log file (JSON structured log format)

Checkpoint Section

Configures checkpoint/restart for long-running simulations. See Checkpoint / Restart for details.

checkpoint:
  enabled: true
  write_interval: 12h            # Write checkpoint every 12 hours
  file_prefix: "cocoa_checkpoint"  # Prefix for checkpoint files
  restart_file: ""               # Path to checkpoint file to restart from

Parameters:

Parameter

Type

Default

Description

enabled

bool

false

Enable the checkpoint/restart system

write_interval

int or duration

0

How often to write checkpoints. 0 (the default) writes a single checkpoint at the end of the run; a positive value writes every N time steps plus one at the final step. Accepts a plain integer (number of time steps) or a duration string (e.g., 6h, 1d). See Duration Syntax for the full syntax.

file_prefix

string

cocoa_checkpoint

Filename prefix. Each write produces a timestamped file {prefix}.{simulation_time}.nc (e.g. cocoa_checkpoint.20250101T120000.nc).

restart_file

string

(none)

Path to a checkpoint file to restart from. When set, the simulation resumes from the saved state. A checkpoint at or after simulation.end_time is refused.

Restarting from a checkpoint:

checkpoint:
  enabled: true
  restart_file: "cocoa_checkpoint.20250101T120000.nc"

Complete Example

A complete configuration file for a tidal simulation:

mesh:
  filename: "gulf_mesh.nc"
  projection:
    type: "EquidistantCylindrical"
    center: [-90.0, 25.0]

simulation:
  start_time: 2025-01-01
  end_time: 2025-01-15
  time_step: 10s

initial_conditions:
  water_level: 0.0

physics:
  manning_n: 0.025
  tau0: 0.005
  cf_lower_limit: 0.001

numeric:
  solver: implicit
  gwce_coefficients: [0.35, 0.30, 0.35]
  linear_solver:
    type: belos_cg
    tolerance: 1.0e-5
    max_iterations: 50

forcing:
  ramp:
    enabled: true
    duration: 1d
  tide:
    potential:
      enabled: false
    boundary:
      enabled: true
      constituents:
        - name: M2
          frequency: 1.405189028e-04
          nodal_factor: 0.964
          equilibrium_arg: 23.06
          boundary_values:
            - { amplitude: 0.50, phase: 340.0 }
            - { amplitude: 0.48, phase: 342.0 }
            # ... additional boundary nodes

output:
  global:
    filename: "gulf_output.nc"
    step_interval: 10m

diagnostics:
  screen_interval: 1h

Duration Syntax

Time-based fields share a common duration grammar. A duration string consists of a numeric value followed by a unit suffix:

Suffix

Unit

Example

s

seconds

30s = 30 seconds

m

minutes

20m = 20 minutes

h

hours

1h = 1 hour

d

days

1d = 1 day

Two groups of fields use this grammar, and they read a bare number (one with no suffix) differently:

  • Pure durations: simulation.time_step and every ramp block’s duration and delay (Ramps). A bare number is seconds (time_step: 2.0 is identical to time_step: 2s).

  • Step intervals: output.global.step_interval, output.stations.step_interval, diagnostics.screen_interval and checkpoint.write_interval. A bare integer is a count of time steps (step_interval: 3600 means every 3600 steps). A suffixed duration must be evenly divisible by time_step; for example 20m with time_step: 7s errors because 1200 s is not a multiple of 7 s.

Rules:

  • Fractional values are allowed: 0.5h = 30 minutes, 0.25d = 6 hours.

  • A duration’s number must be a finite decimal with nothing after the suffix: nan, inf, hexadecimal (0x10s), a leading + and trailing text are startup errors naming the key.

  • 0 is legal only for checkpoint.write_interval, where it means “write a single checkpoint at the end of the run” (see Checkpoint Section); the other intervals must be positive.

Examples:

simulation:
  time_step: 2s                   # 2-second time step (or: 2.0)

forcing:
  ramp:
    duration: 12h                 # 12-hour spinup ramp (or: 43200)

output:
  global:
    step_interval: 15m              # Every 15 minutes (= 450 steps)

diagnostics:
  screen_interval: 2h             # Every 2 hours (= 3600 steps)

checkpoint:
  enabled: true
  write_interval: 12h             # Every 12 hours (= 21600 steps)

Unit Conventions

Input units follow these conventions:

Quantity

Input Units

Internal Units

Tidal frequency

rad/s

rad/hr

Angles (phase, equilibrium_arg)

degrees

radians

Amplitude

meters

meters

Time step

seconds

seconds

Ramp duration

duration string or seconds

seconds

Environment Variables

Cocoa honors these standard environment variables:

Variable

Description

Default

OMP_NUM_THREADS

Number of OpenMP threads (CPU backend)

All available

CUDA_VISIBLE_DEVICES

GPU device selection (CUDA backend)

(unset; all devices visible)

Schema Reference

The schema below is the file the model validates against, included from the source tree (src/cocoa_kernel/core/config/cocoa_config.schema.yaml). The same file is embedded into the binary at build time, so this reference matches the running code. It is a JSON Schema (draft-07) document authored as YAML; the conventions (null handling, weak scalar typing, closed key sets) are documented in its header comment.

# SPDX-License-Identifier: LicenseRef-Proprietary
# Copyright (c) 2026 Zach Cobell
#
# JSON Schema (draft-07) for the Cocoa model configuration, authored as YAML
# and embedded into the binary at configure time (CocoaConfigSchemaText.hpp
# is generated from this file; see src/cocoa_kernel/CMakeLists.txt).
#
# Conventions:
# - Optional keys accept null: "key:" with no value behaves as absent and the
#   parser applies the struct default. Required keys are strict. The engine
#   also treats a quoted empty string as null, so 'key: ""' behaves as absent
#   on optional keys too.
# - Scalar typing is weak (yaml-cpp discards scalar type information):
#   "type: string" accepts any scalar; numbers validate by string parse. The
#   engine details live in cocoa_io/SchemaValidation.cpp.
# - Enum VALUES (solver names, meteo formats, vortex models, ...) validate in
#   the parser's EnumTables, not here; the schema declares plain strings.
# - Closed key sets everywhere (additionalProperties: false) except tidal
#   constituents: a typo must be a schema error, not a silently ignored key.
# - anyOf alternatives stay scalar-shaped except the two structured
#   unions (meteo filenames, meteo variables), which mirror the two legal
#   input shapes; their failures report more verbosely than scalar unions.
# - Schema/parser drift is guarded in one direction only: the kitchen-sink
#   test in test_cocoa_config_schema.cpp fails when a parser-read key is
#   missing here. The converse, a key declared here that no parser reads,
#   validates and is silently ignored; the keep-in-sync comments below are
#   the accepted defense for that direction.

"$schema": "http://json-schema.org/draft-07/schema#"
type: object
additionalProperties: false
required: [mesh, simulation]
properties:
  mesh:
    type: object
    additionalProperties: false
    required: [filename]
    properties:
      filename: {type: string}
      projection:
        type: [object, "null"]
        additionalProperties: false
        properties:
          type: {$ref: "#/definitions/string_or_null"}
          center: {$ref: "#/definitions/coordinate_pair"}
      rotation:
        type: [object, "null"]
        additionalProperties: false
        properties:
          enabled: {$ref: "#/definitions/boolean_or_null"}
          pole_location: {$ref: "#/definitions/coordinate_pair"}
      partition:
        type: [object, "null"]
        additionalProperties: false
        properties:
          wet_balance: {$ref: "#/definitions/wet_balance"}

  simulation:
    type: object
    additionalProperties: false
    # start_time is required too, UNLESS checkpoint.restart_file is set
    # (the checkpoint then supplies it). This schema cannot express that
    # cross-section conditional; parse_simulation_section enforces it.
    required: [end_time]
    properties:
      start_time: {type: string}
      end_time: {type: string}
      time_step: {$ref: "#/definitions/positive_duration"}

  initial_conditions:
    type: [object, "null"]
    additionalProperties: false
    properties:
      water_level: {$ref: "#/definitions/number_or_null"}

  physics:
    type: [object, "null"]
    additionalProperties: false
    properties:
      manning_n: {$ref: "#/definitions/mesh_or_numeric"}
      tau0: {$ref: "#/definitions/mesh_or_numeric"}
      cf_lower_limit: {$ref: "#/definitions/non_negative_number"}
      smagorinsky_coefficient: {$ref: "#/definitions/non_negative_number"}
      min_viscosity: {$ref: "#/definitions/non_negative_number"}
      min_depth_friction: {$ref: "#/definitions/non_negative_number"}
      min_velocity_friction: {$ref: "#/definitions/non_negative_number"}
      internal_tide_friction: {$ref: "#/definitions/mesh_or_numeric"}
      h0: {$ref: "#/definitions/positive_number"}
      velmin: {$ref: "#/definitions/non_negative_number"}
      wet_dry_slope_limiter: {$ref: "#/definitions/positive_number"}
      wetting_velocity: {$ref: "#/definitions/string_or_null"}
      wind_drag_limit: {$ref: "#/definitions/non_negative_number"}
      surface_directional_roughness: {$ref: "#/definitions/mesh_or_numeric"}
      surface_canopy_coefficient: {$ref: "#/definitions/mesh_or_numeric"}
      elemental_slope_limiter: {$ref: "#/definitions/mesh_or_numeric"}
      advection: {$ref: "#/definitions/advection_value"}

  numeric:
    type: [object, "null"]
    additionalProperties: false
    properties:
      solver: {$ref: "#/definitions/string_or_null"}
      gwce_coefficients:
        type: [array, "null"]
        items: {type: number}
        minItems: 3
        maxItems: 3
      linear_solver:
        type: [object, "null"]
        additionalProperties: false
        properties:
          type: {$ref: "#/definitions/string_or_null"}
          tolerance: {$ref: "#/definitions/positive_number"}
          max_iterations: {$ref: "#/definitions/positive_number"}
          verbosity: {$ref: "#/definitions/non_negative_number"}
          convergence_scaling: {$ref: "#/definitions/string_or_null"}

  forcing:
    type: [object, "null"]
    additionalProperties: false
    properties:
      tide:
        type: [object, "null"]
        additionalProperties: false
        properties:
          potential:
            type: [object, "null"]
            additionalProperties: false
            properties:
              enabled: {$ref: "#/definitions/boolean_or_null"}
              type: {$ref: "#/definitions/string_or_null"}
              constituents:
                type: [array, "null"]
                items: {$ref: "#/definitions/tidal_constituent"}
              astronomical:
                type: [object, "null"]
                additionalProperties: false
                properties:
                  order: {$ref: "#/definitions/number_or_null"}
                  include_nutation: {$ref: "#/definitions/boolean_or_null"}
                  k2: {$ref: "#/definitions/number_or_null"}
                  h2: {$ref: "#/definitions/number_or_null"}
          boundary:
            type: [object, "null"]
            additionalProperties: false
            properties:
              enabled: {$ref: "#/definitions/boolean_or_null"}
              constituents:
                type: [array, "null"]
                items: {$ref: "#/definitions/tide_boundary_constituent"}
          sal:
            type: [object, "null"]
            additionalProperties: false
            properties:
              enabled: {$ref: "#/definitions/boolean_or_null"}
              parameter_source: {$ref: "#/definitions/string_or_null"}
              constituents:
                type: [array, "null"]
                items: {$ref: "#/definitions/sal_constituent"}
      meteorological:
        type: [object, "null"]
        additionalProperties: false
        properties:
          enabled: {$ref: "#/definitions/boolean_or_null"}
          format: {$ref: "#/definitions/string_or_null"}
          filename: {$ref: "#/definitions/string_or_null"}
          filenames: {$ref: "#/definitions/meteo_filenames"}
          variables: {$ref: "#/definitions/meteo_variables_selector"}
          product: {$ref: "#/definitions/string_or_null"}
          domains:
            type: [array, "null"]
            items: {$ref: "#/definitions/meteo_source_entry"}
          drag_law: {$ref: "#/definitions/string_or_null"}
          ice: {$ref: "#/definitions/boolean_or_null"}
          ice_drag_law: {$ref: "#/definitions/string_or_null"}
          wind_scale: {$ref: "#/definitions/number_or_null"}
          pressure_scale: {$ref: "#/definitions/number_or_null"}
          ice_scale: {$ref: "#/definitions/number_or_null"}
          wind_reduction: {$ref: "#/definitions/boolean_or_null"}
          vortex: {$ref: "#/definitions/vortex"}
          ramp: {$ref: "#/definitions/ramp"}
      ramp: {$ref: "#/definitions/ramp"}
      flow_boundary:
        type: [object, "null"]
        additionalProperties: false
        properties:
          enabled: {$ref: "#/definitions/boolean_or_null"}
          ramp: {$ref: "#/definitions/ramp"}
          segments:
            type: [array, "null"]
            items:
              type: object
              additionalProperties: false
              properties:
                segment_index: {$ref: "#/definitions/number_or_null"}
                time_series_file: {$ref: "#/definitions/string_or_null"}
      # Baroclinic nodal forcing (BPG + free-surface correction, internal-tide
      # drag, dispersion-as-friction). Keep in sync with parse_baroclinic in
      # ForcingParsers.cpp; see docs/sphinx/user_guide/baroclinic_coupling.rst.
      baroclinic:
        type: [object, "null"]
        additionalProperties: false
        properties:
          enabled: {$ref: "#/definitions/boolean_or_null"}
          filename: {$ref: "#/definitions/string_or_null"}
          ramp: {$ref: "#/definitions/ramp"}
          internal_tide:
            type: [object, "null"]
            additionalProperties: false
            properties:
              enabled: {$ref: "#/definitions/boolean_or_null"}
          dispersion:
            type: [object, "null"]
            additionalProperties: false
            properties:
              enabled: {$ref: "#/definitions/boolean_or_null"}
              mode: {$ref: "#/definitions/string_or_null"}
              deep_water_depth: {$ref: "#/definitions/positive_number"}
              mld_coefficients:
                type: [object, "null"]
                additionalProperties: false
                properties:
                  a: {$ref: "#/definitions/number_or_null"}
                  b: {$ref: "#/definitions/number_or_null"}
      # Data assimilation water level correction (a nodal pseudo-pressure
      # field). Keep in sync with parse_water_level_correction in
      # ForcingParsers.cpp.
      water_level_correction:
        type: [object, "null"]
        additionalProperties: false
        properties:
          enabled: {$ref: "#/definitions/boolean_or_null"}
          filename: {$ref: "#/definitions/string_or_null"}
          format: {$ref: "#/definitions/string_or_null"}
          variable: {$ref: "#/definitions/string_or_null"}
          method: {$ref: "#/definitions/string_or_null"}
          multiplier: {$ref: "#/definitions/number_or_null"}
          ramp: {$ref: "#/definitions/ramp"}

  output:
    type: [object, "null"]
    additionalProperties: false
    properties:
      compression:
        # Shared by every output file.
        type: [object, "null"]
        additionalProperties: false
        properties:
          level: {type: [integer, "null"], minimum: 0, maximum: 9}
          shuffle: {$ref: "#/definitions/boolean_or_null"}
      global:
        type: [object, "null"]
        additionalProperties: false
        properties:
          step_interval: {$ref: "#/definitions/positive_duration"}
          start_time: {type: string}
          end_time: {type: string}
          filename: {$ref: "#/definitions/string_or_null"}
          mode: {$ref: "#/definitions/string_or_null"}
          variables:
            # Closed key set: a typo in a variable toggle (e.g. "velocty")
            # must be a schema error, not a silently ignored key. Keep in sync
            # with Core::OUTPUT_VARIABLES (OutputConfig.hpp).
            type: [object, "null"]
            additionalProperties: false
            properties:
              zeta: {$ref: "#/definitions/boolean_or_null"}
              velocity: {$ref: "#/definitions/boolean_or_null"}
              pressure: {$ref: "#/definitions/boolean_or_null"}
              wind: {$ref: "#/definitions/boolean_or_null"}
              tide_potential: {$ref: "#/definitions/boolean_or_null"}
              ice_concentration: {$ref: "#/definitions/boolean_or_null"}
              bpg_effective: {$ref: "#/definitions/boolean_or_null"}
              water_level_correction: {$ref: "#/definitions/boolean_or_null"}
      stations:
        # partially_wet is an EnumTable string (membership is a parser
        # concern). locations may be an `include:` of a file whose root is the
        # list; it is spliced in before this schema runs. The variable set is
        # output.global's, Core::OUTPUT_VARIABLES; a test checks each name in both.
        type: [object, "null"]
        additionalProperties: false
        required: [locations]
        properties:
          filename: {$ref: "#/definitions/string_or_null"}
          step_interval: {$ref: "#/definitions/positive_duration"}
          start_time: {type: string}
          end_time: {type: string}
          partially_wet: {$ref: "#/definitions/string_or_null"}
          variables:
            type: [object, "null"]
            additionalProperties: false
            properties:
              zeta: {$ref: "#/definitions/boolean_or_null"}
              velocity: {$ref: "#/definitions/boolean_or_null"}
              pressure: {$ref: "#/definitions/boolean_or_null"}
              wind: {$ref: "#/definitions/boolean_or_null"}
              tide_potential: {$ref: "#/definitions/boolean_or_null"}
              ice_concentration: {$ref: "#/definitions/boolean_or_null"}
              bpg_effective: {$ref: "#/definitions/boolean_or_null"}
              water_level_correction: {$ref: "#/definitions/boolean_or_null"}
          locations:
            type: array
            minItems: 1
            items:
              type: object
              additionalProperties: false
              required: [name, location]
              properties:
                name: {type: string, minLength: 1}
                location:
                  type: array
                  items: {type: number}
                  minItems: 2
                  maxItems: 2

  diagnostics:
    type: [object, "null"]
    additionalProperties: false
    properties:
      screen_interval: {$ref: "#/definitions/positive_duration"}
      log_file: {$ref: "#/definitions/string_or_null"}

  checkpoint:
    type: [object, "null"]
    additionalProperties: false
    properties:
      enabled: {$ref: "#/definitions/boolean_or_null"}
      write_interval: {$ref: "#/definitions/non_negative_duration"}
      restart_file: {$ref: "#/definitions/string_or_null"}
      file_prefix: {$ref: "#/definitions/string_or_null"}

definitions:
  string_or_null: {type: [string, "null"]}
  number_or_null: {type: [number, "null"]}
  boolean_or_null: {type: [boolean, "null"]}
  positive_number: {type: [number, "null"], exclusiveMinimum: 0}
  non_negative_number: {type: [number, "null"], minimum: 0}

  # A numeric value or the literal "mesh" (take the field from mesh nodal
  # attributes). SchemaValidation.cpp recognizes this exact anyOf shape and
  # reports its failures with a domain-specific message.
  mesh_or_numeric:
    anyOf:
      - {type: number}
      - {const: mesh}
      - {type: "null"}

  # mesh.partition.wet_balance: a level in metres above the datum, or one of
  # the two named rules.
  wet_balance:
    anyOf:
      - {type: number}
      - {const: initial_condition}
      - {const: none}
      - {type: "null"}

  # A boolean, or a literal naming where the per-node 0/1 mask comes from:
  # "mesh" (the mesh file's nodal attribute) or "off_at_open_boundaries"
  # (synthesized from the open and flow boundary strings). Under weak typing
  # `type: boolean` matches the scalars "true" and "false" only, so
  # yes/on/1 are schema errors rather than silently accepted synonyms.
  advection_value:
    anyOf:
      - {type: boolean}
      - {const: mesh}
      - {const: off_at_open_boundaries}
      - {type: "null"}

  # A positive number of seconds or a duration string such as "4s", "1m",
  # "2d". Under weak typing the string branch accepts any scalar, so numeric
  # positivity here is structural documentation; the parser and the semantic
  # validator enforce the real bounds.
  positive_duration:
    anyOf:
      - {type: number, exclusiveMinimum: 0}
      - {type: string}
      - {type: "null"}

  # As positive_duration, but zero is legal (e.g. "checkpointing disabled").
  non_negative_duration:
    anyOf:
      - {type: number, minimum: 0}
      - {type: string}
      - {type: "null"}

  coordinate_pair:
    type: [array, "null"]
    items: {type: number}
    minItems: 2
    maxItems: 2

  # Every ramp block has this shape (Core::RampConfig; parse_ramp in
  # ForcingParsers.cpp). form values validate via RAMP_FORMS.
  ramp:
    type: [object, "null"]
    additionalProperties: false
    properties:
      enabled: {$ref: "#/definitions/boolean_or_null"}
      duration: {$ref: "#/definitions/non_negative_duration"}
      form: {$ref: "#/definitions/string_or_null"}
      delay: {$ref: "#/definitions/non_negative_duration"}

  # A source's activation fade: the ramp block without delay, since the
  # fade is clocked by the source's own window edges.
  activation_ramp:
    type: [object, "null"]
    x-type-hint: "ramp is a block, e.g. ramp: {duration: 6h}"
    additionalProperties: false
    properties:
      enabled: {$ref: "#/definitions/boolean_or_null"}
      duration: {$ref: "#/definitions/positive_duration"}
      form: {$ref: "#/definitions/string_or_null"}

  # Constituents may carry varying extra fields, so the key set stays open.
  tidal_constituent:
    type: object
    additionalProperties: true
    properties:
      name: {$ref: "#/definitions/string_or_null"}
      frequency: {$ref: "#/definitions/number_or_null"}
      amplitude: {$ref: "#/definitions/number_or_null"}
      etrf: {$ref: "#/definitions/number_or_null"}
      equilibrium_arg: {$ref: "#/definitions/number_or_null"}
      nodal_factor: {$ref: "#/definitions/number_or_null"}

  sal_constituent:
    type: object
    additionalProperties: false
    properties:
      name: {$ref: "#/definitions/string_or_null"}
      frequency: {$ref: "#/definitions/number_or_null"}
      nodal_factor: {$ref: "#/definitions/number_or_null"}
      equilibrium_arg: {$ref: "#/definitions/number_or_null"}

  tide_boundary_constituent:
    type: object
    additionalProperties: false
    properties:
      name: {$ref: "#/definitions/string_or_null"}
      frequency: {$ref: "#/definitions/number_or_null"}
      nodal_factor: {$ref: "#/definitions/number_or_null"}
      equilibrium_arg: {$ref: "#/definitions/number_or_null"}
      boundary_values:
        type: [array, "null"]
        items:
          type: object
          additionalProperties: false
          properties:
            amplitude: {$ref: "#/definitions/number_or_null"}
            phase: {$ref: "#/definitions/number_or_null"}

  # The `filenames` key's shape, shared verbatim by the flat top-level form
  # and each `domains:` entry so the two cannot drift apart: a list of file
  # names, or a list of {pressure, wind} OWI pairs.
  meteo_filenames:
    anyOf:
      - type: array
        items: {type: string}
      - type: array
        items: {$ref: "#/definitions/owi_file_pair"}
      - {type: "null"}

  owi_file_pair:
    type: object
    additionalProperties: false
    required: [pressure, wind]
    properties:
      pressure: {type: string}
      wind: {type: string}

  grib_selector:
    type: [object, "null"]
    additionalProperties: false
    properties:
      discipline: {$ref: "#/definitions/number_or_null"}
      category: {$ref: "#/definitions/number_or_null"}
      number: {$ref: "#/definitions/number_or_null"}
      surface_type: {$ref: "#/definitions/number_or_null"}
      level: {$ref: "#/definitions/number_or_null"}

  # The `variables` key's shape (CF variable-name strings OR GRIB numeric
  # identity selectors), shared verbatim by the flat top-level form and each
  # `domains:` entry so the two cannot drift apart. `ice` is optional in both
  # branches (CF: a variable-name string; GRIB: a numeric selector); a
  # source with no ice omits it. Parser-level rules (rejecting a
  # CF-shaped `ice` string on a GRIB entry, rejecting `ice_scale` on non-CF
  # formats) are defense in depth on top of this shape; see ForcingParsers.cpp.
  meteo_variables_selector:
    anyOf:
      - type: object
        additionalProperties: false
        properties:
          pressure: {$ref: "#/definitions/string_or_null"}
          wind_u: {$ref: "#/definitions/string_or_null"}
          wind_v: {$ref: "#/definitions/string_or_null"}
          time: {$ref: "#/definitions/string_or_null"}
          lon: {$ref: "#/definitions/string_or_null"}
          lat: {$ref: "#/definitions/string_or_null"}
          ice: {$ref: "#/definitions/string_or_null"}
      - type: object
        additionalProperties: false
        properties:
          pressure: {$ref: "#/definitions/grib_selector"}
          wind_u: {$ref: "#/definitions/grib_selector"}
          wind_v: {$ref: "#/definitions/grib_selector"}
          ice: {$ref: "#/definitions/grib_selector"}
      - {type: "null"}

  # One `domains:` list entry: a general, self-contained meteorological
  # source. The structural key set mirrors the flat top-level form (format
  # added, since each entry picks its own); cross-key rules (format required,
  # self-contained vs. the flat form, per-format policies) are a parser
  # concern, not a schema one.
  meteo_source_entry:
    type: object
    additionalProperties: false
    properties:
      format: {$ref: "#/definitions/string_or_null"}
      filename: {$ref: "#/definitions/string_or_null"}
      filenames: {$ref: "#/definitions/meteo_filenames"}
      variables: {$ref: "#/definitions/meteo_variables_selector"}
      product: {$ref: "#/definitions/string_or_null"}
      wind_scale: {$ref: "#/definitions/number_or_null"}
      pressure_scale: {$ref: "#/definitions/number_or_null"}
      ice_scale: {$ref: "#/definitions/number_or_null"}
      wind_reduction: {$ref: "#/definitions/boolean_or_null"}
      ramp: {$ref: "#/definitions/activation_ramp"}

  # model, pressure_estimate, and env_wind VALUES validate via the parser's
  # EnumTables (the schema declares strings; enum membership is a parser
  # concern). tracks is a non-empty list. blend_factor is lower-bounded at
  # 1.0 (no meaningful upper bound, matching the parser and VortexOverlay
  # guards); boundary_layer_factor carries the [0.75, 0.9] range the parser
  # also enforces; the two must never disagree. Keep the
  # key set in sync with parse_vortex in ForcingParsers.cpp.
  vortex:
    type: [object, "null"]
    additionalProperties: false
    properties:
      model: {$ref: "#/definitions/string_or_null"}
      tracks:
        type: [array, "null"]
        items: {type: string}
        minItems: 1
      blend_factor: {type: [number, "null"], minimum: 1.0}
      taper_a: {type: [number, "null"], exclusiveMinimum: 0.0}
      wind_reduction: {$ref: "#/definitions/boolean_or_null"}
      pressure_estimate: {$ref: "#/definitions/string_or_null"}
      pressure_anchor_hours: {type: [number, "null"], minimum: 0}
      env_wind: {$ref: "#/definitions/string_or_null"}
      boundary_layer_factor: {type: [number, "null"], minimum: 0.75, maximum: 0.9}
      ramp: {$ref: "#/definitions/activation_ramp"}