Module curlew.geology.SF
A class defining a structural scalar (implicit) field. This is where a lot of the magic happens, including the chaining of multiple neural fields and the application of deformation functions to implement kinematic events such as faults and sheet intrusions.
Functions
def apply_child_undeform(x, sf)
-
Expand source code
def apply_child_undeform(x, sf): """ Placeholder function to avoid Lambda functions. (Making models picklable) """ if sf.child is not None: return sf.child.undeform(x) else: return x
Placeholder function to avoid Lambda functions. (Making models picklable)
Classes
class SF (name,
child=None,
bound=None,
deformation=None,
deformation_args={},
sharpness: float = 10000.0,
**kwargs)-
Expand source code
class SF( object ): """ A linked list of scalar fields, allowing them to be chained so that they combine into time-aware geological models. """ def __init__( self, name, child=None, bound=None, deformation=None, deformation_args={}, sharpness : float = 1e4, **kwargs ): """ Initialise a new SF. Parameters ------------ name : str A name for this event. child : SF A younger (child) SF instance to link this SF to. Any deformation caused by the younger SF will then be removed prior to evaluation of this one. bound : float, tuple, None A float specifying the minimum value at which this SF overprints parent ones (i.e. unconformities), or a (min, max) range if the overprint occur over a specific range only (dykes, sills, veins). If None, no overprinting will occur and the scalar value outputs ignored (but displacements still applied, i.e. faults). deformation : function, None A function such that f(X1, self) returns an array X0 of shape (N,ndim) that represents the pre-deformation coordinates of X1. deformation_args : dict A dictionary of arguments to pass to the deformation function, if needed. sharpness : float The sharpness of the sigmoid function used to combine this SF with results from parent fields. Keywords ------------- All keywords will be passed to `curlew.fields.NF.__init__(...)`. """ self.name = name self.eid = -1 # will be set to the position of this SF in the event sequence if 'field' in kwargs: self.field = kwargs['field'] # field already constructued :-) else: self.field = NF(name=name, **kwargs) # initialise a neural field self.parent = None # used when chaining multiple SFs self.parent2 = None # will be defined if this SF represents a domain boundary. self.child = None # used when chaining multiple SFs self.bound = bound self.deformation = deformation # deformation function, if defined self.deformation_args = deformation_args # kwargs that will be passed to the deformation function self.sharpness = sharpness self.isosurfaces = {} # this will hold any added isosurfaces # add child-parent relationship if child is not None: self.child = child self.child.parent = self # define transform function of underlying neural field self.field.transform = functools.partial(apply_child_undeform, sf=self) def copy(self): """ Create a copy of this SF object for e.g., incorporation into a different model. Note that this will unlink any parent, parent2 and child relations. """ out = copy.deepcopy(self) out.parent = None out.parent2 = None out.child = None return out def fit(self, epochs, cache=True, faultBuffer=0, **kwargs): """ Utility function for training fields in isolation. Parameters ---------- epochs : int The number of epochs to train for. cache : bool | optional If True, displacements applied by younger SF instances (linked to this one through `self.child`) will be pre-computed to speed up training (by avoiding repeated evaluations of younger fields that will not change). faultBuffer : int | optional If greater that zero, constraints within this distance of younger faults will be removed (as these can often be reconstructed to misleading positions and are very sensitive to interpolation errors in younger fields). Keywords ---------- batch_size : int The size of the batches used when retro-deforming the passed grid (to save RAM). Default is 50000. All other keywords are passed to `curlew.fields.NF.fit(...)`. These include: learning_rate : float, optional Reset each SF's optimiser to the specified learning rate before training. best : bool, optional After training set neural field weights to the best loss. vb : bool, optional Display a tqdm progress bar to monitor training. Returns ------- loss : float The loss of the final (best if best=True) model state. details : dict A more detailed breakdown of the final loss. """ # get constraints set to use if 'C' in kwargs: self.field.bind( kwargs.pop('C', self.field.C) ) C = self.field.C # enable caching to avoid lots # of evaluations of displacement fields # that will not change C0 = C # no need to reconstruct if cache and self.child is not None: if faultBuffer > 0: from curlew.geology.interactions import faultOffset def buffer( arr ): # quick function for masking points on faults f = self.child mask = np.full( len(arr), True ) while f is not None: if ((f.deformation is not None) and (f.deformation == faultOffset)): # faults b = f.buffer( arr, f.deformation_args['contact'], faultBuffer ) elif (f.parent2 is not None): b = f.buffer( arr, f.bound, faultBuffer ) # domain boundary mask[ b ] = False # identify and remove points within buffer distance of a fault f = f.child return mask C0.filter(buffer) # retro-deform other constraints C0 = C.numpy().transform( self.child.undeform ) # fit try: out = self.field.fit(epochs, C=C0, transform=False, **kwargs) finally: if C0.grid is not None: C0.grid._clearCache() self.field.bind(C) # ensure this always runs! (e.g., in case of keyboard interrupts) return out def forward(self, x: torch.Tensor, undef=True) -> torch.Tensor: """ Forward pass of the network to create a scalar value or property estimate. If random Fourier features are enabled, the input is first encoded accordingly. This calls the underlying field's `forward(..)` function, but after calling `self.undeform` to underform constraint positions prior to interpolation. Parameters ---------- x : torch.Tensor A tensor of shape (N, input_dim), where N is the batch size. undef : bool True if x is specified in undeformed (modern-day) coordinates. Default is True. Returns ------- torch.Tensor A tensor of shape (N, output_dim), representing the scalar potential. """ # set transform that removes any child deformation if undef and (self.child is not None): self.field.transform = functools.partial(apply_child_undeform, sf=self) # apply this SF's undeform function prior to field.forward. #x = self.child.undeform( x ) else: self.field.transform = None # don't do any transform in field.forward # evaluate the underlying field return self.field.forward( x ) def predict(self, x: np.ndarray, combine=False, to_numpy=True, transform=True) -> np.ndarray: """ Predict scalar values belonging to this and/or previous SFs. Parameters ---------- x : np.ndarray An array of shape (N, input_dim) containing the modern-day coordinates at which to evaluate this (and previous) SF. combine: bool True if previous scalar fields should be aggregated to give a combined scalar field output. Otherwise, evaluate the scalar field related to this specific structure alone. Default is False. to_numpy : bool True if the results should be cast to a numpy array rather than a `torch.Tensor`. transform : bool True if the coordinates should be undeformed (i.e. represent modern-day coordinates) or not (i.e. represent paleo-coordinates). """ # evaluate field and combine with parent ones if combine and (self.parent is not None): if self.parent2 is not None: # this field represents a domain boundary -- essentially use as a mask # predict value of domain scalar field domain = self.predict( x, combine=False, to_numpy=False, transform=transform ) # evaluate isosurfaces to get threshold values, if needed (allows self.bound to containt str isosurface names) assert self.bound is not None, "Isosurface must be defined for domain boundary." thresh = self.getIsovalue(self.bound) # this thresh separates the domain into two halves mask = torch.sigmoid(self.sharpness * (domain - thresh))[:,0] # fill in each side of domain [ above and below thresh ] out = torch.zeros((len(domain), 2), dtype=curlew.dtype, device=curlew.device) for sf, m in zip([self.parent, self.parent2], [mask, (1-mask)]): if not isinstance( sf, SF ): # this side of domain boundary is a constant value (float or int) out = out + m[:,None] * sf else: # this side of domain boundary is a modelled value parent = sf.predict( x, combine=True, to_numpy=False, transform=transform ) out = out + m[:,None] * parent else: # this field is one in a series of geological "events" -- evaluate accordingly if isinstance(self.parent, int) or isinstance(self.parent, float): # int or float parent out = torch.full((len(x), 2), self.parent, device=curlew.device, dtype=curlew.dtype) # create constant (float or int) value parent else: # parent is a scalar field (or some variety thereof) out = self.parent.predict( x, combine=True, to_numpy=False, transform=transform ) if self.bound is not None: # if bound is None we don't change previous values as this is not a generative event sf1 = self.predict( x, combine=False, to_numpy=False, transform=transform ) thresh = self.getIsovalues(values=self.bound) # evaluate isosurfaces to get threshold values, if needed (allows self.bound to containt str isosurface names) out = overprint( out, sf1, eid=self.eid, thresh=thresh, sharpness=self.sharpness ) else: # simply evaluate field! :-) if not isinstance(x, torch.Tensor): x = torch.tensor( x, device=curlew.device, dtype=curlew.dtype) out = torch.full((len(x), 2), self.eid, device=curlew.device, dtype=curlew.dtype) # create output with event ID out[:,0] = self.forward( x, undef=transform ).squeeze() # populate scalar value part # return if to_numpy: out = out.cpu().detach().numpy() out[:,1] = out[:,1].astype(int) # ensure that the event ID is an integer return out def gradient(self, x: np.ndarray, return_vals=False, normalize=True, **kwds): """ Return the gradient vector of this SF at the specified location. Note that this does not combine the results from previous scalar fields first (i.e. the prediction is done using `combine=False`). Parameters ---------- x : np.ndarray An array of shape (N, input_dim) containing the modern-day coordinates at which to evaluate this (and previous) SF. return_vals : bool True if evaluated scalar values should be returned as well as classes. Default is False. normalize : bool True if gradient vectors should be normalised to length 1 (i.e. to represent poles to planes). Default is True. Keywords --------- Keywords are all passed to `SF.predict(...)`. Returns -------- Gradient vectors at the specified locations (`x`). If `return_vals` is `True`, tuple (gradients, values) will be returned. """ if not isinstance(x, torch.Tensor): x = torch.tensor(x, dtype=curlew.dtype, device=curlew.device, requires_grad=True) # generate predictions to_numpy = kwds.pop('to_numpy', True) pred = self.predict(x, to_numpy=False, **kwds) # keep this as torch tensor # get gradients grad_out = torch.autograd.grad( outputs=pred[:,0], # we only care about scalar values inputs=x, grad_outputs=torch.ones_like(pred[:,0]), create_graph=True, retain_graph=False )[0] # normalise gradients if normalize: norm = torch.norm(grad_out, dim=-1, keepdim=True) + 1e-8 grad_out = grad_out / norm if to_numpy: grad_out = grad_out.cpu().detach().numpy() pred = pred.cpu().detach().numpy() if return_vals: return grad_out, pred else: return grad_out def deform(self, x: torch.Tensor) -> torch.Tensor: """ Apply deformation (displacements) to the passed set of coordinates. Translates from paleo-coordinates (i.e. coordinates at the time of this event) to present-day coordinates, for example, by applying child fault offsets. Parameters ---------- x : torch.Tensor, np.ndarray A tensor of shape (N, input_dim). Returns ------- torch.Tensor A tensor of shape (N, input_dim), containing the deformed coordinates. """ tonp = False if isinstance( x, np.ndarray ): x = torch.tensor( x, device=curlew.device, dtype=curlew.dtype) tonp = True # apply any child deformation if self.child is not None: x = self.child.deform( x ) # handle our own displacement out = x if self._displace is not None: out = self._displace(x, inverse=True) # apply this SF's displacement (if defined). # cast to numpy if needed if tonp: return out.cpu().detach().numpy() else: return out def undeform(self, x: torch.Tensor) -> torch.Tensor: """ Remove deformation (displacements) from the passed set of coordinates. Translates from present-day coordinates to coordinates relevant for this SF by removing, for example, child fault offsets. Parameters ---------- x : torch.Tensor, np.ndarray A tensor of shape (N, input_dim). Returns ------- torch.Tensor A tensor of shape (N, input_dim), containing the undeformed coordinates. """ if (self.child is None) and (self._displace is None): return x # easy # evaluate displacement tonp = False if isinstance( x, np.ndarray ): x = torch.tensor( x, device=curlew.device, dtype=curlew.dtype) tonp = True # remove any child deformation if self.child is not None: x = self.child.undeform( x ) # undeform to the time-step relevant for this SF # handle our own displacement out = x if self._displace is not None: out = self._displace(x) # apply this SF's displacement (if defined). # cast to numpy if needed if tonp: return out.cpu().detach().numpy() else: return out def _displace(self, x: torch.Tensor, inverse=False) -> torch.Tensor: """ Remove deformation (displacements) caused by this specific event. Displaces from post-event coordinates to pre-event coordinates. If this SF has no associated displacement field, the input `x` is returned directly. Parameters ---------- x : torch.Tensor A tensor of shape (N, input_dim). inverse : bool If True, apply the inverse deformation (i.e. from paleo-coordinates to modern-day coordinates). Default is False, which applies the deformation from modern-day coordinates to paleo-coordinates. Returns ------- torch.Tensor A tensor of shape (N, input_dim), containing the undeformed coordinates. """ if self.deformation is None: # this SF causes no deformation return x else: # apply deformation function tonp = False if not isinstance(x, torch.Tensor): # make tensor if needed x = torch.tensor(x, dtype=curlew.dtype, device=curlew.device) tonp = True offset = self.displacement(x, inverse=inverse) # get deformation vectors x = x - offset # apply deformation to the input coordinates if tonp: # cast to numpy if needed return x.detach().cpu().numpy() # return as numpy array else: return x def displacement(self, x: np.ndarray, inverse=False) -> np.ndarray: """ Return the deformation vectors associated with this SF at the specified locations. These are the displacements that would be remoed during `_displace(...)`. Parameters ---------- x : np.ndarray | torch.Tensor An array of shape (N, input_dim) containing the modern-day coordinates at which to evaluate this (and previous) SF. inverse : bool If True, apply the inverse deformation (i.e. from paleo-coordinates to modern-day coordinates). Default is False, which applies the deformation from modern-day coordinates to paleo-coordinates. Returns ------- np.ndarray | torch.Tensor An array of shape (N, input_dim) containing the deformation vectors at the specified locations. If `inverse` is True, these vectors represent the deformation from paleo-coordinates to modern-day coordinates. Otherwise, they represent the deformation from modern-day coordinates to paleo-coordinates. """ tonp = False if not isinstance(x, torch.Tensor): x = torch.tensor(x, dtype=curlew.dtype, device=curlew.device) tonp = True offset = self.deformation(x, self, **self.deformation_args) # apply deformation function if inverse: # flip deformation vectors (B to A instead of A to B) offset = -offset if tonp: # cast to numpy if needed return offset.detach().cpu().numpy() # return as numpy array else: return offset def addIsosurface( self, name :str, *, value = None, seed = None): """ Add a (geologically meaningful) isosurfcae to this scalar field. These represent e.g., stratigraphic contacts. Note that isosurfaces can be defined in two ways: 1. by specifying their value directly (`value=x`) 2. by specifying a location (`seed_point`) at with the scalar field should be evaluated to determine the scalar value. Parameters ---------- name : str A friendly name used to refer to this isosurface. value : float, None A value to explicitely set the isosurface value. seed : np.ndarray, None A position (x,y,[z]) or list of positions that implicitly define the isosurface value. Whatever value is returned by the model at this location will be used to define the isosurface value). If several points are provided (e.g., known contact locations), the mean of their outputs used to determine the isosurface value. """ assert (seed is None) or (value is None), "Either seed or value should be defined, not both." assert not( (seed is None) and (value is None)), "Either seed or value should be defined, not both." if seed is not None: self.isosurfaces[name] = np.array( seed ) if value is not None: self.isosurfaces[name] = value def deleteIsosurface( self, name ): """ Remove the specified isosurface from this SF's `isosurfaces` dict, if it exists. """ if name in self.isosurfaces: del self.isosurfaces[name] def getIsovalue( self, name, offset=0): """ Return the value of a specific isosurface associated with this scalar field. """ return self.getIsovalues( [name], offset=offset )[0] def getIsovalues( self, values=None, offset=0 ): """ Evaluate (if necessary) and return the values of all the isosurfaces associated with this scalar field. Parameters ---------- values : list, None A list of isosurface names to evaluate. If None, all isosurfaces will be evaluated. Returns -------- Either a list of isosurface values or a dictionary of isosurface names and corresponding values (if `values` is None). """ if values is None: keys = list(self.isosurfaces.keys()) out = {} else: keys = [values] if isinstance(values, int) or isinstance(values, float) or isinstance( values, str) else values out = [] for k in keys: if not isinstance(k, str): v = k # someone passed us a value directly; but don't complain else: assert k in self.isosurfaces, f"Isosurface '{k}' not found." v = self.isosurfaces[k] if isinstance(v, np.ndarray) or isinstance(v, list): v = np.array(v) if len(v.shape) == 1: v = v[None, :] # apply offset if specified if offset != 0: g = self.gradient( v, normalize=True ) v = v + g * offset # offset seed points by specified distance in gradient direction # evaluate (and average) value at seed points i = np.mean( self.predict( v )[:, 0] ) else: i = v # easy! if offset != 0: # not so easy... i = i+offset * self.field.mnorm # WARNING: this is a crude hack that assumes a constant gradient if values is None: out[k] = i else: out.append(i) return out def buffer(self, x : np.ndarray, isovalue, width : float, mask = None): """ This method evaluates whether the predicted values for the input `x` fall within a range defined by the isovalue and the specified buffer width. Optionally, a mask can be applied to exclude certain elements from the output. Parameters: ----------- x : np.ndarray The input data for which predictions will be made. isovalue : str | float | tuple The isosurface (name or value) around which the buffer is calculated. If a tuple is passed, values between isovalue[0] and isovalue[1] will be returned. width : float The width of the buffer around the isovalue. mask : np.ndarray or None, optional A boolean mask array of the same shape as `x` that specifies elements to exclude from the output. If `None`, no masking is applied. Default is `None`. Returns: -------- np.ndarray A boolean array indicating whether each element in `x` falls within the buffer range. Elements excluded by the mask are set to `False`. """ pred = self.predict(x)[:,0] # get values at points if isinstance(isovalue, tuple) or isinstance(isovalue, list): i0 = min(isovalue) # easy! i1 = max(isovalue) else: i0 = self.getIsovalue( isovalue, offset=-width ) # calculate lower buffer isovalue i1 = self.getIsovalue( isovalue, offset=width ) # calculate upper buffer isovalue out = (pred >= min(i0, i1)) & (pred <= max(i0, i1)) if mask is not None: out[mask] = False return out def classify( self, x: np.ndarray, return_vals=False, **kwds ): """ Evaluate the model using `self.predict(x, **kwds)` and then bin the resulting scalar values according to the isosurfaces associated with this scalar field. Parameters ---------- x : np.ndarray And (N,d) array containing the locations at which to evaluate the model. return_vals : bool True if evaluated scalar values should be returned as well as classes. Default is False. Returns ------- class_ids : np.ndarray Class ID's after classifying according to the defined isosurfaces. class_names : np.ndarray Array of unit names corresponding to the above class_ids. """ # get and sort isovalues iso = np.array( list(self.getIsovalues().values()) ) ixx = np.argsort(iso) inames = np.array( list(self.getIsovalues().keys()) )[ixx] pred = self.predict(x, **kwds) cids = np.digitize( pred, bins=iso[ixx] ) # return if return_vals: return cids, inames, pred else: return cids, inames
A linked list of scalar fields, allowing them to be chained so that they combine into time-aware geological models.
Initialise a new SF.
Parameters
name
:str
- A name for this event.
child
:SF
- A younger (child) SF instance to link this SF to. Any deformation caused by the younger SF will then be removed prior to evaluation of this one.
bound
:float, tuple, None
- A float specifying the minimum value at which this SF overprints parent ones (i.e. unconformities), or a (min, max) range if the overprint occur over a specific range only (dykes, sills, veins). If None, no overprinting will occur and the scalar value outputs ignored (but displacements still applied, i.e. faults).
deformation
:function, None
- A function such that f(X1, self) returns an array X0 of shape (N,ndim) that represents the pre-deformation coordinates of X1.
deformation_args
:dict
- A dictionary of arguments to pass to the deformation function, if needed.
sharpness
:float
- The sharpness of the sigmoid function used to combine this SF with results from parent fields.
Keywords
All keywords will be passed to
NF(…)
.Methods
def addIsosurface(self, name: str, *, value=None, seed=None)
-
Expand source code
def addIsosurface( self, name :str, *, value = None, seed = None): """ Add a (geologically meaningful) isosurfcae to this scalar field. These represent e.g., stratigraphic contacts. Note that isosurfaces can be defined in two ways: 1. by specifying their value directly (`value=x`) 2. by specifying a location (`seed_point`) at with the scalar field should be evaluated to determine the scalar value. Parameters ---------- name : str A friendly name used to refer to this isosurface. value : float, None A value to explicitely set the isosurface value. seed : np.ndarray, None A position (x,y,[z]) or list of positions that implicitly define the isosurface value. Whatever value is returned by the model at this location will be used to define the isosurface value). If several points are provided (e.g., known contact locations), the mean of their outputs used to determine the isosurface value. """ assert (seed is None) or (value is None), "Either seed or value should be defined, not both." assert not( (seed is None) and (value is None)), "Either seed or value should be defined, not both." if seed is not None: self.isosurfaces[name] = np.array( seed ) if value is not None: self.isosurfaces[name] = value
Add a (geologically meaningful) isosurfcae to this scalar field. These represent e.g., stratigraphic contacts.
Note that isosurfaces can be defined in two ways:
- by specifying their value directly (
value=x
) - by specifying a location (
seed_point
) at with the scalar field should be evaluated to determine the scalar value.
Parameters
name
:str
- A friendly name used to refer to this isosurface.
value
:float, None
- A value to explicitely set the isosurface value.
seed
:np.ndarray, None
-
A position (x,y,[z]) or list of positions that implicitly define the isosurface value. Whatever value is returned by the model at this location will be used to define the isosurface value).
If several points are provided (e.g., known contact locations), the mean of their outputs used to determine the isosurface value.
- by specifying their value directly (
def buffer(self, x: numpy.ndarray, isovalue, width: float, mask=None)
-
Expand source code
def buffer(self, x : np.ndarray, isovalue, width : float, mask = None): """ This method evaluates whether the predicted values for the input `x` fall within a range defined by the isovalue and the specified buffer width. Optionally, a mask can be applied to exclude certain elements from the output. Parameters: ----------- x : np.ndarray The input data for which predictions will be made. isovalue : str | float | tuple The isosurface (name or value) around which the buffer is calculated. If a tuple is passed, values between isovalue[0] and isovalue[1] will be returned. width : float The width of the buffer around the isovalue. mask : np.ndarray or None, optional A boolean mask array of the same shape as `x` that specifies elements to exclude from the output. If `None`, no masking is applied. Default is `None`. Returns: -------- np.ndarray A boolean array indicating whether each element in `x` falls within the buffer range. Elements excluded by the mask are set to `False`. """ pred = self.predict(x)[:,0] # get values at points if isinstance(isovalue, tuple) or isinstance(isovalue, list): i0 = min(isovalue) # easy! i1 = max(isovalue) else: i0 = self.getIsovalue( isovalue, offset=-width ) # calculate lower buffer isovalue i1 = self.getIsovalue( isovalue, offset=width ) # calculate upper buffer isovalue out = (pred >= min(i0, i1)) & (pred <= max(i0, i1)) if mask is not None: out[mask] = False return out
This method evaluates whether the predicted values for the input
x
fall within a range defined by the isovalue and the specified buffer width. Optionally, a mask can be applied to exclude certain elements from the output.Parameters:
x : np.ndarray The input data for which predictions will be made. isovalue : str | float | tuple The isosurface (name or value) around which the buffer is calculated. If a tuple is passed, values between isovalue[0] and isovalue[1] will be returned. width : float The width of the buffer around the isovalue. mask : np.ndarray or None, optional A boolean mask array of the same shape as
x
that specifies elements to exclude from the output. IfNone
, no masking is applied. Default isNone
.Returns:
np.ndarray A boolean array indicating whether each element in
x
falls within the buffer range. Elements excluded by the mask are set toFalse
. def classify(self, x: numpy.ndarray, return_vals=False, **kwds)
-
Expand source code
def classify( self, x: np.ndarray, return_vals=False, **kwds ): """ Evaluate the model using `self.predict(x, **kwds)` and then bin the resulting scalar values according to the isosurfaces associated with this scalar field. Parameters ---------- x : np.ndarray And (N,d) array containing the locations at which to evaluate the model. return_vals : bool True if evaluated scalar values should be returned as well as classes. Default is False. Returns ------- class_ids : np.ndarray Class ID's after classifying according to the defined isosurfaces. class_names : np.ndarray Array of unit names corresponding to the above class_ids. """ # get and sort isovalues iso = np.array( list(self.getIsovalues().values()) ) ixx = np.argsort(iso) inames = np.array( list(self.getIsovalues().keys()) )[ixx] pred = self.predict(x, **kwds) cids = np.digitize( pred, bins=iso[ixx] ) # return if return_vals: return cids, inames, pred else: return cids, inames
Evaluate the model using
self.predict(x, **kwds)
and then bin the resulting scalar values according to the isosurfaces associated with this scalar field.Parameters
x
:np.ndarray
- And (N,d) array containing the locations at which to evaluate the model.
return_vals
:bool
- True if evaluated scalar values should be returned as well as classes. Default is False.
Returns
class_ids
:np.ndarray
- Class ID's after classifying according to the defined isosurfaces.
class_names
:np.ndarray
- Array of unit names corresponding to the above class_ids.
def copy(self)
-
Expand source code
def copy(self): """ Create a copy of this SF object for e.g., incorporation into a different model. Note that this will unlink any parent, parent2 and child relations. """ out = copy.deepcopy(self) out.parent = None out.parent2 = None out.child = None return out
Create a copy of this SF object for e.g., incorporation into a different model. Note that this will unlink any parent, parent2 and child relations.
def deform(self, x: torch.Tensor) ‑> torch.Tensor
-
Expand source code
def deform(self, x: torch.Tensor) -> torch.Tensor: """ Apply deformation (displacements) to the passed set of coordinates. Translates from paleo-coordinates (i.e. coordinates at the time of this event) to present-day coordinates, for example, by applying child fault offsets. Parameters ---------- x : torch.Tensor, np.ndarray A tensor of shape (N, input_dim). Returns ------- torch.Tensor A tensor of shape (N, input_dim), containing the deformed coordinates. """ tonp = False if isinstance( x, np.ndarray ): x = torch.tensor( x, device=curlew.device, dtype=curlew.dtype) tonp = True # apply any child deformation if self.child is not None: x = self.child.deform( x ) # handle our own displacement out = x if self._displace is not None: out = self._displace(x, inverse=True) # apply this SF's displacement (if defined). # cast to numpy if needed if tonp: return out.cpu().detach().numpy() else: return out
Apply deformation (displacements) to the passed set of coordinates.
Translates from paleo-coordinates (i.e. coordinates at the time of this event) to present-day coordinates, for example, by applying child fault offsets.
Parameters
x
:torch.Tensor, np.ndarray
- A tensor of shape (N, input_dim).
Returns
torch.Tensor
- A tensor of shape (N, input_dim), containing the deformed coordinates.
def deleteIsosurface(self, name)
-
Expand source code
def deleteIsosurface( self, name ): """ Remove the specified isosurface from this SF's `isosurfaces` dict, if it exists. """ if name in self.isosurfaces: del self.isosurfaces[name]
Remove the specified isosurface from this SF's
isosurfaces
dict, if it exists. def displacement(self, x: numpy.ndarray, inverse=False) ‑> numpy.ndarray
-
Expand source code
def displacement(self, x: np.ndarray, inverse=False) -> np.ndarray: """ Return the deformation vectors associated with this SF at the specified locations. These are the displacements that would be remoed during `_displace(...)`. Parameters ---------- x : np.ndarray | torch.Tensor An array of shape (N, input_dim) containing the modern-day coordinates at which to evaluate this (and previous) SF. inverse : bool If True, apply the inverse deformation (i.e. from paleo-coordinates to modern-day coordinates). Default is False, which applies the deformation from modern-day coordinates to paleo-coordinates. Returns ------- np.ndarray | torch.Tensor An array of shape (N, input_dim) containing the deformation vectors at the specified locations. If `inverse` is True, these vectors represent the deformation from paleo-coordinates to modern-day coordinates. Otherwise, they represent the deformation from modern-day coordinates to paleo-coordinates. """ tonp = False if not isinstance(x, torch.Tensor): x = torch.tensor(x, dtype=curlew.dtype, device=curlew.device) tonp = True offset = self.deformation(x, self, **self.deformation_args) # apply deformation function if inverse: # flip deformation vectors (B to A instead of A to B) offset = -offset if tonp: # cast to numpy if needed return offset.detach().cpu().numpy() # return as numpy array else: return offset
Return the deformation vectors associated with this SF at the specified locations. These are the displacements that would be remoed during
_displace(…)
.Parameters
x
:np.ndarray | torch.Tensor
- An array of shape (N, input_dim) containing the modern-day coordinates at which to evaluate this (and previous) SF.
inverse
:bool
- If True, apply the inverse deformation (i.e. from paleo-coordinates to modern-day coordinates). Default is False, which applies the deformation from modern-day coordinates to paleo-coordinates.
Returns
np.ndarray | torch.Tensor
- An array of shape (N, input_dim) containing the deformation vectors at the specified locations.
If
inverse
is True, these vectors represent the deformation from paleo-coordinates to modern-day coordinates. Otherwise, they represent the deformation from modern-day coordinates to paleo-coordinates.
def fit(self, epochs, cache=True, faultBuffer=0, **kwargs)
-
Expand source code
def fit(self, epochs, cache=True, faultBuffer=0, **kwargs): """ Utility function for training fields in isolation. Parameters ---------- epochs : int The number of epochs to train for. cache : bool | optional If True, displacements applied by younger SF instances (linked to this one through `self.child`) will be pre-computed to speed up training (by avoiding repeated evaluations of younger fields that will not change). faultBuffer : int | optional If greater that zero, constraints within this distance of younger faults will be removed (as these can often be reconstructed to misleading positions and are very sensitive to interpolation errors in younger fields). Keywords ---------- batch_size : int The size of the batches used when retro-deforming the passed grid (to save RAM). Default is 50000. All other keywords are passed to `curlew.fields.NF.fit(...)`. These include: learning_rate : float, optional Reset each SF's optimiser to the specified learning rate before training. best : bool, optional After training set neural field weights to the best loss. vb : bool, optional Display a tqdm progress bar to monitor training. Returns ------- loss : float The loss of the final (best if best=True) model state. details : dict A more detailed breakdown of the final loss. """ # get constraints set to use if 'C' in kwargs: self.field.bind( kwargs.pop('C', self.field.C) ) C = self.field.C # enable caching to avoid lots # of evaluations of displacement fields # that will not change C0 = C # no need to reconstruct if cache and self.child is not None: if faultBuffer > 0: from curlew.geology.interactions import faultOffset def buffer( arr ): # quick function for masking points on faults f = self.child mask = np.full( len(arr), True ) while f is not None: if ((f.deformation is not None) and (f.deformation == faultOffset)): # faults b = f.buffer( arr, f.deformation_args['contact'], faultBuffer ) elif (f.parent2 is not None): b = f.buffer( arr, f.bound, faultBuffer ) # domain boundary mask[ b ] = False # identify and remove points within buffer distance of a fault f = f.child return mask C0.filter(buffer) # retro-deform other constraints C0 = C.numpy().transform( self.child.undeform ) # fit try: out = self.field.fit(epochs, C=C0, transform=False, **kwargs) finally: if C0.grid is not None: C0.grid._clearCache() self.field.bind(C) # ensure this always runs! (e.g., in case of keyboard interrupts) return out
Utility function for training fields in isolation.
Parameters
epochs
:int
- The number of epochs to train for.
cache
:bool | optional
- If True, displacements applied by younger SF instances (linked to this one through
self.child
) will be pre-computed to speed up training (by avoiding repeated evaluations of younger fields that will not change). faultBuffer
:int | optional
- If greater that zero, constraints within this distance of younger faults will be removed (as these can often be reconstructed to misleading positions and are very sensitive to interpolation errors in younger fields).
Keywords
batch_size : int The size of the batches used when retro-deforming the passed grid (to save RAM). Default is 50000.
All other keywords are passed to
NF.fit()(…)
. These include: learning_rate : float, optional Reset each SF's optimiser to the specified learning rate before training. best : bool, optional After training set neural field weights to the best loss. vb : bool, optional Display a tqdm progress bar to monitor training.Returns
loss
:float
- The loss of the final (best if best=True) model state.
details
:dict
- A more detailed breakdown of the final loss.
def forward(self, x: torch.Tensor, undef=True) ‑> torch.Tensor
-
Expand source code
def forward(self, x: torch.Tensor, undef=True) -> torch.Tensor: """ Forward pass of the network to create a scalar value or property estimate. If random Fourier features are enabled, the input is first encoded accordingly. This calls the underlying field's `forward(..)` function, but after calling `self.undeform` to underform constraint positions prior to interpolation. Parameters ---------- x : torch.Tensor A tensor of shape (N, input_dim), where N is the batch size. undef : bool True if x is specified in undeformed (modern-day) coordinates. Default is True. Returns ------- torch.Tensor A tensor of shape (N, output_dim), representing the scalar potential. """ # set transform that removes any child deformation if undef and (self.child is not None): self.field.transform = functools.partial(apply_child_undeform, sf=self) # apply this SF's undeform function prior to field.forward. #x = self.child.undeform( x ) else: self.field.transform = None # don't do any transform in field.forward # evaluate the underlying field return self.field.forward( x )
Forward pass of the network to create a scalar value or property estimate. If random Fourier features are enabled, the input is first encoded accordingly.
This calls the underlying field's
forward(..)
function, but after callingself.undeform
to underform constraint positions prior to interpolation.Parameters
x
:torch.Tensor
- A tensor of shape (N, input_dim), where N is the batch size.
undef
:bool
- True if x is specified in undeformed (modern-day) coordinates. Default is True.
Returns
torch.Tensor
- A tensor of shape (N, output_dim), representing the scalar potential.
def getIsovalue(self, name, offset=0)
-
Expand source code
def getIsovalue( self, name, offset=0): """ Return the value of a specific isosurface associated with this scalar field. """ return self.getIsovalues( [name], offset=offset )[0]
Return the value of a specific isosurface associated with this scalar field.
def getIsovalues(self, values=None, offset=0)
-
Expand source code
def getIsovalues( self, values=None, offset=0 ): """ Evaluate (if necessary) and return the values of all the isosurfaces associated with this scalar field. Parameters ---------- values : list, None A list of isosurface names to evaluate. If None, all isosurfaces will be evaluated. Returns -------- Either a list of isosurface values or a dictionary of isosurface names and corresponding values (if `values` is None). """ if values is None: keys = list(self.isosurfaces.keys()) out = {} else: keys = [values] if isinstance(values, int) or isinstance(values, float) or isinstance( values, str) else values out = [] for k in keys: if not isinstance(k, str): v = k # someone passed us a value directly; but don't complain else: assert k in self.isosurfaces, f"Isosurface '{k}' not found." v = self.isosurfaces[k] if isinstance(v, np.ndarray) or isinstance(v, list): v = np.array(v) if len(v.shape) == 1: v = v[None, :] # apply offset if specified if offset != 0: g = self.gradient( v, normalize=True ) v = v + g * offset # offset seed points by specified distance in gradient direction # evaluate (and average) value at seed points i = np.mean( self.predict( v )[:, 0] ) else: i = v # easy! if offset != 0: # not so easy... i = i+offset * self.field.mnorm # WARNING: this is a crude hack that assumes a constant gradient if values is None: out[k] = i else: out.append(i) return out
Evaluate (if necessary) and return the values of all the isosurfaces associated with this scalar field.
Parameters
values
:list, None
- A list of isosurface names to evaluate. If None, all isosurfaces will be evaluated.
Returns
Either a list
ofisosurface values
ora dictionary
ofisosurface names and corresponding
values (if
values
is None). def gradient(self, x: numpy.ndarray, return_vals=False, normalize=True, **kwds)
-
Expand source code
def gradient(self, x: np.ndarray, return_vals=False, normalize=True, **kwds): """ Return the gradient vector of this SF at the specified location. Note that this does not combine the results from previous scalar fields first (i.e. the prediction is done using `combine=False`). Parameters ---------- x : np.ndarray An array of shape (N, input_dim) containing the modern-day coordinates at which to evaluate this (and previous) SF. return_vals : bool True if evaluated scalar values should be returned as well as classes. Default is False. normalize : bool True if gradient vectors should be normalised to length 1 (i.e. to represent poles to planes). Default is True. Keywords --------- Keywords are all passed to `SF.predict(...)`. Returns -------- Gradient vectors at the specified locations (`x`). If `return_vals` is `True`, tuple (gradients, values) will be returned. """ if not isinstance(x, torch.Tensor): x = torch.tensor(x, dtype=curlew.dtype, device=curlew.device, requires_grad=True) # generate predictions to_numpy = kwds.pop('to_numpy', True) pred = self.predict(x, to_numpy=False, **kwds) # keep this as torch tensor # get gradients grad_out = torch.autograd.grad( outputs=pred[:,0], # we only care about scalar values inputs=x, grad_outputs=torch.ones_like(pred[:,0]), create_graph=True, retain_graph=False )[0] # normalise gradients if normalize: norm = torch.norm(grad_out, dim=-1, keepdim=True) + 1e-8 grad_out = grad_out / norm if to_numpy: grad_out = grad_out.cpu().detach().numpy() pred = pred.cpu().detach().numpy() if return_vals: return grad_out, pred else: return grad_out
Return the gradient vector of this SF at the specified location. Note that this does not combine the results from previous scalar fields first (i.e. the prediction is done using
combine=False
).Parameters
x
:np.ndarray
- An array of shape (N, input_dim) containing the modern-day coordinates at which to evaluate this (and previous) SF.
return_vals
:bool
- True if evaluated scalar values should be returned as well as classes. Default is False.
normalize
:bool
- True if gradient vectors should be normalised to length 1 (i.e. to represent poles to planes). Default is True.
Keywords
Keywords are all passed to
SF.predict()(…)
.Returns
Gradient vectors at the specified locations (
x
). Ifreturn_vals
isTrue
, tuple (gradients, values) will be returned. def predict(self, x: numpy.ndarray, combine=False, to_numpy=True, transform=True) ‑> numpy.ndarray
-
Expand source code
def predict(self, x: np.ndarray, combine=False, to_numpy=True, transform=True) -> np.ndarray: """ Predict scalar values belonging to this and/or previous SFs. Parameters ---------- x : np.ndarray An array of shape (N, input_dim) containing the modern-day coordinates at which to evaluate this (and previous) SF. combine: bool True if previous scalar fields should be aggregated to give a combined scalar field output. Otherwise, evaluate the scalar field related to this specific structure alone. Default is False. to_numpy : bool True if the results should be cast to a numpy array rather than a `torch.Tensor`. transform : bool True if the coordinates should be undeformed (i.e. represent modern-day coordinates) or not (i.e. represent paleo-coordinates). """ # evaluate field and combine with parent ones if combine and (self.parent is not None): if self.parent2 is not None: # this field represents a domain boundary -- essentially use as a mask # predict value of domain scalar field domain = self.predict( x, combine=False, to_numpy=False, transform=transform ) # evaluate isosurfaces to get threshold values, if needed (allows self.bound to containt str isosurface names) assert self.bound is not None, "Isosurface must be defined for domain boundary." thresh = self.getIsovalue(self.bound) # this thresh separates the domain into two halves mask = torch.sigmoid(self.sharpness * (domain - thresh))[:,0] # fill in each side of domain [ above and below thresh ] out = torch.zeros((len(domain), 2), dtype=curlew.dtype, device=curlew.device) for sf, m in zip([self.parent, self.parent2], [mask, (1-mask)]): if not isinstance( sf, SF ): # this side of domain boundary is a constant value (float or int) out = out + m[:,None] * sf else: # this side of domain boundary is a modelled value parent = sf.predict( x, combine=True, to_numpy=False, transform=transform ) out = out + m[:,None] * parent else: # this field is one in a series of geological "events" -- evaluate accordingly if isinstance(self.parent, int) or isinstance(self.parent, float): # int or float parent out = torch.full((len(x), 2), self.parent, device=curlew.device, dtype=curlew.dtype) # create constant (float or int) value parent else: # parent is a scalar field (or some variety thereof) out = self.parent.predict( x, combine=True, to_numpy=False, transform=transform ) if self.bound is not None: # if bound is None we don't change previous values as this is not a generative event sf1 = self.predict( x, combine=False, to_numpy=False, transform=transform ) thresh = self.getIsovalues(values=self.bound) # evaluate isosurfaces to get threshold values, if needed (allows self.bound to containt str isosurface names) out = overprint( out, sf1, eid=self.eid, thresh=thresh, sharpness=self.sharpness ) else: # simply evaluate field! :-) if not isinstance(x, torch.Tensor): x = torch.tensor( x, device=curlew.device, dtype=curlew.dtype) out = torch.full((len(x), 2), self.eid, device=curlew.device, dtype=curlew.dtype) # create output with event ID out[:,0] = self.forward( x, undef=transform ).squeeze() # populate scalar value part # return if to_numpy: out = out.cpu().detach().numpy() out[:,1] = out[:,1].astype(int) # ensure that the event ID is an integer return out
Predict scalar values belonging to this and/or previous SFs.
Parameters
x
:np.ndarray
- An array of shape (N, input_dim) containing the modern-day coordinates at which to evaluate this (and previous) SF.
combine
:bool
- True if previous scalar fields should be aggregated to give a combined scalar field output. Otherwise, evaluate the scalar field related to this specific structure alone. Default is False.
to_numpy
:bool
- True if the results should be cast to a numpy array rather than a
torch.Tensor
. transform
:bool
- True if the coordinates should be undeformed (i.e. represent modern-day coordinates) or not (i.e. represent paleo-coordinates).
def undeform(self, x: torch.Tensor) ‑> torch.Tensor
-
Expand source code
def undeform(self, x: torch.Tensor) -> torch.Tensor: """ Remove deformation (displacements) from the passed set of coordinates. Translates from present-day coordinates to coordinates relevant for this SF by removing, for example, child fault offsets. Parameters ---------- x : torch.Tensor, np.ndarray A tensor of shape (N, input_dim). Returns ------- torch.Tensor A tensor of shape (N, input_dim), containing the undeformed coordinates. """ if (self.child is None) and (self._displace is None): return x # easy # evaluate displacement tonp = False if isinstance( x, np.ndarray ): x = torch.tensor( x, device=curlew.device, dtype=curlew.dtype) tonp = True # remove any child deformation if self.child is not None: x = self.child.undeform( x ) # undeform to the time-step relevant for this SF # handle our own displacement out = x if self._displace is not None: out = self._displace(x) # apply this SF's displacement (if defined). # cast to numpy if needed if tonp: return out.cpu().detach().numpy() else: return out
Remove deformation (displacements) from the passed set of coordinates.
Translates from present-day coordinates to coordinates relevant for this SF by removing, for example, child fault offsets.
Parameters
x
:torch.Tensor, np.ndarray
- A tensor of shape (N, input_dim).
Returns
torch.Tensor
- A tensor of shape (N, input_dim), containing the undeformed coordinates.