-
Notifications
You must be signed in to change notification settings - Fork 40
Allow PyTree inputs for coil objectives #1921
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Allow PyTree inputs for coil objectives #1921
Conversation
- imports jax.tree_util.tree_broadcast in DESC backend - Allows for "target, bounds, weight" parameters in _CoilObjective to be given as pytrees - New parameter option "indices" to only compute error on a subset of coils in a MixedCoilSet - Adds attribute "_mask" to _CoilObjective containing array of 0's (for coils not optimized) and 1's (for those that are) - Adds conditional to compute_unscaled, computed_scaled, and compute_scaled_error in _Objective which applies the "_mask" array when applicable.
- imports jax.tree_util.tree_broadcast in DESC backend - Allows for "target, bounds, weight" parameters in _CoilObjective to be given as pytrees - New parameter option "indices" to only compute error on a subset of coils in a MixedCoilSet - Adds attribute "_mask" to _CoilObjective containing array of 0's (for coils not optimized) and 1's (for those that are) - Adds conditional to compute_unscaled, computed_scaled, and compute_scaled_error in _Objective which applies the "_mask" array when applicable.
- imports jax.tree_util.tree_broadcast in DESC backend - Allows for "target, bounds, weight" parameters in _CoilObjective to be given as pytrees - New parameter option "indices" to only compute error on a subset of coils in a MixedCoilSet - Adds attribute "_mask" to _CoilObjective containing array of 0's (for coils not optimized) and 1's (for those that are) - Adds conditional to compute_unscaled, computed_scaled, and compute_scaled_error in _Objective which applies the "_mask" array when applicable.
Memory benchmark result| Test Name | %Δ | Master (MB) | PR (MB) | Δ (MB) | Time PR (s) | Time Master (s) |
| -------------------------------------- | ------------ | ------------------ | ------------------ | ------------ | ------------------ | ------------------ |
test_objective_jac_w7x | 3.20 % | 3.793e+03 | 3.914e+03 | 121.38 | 38.76 | 36.04 |
test_proximal_jac_w7x_with_eq_update | 0.32 % | 6.638e+03 | 6.659e+03 | 21.04 | 161.52 | 162.00 |
test_proximal_freeb_jac | -0.55 % | 1.324e+04 | 1.317e+04 | -72.96 | 85.95 | 84.04 |
test_proximal_freeb_jac_blocked | -0.45 % | 7.522e+03 | 7.488e+03 | -33.92 | 72.79 | 73.31 |
test_proximal_freeb_jac_batched | 0.12 % | 7.414e+03 | 7.423e+03 | 9.01 | 72.10 | 72.39 |
test_proximal_jac_ripple | 0.74 % | 3.449e+03 | 3.475e+03 | 25.68 | 65.29 | 64.39 |
test_proximal_jac_ripple_bounce1d | 0.21 % | 3.566e+03 | 3.573e+03 | 7.32 | 75.23 | 77.65 |
test_eq_solve | -0.34 % | 2.028e+03 | 2.021e+03 | -6.95 | 93.07 | 93.18 |For the memory plots, go to the summary of |
-Given user pytree input for "weight", "target/bounds", and "indices", broadcasts to larger arrays of size = number of objectives. Was done for objectives which are scalar-valued for single coils, but not vector-valued ones. - Adds an attribute "_broadcast_input" to classes inheriting from _CoilObjective. Equals "Coil" for objectives which evaluate to a single number per coil (e.g. length), and "Node" for objectives evaluated on individual nodes.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #1921 +/- ##
==========================================
- Coverage 95.76% 95.72% -0.05%
==========================================
Files 102 102
Lines 28344 28424 +80
==========================================
+ Hits 27143 27208 +65
- Misses 1201 1216 +15
🚀 New features to boost your workflow:
|
|
@singh-jaydeep is this ready for review? |
|
I'll have it ready by Monday. There's one thing related to memory usage I wanted to check. |
|
For tests, two examples to modify with the new input structure
|
desc/objectives/_coils.py
Outdated
| "Only use toroidal resolution for coil grids.", | ||
| ) | ||
|
|
||
| def broadcast_input(arr): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might make more sense to have this as a (maybe private) method of this objective, then when you make the target setter, you can use this as well.
desc/objectives/_coils.py
Outdated
| Boolean indicating whether particular coils in a CoilSet | ||
| should be optimized according to the objective. Default | ||
| is to optimize all coils. If a list, must have the same structure | ||
| as coil. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is probably worth some additional description as this is I think the first time indices is being used in this specific way (usually it is with a FixedParameter subclass and so indices are the specific parts of params being fixed by the constraint).
Maybe something additional like "Boolean indicating coils in the CoilSet to be targeted by the objective. If True, includes every coil."
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry I missed this! Will do.
tests/test_examples.py
Outdated
| [obj._target[i] if obj._mask[i] else length_init[i] for i in range(0, 6)] | ||
| ) | ||
|
|
||
| np.testing.assert_allclose(length_fin, length_ref, rtol=1e-4) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So in this test, the first coil of the second element of the MixedCoilSet is not included in the optimization, that just means that the gradient of the objective w.r.t the params associated with that coil is zero right?
I guess what is unclear to me is why we would expect no change in its length? It is not like we are not including it at all as an optimizable DOF, it is instead a nullspace direction of our jacobian (it has no effect on f) and thus nothing really is keeping it from changing a lot vs not at all
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, yes this just zeros out the gradient associated to that coil's parameters. Would the behavior (whether the coil length still changes, or none of its params change) then depend on the particular optimizer? It passed locally, but maybe it's not a robust test.
Maybe I just test that the explicitly targeted coils do have the length we want, and check the gradients of the objective wrt. the non-optimized coils are zero? Or is there something else which makes sense
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I updated the test to separately check the gradients of unoptimized coils and the lengths of optimized coils.
dpanici
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
last thing I think I want for this PR: adding an example use of this to the coil tutorial notebook. In that notebook as one of the last cells, we have an optimization of a MixedCoilSet of modular coils and saddle coils. It would be nice to say have the length penalty only target the modular coils (as it effectively does now) and then maybe add some other penalty for the saddle coils, and have this take advantage of the new API as well as show the use of indices
And also while we are changing that notebook, adding to the constraints for that last finite beta coil optimization this constraint
LinkingCurrentConsistency(eq,coilset,eq_fixed=True) is something I've been meaning to add, and this would be a good spot to change that
@YigitElma do not change the coil notebook on #1877 until this PR is in to avoid annoying conflicts
|
I'll work today on adding to the coil tutorial notebook! |
|
Check out this pull request on See visual diffs & provide feedback on Jupyter Notebooks. Powered by ReviewNB |
|
hm weird that CI cannot run the noteboook due to memory, try adding Also I cannot tell bc the docs did not build but did you rerun the notebook? |
- Tries to reduce memory usage in notebook - changes test_coil_individual_targets_optimization to verify that the optimized coils have the prescribed targets, and that the gradient with respect to unoptimized coils is zero.
|
I did rerun the notebook and it seemed to work fine (even when I run the action individually on my branch?), but this isn't something I'm too familiar with. I changed as you recommended and will see |
|
lmao I'll fix that |
desc/objectives/objective_funs.py
Outdated
| f = self.compute(*args, **kwargs) | ||
| if self._loss_function is not None: | ||
| f = self._loss_function(f) | ||
| if hasattr(self, "_mask"): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
perhaps would be good to leave a comment here saying this is due to the coils objectives, or to add to the _Objective docstring that if _mask is an attribute, it will multiply f here
dpanici
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
my last change request I promise... I think either leave a comment where mask is used in objective funs to describe why it is there, or add a line about mask to the docstring of _Objective
|
The "mask" option is proving to be a bit clunky. In addition to getting the normalization somewhat wrong, it doesn't work well for scalar valued loss functions unless the mask gets passed in (e.g. the mean by default includes the masked values). Fixing these things would further clutter up the objective compute functions, which I don't want to do unless masking is useful for other objectives as well. Is there a preferred way of handling this? (Another option is if "indices" gets passed into a coilobjective, when building it changes self.things to only include the coils which are being optimized. This is cleaner in that it keeps the masking within the coil objective only. But the optimizer objects when passing in a coilset and objective with "indices", since the set of optimizable things doesn't match.) |
Yes I see the issue... While this would be a nice feature to have, I think that we have some eventual future plans try to change the optimization API itself to allow us to, say, apply the CoilLength objective to a sub-member of the coilset and have the optimization machinery understand that this means only target one and dont touch the ohters I think this PR even without the mask object is useful in that it opens the door to more easily do this exact sort of function. By that I mean, already one could say provide bounds as an array and just make the array index of the coil(s) you don't care about have (-np.inf,np.inf), for example, or if providing target one could also provide a weights array that sets a tiny weight on the ones you dont care about. I would be ok with punting on something more complicated like this indices feature, as I see that it is a bit clunky and as of right now don't have any further ideas to help with it. @PlasmaControl/desc-dev if anyone else has ideas please chime in, but this is all I have for now |
|
@singh-jaydeep can you try fixing the notebook conflict? I know that can be annoying, I think there is a jupyter nbdiff tool you can use to make it easier to merge locally |

Adds new input formats for objectives inheriting from
_CoilObjective. In particular,bounds,target, andweightcan accept pytrees with values for individual coils, provided the input is a prefix of the inputCoilSet. weights can be zero, in which case the corresponding coil is masked.A new optionindicesaccepts pytrees with the same structure, indicating whether the bound/target should be applied to given coils._broadcast_inputis required for coil objectives, stating if they are computed per-coil or per-grid node.Notes
_Objective._Objectivewill have issues with pytree inputs. Would it make sense to add specific setters in_CoilObjective? [resolved]