Maxwell Operators
MaxwellTM and Maxwell3D represent fixed electromagnetic experiments. They own discretization, acquisition, source definition, execution policy, storage policy, and gradient sampling. The material model is supplied at evaluation time.
Constructors
Section titled “Constructors”MaxwellTM( discretization, experiment, execution=ExecutionOptions(), storage=StorageOptions(), model_gradient_sampling_interval=1,)
Maxwell3D( discretization, experiment, execution=ExecutionOptions(), storage=StorageOptions(), model_gradient_sampling_interval=1,)MaxwellTM expects 2D [ny, nx] materials or [batch, ny, nx] batched materials. Maxwell3D expects [nz, ny, nx] or [batch, nz, ny, nx].
Forward
Section titled “Forward”result = operator.forward(model)# Equivalent:result = operator(model)The nonlinear map is
result.receiver_data is [nt, shots, receivers] for a shared model and [nt, batch, shots, receivers] for batched models. result.final_state is TMState or EM3DState with named field and CPML state tensors.
model = tide.EMModel(epsilon, sigma, mu)result = operator(model)print(result.receiver_data.shape)print(type(result.final_state).__name__)A forward call remains differentiable through standard PyTorch autograd when model tensors require gradients.
Linearize
Section titled “Linearize”linearized = operator.linearize( model, storage=None, targets=("epsilon", "sigma"),)The session fixes a background model and selected derivative targets. storage overrides the operator’s default for the session. Use the object as a context manager because it may retain forward snapshots, host allocations, or disk files.
with operator.linearize(model, targets=("epsilon",)) as linearized: primal = linearized.primalprimal is evaluated lazily and cached for the lifetime of the open session.
tangent = linearized.jvp(direction)direction is an EMDirection with one or more material perturbations. The result applies
and returns TangentResult.receiver_data plus a final tangent state. Missing direction fields mean zero perturbation. Each supplied tensor must match the corresponding background material shape, dtype, and device.
For MaxwellTM and Maxwell3D, model edge values are part of the parameter space. Materials and their epsilon/sigma directions are replicate-extended through both the PML and finite-difference halo. Changing an edge value therefore changes the corresponding PML material; the VJP accumulates those exterior sensitivities back onto that edge. To keep boundary material fixed during inversion, constrain those model values and set their direction entries to zero.
direction = tide.EMDirection( epsilon=delta_epsilon, sigma=delta_sigma,)with operator.linearize(model) as linearized: born_receiver = linearized.jvp(direction).receiver_dataThis operation is useful for Born modeling, linearized imaging, and the inner action of a Gauss-Newton Hessian product.
gradient = linearized.vjp(receiver_cotangent)The receiver cotangent must have exactly the same shape as linearized.primal.receiver_data. The method applies
and returns EMGradient. A field is None when it was not selected as a target or no derivative is available for it.
with operator.linearize(model, targets=("epsilon", "sigma")) as linearized: residual = linearized.primal.receiver_data - observed gradient = linearized.vjp(residual)
print(gradient.epsilon.shape)print(gradient.sigma.shape)For a least-squares loss defined as 0.5 * residual.square().sum(), this VJP is the data-term gradient.
Second VJP
Section titled “Second VJP”nonlinear_term = linearized.second_vjp(direction, receiver_cotangent)This applies
which is the nonlinear-physics contribution to a full Hessian-vector product. It is not the complete Hessian of an arbitrary receiver loss.
For , the full product contains
tide.workflow.ReceiverObjective.hvp composes these terms for a receiver objective and supports mode="gauss_newton" or mode="full".
For a leading direction axis (K, *model.shape), use
ReceiverObjective.hvp_batch(linearized, directions, mode="gauss_newton", block_size=4).
Eligible native CUDA TM configurations reuse one background history across
direction blocks and repeated calls; closing the linearization releases it.
See ReceiverObjective for loss and lifecycle semantics.
Standard autograd and explicit VJP
Section titled “Standard autograd and explicit VJP”These two gradients are mathematically aligned for a scalar objective:
predicted = operator(model).receiver_dataloss = 0.5 * (predicted - observed).square().sum()loss.backward()with operator.linearize(model) as linearized: residual = linearized.primal.receiver_data - observed gradient = linearized.vjp(residual)Use standard autograd for ordinary scalar-loss training loops. Use explicit derivative methods for linear operators, adjoint tests, controlled snapshot reuse, and Hessian-vector composition.
Lifetime and reuse
Section titled “Lifetime and reuse”A linearized session caches its primal state. Reuse it for several JVP or VJP actions at the same background model when supported. Do not mutate the background tensors in place while the session is open. Close the session before changing model values.
After close(), access to primal or derivative methods raises. Context-manager exit calls close() automatically.
Errors and capability selection
Section titled “Errors and capability selection”Operator construction validates dimensional agreement between acquisition and solver. Calls also validate material shapes, location bounds, dtype, and supported backend combinations. A native request can fail because of derivative target, storage mode, callbacks, dispersion, device, or missing ABI symbols.
BackendPreference.NATIVE requests native execution explicitly and raises when the requested operation is unavailable, even with FallbackPolicy.REFERENCE. BackendPreference.AUTO allows backend selection; pair it with FallbackPolicy.ERROR to reject a reference fallback.
Use FallbackPolicy.ERROR when backend identity matters. Use the capability matrix to inspect supported combinations and the limitations guide for current boundaries.