Skip to content

Commit 54fba19

Browse files
author
Lachlan Grose
committed
fix: add caching of reused values
1 parent ce082f6 commit 54fba19

3 files changed

Lines changed: 53 additions & 29 deletions

File tree

LoopStructural/modelling/features/_lambda_geological_feature.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,25 @@ def evaluate_value(self, pos: np.ndarray, ignore_regions=False) -> np.ndarray:
6666
v = np.zeros((pos.shape[0]))
6767
v[:] = np.nan
6868

69-
mask = self._calculate_mask(pos, ignore_regions=ignore_regions)
70-
pos = self._apply_faults(pos)
69+
# Precompute each fault's scalar value (gx = fault.__getitem__(0).evaluate_value)
70+
# once and reuse for both the region mask check and fault application.
71+
# FaultSegment.evaluate_value(pos) == self.__getitem__(0).evaluate_value(pos) == gx.
72+
# apply_to_points also needs gx to determine the displacement mask — passing it
73+
# avoids the duplicate evaluation on the np.copy of pos.
74+
precomputed_gx = {id(f): f.evaluate_value(pos) for f in self.faults}
75+
76+
# Region mask: pass precomputed gx so PositiveRegion/NegativeRegion skip re-evaluation
77+
mask = np.ones(pos.shape[0], dtype=bool)
78+
if not ignore_regions:
79+
for r in self.regions:
80+
pre = precomputed_gx.get(id(getattr(r, 'feature', None)))
81+
mask = np.logical_and(mask, r(pos, precomputed_val=pre))
82+
83+
# Apply faults: pass precomputed gx so apply_to_points skips one evaluate_value call
84+
if self.faults_enabled:
85+
for f in self.faults:
86+
pos = f.apply_to_points(pos, precomputed_gx=precomputed_gx.get(id(f)))
87+
7188
if self.function is not None:
7289
v[mask] = self.function(pos[mask,:])
7390
return v

LoopStructural/modelling/features/fault/_fault_segment.py

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -311,13 +311,14 @@ def evaluate_displacement(self, points):
311311
d[mask] = self.faultfunction(gx[mask] + self.fault_offset, gy[mask], gz[mask])
312312
return d * self.displacement
313313

