Skip to content

Commit 1ebeb93

Browse files
authored
Merge pull request #100 from FusionComputingLab/share-profile-geometry
Share immutable profile geometry
2 parents 7ff7434 + c96a7e4 commit 1ebeb93

3 files changed

Lines changed: 66 additions & 49 deletions

File tree

freegsnke/jtor_update.py

Lines changed: 23 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ def copy(self, obj=None):
9898
-----
9999
Copy semantics:
100100
- Immutable / scalar attributes are copied directly
101-
- NumPy arrays and simple containers are shallow-copied where safe
101+
- Grid geometry and limiter masks are shared through limiter_handler
102102
- Selected complex objects are deep-copied (e.g. via copy.deepcopy)
103103
- Some large system objects are shared by reference (e.g. limiter_handler)
104104
@@ -122,17 +122,13 @@ def copy(self, obj=None):
122122
copy_into(self, obj, "dRdZ")
123123
copy_into(self, obj, "nx")
124124

125-
copy_into(self, obj, "dR_dZ", mutable=True)
126-
copy_into(self, obj, "grid_points", mutable=True)
127-
copy_into(self, obj, "eqRidx", mutable=True)
128-
copy_into(self, obj, "eqZidx", mutable=True)
129-
copy_into(self, obj, "idx_grid_points", mutable=True)
130-
copy_into(self, obj, "R0Z0", mutable=True)
131-
copy_into(self, obj, "mask_inside_limiter", mutable=True)
132-
copy_into(self, obj, "mask_outside_limiter", mutable=True)
133-
copy_into(self, obj, "limiter_mask_out", mutable=True)
134-
copy_into(self, obj, "limiter_mask_for_plotting", mutable=True)
135-
copy_into(self, obj, "edge_mask", mutable=True)
125+
copy_into(self, obj, "dR_dZ")
126+
copy_into(self, obj, "eqRidx")
127+
copy_into(self, obj, "eqZidx")
128+
copy_into(self, obj, "R0Z0")
129+
copy_into(self, obj, "mask_inside_limiter")
130+
copy_into(self, obj, "mask_outside_limiter")
131+
copy_into(self, obj, "limiter_mask_out")
136132
obj.inputs = self.inputs[::] # shallow copy suffices
137133

138134
# *Should* not be necessary to copy this
@@ -187,46 +183,25 @@ def set_masks(self, eq):
187183
eq : FreeGSNKE Equilibrium object
188184
Equilibrium defining the computational domain, including 1D and 2D
189185
coordinate grids and limiter geometry.
186+
187+
Notes
188+
-----
189+
Fixed grid arrays are shared with ``eq.limiter_handler`` and treated as
190+
read-only by profile calculations.
190191
"""
191192
self.dR = eq.R_1D[1] - eq.R_1D[0]
192193
self.dZ = eq.Z_1D[1] - eq.Z_1D[0]
193-
self.dR_dZ = np.array([self.dR, self.dZ])
194-
self.R0Z0 = np.array([eq.R_1D[0], eq.Z_1D[0]])
195194
self.dRdZ = self.dR * self.dZ
196-
self.grid_points = np.concatenate(
197-
(eq.R[:, :, np.newaxis], eq.Z[:, :, np.newaxis]), axis=-1
198-
)
199195
self.nx, self.ny = np.shape(eq.R)
200-
self.eqRidx = np.tile(np.arange(self.nx)[:, np.newaxis], (1, self.ny))
201-
self.eqZidx = np.tile(np.arange(self.ny)[:, np.newaxis], (1, self.nx)).T
202-
self.idx_grid_points = np.concatenate(
203-
(self.eqRidx[:, :, np.newaxis], self.eqZidx[:, :, np.newaxis]), axis=-1
204-
).reshape(-1, 2)
205196

206197
self.limiter_handler = eq.limiter_handler
207-
208-
# self.core_mask_limiter = eq.limiter_handler.core_mask_limiter
209-
210-
self.mask_inside_limiter = eq.limiter_handler.mask_inside_limiter
211-
212-
mask_outside_limiter = np.logical_not(eq.limiter_handler.mask_inside_limiter)
213-
# Note the factor 2 is not a typo: used in critical.inside_mask
214-
self.mask_outside_limiter = (2 * mask_outside_limiter).astype(float)
215-
216-
self.limiter_mask_out = eq.limiter_handler.limiter_mask_out
217-
218-
self.limiter_mask_for_plotting = (
219-
eq.limiter_handler.mask_inside_limiter
220-
+ eq.limiter_handler.make_layer_mask(
221-
eq.limiter_handler.mask_inside_limiter, layer_size=1
222-
)
223-
) > 0
224-
225-
# set mask of the edge domain pixels
226-
self.edge_mask = np.zeros_like(eq.R)
227-
self.edge_mask[0, :] = self.edge_mask[:, 0] = self.edge_mask[-1, :] = (
228-
self.edge_mask[:, -1]
229-
) = 1
198+
self.dR_dZ = self.limiter_handler.dR_dZ
199+
self.R0Z0 = self.limiter_handler.R0Z0
200+
self.eqRidx = self.limiter_handler.eqRidx
201+
self.eqZidx = self.limiter_handler.eqZidx
202+
self.mask_inside_limiter = self.limiter_handler.mask_inside_limiter
203+
self.mask_outside_limiter = self.limiter_handler.mask_outside_limiter
204+
self.limiter_mask_out = self.limiter_handler.limiter_mask_out
230205

231206
def select_refinement(self, eq, refine_jtor, nnx, nny):
232207
"""
@@ -408,9 +383,9 @@ def diverted_critical(
408383
)
409384
)[np.newaxis]
410385
# build diverted_core_mask
411-
diverted_core_mask = path.contains_points(self.idx_grid_points).reshape(
412-
(self.nx, self.ny)
413-
)
386+
diverted_core_mask = path.contains_points(
387+
self.limiter_handler.idx_grid_points
388+
).reshape((self.nx, self.ny))
414389

