Source code for jax_sn.variance_reduction.continuous_adjoint
r'''
Continuous adjoint of the multi-group operator.
The *continuous* adjoint :math:`\mathbf{A}^\dagger` is the discretisation of the adjoint transport
equation: it negates the streaming direction (:math:`\Omega \to -\Omega`) but keeps the moment/discrete
operators :math:`\mathbf{M}, \mathbf{D}` at :math:`+\Omega`, and gathers the *transposed* group scatter.
It is therefore **not** the algebraic transpose :math:`\mathbf{A}^T` (which reverses the whole operator
chain); see :class:`MultiGroupOperator <jax_sn.operators.multi_group_operator.MultiGroupOperator>`.
Because the harmonics ``wk_ylm``/``ylm`` are frozen at :class:`AngularData
<jax_sn.operators.operator_data.AngularData>` construction and only the transport sweep re-reads
``quadrature_set.angles``, negating the angles *after* that freeze flips only the streaming, leaving
:math:`\mathbf{M}, \mathbf{D}` at :math:`+\Omega`. Combined with (a) reversing the sweep ordering to match
the negated direction and (b) transposing the group axes of the scatter tensor (so the forward
``incoming_gather`` computes the *outgoing* sum), the ordinary forward
:class:`MultiGroupOperator <jax_sn.operators.multi_group_operator.MultiGroupOperator>` run on this
transformed data *is* :math:`\mathbf{A}^\dagger`. No new operator/strategy/solver is introduced.
The transformed data is deliberately kept private and consumed immediately inside
:func:`solve_continuous_adjoint`: it is a valid computational object but *internally inconsistent* (its
angles no longer match its frozen harmonics; its scatter tensor is transposed), so it must never escape
into general use.
'''
import equinox as eqx
import jax.numpy as jnp
import lineax as lx
from jax_sn.operators.multi_group_operator import MultiGroupOperator, ComposedMultiGroupChainStrategy
from jax_sn.operators.operator_data import OperatorData
from jax_sn.solvers.base_solvers import MultiGroupLowerTriangularBlockSolver
from jax_sn.solvers.right_hand_side import solve_multigroup_operator_rhs
def _continuous_adjoint_data(data: OperatorData) -> OperatorData:
r'''Transform ``data`` into its continuous-adjoint form: negate the streaming angles, reverse the
sweep ordering to match, and transpose the group axes of the scatter tensor. Private on purpose --
the result is internally inconsistent (angles vs. frozen harmonics) and must not leave this module.'''
angles = data.discretisation_data.angular.quadrature_set.angles
scattering = data.discretisation_data.spatial.domain.cross_sections_indexed.cross_section_data.scattering
# Negate streaming + transpose group scatter (sigma_{g'<-g} stored at [..., l, g', g]; swap g'/g).
data = eqx.tree_at(
lambda d: (d.discretisation_data.angular.quadrature_set.angles,
d.discretisation_data.spatial.domain.cross_sections_indexed.cross_section_data.scattering),
data,
(-angles, jnp.swapaxes(scattering, -1, -2)),
)
# Reverse the sweep ordering (partner of the angle negation; same concrete SweepData type).
return eqx.tree_at(
lambda d: d.non_differentiable_data.sweep_data,
data,
data.non_differentiable_data.sweep_data.reverse,
)
[docs]
@eqx.filter_jit
def solve_continuous_adjoint(operator: MultiGroupOperator, rhs, solver: lx.AbstractLinearSolver):
r'''
Solve :math:`\mathbf{A}^\dagger \psi^\dagger = \mathbf{C}^\dagger[b]` for the continuous adjoint of
``operator``, where the source is conditioned by the adjoint chain :math:`\mathbf{C}^\dagger =
\mathbf{M}\,(\mathbf{T}^\dagger)^{-1}\mathbf{H}\mathbf{D}` (built automatically because the chain
strategy runs on the adjoint-transformed data).
The adjoint operator is the ordinary forward :class:`MultiGroupOperator` run on
:func:`_continuous_adjoint_data`; it is built inside the jit so gradients w.r.t. cross sections /
geometry still flow through the adjoint solve.
Parameters
----------
operator : MultiGroupOperator
The forward multi-group operator whose continuous adjoint is solved.
rhs : RightHandSide
The (physical) adjoint source.
solver : lx.AbstractLinearSolver
An iterative solver (GMRES / BiCGStab). The direct block solver is rejected: for downscattering
the continuous adjoint is *upper*-triangular, which that solver does not handle.
Returns
-------
lx.LinearSolveOutput
'''
assert not isinstance(solver, MultiGroupLowerTriangularBlockSolver), (
"The continuous adjoint is upper-triangular for a downscattering problem; the lower-triangular "
"block solver is not applicable. Use an iterative solver (e.g. lineax GMRES or BiCGStab)."
)
adjoint_operator = eqx.tree_at(lambda o: o.data, operator, _continuous_adjoint_data(operator.data))
chain_strategy = ComposedMultiGroupChainStrategy.from_composed(operator.strategy)
return solve_multigroup_operator_rhs(adjoint_operator, rhs, solver, chain_strategy)