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
| Parameter | Value | Description |
|---|---|---|
| Grid size | im=90, jm=82 | Typical coarse grid resolution |
| Vertical levels | kb=21 | Standard sigma-coordinate levels |
| Depth | Uniform | Flat bottom, no topography |
| Domain | Closed basin | All four lateral boundaries are walls |
| Forcing | Zero or simple | Minimal to no external forcing by default |
| Stratification | Uniform or prescribed | Simple 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:
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:
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
| Parameter | Value | Description |
|---|---|---|
iproblem | 2 | Case identifier |
dte | 5.0 s | External time step |
isplit | 30 | Mode splitting ratio |
dti | 150.0 s | Internal time step |
| Typical duration | 1--10 days | Short 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:
- No net circulation -- Initial perturbations (if any) will be damped by viscosity.
- Mass conservation -- The volume-integrated free-surface elevation remains zero:
. - Heat and salt conservation -- Domain-integrated temperature and salinity remain constant:
, . - 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:
This produces a surface Ekman layer with transport:
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
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
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
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
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
| Feature | Box | Double Gyre | Seamount |
|---|---|---|---|
| Bathymetry | Flat | Flat | Gaussian seamount |
| Forcing | None / simple | Wind stress curl | Background flow |
| Circulation | Minimal | Full gyre system | Taylor cap + wake |
| Complexity | Minimal | High | Moderate |
| Primary use | Debugging, CI | Training, science | Regression |
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.
