Skip to content

Architecture Boundaries

TIDE is maintained as one Maxwell-focused package. The package is split into four dependency layers:

public API (tide.__init__, tide.maxwell)
|
v
core contracts (plan, types, capability decisions)
|
v
solver implementations (reference Python and native adapters)
|
v
C/CUDA kernels and ABI declarations

The workflow and optim packages are consumers of the public solver API; the solver must not import either package. core contains only cross-cutting execution contracts and must not import a solver, workflow, or optimizer.

The supported surface is the API exported by tide.__all__ and tide.maxwell.__all__. Files under tide.maxwell that end in _python, _cuda, or _autograd are implementation details. A change to them is safe to review as an internal change only when the public behavior and numerical parity tests remain unchanged.

Research prototypes, generated outputs, large datasets, and profiling scripts stay outside the supported package and documentation surfaces. A prototype is promoted only when it has a stable API, a reference path, capability coverage, tests, and user documentation.

Public operators compile each request into SimulationPlan, pass it through tide.maxwell.dispatch.ExecutionPolicy, ask the central capability matrix for a BackendDecision, and then call one execution adapter. Forward, JVP, VJP, and second-VJP paths in TM2D and EM3D use this transition. New options must be represented in the plan and matrix before reaching a kernel. Unsupported combinations either select the declared reference fallback or raise a descriptive error.

The native boundary is ABI v5 and is declared once in tide/csrc/native/native_abi.h. Python binds fixed, unmangled C entrypoints for plan creation and destruction, workspace queries, forward execution, and VJP execution. It does not discover per-kernel symbols or maintain a parallel ctypes signature registry.

Each adapter builds an immutable NativeExecutionPlan for one dimension, operation, scalar type, device, stencil, shape, and time interval. Forward, JVP, VJP, second-VJP, material preprocessing, Born-material preprocessing, source preprocessing, and source VJP are operation values on this same plan contract. Preprocessing therefore uses the same fixed entrypoints rather than a separate family of exported function signatures.

Tensor addresses cross the ABI in versioned, role-indexed descriptors:

  • TideNativeBufferSet contains operation inputs and outputs;
  • TideNativeStorage contains device-resident primal or tangent snapshots;
  • TideNativeWorkspace contains adapter-owned scratch allocations; and
  • TideNativeWorkspaceRequirements lets a plan declare each required scratch slot and alignment before execution.

Every descriptor carries its ABI version and structure size. Role enums in native_abi.h and tide.native_execution must change together; positional tuples and unversioned pointer lists are not valid extension points.

CUDA solver configuration is immutable for an invocation. Each launch receives its dimensions, spacing, PML bounds, batching flags, and storage layout in a by-value configuration object. Native code must not place request-specific values in mutable device constant symbols. This keeps simultaneous executions with different plans isolated across CUDA streams.

The following import directions are enforced by tests:

  • core may depend on the standard library and PyTorch, but not on solvers, workflow, optimizers, or the native ABI loader;
  • maxwell may depend on core and shared numerical utilities, but not on workflow or optim;
  • optim is self-contained and must not depend on maxwell, workflow, or core;
  • workflow may consume public solver behavior, but solver code cannot depend on workflow helpers.
flowchart TD
    A[Domain objects] --> B[MaxwellTM or Maxwell3D]
    B --> C[SimulationPlan]
    C --> D[BackendDecision]
    D --> E[Reference or native adapter]
    E --> F[Kernel execution]
    F --> G[Named result objects]
    G --> H[workflow and optim]

The public operator owns model-independent configuration. EMModel remains an input. Linearization fixes one background model and owns the lifetime of primal snapshots used by JVP, VJP, and second-VJP actions.

Native and reference kernels may return different internal tuples, but public callers receive ForwardResult, TangentResult, dimension-specific state, and EMGradient. Adapter code is responsible for converting internal ordering into these named values.

Workflow code must not inspect tuple positions or import a kernel adapter. This keeps shot batching, objectives, and optimizers independent of backend layout.

A native optimization is complete only when it matches the reference path for the public quantity it implements:

  • Receiver traces and final state for forward.
  • Tangent receiver traces for JVP.
  • Material gradients for VJP.
  • Nonlinear material products for second VJP.

Tests should also cover rejected cells, fallback decisions, storage modes, and resource cleanup. Compilation success alone is not evidence of parity.

Operators do not own trainable parameters. Linearized sessions may own cached primal graphs and device-resident snapshots used by JVP, VJP, and second-VJP actions. Native plans own only immutable compiled configuration and their native handles. Adapters own the tensors referenced by buffer, storage, and workspace descriptors for the full duration of an invocation.

Snapshot storage is device-only. Scratch storage is role-indexed, declared by the plan, and supplied by the adapter; it is not hidden process-global state. Workflow helpers own only indexing and composition. Optimizers own only algorithm state.

This ownership split prevents hidden model mutation, makes resource lifetime visible at the adapter boundary, and permits independent native plans to run concurrently.