415390
return opt, xpt, diverted_core_mask, psi_bndry
416391

freegsnke/limiter_func.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ class Limiter_handler:
3232
objects to determine whether grid points lie inside the allowable plasma
3333
region and to support limiter-dependent calculations.
3434
35-
Each profile object typically instantiates its own Limiter_handler.
35+
An equilibrium owns one handler; profile objects share its fixed grid geometry
36+
and limiter masks.
3637
3738
Notes
3839
-----
@@ -61,6 +62,8 @@ def __init__(self, eq, limiter):
6162

6263
self.dR = self.eqR[1, 0] - self.eqR[0, 0]
6364
self.dZ = self.eqZ[0, 1] - self.eqZ[0, 0]
65+
self.dR_dZ = np.array([self.dR, self.dZ])
66+
self.R0Z0 = np.array([self.eqR_1D[0], self.eqZ_1D[0]])
6467
self.dRdZ = self.dR * self.dZ
6568
self.nx, self.ny = np.shape(eq.R)
6669
self.nxny = self.nx * self.ny
@@ -70,9 +73,22 @@ def __init__(self, eq, limiter):
7073

7174
self.validate_limiter_inside_domain()
7275
self.build_mask_inside_limiter()
76+
self.mask_outside_limiter = (
77+
2 * np.logical_not(self.mask_inside_limiter)
78+
).astype(float)
7379
self.limiter_points()
7480
self.plasma_pts = self.extract_plasma_pts(eq.R, eq.Z, self.mask_inside_limiter)
7581
self.idxs_mask = self.extract_index_mask(self.mask_inside_limiter)
82+
self._idx_grid_points = None
83+
84+
@property
85+
def idx_grid_points(self):
86+
"""Grid indices for contour fallback, built only when first required."""
87+
if self._idx_grid_points is None:
88+
self._idx_grid_points = np.column_stack(
89+
(self.eqRidx.reshape(-1), self.eqZidx.reshape(-1))
90+
)
91+
return self._idx_grid_points
7692

7793
def validate_limiter_inside_domain(self):
7894
"""Raise a clear error if the limiter is not inside the solution domain.

freegsnke/tests/test_jtor_update.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,32 @@ def test_profiles_BetapIp(create_machine):
101101
), "The profiles object does not have the xpt, opt and jtor attributes"
102102

103103

104+
def test_profile_geometry_is_shared_and_fallback_indices_are_lazy(create_machine):
105+
eq, _ = create_machine
106+
profiles = jtor.ConstrainPaxisIp(eq, 8.1e3, 6.2e5, 0.5)
107+
profiles.inputs = []
108+
copied_profiles = jtor.Jtor_universal.copy(profiles)
109+
handler = eq.limiter_handler
110+
111+
for name in (
112+
"dR_dZ",
113+
"R0Z0",
114+
"eqRidx",
115+
"eqZidx",
116+
"mask_inside_limiter",
117+
"mask_outside_limiter",
118+
"limiter_mask_out",
119+
):
120+
assert getattr(profiles, name) is getattr(handler, name)
121+
assert getattr(copied_profiles, name) is getattr(handler, name)
122+
123+
assert handler._idx_grid_points is None
124+
assert handler.idx_grid_points.shape == (eq.nx * eq.ny, 2)
125+
expected_indices = np.indices((eq.nx, eq.ny)).reshape(2, -1).T
126+
assert np.array_equal(handler.idx_grid_points, expected_indices)
127+
assert handler.idx_grid_points is handler._idx_grid_points
128+
129+
104130
def test_contour_fallback_coordinates_and_current_sign(create_machine):
105131
"""The contour fallback is invariant under simultaneous psi and Ip reversal."""
106132
eq, _ = create_machine

0 commit comments

Comments
 (0)