Skip to content

Commit efdf6f0

Browse files
committed
Fix broadcasting for coil objectives
-Handles case when objective is computed per-grid node and at least one weight is zero.
1 parent c119da0 commit efdf6f0

3 files changed

Lines changed: 86 additions & 42 deletions

File tree

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
*.ipynb -linguist-detectable
22
documentation/* linguist-documentation
33
desc/_version.py export-subst
4+
*.sh text eol=lf

desc/objectives/_coils.py

Lines changed: 38 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ class _CoilObjective(_Objective):
5959
"Coil" if the objective returns a single scalar per coil, and "Node"
6060
if it returns a scalar at every grid point. To be compatible with
6161
masking, compute function should apply the mask
62-
self._coilset_tree["coilset_mask"] before returning data.
62+
self._coilset_tree["objective_mask"] before returning data.
6363
"""
6464

6565
__doc__ = __doc__.rstrip() + collect_docs(coil=True)
@@ -133,7 +133,9 @@ def _build_coilset_tree():
133133
params_tree["coils"] contains a nested list of 0s representing
134134
individual coils and the coilsets to which they belong. Similarly,
135135
params_tree["nodes"] lists the grid nodes associated with each coil.
136-
params_tree["coilset_mask"] contains the indices in [0,self._dim_f-1]
136+
params_tree["coil_mask"] contains the indices in [0,self._num_coils]
137+
for which the corresponding weight is positive. Similarly,
138+
params_tree["objective_mask"] contains the indices in [0,self._dim_f-1]
137139
for which the corresponding weight is positive. If all weights are
138140
positive (i.e. no masking needed), contains default slice(None).
139141
"""
@@ -166,12 +168,16 @@ def expand(t, idx=0):
166168
self._coilset_tree = {
167169
"coils": tree[0],
168170
"nodes": tree[1],
169-
"coilset_mask": slice(None),
171+
"coil_mask": np.arange(self._num_coils),
172+
"objective_mask": slice(None),
170173
}
171174
if np.any([w == 0 for w in tree_leaves(self._weight)]):
172-
mask = self._coilset_broadcast(self._weight)
173-
mask = np.nonzero(mask)[0]
174-
self._coilset_tree["coilset_mask"] = mask
175+
coil_mask = self._coilset_broadcast(self._weight)
176+
objective_mask = self._coilset_broadcast(
177+
self._weight, self._broadcast_input
178+
)
179+
self._coilset_tree["coil_mask"] = np.nonzero(coil_mask)[0]
180+
self._coilset_tree["objective_mask"] = np.nonzero(objective_mask)[0]
175181

176182
coil = self.things[0]
177183
grid = self._grid
@@ -211,33 +217,31 @@ def expand(t, idx=0):
211217

212218
_build_coilset_tree()
213219
quad_weights = np.concatenate([g.spacing[:, 2] for g in grid])[
214-
self._coilset_tree["coilset_mask"]
220+
self._coilset_tree["objective_mask"]
215221
]
216222

217223
if self._broadcast_input == "Node":
218224
grid_nodes_unmasked = [
219-
g.num_nodes for g in grid[self._coilset_tree["coilset_mask"]]
225+
grid[i].num_nodes for i in self._coilset_tree["coil_mask"]
220226
]
221227
self._dim_f = np.sum(grid_nodes_unmasked)
222228
else:
223-
coils_unmasked = np.ones(self._num_coils)[
224-
self._coilset_tree["coilset_mask"]
225-
]
229+
coils_unmasked = np.ones(self._num_coils)[self._coilset_tree["coil_mask"]]
226230
self._dim_f = len(coils_unmasked)
227231

228232
# map grid to the same structure as coil and then remove unnecessary members
229233
grid = tree_unflatten(structure, grid)
230234
grid = _prune_coilset_tree(grid)
231235
coil = _prune_coilset_tree(coil)
232236

233-
self._weight = self._coilset_broadcast(self._weight)
237+
self._weight = self._coilset_broadcast(self._weight, self._broadcast_input)
234238
if self._bounds:
235239
self._bounds = (
236-
self._coilset_broadcast(self._bounds[0]),
237-
self._coilset_broadcast(self._bounds[1]),
240+
self._coilset_broadcast(self._bounds[0], self._broadcast_input),
241+
self._coilset_broadcast(self._bounds[1], self._broadcast_input),
238242
)
239243
elif self._target:
240-
self._target = self._coilset_broadcast(self._target)
244+
self._target = self._coilset_broadcast(self._target, self._broadcast_input)
241245

242246
timer = Timer()
243247
if verbose > 0:
@@ -294,14 +298,18 @@ def bounds(self, bounds):
294298
assert (bounds is None) or (isinstance(bounds, tuple) and len(bounds) == 2)
295299
if bounds:
296300
self._bounds = (
297-
self._coilset_broadcast(bounds[0]),
298-
self._coilset_broadcast(bounds[1]),
301+
self._coilset_broadcast(bounds[0], self._broadcast_input),
302+
self._coilset_broadcast(bounds[1], self._broadcast_input),
299303
)
300304
self._check_dimensions()
301305

302306
@_Objective.target.setter
303307
def target(self, target):
304-
self._target = self._coilset_broadcast(target) if target is not None else target
308+
self._target = (
309+
self._coilset_broadcast(target, self._broadcast_input)
310+
if target is not None
311+
else target
312+
)
305313
self._check_dimensions()
306314

307315
@_Objective.weight.setter
@@ -311,13 +319,15 @@ def weight(self, weight):
311319
# objective should be rebuilt to account for masking
312320
self._built = False
313321

314-
def _coilset_broadcast(self, x):
315-
"""Expand an array in accordance with the attribute _broadcast_input.
322+
def _coilset_broadcast(self, x, target="Coil"):
323+
"""Broadcast an array to dimensions consistent with "target".
316324
317325
Parameters
318326
----------
319327
x : float or list[float]
320328
Must be broadcastable to the structure of self._things[0].
329+
target: str, optional
330+
Optional string taking values "Coil" or "Node". Defaults to "Coil".
321331
322332
Returns
323333
-------
@@ -331,10 +341,10 @@ def _coilset_broadcast(self, x):
331341
return np.atleast_1d(arr_flat[0])
332342

333343
arr = jax_tree_broadcast(x, self._coilset_tree["coils"])
334-
if self._broadcast_input == "Node":
344+
if target == "Node":
335345
arr = tree_map(lambda a, b: [a] * b, arr, self._coilset_tree["nodes"])
336346
arr, _ = tree_flatten(arr)
337-
return np.asarray(arr)[self._coilset_tree["coilset_mask"]]
347+
return np.asarray(arr)[self._coilset_tree["objective_mask"]]
338348

339349

340350
class CoilLength(_CoilObjective):
@@ -433,7 +443,7 @@ def compute(self, params, constants=None):
433443
data = super().compute(params, constants=constants)
434444
data = tree_leaves(data, is_leaf=lambda x: isinstance(x, dict))
435445
out = jnp.array([dat["length"] for dat in data])
436-
return out[self._coilset_tree["coilset_mask"]]
446+
return out[self._coilset_tree["objective_mask"]]
437447

438448

439449
class CoilCurvature(_CoilObjective):
@@ -535,7 +545,7 @@ def compute(self, params, constants=None):
535545
data = super().compute(params, constants=constants)
536546
data = tree_leaves(data, is_leaf=lambda x: isinstance(x, dict))
537547
out = jnp.concatenate([dat["curvature"] for dat in data])
538-
return out[self._coilset_tree["coilset_mask"]]
548+
return out[self._coilset_tree["objective_mask"]]
539549

540550

541551
class CoilTorsion(_CoilObjective):
@@ -635,7 +645,7 @@ def compute(self, params, constants=None):
635645
data = super().compute(params, constants=constants)
636646
data = tree_leaves(data, is_leaf=lambda x: isinstance(x, dict))
637647
out = jnp.concatenate([dat["torsion"] for dat in data])
638-
return out[self._coilset_tree["coilset_mask"]]
648+
return out[self._coilset_tree["objective_mask"]]
639649

640650

641651
class CoilCurrentLength(CoilLength):
@@ -741,7 +751,7 @@ def compute(self, params, constants=None):
741751
lengths = super().compute(params, constants=constants)
742752
params = tree_leaves(params, is_leaf=lambda x: isinstance(x, dict))
743753
currents = jnp.concatenate([param["current"] for param in params])
744-
out = jnp.atleast_1d(lengths * currents[self._coilset_tree["coilset_mask"]])
754+
out = jnp.atleast_1d(lengths * currents[self._coilset_tree["objective_mask"]])
745755
return out
746756

747757

@@ -848,7 +858,7 @@ def compute(self, params, constants=None):
848858
for dat in data
849859
]
850860
)
851-
return out[self._coilset_tree["coilset_mask"]]
861+
return out[self._coilset_tree["objective_mask"]]
852862

853863

854864
class CoilSetMinDistance(_Objective):
@@ -1566,7 +1576,7 @@ def compute(self, params, constants=None):
15661576
constants = self._get_deprecated_constants(constants)
15671577
data = tree_leaves(data, is_leaf=lambda x: isinstance(x, dict))
15681578
out = jnp.array([jnp.var(jnp.linalg.norm(dat["x_s"], axis=1)) for dat in data])
1569-
return (out * constants["mask"])[self._coilset_tree["coilset_mask"]]
1579+
return (out * constants["mask"])[self._coilset_tree["objective_mask"]]
15701580

15711581

15721582
class QuadraticFlux(_Objective):

tests/test_objective_funs.py

Lines changed: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4614,31 +4614,64 @@ def test_coil_objective_indices(DummyMixedCoilSet):
46144614
compB = objB.compute_scaled_error(None)
46154615
np.testing.assert_allclose(compA, compB, atol=1e-13)
46164616

4617+
objC = CoilCurvature(coilsetA, weight=weight, normalize=False)
4618+
objD = CoilCurvature(coilsetB, normalize=False)
4619+
objC.build()
4620+
objD.build()
4621+
compC = objC.compute_scaled_error(None)
4622+
compD = objD.compute_scaled_error(None)
4623+
np.testing.assert_allclose(compC, compD, atol=1e-13)
4624+
4625+
objE = CoilTorsion(coilsetA, weight=weight)
4626+
objE.build()
4627+
46174628

46184629
@pytest.mark.unit
46194630
def test_coil_objective_setter(DummyMixedCoilSet):
46204631
"""Tests setters for _CoilObjectives."""
4621-
coilset = load(load_from=str(DummyMixedCoilSet["output_path"]), file_format="hdf5")
4622-
obj = CoilLength(coilset)
4632+
coilsetA = load(load_from=str(DummyMixedCoilSet["output_path"]), file_format="hdf5")
4633+
coilsetB = MixedCoilSet((coilsetA[0], coilsetA[1]), check_intersection=False)
46234634

4624-
weight = [1.0, 1.0, 0.0, 0.0]
4625-
weight_expanded = [1.0, 1.0, 1.0, 1.0]
4626-
obj.weight = weight
4627-
obj.build()
4628-
np.testing.assert_allclose(obj.weight, weight_expanded, atol=1e-13)
4635+
objA = CoilLength(coilsetA)
4636+
objB = CoilLength(coilsetB)
4637+
4638+
weightA = [1.0, 2.0, 0.0, 0.0]
4639+
weightB = [1.0, 2.0]
4640+
weight_expanded = [1.0, 2.0, 2.0, 2.0]
4641+
objA.weight = weightA
4642+
objB.weight = weightB
4643+
objA.build()
4644+
objB.build()
4645+
np.testing.assert_allclose(objA.weight, objB.weight, atol=1e-13)
4646+
np.testing.assert_allclose(objA.weight, weight_expanded, atol=1e-13)
46294647

46304648
bounds = ([0.0, 1.0, 2.0, 3.0], [[4.0], [5.0, 6.0, 7.0], 8.0, 9.0])
46314649
bounds_expanded = ([0.0, 1.0, 1.0, 1.0], [4.0, 5.0, 6.0, 7.0])
46324650
target = [[4.0], 5.0, 8.0, 9.0]
46334651
target_expanded = [4.0, 5.0, 5.0, 5.0]
46344652

4635-
obj.bounds = bounds
4636-
np.testing.assert_allclose(obj.bounds[0], bounds_expanded[0], atol=1e-13)
4637-
np.testing.assert_allclose(obj.bounds[1], bounds_expanded[1], atol=1e-13)
4638-
4639-
obj.bounds = None
4640-
obj.target = target
4641-
np.testing.assert_allclose(obj.target, target_expanded, atol=1e-13)
4653+
objA.bounds = bounds
4654+
np.testing.assert_allclose(objA.bounds[0], bounds_expanded[0], atol=1e-13)
4655+
np.testing.assert_allclose(objA.bounds[1], bounds_expanded[1], atol=1e-13)
4656+
4657+
objA.bounds = None
4658+
objA.target = target
4659+
np.testing.assert_allclose(objA.target, target_expanded, atol=1e-13)
4660+
4661+
objC = CoilCurvature(coilsetA)
4662+
objD = CoilCurvature(coilsetB)
4663+
4664+
objC.weight = weightA
4665+
objD.weight = weightB
4666+
objC.build()
4667+
objD.build()
4668+
objC.bounds = bounds
4669+
objC.target = target
4670+
num_nodes = len(objD.weight)
4671+
np.testing.assert_allclose(objC.weight, objD.weight, atol=1e-13)
4672+
assert len(objC.weight) == num_nodes
4673+
assert len(objC.bounds[0]) == num_nodes
4674+
assert len(objC.target) == num_nodes
46424675

46434676

46444677
@pytest.mark.unit

0 commit comments

Comments
 (0)