Skip to content

Box Case

The Box case (problem_id = 2) is the simplest configuration in NeuralPOM: a closed rectangular basin with uniform depth. It is designed for debugging, rapid testing, and verifying that the dynamical core preserves basic conservation properties.

Purpose

The box case is intended for:

  • Smoke testing: Verify that a newly built or modified core can complete initialization and at least a few time steps without crashing.
  • Conservation checks: In a closed basin with zero forcing, the model should conserve total mass, heat, and salt to within time-stepping truncation error.
  • Minimal working example: The simplest possible configuration for demonstrating the core API and workflow.
  • Gradient validation: A no-forcing, no-topography case is ideal for checking that gradients propagate correctly through the differentiable dynamical core.

Physical Configuration

ParameterValueDescription
Grid sizeim=90, jm=82Typical coarse grid resolution
Vertical levelskb=21Standard sigma-coordinate levels
DepthUniformFlat bottom, no topography
DomainClosed basinAll four lateral boundaries are walls
ForcingZero or simpleMinimal to no external forcing by default
StratificationUniform or prescribedSimple initial profiles

The domain is a flat-bottomed rectangular box enclosed by rigid lateral walls. There is no bathymetry variation, which eliminates sigma-coordinate pressure gradient errors and simplifies the dynamics to their most fundamental form.

Configuration

The box case is defined with these CaseSettings:

python
from neuralpom.cases.common import CaseSettings

CASE_SETTINGS = CaseSettings(
    name='box',
    problem_id=2,
    im=90,
    jm=82,
    description='Closed conservation-box benchmark.',
    overrides={},
)

In the catalog, the box is registered as:

python
CASE_SETTINGS_REGISTRY["box"] = CaseSettings(
    name="box",
    problem_id=2,
    im=129,       # Legacy grid size in catalog
    jm=129,
    description="Rectangular box benchmark retained for legacy compatibility.",
)

The per-variant files typically override to im=90, jm=82 for the standard training grid.

Simulation Parameters

ParameterValueDescription
iproblem2Case identifier
dte5.0 sExternal time step
isplit30Mode splitting ratio
dti150.0 sInternal time step
Typical duration1--10 daysShort runs for testing

These parameters are intentionally minimal. For conservation tests, you would set zero surface fluxes and verify that initial values of mass, heat, and salt are maintained over the integration.

Physical Behavior

Zero-Forcing Regime

In the absence of forcing, the box solution should exhibit:

  1. No net circulation -- Initial perturbations (if any) will be damped by viscosity.
  2. Mass conservation -- The volume-integrated free-surface elevation remains zero: η dxdy=0.
  3. Heat and salt conservation -- Domain-integrated temperature and salinity remain constant: ddtT dV=0, ddtS dV=0.
  4. Energy decay -- In the unforced case, total mechanical energy monotonically decreases due to viscous dissipation.

Simple Forcing Regime

A uniform zonal wind stress can be applied to drive a simple Ekman-driven circulation:

τx=const

This produces a surface Ekman layer with transport:

UE=τxρ0f

directed to the right of the wind (in the Northern Hemisphere). The resulting circulation is a simple anti-clockwise (cyclonic) or clockwise (anticyclonic) gyre depending on the sign of the wind stress curl.

Usage

Minimum Working Example

python
from neuralpom.cases.explicit.box import CASE_SETTINGS
from neuralpom.cores.explicit import ExplicitPOM2KCore

core = ExplicitPOM2KCore(
    im=CASE_SETTINGS.im,
    jm=CASE_SETTINGS.jm
)
core.iproblem = CASE_SETTINGS.problem_id  # problem_id = 2
core.run_initialization()

# Take a few time steps
core.time0 = core.time
core.iend = 100
core.run_time_loop()

# Verify state is finite
assert torch.isfinite(core.u).all()
assert torch.isfinite(core.v).all()
print("Box test passed: state is finite after 100 steps")

Via the Implicit Core

python
from neuralpom.cases.implicit.box import CASE_SETTINGS
from neuralpom.cores.implicit import ImplicitPOM2KCore

core = ImplicitPOM2KCore(
    im=CASE_SETTINGS.im,
    jm=CASE_SETTINGS.jm
)
core.iproblem = CASE_SETTINGS.problem_id
core.run_initialization()

core.time0 = core.time
core.iend = 500
core.run_time_loop()

Via the Catalog

python
from neuralpom.cases.catalog import get_case_settings
from neuralpom.core_factory import resolve_core_class

settings = get_case_settings("box")
core = resolve_core_class("explicit")(im=settings.im, jm=settings.jm)
core.iproblem = settings.problem_id
core.run_initialization()

Conservation Check

python
from neuralpom.cases.catalog import get_case_settings
from neuralpom.cores.explicit import ExplicitPOM2KCore

settings = get_case_settings("box")
core = ExplicitPOM2KCore(im=settings.im, jm=settings.jm)
core.iproblem = settings.problem_id
core.run_initialization()

# Record initial mass/heat/salt
area = core.art  # cell areas at T-points
dz = core.dz     # layer thicknesses at T-points
mask = core.fsm  # land-sea mask

vol = (area.unsqueeze(-1) * dz) * mask.unsqueeze(-1)
initial_heat = (core.t * vol).sum()
initial_salt = (core.s * vol).sum()

# Integrate
core.time0 = core.time
core.iend = 1000
core.run_time_loop()

final_heat = (core.t * vol).sum()
final_salt = (core.s * vol).sum()

print(f"Heat conservation error: {abs(final_heat - initial_heat) / abs(initial_heat):.2e}")
print(f"Salt conservation error: {abs(final_salt - initial_salt) / abs(initial_salt):.2e}")

Comparison with Other Cases

FeatureBoxDouble GyreSeamount
BathymetryFlatFlatGaussian seamount
ForcingNone / simpleWind stress curlBackground flow
CirculationMinimalFull gyre systemTaylor cap + wake
ComplexityMinimalHighModerate
Primary useDebugging, CITraining, scienceRegression

The box case is the go-to starting point for new development. If something works on the box but fails on the double gyre, the issue is likely in the forcing response or stratified dynamics rather than the core time-stepping or grid infrastructure.

Released under the MIT License.