314-
def apply_to_points(self, points, reverse=False):
314+
def apply_to_points(self, points, reverse=False, precomputed_gx=None):
315315
"""
316316
Unfault the array of points
317317
318318
Parameters
319319
----------
320320
points - numpy array Nx3
321+
precomputed_gx - optional pre-evaluated gx values (same points, avoids duplicate evaluation)
321322
322323
Returns
323324
-------
@@ -328,10 +329,12 @@ def apply_to_points(self, points, reverse=False):
328329
newp = np.copy(points).astype(float)
329330
# evaluate fault function for all points
330331
# then define mask for only points affected by fault
331-
gx = None
332-
gy = None
333-
gz = None
334-
if use_threads:
332+
# gx may be supplied by caller to avoid re-evaluation (precomputed from region check)
333+
if precomputed_gx is not None:
334+
gx = precomputed_gx
335+
gy = self.__getitem__(1).evaluate_value(newp)
336+
gz = self.__getitem__(2).evaluate_value(newp)
337+
elif use_threads:
335338
with ThreadPoolExecutor(max_workers=8) as executor:
336339
# all of these operations should be
337340
# independent so just run as different threads
@@ -361,27 +364,32 @@ def apply_to_points(self, points, reverse=False):
361364
d *= -1.0
362365
# calculate the fault frame for the evaluation points
363366
for _i in range(steps):
364-
gx = None
365-
gy = None
366-
gz = None
367367
g = None
368-
if use_threads:
368+
if _i == 0:
369+
# Reuse gx/gy/gz from the initial full-array evaluation above — newp[mask] hasn't
370+
# moved yet on the first iteration, so values are identical.
371+
gx_m = gx[mask]
372+
gy_m = gy[mask]
373+
gz_m = gz[mask]
374+
g = self.__getitem__(1).evaluate_gradient(newp[mask, :], ignore_regions=True)
375+
elif use_threads:
369376
with ThreadPoolExecutor(max_workers=8) as executor:
370377
# all of these operations should be
371378
# independent so just run as different threads
372379
gx_future = executor.submit(self.__getitem__(0).evaluate_value, newp[mask, :])
373380
g_future = executor.submit(self.__getitem__(1).evaluate_gradient, newp[mask, :])
374381
gy_future = executor.submit(self.__getitem__(1).evaluate_value, newp[mask, :])
375382
gz_future = executor.submit(self.__getitem__(2).evaluate_value, newp[mask, :])
376-
gx = gx_future.result()
383+
gx_m = gx_future.result()
377384
g = g_future.result()
378-
gy = gy_future.result()
379-
gz = gz_future.result()
385+
gy_m = gy_future.result()
386+
gz_m = gz_future.result()
380387
else:
381-
gx = self.__getitem__(0).evaluate_value(newp[mask, :])
382-
gy = self.__getitem__(1).evaluate_value(newp[mask, :])
383-
gz = self.__getitem__(2).evaluate_value(newp[mask, :])
388+
gx_m = self.__getitem__(0).evaluate_value(newp[mask, :])
389+
gy_m = self.__getitem__(1).evaluate_value(newp[mask, :])
390+
gz_m = self.__getitem__(2).evaluate_value(newp[mask, :])
384391
g = self.__getitem__(1).evaluate_gradient(newp[mask, :], ignore_regions=True)
392+
gx, gy, gz = gx_m, gy_m, gz_m # alias for block below
385393
# # get the fault frame val/grad for the points
386394
# determine displacement magnitude, for constant displacement
387395
# hanging wall should be > 0

LoopStructural/utils/regions.py

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -45,23 +45,22 @@ def __init__(self, feature, vector=None, point=None):
4545
self.name = 'PositiveRegion'
4646
self.parent = feature
4747

48-
def _calculate_value_and_distance(self, xyz)-> Tuple[np.ndarray, np.ndarray]:
49-
val = self.feature.evaluate_value(xyz)
50-
# find a point on/near 0 isosurface
48+
def _calculate_value_and_distance(self, xyz, precomputed_val=None)-> Tuple[np.ndarray, np.ndarray]:
49+
val = precomputed_val if precomputed_val is not None else self.feature.evaluate_value(xyz)
50+
# find a point on/near 0 isosurface — compute once and cache on self
5151
if self.point is None:
5252
mask = np.zeros(xyz.shape[0], dtype="bool")
5353
mask[:] = val < 0
5454
if np.sum(mask) == 0:
5555
raise ValueError("Cannot find point on surface")
56-
centre = xyz[mask, :][0, :]
57-
else:
58-
centre = self.point
56+
self.point = xyz[mask, :][0, :] # cache: characterises the fault, not the query points
57+
centre = self.point
5958
if self.vector is None:
6059
average_gradient = self.feature.evaluate_gradient(np.array([centre]))[0]
6160
average_gradient[2] = 0
6261
average_gradient /= np.linalg.norm(average_gradient)
63-
else:
64-
average_gradient = self.vector
62+
self.vector = average_gradient # cache: fault geometry doesn't change between calls
63+
average_gradient = self.vector
6564

6665
distance = np.einsum(
6766
"ij,j->i", centre[None, :] - xyz, average_gradient.reshape(-1, 3)[0, :]
@@ -80,8 +79,8 @@ def __init__(self, feature, vector=None, point=None):
8079
self.name = 'PositiveRegion'
8180
self.parent = feature
8281

83-
def __call__(self, xyz) -> np.ndarray:
84-
val, distance = self._calculate_value_and_distance(xyz)
82+
def __call__(self, xyz, precomputed_val=None) -> np.ndarray:
83+
val, distance = self._calculate_value_and_distance(xyz, precomputed_val=precomputed_val)
8584
return np.logical_or(
8685
np.logical_and(~np.isnan(val), val > 0),
8786
np.logical_and(np.isnan(val), distance > 0),
@@ -99,8 +98,8 @@ def __init__(self, feature, vector=None, point=None):
9998
self.name = 'NegativeRegion'
10099
self.parent = feature
101100

102-
def __call__(self, xyz) -> np.ndarray:
103-
val, distance = self._calculate_value_and_distance(xyz)
101+
def __call__(self, xyz, precomputed_val=None) -> np.ndarray:
102+
val, distance = self._calculate_value_and_distance(xyz, precomputed_val=precomputed_val)
104103
return np.logical_or(
105104
np.logical_and(~np.isnan(val), val < 0),
106105
np.logical_and(np.isnan(val), distance < 0),

0 commit comments

Comments
 (0)