@@ -149,7 +149,13 @@ def forward(self, x):
149149plt .tight_layout ()
150150
151151###############################################################################
152- # And finally we do the same with a batch of 3 training samples.
152+ # And we can do the same with a batch of 3 training samples. Note that under
153+ # the hood, this effectively calls the matrix-matrix version of the forward
154+ # and adjoint operator (i.e., `matmat` and `rmatmat`); for operators that do
155+ # not implement these methods directly, this is simply implemented by calling
156+ # the matrix-vector of the forward and adjoint operator (i.e., `matvec` and
157+ # `rmatvec`)multiple times, which is less efficient.
158+
153159net = Network (4 )
154160Cop = pylops .TorchOperator (pylops .Smoothing2D ((5 , 5 ), dims = (32 , 32 )), batch = True )
155161
@@ -169,3 +175,53 @@ def forward(self, x):
169175axs [1 ].set_title ("Gradient" )
170176axs [1 ].axis ("tight" )
171177plt .tight_layout ()
178+
179+ ###############################################################################
180+ # Finally, whilst :class:`pylops.TorchOperator` is designed such that
181+ # when a PyLops linear operator is inserted into a Torch graph, the backward
182+ # pass will automatically call the adjoint of the operator, it is also possible to
183+ # explicitly call the forward and adjoint of the operator in the forward pass of
184+ # an AD chain. This can be useful in some scenarios, for example in the
185+ # implementation of so-called unrolled networks. In this case, we can simply
186+ # use the ``forward`` and ``adjoint`` methods of the :class:`pylops.TorchOperator`
187+ # class; Torch's AD will instead call the two methods swapped, namely ``adjoint``
188+ # and ``forward``.
189+ #
190+ # Let's consider the following example:
191+ #
192+ # .. math::
193+ # \mathbf{y}=\textbf{A}^H (\textbf{A} \mathbf{x} - \mathbf{d})
194+ #
195+ # whose Jacobian is given by:
196+ #
197+ # .. math::
198+ # \mathbf{J}=-\textbf{A}^H \textbf{A}
199+ #
200+ # Let's once again verify that the result of the product between
201+ # the transposed Jacobian and a vector :math:`\mathbf{v}` matches
202+ # with the analytical one.
203+
204+ nx , ny = 10 , 6
205+ xt0 = torch .arange (nx , dtype = torch .double , requires_grad = True )
206+ x0 = xt0 .detach ().numpy ()
207+ yt0 = - 2 * torch .arange (ny , dtype = torch .double )
208+ y0 = xt0 .detach ().numpy ()
209+
210+ # Forward
211+ A = np .random .normal (0.0 , 1.0 , (ny , nx ))
212+ At = torch .from_numpy (A )
213+ Atop = pylops .TorchOperator (pylops .MatrixMult (A ))
214+ yt = Atop .adjoint (yt0 - Atop .forward (xt0 ))
215+
216+ # AD
217+ v = torch .ones (nx , dtype = torch .double )
218+ yt .backward (v , retain_graph = True )
219+ adgrad = xt0 .grad
220+
221+ # Analytical
222+ JT = - At .T @ At
223+ anagrad = torch .matmul (JT , v )
224+
225+ print ("Input: " , x0 )
226+ print ("AD gradient: " , adgrad )
227+ print ("Analytical gradient: " , anagrad )
0 commit comments