Skip to content

Add SymmetrizedGradientOp and unit tests#903

Open
trung-vt wants to merge 4 commits into
PTB-MR:mainfrom
trung-vt:sym_grad_op
Open

Add SymmetrizedGradientOp and unit tests#903
trung-vt wants to merge 4 commits into
PTB-MR:mainfrom
trung-vt:sym_grad_op

Conversation

@trung-vt

@trung-vt trung-vt commented Nov 17, 2025

Copy link
Copy Markdown

(#904 adds to this PR with an example notebook using SymmetrizedGradientOp for TGV image reconstruction.)

Summary

Add SymmetrizedGradientOp as a linear operator, and unit tests for correctness and adjointness.

Motivation

The symmetrized gradient is required for total generalised variation (TGV) type regularisation, in particular for
second-order TGV as in Bredies (2014). Having a dedicated operator in mrpro makes it easier to implement TGV-based
reconstruction methods in a consistent way with the rest of the library.

Changes

  • New class SymmetrizedGradientOp in mrpro.operators
  • Update docstring for FiniteDifferenceOp and change default value of mode to forward.

Testing

New tests tests/operators/test_symmetrized_gradient_op.py. All tests pass locally.

Notes

No breaking changes expected; existing APIs remain unchanged.


Note

Introduce SymmetrizedGradientOp with forward/adjoint implementations, export it, and add comprehensive unit tests including CUDA coverage.

  • Operators:
    • New SymmetrizedGradientOp: Implements symmetrized gradient as 0.5 * (1 + transpose) @ FiniteDifferenceOp; supports dim, mode={'central','forward','backward'}, and pad_mode={'zeros','circular'}; provides forward and adjoint.
    • Exported via mrpro.operators.__init__ and added to __all__.
  • Tests (tests/operators/test_symmetrized_gradient_op.py):
    • Validates forward on a 2D linear vector field.
    • Checks adjointness, gradient, and forward-mode autodiff across multiple dim/mode/pad_mode settings.
    • Verifies CPU/GPU (CUDA) execution.

Written by Cursor Bugbot for commit 7faa03d. This will update automatically on new commits. Configure here.

@github-actions

github-actions Bot commented Nov 20, 2025

Copy link
Copy Markdown
Contributor

Coverage

Coverage Report
FileStmtsMissCoverMissing
src/mrpro
   _version.py6267%7–8
src/mrpro/algorithms
   varimax.py16194%25
src/mrpro/algorithms/csm
   inati.py25196%43
src/mrpro/algorithms/dcf
   dcf_voronoi.py55493%15, 55–56, 89
src/mrpro/algorithms/optimizers
   adam.py30680%108, 125–129
   cg.py52198%139
   pdhg.py82396%187–188, 194
   pgd.py53492%110, 158–161
src/mrpro/algorithms/reconstruction
   DirectReconstruction.py30293%64, 80
   Reconstruction.py56886%57–59, 123–126, 128, 136
   RegularizedIterativeSENSEReconstruction.py52296%125, 153
   TotalVariationRegularizedReconstruction.py54493%106, 112, 129, 132
src/mrpro/data
   AcqInfo.py165796%49, 56, 134–135, 137, 243, 367
   Dataclass.py3112692%58, 319, 335, 401, 465–467, 480, 575, 595–596, 598, 613–614, 616, 663–664, 669–670, 855–856, 881, 888, 893–894, 896
   DcfData.py33197%62
   EncodingLimits.py97397%37, 127, 130
   IData.py1781293%138, 155–156, 217–225, 258, 304, 322
   IHeader.py174895%49, 98–101, 284, 288, 292, 296
   KData.py2442490%128–129, 144, 151, 162–173, 184, 192, 203, 242, 264–266, 313–314, 379, 607, 609, 681
   KHeader.py1711392%119–125, 152, 200, 207–208, 240–247
   KNoise.py22195%44
   KTrajectory.py95397%163, 165, 185
   Rotation.py7374294%104, 202, 339, 437, 481, 499, 586, 588, 597, 634, 636, 699, 776, 781, 784, 799, 816, 821, 897, 1085, 1090, 1093, 1117, 1121, 1145, 1265, 1267, 1275–1276, 1340, 1422, 1626, 1633–1635, 1694, 1790, 1942, 1977, 1981, 2156, 2177
   SpatialDimension.py1501987%34, 103, 146, 158, 278–280, 293–295, 329, 347, 360, 373, 386, 399, 408–409, 437
src/mrpro/data/traj_calculators
   KTrajectoryCalculator.py26292%84, 95
   KTrajectoryCartesian.py31487%129–132, 136
   KTrajectoryIsmrmrd.py19195%57
   KTrajectorySpiral2D.py571377%63–66, 69, 71, 73, 75, 77, 105, 107, 134–136
src/mrpro/operators
   AveragingOp.py38295%73, 115
   B0InformedFourierOp.py75297%151–154
   CartesianSamplingOp.py112496%152, 191, 266, 387
   ConjugateGradientOp.py89792%62, 64, 100, 106, 228, 230, 233
   ConstraintsOp.py85495%78, 80, 250, 255
   EndomorphOperator.py28293%71, 77
   FiniteDifferenceOp.py29293%62, 148
   FourierOp.py105991%85–87, 192–193, 212, 257, 321, 326
   Functional.py70297%116, 118
   GridSamplingOp.py1651591%72–73, 82–83, 90–91, 94, 96, 98, 282, 290–291, 306, 312–313
   LinearOperator.py202697%217, 255, 296, 305, 313, 330
   LinearOperatorMatrix.py1741989%99, 136, 169, 178, 183, 192–195, 208–211, 219–220, 225–226, 238, 347, 377, 404
   MultiIdentityOp.py16288%58, 63
   NonUniformFastFourierOp.py1981095%72, 99, 221, 223, 261, 263, 340, 397, 471, 476
   Operator.py88397%82, 115, 125
   PatchOp.py49394%93, 129, 144
   ProximableFunctionalSeparableSum.py42393%118, 213, 224
   SliceProjectionOp.py1781094%45, 62, 64, 70, 154, 180, 216, 253, 290, 330
   WaveletOp.py119397%184, 228, 254
   ZeroPadOp.py18194%30
src/mrpro/operators/functionals
   SSIM.py73495%82, 86, 114, 147
src/mrpro/operators/models
   EPG.py2561993%31–32, 283, 288, 304–306, 326–327, 332, 356, 361, 386, 391, 545, 599, 738, 755, 782
src/mrpro/phantoms
   EllipsePhantom.py43295%66, 131
   b0map.py262119%37–63
   brainweb.py2974087%276, 290–294, 349–359, 398, 454–457, 479–480, 485–486, 488–489, 493, 501, 508–509, 550, 621, 624–625, 644, 653–656, 667, 669, 700–701, 715, 723
   fastmri.py1061091%50–51, 59, 65, 162, 169–171, 174–175, 189
   m4raw.py74495%58–59, 74, 76
   mdcnn.py71790%58, 62–63, 70, 82, 88, 135
src/mrpro/utils
   RandomGenerator.py1551590%23–24, 36, 38, 188, 212, 428, 446, 528, 799, 829–832, 895, 898
   ema.py42490%49, 74, 78, 82
   filters.py61198%46
   indexing.py177199%321
   pad_or_crop.py39685%40, 43, 46, 49, 66, 73
   reshape.py1531093%176, 370, 482–484, 505, 508–509, 515, 530
   slice_profiles.py49688%21, 37, 119–122, 155
   split_idx.py10280%43, 47
   summarize.py57689%40–41, 70–73, 81
   unit_conversion.py721579%34, 44, 51, 53, 62, 69, 71, 78, 80, 89, 100, 121, 123, 144, 146
TOTAL816249994% 

Tests Skipped Failures Errors Time
3406 0 💤 0 ❌ 0 🔥 2m 39s ⏱️

@github-actions

github-actions Bot commented Nov 20, 2025

Copy link
Copy Markdown
Contributor

📚 Documentation

📁 Download as zip
🔍 View online

@koflera koflera left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me. I would first merge this and then, once this is merged, get rid of the changes regarding the symmetrized gradient op and the related tests in #904, so that in #904 we can focus only on the example notebook.

What do you think @ckolbPTB, @fzimmermann89?

class SymmetrizedGradientOp(LinearOperator):
r"""Symmetrized gradient operator.

The symmetrized gradient :math:`\mathcal{E}: \mathcal{R}^d \to \mathcal{R}^{d+1}`

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it would be helpful to explicitly state that this here is understood to be point-wise for an image, i.e. the R^d or the C^d is, for example, supposed to denote the space of the derivatives. Potentially, one could think of R^d or C^d denoting the space of the vectorized image and yielding some confusion.


\mathcal{E}v = \frac{1}{2}(\nabla v + (\nabla v)^{\top})

where :math:`(\nabla v)^{\top}` denotes the transpose of the Jacobian matrix :math:`\nabla v`.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe here, one could provide an example, e.g. to say that v = v(x,y) would correspond to a scalar-valued function v: R^2 --> R, and then the connection between nabla as an operator applied to the whole image and the Jacobian being meant as for each point becomes clearer.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes - this operator if a finite difference approximation of this, as we dont use the Jacobian of a function v, but always a finite diffrerence for \nabla v.
Maybe rephrase to make this more more clear? I would even consider avoiding "Jacobian" here completly, as we here always consider discrete images - and have a Jacobian Operator that operates on functions.

Would it be Ok with you to formulate this first as the finite difference version and then mention that this is the discrete version of the Jacobian?

Could you add a link to the finite difference operator in the docstring?

Also, could @kofler or @trung-vt add a similiar short description to finite difference operator?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I also agree with getting rid of the word "Jacobian" here

Comment thread src/mrpro/operators/SymmetrizedGradientOp.py Outdated
Comment thread src/mrpro/operators/SymmetrizedGradientOp.py Outdated
@fzimmermann89

fzimmermann89 commented Nov 24, 2025

Copy link
Copy Markdown
Member

I would prefer to have similiar docstrings for the finite difference operator and the symmetrized gradient op, i.e. write the description here already for the discrete version of it (as we can only apply it to images, not to functions v)

Also, would be great to use similar notation in both operators and link from Open
SymmetrizedGradientOp docstring to the finite difference op.

@koflera lgtm. your decision if you want to include the finite difference docstring change here or make a tiny seperate PR. I think it would be great if @trung-vt or you could do it.

@koflera

koflera commented Nov 26, 2025

Copy link
Copy Markdown
Collaborator

@koflera lgtm. your decision if you want to include the finite difference docstring change here or make a tiny seperate PR. I think it would be great if @trung-vt or you could do it.

@trung-vt I would be super happy if you could also include the suggested changes in the docstring of the finite difference operator in this PR :)

@trung-vt trung-vt force-pushed the sym_grad_op branch 3 times, most recently from 746da2b to d6d6c11 Compare December 1, 2025 16:24
@trung-vt

trung-vt commented Dec 1, 2025

Copy link
Copy Markdown
Author

Thank you for your suggestions! I have updated the docstrings for both classes (for convenience, I attached the renderings below)

image image

@fzimmermann89

Copy link
Copy Markdown
Member

Mh, this looks nice and mathematical, but I am not convinced that the average user of mrpro will understand it.
image
might be precise, but we usually just talk about a nd-tensor (in the programmer's definition as an array, not as a multilinear map) anoperator is applied on -- think numpy or torch documentation, not math book.

Same for "Assuming unit spaceing", "for a continous function" ... etc
This specific class can only be applied for discrete images.

Can we dumb it down? For an average undergrad phycicist/SWE?

@koflera

koflera commented Dec 11, 2025

Copy link
Copy Markdown
Collaborator

Mh, this looks nice and mathematical, but I am not convinced that the average user of mrpro will understand it. image might be precise, but we usually just talk about a nd-tensor (in the programmer's definition as an array, not as a multilinear map) anoperator is applied on -- think numpy or torch documentation, not math book.

Same for "Assuming unit spaceing", "for a continous function" ... etc This specific class can only be applied for discrete images.

Can we dumb it down? For an average undergrad phycicist/SWE?

I wouldn't actually mind being more precise here. But if we have to make it simpler, we could perhaps for the Finite differences operator just keep the following formula (without the h) and say something like: "The finite differences operator implements this operation for the entire tensor for all dimensions i indexed by dim by means of a separable convolution with appropriate filters, where x is a coordinate on the equispacced grid implicitly defined by the tensor. How the boundaries are handled is defined by pad_mode."
grafik

For the symmetrized gradient, we could then perhaps just keep the formula
grafik
and again point out what x is meant to denote, and mention that the \nabla is implemented by means of the finite differences operator and reference it.

What do you think @fzimmermann89 @trung-vt?

@trung-vt

Copy link
Copy Markdown
Author

Thank you for your comments! Would this be a more straightforward approach?
I assume only discrete settings now and switched the formulas to pseudo-code, hopefully it still keeps things readable and precise.

image image

@koflera

koflera commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator

What's the status on this? i think it would be nice to merge this. to me the docs look good as they are now, but they also looked good before
@ckolbPTB @fzimmermann89 @trung-vt

@ckolbPTB

Copy link
Copy Markdown
Collaborator

Is the idea to do #904 afterwards also including a TGV reconstruction algorithm like we have for TV?

Copilot AI review requested due to automatic review settings June 19, 2026 10:29

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review is ineligible. To be eligible to request a review, you need a paid Copilot license, or your organization must enable Copilot code review.

@koflera

koflera commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator

Is the idea to do #904 afterwards also including a TGV reconstruction algorithm like we have for TV?

yes, I think it makes sense to split the two things. so that we can start using TGV and have the example at a later point.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants