2626from typing import ClassVar , Generic , Type , TypeVar
2727
2828import equinox as eqx
29+ import jax .numpy as jnp
30+ import jax .tree_util as jtu
2931import lineax as lx
3032from jaxtyping import Array , Bool , Scalar
3133
3537 SearchState ,
3638 Y ,
3739)
38- from ._misc import sum_squares
40+ from ._misc import sum_squares , tree_dot
3941from ._solution import RESULTS
4042
4143
@@ -89,6 +91,9 @@ class EvalGrad(FunctionInfo, Generic[Y], strict=True):
8991 def as_min (self ):
9092 return self .f
9193
94+ def compute_grad_dot (self , y : Y ):
95+ return tree_dot (self .grad , y )
96+
9297
9398# NOT PUBLIC, despite lacking an underscore. This is so pyright gets the name right.
9499class EvalGradHessian (FunctionInfo , Generic [Y ], strict = True ):
@@ -104,6 +109,9 @@ class EvalGradHessian(FunctionInfo, Generic[Y], strict=True):
104109 def as_min (self ):
105110 return self .f
106111
112+ def compute_grad_dot (self , y : Y ):
113+ return tree_dot (self .grad , y )
114+
107115
108116# NOT PUBLIC, despite lacking an underscore. This is so pyright gets the name right.
109117class EvalGradHessianInv (FunctionInfo , Generic [Y ], strict = True ):
@@ -118,6 +126,9 @@ class EvalGradHessianInv(FunctionInfo, Generic[Y], strict=True):
118126 def as_min (self ):
119127 return self .f
120128
129+ def compute_grad_dot (self , y : Y ):
130+ return tree_dot (self .grad , y )
131+
121132
122133# NOT PUBLIC, despite lacking an underscore. This is so pyright gets the name right.
123134class Residual (FunctionInfo , Generic [Out ], strict = True ):
@@ -134,28 +145,43 @@ def as_min(self):
134145# NOT PUBLIC, despite lacking an underscore. This is so pyright gets the name right.
135146class ResidualJac (FunctionInfo , Generic [Y , Out ], strict = True ):
136147 """Records the Jacobian `d(fn)/dy` as a linear operator. Used for least squares
137- problems, for which `fn` returns residuals. Has `.residual` and `.jac` and `.grad`
138- attributes, where `residual = fn(y)`, `jac = d(fn)/dy` and
139- `grad = jac^T residual`.
140-
141- Takes just `residual` and `jac` as `__init__`-time arguments, from which `grad` is
142- computed.
148+ problems, for which `fn` returns residuals. Has `.residual` and `.jac` attributes,
149+ where `residual = fn(y)`, `jac = d(fn)/dy`.
143150 """
144151
145152 residual : Out
146153 jac : lx .AbstractLinearOperator
147- grad : Y
148-
149- def __init__ (self , residual : Out , jac : lx .AbstractLinearOperator ):
150- self .residual = residual
151- self .jac = jac
152- # The gradient is used ubiquitously, so compute it once here, so that it can be
153- # used without recomputation in both the descent and search.
154- self .grad = jac .transpose ().mv (residual )
155154
156155 def as_min (self ):
157156 return 0.5 * sum_squares (self .residual )
158157
158+ def compute_grad (self ):
159+ conj_residual = jtu .tree_map (jnp .conj , self .residual )
160+ return self .jac .transpose ().mv (conj_residual )
161+
162+ def compute_grad_dot (self , y : Y ):
163+ # If `self.jac` is a `lx.JacobianLinearOperator` (or a
164+ # `lx.FunctionLinearOperator` wrapping the result of `jax.linearize`), then
165+ # `min = 0.5 * residual^2`, so `grad = jac^T residual`, i.e. the gradient of
166+ # this. So that what we want to compute is `residual^T jac y`. Doing the
167+ # reduction in this order means we hit forward-mode rather than reverse-mode
168+ # autodiff.
169+ #
170+ # For the complex case: in this case then actually
171+ # `min = 0.5 * residual residual^bar`
172+ # which implies
173+ # `grad = jac^T residual^bar`
174+ # and thus that we want
175+ # `grad^T^bar y = residual^T jac^bar y = (jac y^bar)^T^bar residual`.
176+ # Notes:
177+ # (a) the `grad` derivation is not super obvious. Note that
178+ # `grad(z -> 0.5 z z^bar)` is `z^bar` in JAX (yes, twice the Wirtinger
179+ # derivative!) It uses a non-Wirtinger derivative for nonholomorphic functions:
180+ # https://jax.readthedocs.io/en/latest/notebooks/autodiff_cookbook.html#complex-numbers-and-differentiation
181+ # (b) our convention is that the first term of a dot product gets the conjugate:
182+ # https://github.com/patrick-kidger/diffrax/pull/454#issuecomment-2210296643
183+ return tree_dot (self .jac .mv (jtu .tree_map (jnp .conj , y )), self .residual )
184+
159185
160186Eval .__qualname__ = "FunctionInfo.Eval"
161187EvalGrad .__qualname__ = "FunctionInfo.EvalGrad"
0 commit comments