Note: This AGENTS.md file is adapted from Oceananigans.jl. See relevant copyright notices therein.
Terrarium.jl is a Julia package for fast, friendly, flexible, and process-based land and terrestrial ecosystem modeling on CPUs and GPUs. It solves the coupled heat and Richards equations in 1D for the soil along with common 0D parameterizations of vegetation and surface hydrology processes. Terrarium is designed to be modular in the sense that (almost) all components should be exchangeable with alternative implementations. Terrarium is also intended to be a fully differentiable land model with continuous-time dynamics. All process implementations must be defined in terms of well-formed ordinary or partial differential equations. No instantaneous or discrete-time dynamics are allowed except in very special cases where they must be clearly documented and justified.
- Julia 1.10+ | CPU and GPU (CUDA)
- Key packages: KernelAbstractions.jl, CUDA.jl, Enzyme.jl
- Style: ExplicitImports.jl for source code;
using Terrariumfor examples/tests
- Use
@kernel/@index(KernelAbstractions.jl) to define device-agnostic kernels - Functions marked with
@kernelare called kernels which then invoke kernel functions with call patterncompute_something(i, j, k, grid, fields, process::ProcessType, args...)orcompute_something!(out, i, j, k, grid, fields, proces::ProcessType)- Kernel functions defined for 2D kernels instead have
i, jinstead ofi, j, k
- Kernel functions defined for 2D kernels instead have
- Kernels and their subsequent call graph must be fully type-stable and allocation-free
- Use
ifelse— never short-circuitingif/elsein kernels - No error messages, no
AbstractModels, and nostateinside kernels - Always extract relevant input/output
Fields withget_fieldsand related methods - Favor explicit enumeration of process types when invoking kernels rather than passing
AbstractCoupledProcessestypes - Mark functions called inside kernels with
@inlineor@propagate_inboundswhen including indices - Never loop over grid points outside kernels — use
launch!
Terrarium targets full differentiability for inverse modeling and sensitivity analysis. Compatibility with Enzyme.jl is a top priority and must be continuously tested.
- Ensure type stability: All code in kernels and within state-mutating methods (e.g.
initialize!,compute_tendencies!,compute_auxiliary!) must be fully type stable. - Minimize allocations: Allocations should be avoided wherever possible. Prefer mutation of output
Fields. Never mutate input or prognosticFields outside ofupdate_inputs!or timesteppertimestep!respectively. - No global state: Initialize all parameters explicitly; never rely on global variables or implicit state
- Test differentiability: Use Enzyme to test that critical functions compute valid adjoints; include in test suite. See existing tests in
test/differentiabilityfor reference. - Document AD limitations: If a function cannot be differentiated, mark it clearly with comments and docstrings
- All structs must be concretely typed
- Minimize allocation; favor inline computation
- Never hardcode Float64: no literal
0.0or1.0in kernels or constructors. Usezero(grid),one(grid),NF(x)wherexis a number,convert(FT, 1//2), or rational literals
- Type annotations are used to dispatch to relevant types, restrict method signatures, and enable compiler optimizations
- Type annotations express intent and document assumptions
- Annotate function arguments and struct fields to be as specific as possible but not more specific than necessary
- Avoid use of overly broad types like
Anyunless absolutely necessary; instead, use Union types or create a common abstract type. Use ofAnyis permitted for generic containers such asstateandgrid. - Use
whereclauses to express type constraints that improve clarity and dispatch precision
- Source code: explicit imports (checked by tests)
- Examples/docs: rely on
using Terrarium; never explicitly import exported names
- Use DocStringExtensions.jl with
$TYPEDSIGNATURES - ALWAYS
jldoctestblocks, NEVER plainjuliablocks — doctests are tested; plain blocks rot - Include
# outputwith verifiable output; prefershowmethods over boolean comparisons - Use unicode for math (
Δt,η,ρ), not LaTeX — LaTeX doesn't render in the REPL
- Doc pages for processes and models should always consist of the following sections:
- Overview: General overview of of the physical process, what the main inputs and output variables typically are, and general equations relevant for understanding the implementations. This section should not contain implementation-specific details.
- Implementations: There should be sections for each implementation of the abstract process type, describing the general theory and any relevant implementation details. These sections should also each include a non-canonical docstring of the concrete process type.
- For each concrete process implementation, provide docstrings for the implementation-specific dispatches of
initialize!,compute_auxiliary!, andcompute_tendencies!corresponding to each coupling interface.
- For each concrete process implementation, provide docstrings for the implementation-specific dispatches of
- Methods: Enumeration of process-specific methods.
- Kernel functions: Enumeration of kernel functions of the form
compute_something(i, j, k, grid, fields, ...)orcompute_something!(out, i, j, k, grid, fields, ...); do NOT include kernels (i.e. functions annotated with@kernel)
- All functions referenced in doc pages should be marked with
canonical = falsesince the canonical versions of the docstrings are defined in a separate@autodocsblock in the index - All types and functions referenced in the doc pages must have docstrings otherwise the doc build will fail. Ensure docstrings are defined and add them if they are missing.
- Doc pages should always be prefaced with appropriate
@metaand@setupblocks - If a model or process is not fully implemented, an appropriate warning should be displayed on the doc page
- Do not use brackets for expressing units as this conflicts with Markdown link syntax; use parentheses instead
- All code examples should be given as
@example nameblocks, replacingnamewith an appropriate identifier for the page, which are executed by Documenter.jl.
gridis positional:SoilModel(grid; initializer)- Omit semicolon when there are no keyword arguments:
SoilModel(grid)
- Files: snake_case matching the type they define —
soil_hydrology.jl - Types/Constructors: PascalCase only for true constructors —
SoilHydrology - Functions: snake_case —
compute_tendencies!unless commonly combined in English, e.g.timestep!. This is not a hard rule, exceptions are permitted. - Kernels: should always be suffixed with
_kernel!—compute_tendencies_kernel! - Kernel functions: should always be prefixed with
compute_; mutating variants should use the standard bang!convention. - Variables: Prefer English long name or readable unicode math notation — do not use abbreviations that may introduce ambiguity, e.g.
condcould be either "condition" or "conductivity"; be as specific as possible.
All dynamical processes must be grounded in physics:
- Equation reference: Every process implementation should cite the governing equations in code comments or docstrings
- Continuous time: Discrete-time updates (e.g., "update this once per day") are prohibited
- Conservation where applicable: If a process conserves a quantity (mass, energy), verify conservation in tests
- Nondimensionalization: Consider whether nondimensionalization would improve solver stability; document choices if used
src/
├── Terrarium.jl # Main module, exports
├── abstract_model.jl # CPU/GPU architecture abstractions
├── diagnostics/ # Diagnostic and debugging utilities
├── grids/ # Grid types and discretizations
├── input_output/ # Types and functions for managing inputs and outputs
├── models/ # Model implementations
├── processes/ # Process implementations
├── timesteppers/ # Time stepping schemes, integrators, and integration with Oceananigans `AbstractModel`
├── utils/ # Miscellaneous utilities
- Type instability in kernels ruins GPU performance
- Missing imports: tests will catch this — add to
usingstatements - Plain
juliablocks in docstrings: always usejldoctest - Subtle bugs from missing method imports, especially in extensions
- Expecting unexported names: consider exporting them rather than changing user scripts
- Extending
getpropertyto fix undefined property bugs: fix on the caller side instead - "Type is not callable" errors: variable name shadows a function — rename or qualify
- Quick fixes that break correctness: if a test fails after a change, revisit the original edit
- Commented-out code: delete it. Git is the journal — don't leave commented code, debugging artifacts, or stale copy-paste remnants
- 2D indexing on fields: always use 3D indexing (
field[i, j, k]). 2D indexing works by coincidence on some fields but is unsupported and will break - Hardcoded Float64: never use
0.0,1.0in kernels or constructors; usezero(grid)etc. - Scope creep in PRs: keep changes focused on a single concern. Unrelated cleanup goes in a separate PR
- Modifying Project.toml dependencies: never add, remove, or change
[deps]or[weakdeps]in the rootProject.tomlunless the task absolutely requires it. Dependency changes have wide-reaching consequences — they affect CI, load time, and downstream compatibility. Only touch[compat]bounds when explicitly asked. - Mutable function closures in kernels: function closures that capture mutable state will not differentiate correctly — use explicit parameters instead
- Non-local dependencies in process equations: process functions must not depend on global state; pass all dependencies as arguments for traceability and differentiability
Follow ColPrac. Feature branches, descriptive commits, update tests and docs with code changes, check CI before merging.
- Dispatch over conditionals: use Julia's type system and multiple dispatch instead of
if/elsebranching. Backend-specific code goes inext/extensions, notifbranches insrc/ - Use
on_architecturefor data transfers — never manualArray()/CuArray()calls - Defaults serve the common case: avoid
nothingdefaults when a concrete default (likeCPU()) covers 80% of usage. Minimize boilerplate for the typical user. - Keyword argument names must be consistent across related types and constructors
- Always use explicit
returnin functions longer than one expression - One operation per line as default; break long expressions across lines
- Prioritize type stability, GPU compatibility, and differentiability
- Follow established patterns in existing code
- Add tests for new functionality; update exports when adding public API
- Reference physics equations in comments when implementing dynamics
- Do not make unsolicited changes; focus on specific tasks
- When extending Enzyme.jl compatibility, verify adjoints with
Enzyme.autodiff - Ensure type annotations are restrictive enough to guide dispatch and minimize misuse
For detailed guidance on specific workflows:
test/runtests.jl— test organization and running conventions