LoopStructural 1.6.14__py3-none-any.whl → 1.6.16__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of LoopStructural might be problematic. Click here for more details.

Files changed (30) hide show
  1. LoopStructural/__init__.py +30 -12
  2. LoopStructural/datatypes/_bounding_box.py +22 -13
  3. LoopStructural/datatypes/_point.py +0 -1
  4. LoopStructural/export/exporters.py +2 -2
  5. LoopStructural/interpolators/__init__.py +33 -30
  6. LoopStructural/interpolators/_constant_norm.py +205 -0
  7. LoopStructural/interpolators/_discrete_interpolator.py +15 -14
  8. LoopStructural/interpolators/_finite_difference_interpolator.py +10 -10
  9. LoopStructural/interpolators/_geological_interpolator.py +9 -3
  10. LoopStructural/interpolators/_interpolatortype.py +22 -0
  11. LoopStructural/interpolators/_p1interpolator.py +6 -2
  12. LoopStructural/interpolators/_surfe_wrapper.py +4 -1
  13. LoopStructural/interpolators/supports/_2d_base_unstructured.py +1 -1
  14. LoopStructural/interpolators/supports/_2d_structured_grid.py +16 -0
  15. LoopStructural/interpolators/supports/_3d_base_structured.py +16 -0
  16. LoopStructural/interpolators/supports/_3d_structured_tetra.py +7 -3
  17. LoopStructural/modelling/core/geological_model.py +250 -312
  18. LoopStructural/modelling/core/stratigraphic_column.py +473 -0
  19. LoopStructural/modelling/features/_base_geological_feature.py +38 -2
  20. LoopStructural/modelling/features/builders/_fault_builder.py +1 -0
  21. LoopStructural/modelling/features/builders/_geological_feature_builder.py +1 -1
  22. LoopStructural/modelling/features/fault/_fault_segment.py +1 -1
  23. LoopStructural/modelling/intrusions/intrusion_builder.py +1 -1
  24. LoopStructural/modelling/intrusions/intrusion_frame_builder.py +1 -1
  25. LoopStructural/version.py +1 -1
  26. {loopstructural-1.6.14.dist-info → loopstructural-1.6.16.dist-info}/METADATA +2 -2
  27. {loopstructural-1.6.14.dist-info → loopstructural-1.6.16.dist-info}/RECORD +30 -27
  28. {loopstructural-1.6.14.dist-info → loopstructural-1.6.16.dist-info}/WHEEL +0 -0
  29. {loopstructural-1.6.14.dist-info → loopstructural-1.6.16.dist-info}/licenses/LICENSE +0 -0
  30. {loopstructural-1.6.14.dist-info → loopstructural-1.6.16.dist-info}/top_level.txt +0 -0
@@ -2,11 +2,11 @@
2
2
  Main entry point for creating a geological model
3
3
  """
4
4
 
5
- from ...utils import getLogger, log_to_file
5
+ from ...utils import getLogger
6
6
 
7
7
  import numpy as np
8
8
  import pandas as pd
9
- from typing import List
9
+ from typing import List, Optional
10
10
  import pathlib
11
11
  from ...modelling.features.fault import FaultSegment
12
12
 
@@ -37,7 +37,7 @@ from ...datatypes import BoundingBox
37
37
  from ...modelling.intrusions import IntrusionBuilder
38
38
 
39
39
  from ...modelling.intrusions import IntrusionFrameBuilder
40
-
40
+ from .stratigraphic_column import StratigraphicColumn
41
41
 
42
42
  logger = getLogger(__name__)
43
43
 
@@ -61,33 +61,21 @@ class GeologicalModel:
61
61
  the origin of the model box
62
62
  parameters : dict
63
63
  a dictionary tracking the parameters used to build the model
64
- scale_factor : double
65
- the scale factor used to rescale the model
66
64
 
67
65
 
68
66
  """
69
67
 
70
- def __init__(
71
- self,
72
- origin: np.ndarray,
73
- maximum: np.ndarray,
74
- data=None,
75
- nsteps=(50, 50, 25),
76
- reuse_supports=False,
77
- logfile=None,
78
- loglevel="info",
79
- ):
68
+ def __init__(self, *args):
80
69
  """
81
70
  Parameters
82
71
  ----------
83
- origin : numpy array
84
- specifying the origin of the model
85
- maximum : numpy array
86
- specifying the maximum extent of the model
87
- rescale : bool
88
- whether to rescale the model to between 0/1
89
- epsion : float
90
- a fudge factor for isosurfacing, used to make sure surfaces appear
72
+ bounding_box : BoundingBox
73
+ the bounding box of the model
74
+ origin : np.array(3,dtype=doubles)
75
+ the origin of the model
76
+ maximum : np.array(3,dtype=doubles)
77
+ the maximum of the model
78
+
91
79
  Examples
92
80
  --------
93
81
  Demo data
@@ -111,39 +99,32 @@ class GeologicalModel:
111
99
 
112
100
 
113
101
  """
114
- if logfile:
115
- self.logfile = logfile
116
- log_to_file(logfile, level=loglevel)
117
-
102
+ args = list(args)
103
+ if len(args) == 0:
104
+ raise ValueError("Must provide either bounding_box or origin and maximum")
105
+ if len(args) == 1:
106
+ bounding_box = args[0]
107
+ if not isinstance(bounding_box, BoundingBox):
108
+ raise ValueError("Must provide a bounding box")
109
+ self.bounding_box = bounding_box
110
+ if len(args) == 2:
111
+ origin = np.array(args[0])
112
+ maximum = np.array(args[1])
113
+ if not isinstance(origin, np.ndarray) or not isinstance(maximum, np.ndarray):
114
+ raise ValueError("Must provide origin and maximum as numpy arrays")
115
+ self.bounding_box = BoundingBox(
116
+ dimensions=3,
117
+ origin=np.zeros(3),
118
+ maximum=maximum - origin,
119
+ global_origin=origin,
120
+ )
118
121
  logger.info("Initialising geological model")
119
122
  self.features = []
120
123
  self.feature_name_index = {}
121
124
  self._data = pd.DataFrame() # None
122
- if data is not None:
123
- self.data = data
124
- self.nsteps = nsteps
125
-
126
- # we want to rescale the model area so that the maximum length is
127
- # 1
128
- self.origin = np.array(origin).astype(float)
129
- originstr = f"Model origin: {self.origin[0]} {self.origin[1]} {self.origin[2]}"
130
- logger.info(originstr)
131
- self.maximum = np.array(maximum).astype(float)
132
- maximumstr = "Model maximum: {} {} {}".format(
133
- self.maximum[0], self.maximum[1], self.maximum[2]
134
- )
135
- logger.info(maximumstr)
136
-
137
- self.scale_factor = 1.0
138
125
 
139
- self.bounding_box = BoundingBox(
140
- dimensions=3,
141
- origin=np.zeros(3),
142
- maximum=self.maximum - self.origin,
143
- global_origin=self.origin,
144
- )
126
+ self.stratigraphic_column = StratigraphicColumn()
145
127
 
146
- self.stratigraphic_column = None
147
128
 
148
129
  self.tol = 1e-10 * np.max(self.bounding_box.maximum - self.bounding_box.origin)
149
130
  self._dtm = None
@@ -160,119 +141,83 @@ class GeologicalModel:
160
141
  json = {}
161
142
  json["model"] = {}
162
143
  json["model"]["features"] = [f.name for f in self.features]
163
- # json["model"]["data"] = self.data.to_json()
164
- # json["model"]["origin"] = self.origin.tolist()
165
- # json["model"]["maximum"] = self.maximum.tolist()
166
- # json["model"]["nsteps"] = self.nsteps
144
+ json['model']['bounding_box'] = self.bounding_box.to_dict()
167
145
  json["model"]["stratigraphic_column"] = self.stratigraphic_column
168
146
  # json["features"] = [f.to_json() for f in self.features]
169
147
  return json
170
148
 
171
- # @classmethod
172
- # def from_json(cls,json):
173
- # """
174
- # Create a geological model from a json string
175
-
176
- # Parameters
177
- # ----------
178
- # json : str
179
- # json string of the geological model
180
-
181
- # Returns
182
- # -------
183
- # model : GeologicalModel
184
- # a geological model
185
- # """
186
- # model = cls(json["model"]["origin"],json["model"]["maximum"],data=None)
187
- # model.stratigraphic_column = json["model"]["stratigraphic_column"]
188
- # model.nsteps = json["model"]["nsteps"]
189
- # model.data = pd.read_json(json["model"]["data"])
190
- # model.features = []
191
- # for feature in json["features"]:
192
- # model.features.append(GeologicalFeature.from_json(feature,model))
193
- # return model
194
149
  def __str__(self):
195
- lengths = self.maximum - self.origin
196
- _str = "GeologicalModel - {} x {} x {}\n".format(*lengths)
197
- _str += "------------------------------------------ \n"
198
- _str += "The model contains {} GeologicalFeatures \n".format(len(self.features))
199
- _str += ""
200
- _str += "------------------------------------------ \n"
201
- _str += ""
202
- _str += "Model origin: {} {} {}\n".format(self.origin[0], self.origin[1], self.origin[2])
203
- _str += "Model maximum: {} {} {}\n".format(
204
- self.maximum[0], self.maximum[1], self.maximum[2]
205
- )
206
- _str += "Model rescale factor: {} \n".format(self.scale_factor)
207
- _str += "------------------------------------------ \n"
208
- _str += "Feature list: \n"
209
- for feature in self.features:
210
- _str += " {} \n".format(feature.name)
211
- return _str
150
+ return f"GeologicalModel with {len(self.features)} features"
212
151
 
213
152
  def _ipython_key_completions_(self):
214
153
  return self.feature_name_index.keys()
215
154
 
216
- @classmethod
217
- def from_map2loop_directory(
218
- cls,
219
- m2l_directory,
220
- foliation_params={},
221
- fault_params={},
222
- use_thickness=True,
223
- vector_scale=1,
224
- gradient=False,
225
- **kwargs,
226
- ):
227
- """Alternate constructor for a geological model using m2l output
228
-
229
- Uses the information saved in the map2loop files to build a geological model.
230
- You can specify kwargs for building foliation using foliation_params and for
231
- faults using fault_params. faults is a flag that allows for the faults to be
232
- skipped.
233
-
234
- Parameters
235
- ----------
236
- m2l_directory : string
237
- path to map2loop directory
155
+ def prepare_data(self, data: pd.DataFrame) -> pd.DataFrame:
156
+ data = data.copy()
157
+ data[['X', 'Y', 'Z']] = self.bounding_box.project(data[['X', 'Y', 'Z']].to_numpy())
238
158
 
239
- Returns
240
- -------
241
- (GeologicalModel, dict)
242
- the created geological model and a dictionary of the map2loop data
243
-
244
- Notes
245
- ------
246
- For additional information see :class:`LoopStructural.modelling.input.Map2LoopProcessor`
247
- and :meth:`LoopStructural.GeologicalModel.from_processor`
248
- """
249
- from LoopStructural.modelling.input.map2loop_processor import Map2LoopProcessor
250
-
251
- log_to_file(f"{m2l_directory}/loopstructural_log.txt")
252
- logger.info("Creating model from m2l directory")
253
- processor = Map2LoopProcessor(m2l_directory, use_thickness)
254
- processor._gradient = gradient
255
- processor.vector_scale = vector_scale
256
- for foliation_name in processor.stratigraphic_column.keys():
257
- if foliation_name != "faults":
258
- if foliation_name in foliation_params.keys():
259
- processor.foliation_properties[foliation_name] = foliation_params[
260
- foliation_name
261
- ]
262
- else:
263
- processor.foliation_properties[foliation_name] = foliation_params
264
-
265
- for fault_name in processor.fault_names:
266
- if fault_name in fault_params.keys():
267
- for param_name, value in fault_params[fault_name].items():
268
- processor.fault_properties.loc[fault_name, param_name] = value
269
- else:
270
- for param_name, value in fault_params.items():
271
- processor.fault_properties.loc[fault_name, param_name] = value
159
+ if "type" in data:
160
+ logger.warning("'type' is deprecated replace with 'feature_name' \n")
161
+ data.rename(columns={"type": "feature_name"}, inplace=True)
162
+ if "feature_name" not in data:
163
+ logger.error("Data does not contain 'feature_name' column")
164
+ raise BaseException("Cannot load data")
165
+ for h in all_heading():
166
+ if h not in data:
167
+ data[h] = np.nan
168
+ if h == "w":
169
+ data[h] = 1.0
170
+ if h == "coord":
171
+ data[h] = 0
172
+ if h == "polarity":
173
+ data[h] = 1.0
174
+ # LS wants polarity as -1 or 1, change 0 to -1
175
+ data.loc[data["polarity"] == 0, "polarity"] = -1.0
176
+ data.loc[np.isnan(data["w"]), "w"] = 1.0
177
+ if "strike" in data and "dip" in data:
178
+ logger.info("Converting strike and dip to vectors")
179
+ mask = np.all(~np.isnan(data.loc[:, ["strike", "dip"]]), axis=1)
180
+ data.loc[mask, gradient_vec_names()] = (
181
+ strikedip2vector(data.loc[mask, "strike"], data.loc[mask, "dip"])
182
+ * data.loc[mask, "polarity"].to_numpy()[:, None]
183
+ )
184
+ data.drop(["strike", "dip"], axis=1, inplace=True)
185
+ data[['X', 'Y', 'Z', 'val', 'nx', 'ny', 'nz', 'gx', 'gy', 'gz', 'tx', 'ty', 'tz']] = data[
186
+ ['X', 'Y', 'Z', 'val', 'nx', 'ny', 'nz', 'gx', 'gy', 'gz', 'tx', 'ty', 'tz']
187
+ ].astype(float)
188
+ return data
272
189
 
273
- model = GeologicalModel.from_processor(processor)
274
- return model, processor
275
190
 
191
+ if "type" in data:
192
+ logger.warning("'type' is deprecated replace with 'feature_name' \n")
193
+ data.rename(columns={"type": "feature_name"}, inplace=True)
194
+ if "feature_name" not in data:
195
+ logger.error("Data does not contain 'feature_name' column")
196
+ raise BaseException("Cannot load data")
197
+ for h in all_heading():
198
+ if h not in data:
199
+ data[h] = np.nan
200
+ if h == "w":
201
+ data[h] = 1.0
202
+ if h == "coord":
203
+ data[h] = 0
204
+ if h == "polarity":
205
+ data[h] = 1.0
206
+ # LS wants polarity as -1 or 1, change 0 to -1
207
+ data.loc[data["polarity"] == 0, "polarity"] = -1.0
208
+ data.loc[np.isnan(data["w"]), "w"] = 1.0
209
+ if "strike" in data and "dip" in data:
210
+ logger.info("Converting strike and dip to vectors")
211
+ mask = np.all(~np.isnan(data.loc[:, ["strike", "dip"]]), axis=1)
212
+ data.loc[mask, gradient_vec_names()] = (
213
+ strikedip2vector(data.loc[mask, "strike"], data.loc[mask, "dip"])
214
+ * data.loc[mask, "polarity"].to_numpy()[:, None]
215
+ )
216
+ data.drop(["strike", "dip"], axis=1, inplace=True)
217
+ data[['X', 'Y', 'Z', 'val', 'nx', 'ny', 'nz', 'gx', 'gy', 'gz', 'tx', 'ty', 'tz']] = data[
218
+ ['X', 'Y', 'Z', 'val', 'nx', 'ny', 'nz', 'gx', 'gy', 'gz', 'tx', 'ty', 'tz']
219
+ ].astype(float)
220
+ return data
276
221
  @classmethod
277
222
  def from_processor(cls, processor):
278
223
  """Builds a model from a :class:`LoopStructural.modelling.input.ProcessInputData` object
@@ -464,12 +409,6 @@ class GeologicalModel:
464
409
  """
465
410
  return [f.name for f in self.faults]
466
411
 
467
- def check_inialisation(self):
468
- if self.data is None:
469
- logger.error("Data not associated with GeologicalModel. Run set_data")
470
- return False
471
- if self.data.shape[0] > 0:
472
- return True
473
412
 
474
413
  def to_file(self, file):
475
414
  """Save a model to a pickle file requires dill
@@ -543,14 +482,14 @@ class GeologicalModel:
543
482
  ----------
544
483
  data : pandas data frame
545
484
  with column headers corresponding to the
546
- type, X, Y, Z, nx, ny, nz, val, strike, dip, dip_dir, plunge,
485
+ feature_name, X, Y, Z, nx, ny, nz, val, strike, dip, dip_dir, plunge,
547
486
  plunge_dir, azimuth
548
487
 
549
488
  Returns
550
489
  -------
551
490
  Note
552
491
  ----
553
- Type can be any unique identifier for the feature the data point
492
+ feature_name can be any unique identifier for the feature the data point
554
493
  'eg' 'S0', 'S2', 'F1_axis'
555
494
  it is then used by the create functions to get the correct data
556
495
  """
@@ -565,48 +504,12 @@ class GeologicalModel:
565
504
  raise BaseException("Cannot load data")
566
505
  logger.info(f"Adding data to GeologicalModel with {len(data)} data points")
567
506
  self._data = data.copy()
507
+ # self._data[['X','Y','Z']] = self.bounding_box.project(self._data[['X','Y','Z']].to_numpy())
568
508
 
569
- self._data["X"] -= self.origin[0]
570
- self._data["Y"] -= self.origin[1]
571
- self._data["Z"] -= self.origin[2]
572
- self._data["X"] /= self.scale_factor
573
- self._data["Y"] /= self.scale_factor
574
- self._data["Z"] /= self.scale_factor
575
- if "type" in self._data:
576
- logger.warning("'type' is deprecated replace with 'feature_name' \n")
577
- self._data.rename(columns={"type": "feature_name"}, inplace=True)
578
- if "feature_name" not in self._data:
579
- logger.error("Data does not contain 'feature_name' column")
580
- raise BaseException("Cannot load data")
581
- for h in all_heading():
582
- if h not in self._data:
583
- self._data[h] = np.nan
584
- if h == "w":
585
- self._data[h] = 1.0
586
- if h == "coord":
587
- self._data[h] = 0
588
- if h == "polarity":
589
- self._data[h] = 1.0
590
- # LS wants polarity as -1 or 1, change 0 to -1
591
- self._data.loc[self._data["polarity"] == 0, "polarity"] = -1.0
592
- self._data.loc[np.isnan(self._data["w"]), "w"] = 1.0
593
- if "strike" in self._data and "dip" in self._data:
594
- logger.info("Converting strike and dip to vectors")
595
- mask = np.all(~np.isnan(self._data.loc[:, ["strike", "dip"]]), axis=1)
596
- self._data.loc[mask, gradient_vec_names()] = (
597
- strikedip2vector(self._data.loc[mask, "strike"], self._data.loc[mask, "dip"])
598
- * self._data.loc[mask, "polarity"].to_numpy()[:, None]
599
- )
600
- self._data.drop(["strike", "dip"], axis=1, inplace=True)
601
- self._data[['X', 'Y', 'Z', 'val', 'nx', 'ny', 'nz', 'gx', 'gy', 'gz', 'tx', 'ty', 'tz']] = (
602
- self._data[
603
- ['X', 'Y', 'Z', 'val', 'nx', 'ny', 'nz', 'gx', 'gy', 'gz', 'tx', 'ty', 'tz']
604
- ].astype(float)
605
- )
606
509
 
607
510
  def set_model_data(self, data):
608
511
  logger.warning("deprecated method. Model data can now be set using the data attribute")
609
- self.data = data
512
+ self.data = data.copy()
610
513
 
611
514
  def set_stratigraphic_column(self, stratigraphic_column, cmap="tab20"):
612
515
  """
@@ -629,32 +532,40 @@ class GeologicalModel:
629
532
  }
630
533
 
631
534
  """
535
+ self.stratigraphic_column.clear()
632
536
  # if the colour for a unit hasn't been specified we can just sample from
633
537
  # a colour map e.g. tab20
634
538
  logger.info("Adding stratigraphic column to model")
635
- random_colour = True
636
- n_units = 0
539
+ DeprecationWarning(
540
+ "set_stratigraphic_column is deprecated, use model.stratigraphic_column.add_units instead"
541
+ )
637
542
  for g in stratigraphic_column.keys():
638
543
  for u in stratigraphic_column[g].keys():
639
- if "colour" in stratigraphic_column[g][u]:
640
- random_colour = False
641
- break
642
- n_units += 1
643
- if random_colour:
644
- import matplotlib.cm as cm
645
-
646
- cmap = cm.get_cmap(cmap, n_units)
647
- cmap_colours = cmap.colors
648
- ci = 0
649
- for g in stratigraphic_column.keys():
650
- for u in stratigraphic_column[g].keys():
651
- stratigraphic_column[g][u]["colour"] = cmap_colours[ci, :]
652
- ci += 1
653
- self.stratigraphic_column = stratigraphic_column
544
+ thickness = 0
545
+ if "min" in stratigraphic_column[g][u] and "max" in stratigraphic_column[g][u]:
546
+ min_val = stratigraphic_column[g][u]["min"]
547
+ max_val = stratigraphic_column[g][u].get("max", None)
548
+ thickness = max_val - min_val if max_val is not None else None
549
+ logger.warning(
550
+ f"""
551
+ model.stratigraphic_column.add_unit({u},
552
+ colour={stratigraphic_column[g][u].get("colour", None)},
553
+ thickness={thickness})"""
554
+ )
555
+ self.stratigraphic_column.add_unit(
556
+ u,
557
+ colour=stratigraphic_column[g][u].get("colour", None),
558
+ thickness=thickness,
559
+ )
560
+ self.stratigraphic_column.add_unconformity(
561
+ name=''.join([g, 'unconformity']),
562
+ )
654
563
 
655
564
  def create_and_add_foliation(
656
565
  self,
657
- series_surface_data: str,
566
+ series_surface_name: str,
567
+ *,
568
+ series_surface_data: pd.DataFrame = None,
658
569
  interpolatortype: str = "FDI",
659
570
  nelements: int = 1000,
660
571
  tol=None,
@@ -664,8 +575,18 @@ class GeologicalModel:
664
575
  """
665
576
  Parameters
666
577
  ----------
667
- series_surface_data : string
578
+ series_surface_name : string
668
579
  corresponding to the feature_name in the data
580
+ series_surface_data : pd.DataFrame, optional
581
+ data frame containing the surface data
582
+ interpolatortype : str
583
+ the type of interpolator to use, default is 'FDI'
584
+ nelements : int
585
+ the number of elements to use in the series surface
586
+ tol : float, optional
587
+ tolerance for the solver, if not specified uses the model default
588
+ faults : list, optional
589
+ list of faults to be used in the series surface, if not specified uses the model faults
669
590
  kwargs
670
591
 
671
592
  Returns
@@ -685,9 +606,7 @@ class GeologicalModel:
685
606
  An interpolator will be chosen by calling :meth:`LoopStructural.GeologicalModel.get_interpolator`
686
607
 
687
608
  """
688
- if not self.check_inialisation():
689
- logger.warning(f"{series_surface_data} not added, model not initialised")
690
- return
609
+
691
610
  # if tol is not specified use the model default
692
611
  if tol is None:
693
612
  tol = self.tol
@@ -696,16 +615,18 @@ class GeologicalModel:
696
615
  bounding_box=self.bounding_box,
697
616
  interpolatortype=interpolatortype,
698
617
  nelements=nelements,
699
- name=series_surface_data,
618
+ name=series_surface_name,
700
619
  model=self,
701
620
  **kwargs,
702
621
  )
703
622
  # add data
704
- series_data = self.data[self.data["feature_name"] == series_surface_data]
705
- if series_data.shape[0] == 0:
623
+ if series_surface_data is None:
624
+ series_surface_data = self.data.loc[self.data["feature_name"] == series_surface_name]
625
+
626
+ if series_surface_data.shape[0] == 0:
706
627
  logger.warning("No data for {series_surface_data}, skipping")
707
628
  return
708
- series_builder.add_data_from_data_frame(series_data)
629
+ series_builder.add_data_from_data_frame(self.prepare_data(series_surface_data))
709
630
  self._add_faults(series_builder, features=faults)
710
631
 
711
632
  # build feature
@@ -721,7 +642,9 @@ class GeologicalModel:
721
642
 
722
643
  def create_and_add_fold_frame(
723
644
  self,
724
- foldframe_data,
645
+ fold_frame_name: str,
646
+ *,
647
+ fold_frame_data=None,
725
648
  interpolatortype="FDI",
726
649
  nelements=1000,
727
650
  tol=None,
@@ -731,18 +654,31 @@ class GeologicalModel:
731
654
  """
732
655
  Parameters
733
656
  ----------
734
- foldframe_data : string
657
+ fold_frame_name : string
735
658
  unique string in feature_name column
659
+ fold_frame_data : pandas data frame
660
+ if not specified uses the model data
661
+ interpolatortype : str
662
+ the type of interpolator to use, default is 'FDI'
663
+ nelements : int
664
+ the number of elements to use in the fold frame
665
+ tol : float, optional
666
+ tolerance for the solver
667
+ buffer : float
668
+ buffer to add to the bounding box of the fold frame
669
+ **kwargs : dict
670
+ additional parameters to be passed to the
671
+ :class:`LoopStructural.modelling.features.builders.StructuralFrameBuilder`
672
+ and :meth:`LoopStructural.modelling.features.builders.StructuralFrameBuilder.setup`
673
+ and the interpolator, such as `domain` or `tol`
736
674
 
737
- kwargs
738
675
 
739
676
  Returns
740
677
  -------
741
678
  fold_frame : FoldFrame
742
679
  the created fold frame
743
680
  """
744
- if not self.check_inialisation():
745
- return False
681
+
746
682
  if tol is None:
747
683
  tol = self.tol
748
684
 
@@ -751,15 +687,19 @@ class GeologicalModel:
751
687
  fold_frame_builder = StructuralFrameBuilder(
752
688
  interpolatortype=interpolatortype,
753
689
  bounding_box=self.bounding_box.with_buffer(buffer),
754
- name=foldframe_data,
690
+ name=fold_frame_name,
755
691
  frame=FoldFrame,
756
692
  nelements=nelements,
757
693
  model=self,
758
694
  **kwargs,
759
695
  )
760
696
  # add data
761
- fold_frame_data = self.data[self.data["feature_name"] == foldframe_data]
762
- fold_frame_builder.add_data_from_data_frame(fold_frame_data)
697
+ if fold_frame_data is None:
698
+ fold_frame_data = self.data.loc[self.data["feature_name"] == fold_frame_name]
699
+ if fold_frame_data.shape[0] == 0:
700
+ logger.warning(f"No data for {fold_frame_name}, skipping")
701
+ return
702
+ fold_frame_builder.add_data_from_data_frame(self.prepare_data(fold_frame_data))
763
703
  self._add_faults(fold_frame_builder[0])
764
704
  self._add_faults(fold_frame_builder[1])
765
705
  self._add_faults(fold_frame_builder[2])
@@ -775,7 +715,9 @@ class GeologicalModel:
775
715
 
776
716
  def create_and_add_folded_foliation(
777
717
  self,
778
- foliation_data,
718
+ foliation_name,
719
+ *,
720
+ foliation_data=None,
779
721
  interpolatortype="DFI",
780
722
  nelements=10000,
781
723
  buffer=0.1,
@@ -812,8 +754,7 @@ class GeologicalModel:
812
754
  :class:`LoopStructural.modelling.features.builders.FoldedFeatureBuilder`
813
755
 
814
756
  """
815
- if not self.check_inialisation():
816
- return False
757
+
817
758
  if tol is None:
818
759
  tol = self.tol
819
760
 
@@ -832,15 +773,18 @@ class GeologicalModel:
832
773
  bounding_box=self.bounding_box.with_buffer(buffer),
833
774
  nelements=nelements,
834
775
  fold=fold,
835
- name=foliation_data,
776
+ name=foliation_name,
836
777
  svario=svario,
837
778
  model=self,
838
779
  **kwargs,
839
780
  )
781
+ if foliation_data is None:
782
+ foliation_data = self.data.loc[self.data["feature_name"] == foliation_name]
783
+ if foliation_data.shape[0] == 0:
784
+ logger.warning(f"No data for {foliation_name}, skipping")
785
+ return
786
+ series_builder.add_data_from_data_frame(self.prepare_data(foliation_data))
840
787
 
841
- series_builder.add_data_from_data_frame(
842
- self.data[self.data["feature_name"] == foliation_data]
843
- )
844
788
  self._add_faults(series_builder)
845
789
  # series_builder.add_data_to_interpolator(True)
846
790
  # build feature
@@ -858,7 +802,9 @@ class GeologicalModel:
858
802
 
859
803
  def create_and_add_folded_fold_frame(
860
804
  self,
861
- fold_frame_data,
805
+ fold_frame_name: str,
806
+ *,
807
+ fold_frame_data: Optional[pd.DataFrame] = None,
862
808
  interpolatortype="FDI",
863
809
  nelements=10000,
864
810
  fold_frame=None,
@@ -869,14 +815,22 @@ class GeologicalModel:
869
815
 
870
816
  Parameters
871
817
  ----------
872
- fold_frame_data : string
818
+ fold_frame_name : string
873
819
  name of the feature to be added
874
-
820
+ fold_frame_data : pandas data frame, optional
821
+ data frame containing the fold frame data, if not specified uses the model data
822
+ interpolatortype : str
823
+ the type of interpolator to use, default is 'FDI' (unused) 5/6/2025
875
824
  fold_frame : StructuralFrame, optional
876
825
  the fold frame for the fold if not specified uses last feature added
877
-
878
- kwargs : dict
879
- parameters passed to child functions
826
+ nelements : int
827
+ the number of elements to use in the fold frame
828
+ tol : float, optional
829
+ tolerance for the solver, if not specified uses the model default
830
+ **kwargs : dict
831
+ additional parameters to be passed to the
832
+ :class:`LoopStructural.modelling.features.builders.StructuralFrameBuilder`
833
+ and :meth:`LoopStructural.modelling.features.builders.StructuralFrameBuilder.setup`
880
834
 
881
835
  Returns
882
836
  -------
@@ -897,8 +851,7 @@ class GeologicalModel:
897
851
  see :class:`LoopStructural.modelling.features.fold.FoldEvent`,
898
852
  :class:`LoopStructural.modelling.features.builders.FoldedFeatureBuilder`
899
853
  """
900
- if not self.check_inialisation():
901
- return False
854
+
902
855
  if tol is None:
903
856
  tol = self.tol
904
857
 
@@ -917,15 +870,15 @@ class GeologicalModel:
917
870
  interpolatortype=interpolatortypes,
918
871
  bounding_box=self.bounding_box.with_buffer(kwargs.get("buffer", 0.1)),
919
872
  nelements=[nelements, nelements, nelements],
920
- name=fold_frame_data,
873
+ name=fold_frame_name,
921
874
  fold=fold,
922
875
  frame=FoldFrame,
923
876
  model=self,
924
877
  **kwargs,
925
878
  )
926
- fold_frame_builder.add_data_from_data_frame(
927
- self.data[self.data["feature_name"] == fold_frame_data]
928
- )
879
+ if fold_frame_data is None:
880
+ fold_frame_data = self.data[self.data["feature_name"] == fold_frame_name]
881
+ fold_frame_builder.add_data_from_data_frame(self.prepare_data(fold_frame_data))
929
882
 
930
883
  for i in range(3):
931
884
  self._add_faults(fold_frame_builder[i])
@@ -947,6 +900,7 @@ class GeologicalModel:
947
900
  self,
948
901
  intrusion_name,
949
902
  intrusion_frame_name,
903
+ *,
950
904
  intrusion_frame_parameters={},
951
905
  intrusion_lateral_extent_model=None,
952
906
  intrusion_vertical_extent_model=None,
@@ -1224,7 +1178,7 @@ class GeologicalModel:
1224
1178
  return uc_feature
1225
1179
 
1226
1180
  def create_and_add_domain_fault(
1227
- self, fault_surface_data, nelements=10000, interpolatortype="FDI", **kwargs
1181
+ self, fault_surface_data, *, nelements=10000, interpolatortype="FDI", **kwargs
1228
1182
  ):
1229
1183
  """
1230
1184
  Parameters
@@ -1252,7 +1206,7 @@ class GeologicalModel:
1252
1206
  )
1253
1207
 
1254
1208
  # add data
1255
- unconformity_data = self.data[self.data["feature_name"] == fault_surface_data]
1209
+ unconformity_data = self.data.loc[self.data["feature_name"] == fault_surface_data]
1256
1210
 
1257
1211
  domain_fault_feature_builder.add_data_from_data_frame(unconformity_data)
1258
1212
  # look through existing features if there is a fault before an
@@ -1275,8 +1229,10 @@ class GeologicalModel:
1275
1229
 
1276
1230
  def create_and_add_fault(
1277
1231
  self,
1278
- fault_surface_data,
1279
- displacement,
1232
+ fault_name: str,
1233
+ displacement: float,
1234
+ *,
1235
+ fault_data: Optional[pd.DataFrame] = None,
1280
1236
  interpolatortype="FDI",
1281
1237
  tol=None,
1282
1238
  fault_slip_vector=None,
@@ -1299,9 +1255,12 @@ class GeologicalModel:
1299
1255
  """
1300
1256
  Parameters
1301
1257
  ----------
1302
- fault_surface_data : string
1258
+ fault_name : string
1303
1259
  name of the fault surface data in the dataframe
1304
1260
  displacement : displacement magnitude
1261
+ displacement magnitude of the fault, in model units
1262
+ fault_data : pd.DataFrame, optional
1263
+ data frame containing the fault data, if not specified uses the model data
1305
1264
  major_axis : [type], optional
1306
1265
  [description], by default None
1307
1266
  minor_axis : [type], optional
@@ -1328,7 +1287,7 @@ class GeologicalModel:
1328
1287
  if "fault_vectical_radius" in kwargs and intermediate_axis is None:
1329
1288
  intermediate_axis = kwargs["fault_vectical_radius"]
1330
1289
 
1331
- logger.info(f'Creating fault "{fault_surface_data}"')
1290
+ logger.info(f'Creating fault "{fault_name}"')
1332
1291
  logger.info(f"Displacement: {displacement}")
1333
1292
  logger.info(f"Tolerance: {tol}")
1334
1293
  logger.info(f"Fault function: {faultfunction}")
@@ -1353,35 +1312,39 @@ class GeologicalModel:
1353
1312
  # tol *= 0.1*minor_axis
1354
1313
 
1355
1314
  if displacement == 0:
1356
- logger.warning(f"{fault_surface_data} displacement is 0")
1315
+ logger.warning(f"{fault_name} displacement is 0")
1357
1316
 
1358
1317
  if "data_region" in kwargs:
1359
1318
  kwargs.pop("data_region")
1360
1319
  logger.error("kwarg data_region currently not supported, disabling")
1361
- displacement_scaled = displacement / self.scale_factor
1320
+ displacement_scaled = displacement
1362
1321
  fault_frame_builder = FaultBuilder(
1363
1322
  interpolatortype,
1364
1323
  bounding_box=self.bounding_box,
1365
1324
  nelements=kwargs.pop("nelements", 1e4),
1366
- name=fault_surface_data,
1325
+ name=fault_name,
1367
1326
  model=self,
1368
1327
  **kwargs,
1369
1328
  )
1370
- fault_frame_data = self.data.loc[self.data["feature_name"] == fault_surface_data].copy()
1329
+ if fault_data is None:
1330
+ fault_data = self.data.loc[self.data["feature_name"] == fault_name]
1331
+ if fault_data.shape[0] == 0:
1332
+ logger.warning(f"No data for {fault_name}, skipping")
1333
+ return
1334
+
1371
1335
  self._add_faults(fault_frame_builder, features=faults)
1372
1336
  # add data
1373
- fault_frame_data = self.data.loc[self.data["feature_name"] == fault_surface_data].copy()
1374
1337
 
1375
1338
  if fault_center is not None and ~np.isnan(fault_center).any():
1376
1339
  fault_center = self.scale(fault_center, inplace=False)
1377
1340
  if minor_axis:
1378
- minor_axis = minor_axis / self.scale_factor
1341
+ minor_axis = minor_axis
1379
1342
  if major_axis:
1380
- major_axis = major_axis / self.scale_factor
1343
+ major_axis = major_axis
1381
1344
  if intermediate_axis:
1382
- intermediate_axis = intermediate_axis / self.scale_factor
1345
+ intermediate_axis = intermediate_axis
1383
1346
  fault_frame_builder.create_data_from_geometry(
1384
- fault_frame_data=fault_frame_data,
1347
+ fault_frame_data=self.prepare_data(fault_data),
1385
1348
  fault_center=fault_center,
1386
1349
  fault_normal_vector=fault_normal_vector,
1387
1350
  fault_slip_vector=fault_slip_vector,
@@ -1418,7 +1381,7 @@ class GeologicalModel:
1418
1381
  return fault
1419
1382
 
1420
1383
  # TODO move rescale to bounding box/transformer
1421
- def rescale(self, points: np.ndarray, inplace: bool = False) -> np.ndarray:
1384
+ def rescale(self, points: np.ndarray, *, inplace: bool = False) -> np.ndarray:
1422
1385
  """
1423
1386
  Convert from model scale to real world scale - in the future this
1424
1387
  should also do transformations?
@@ -1434,14 +1397,12 @@ class GeologicalModel:
1434
1397
  points : np.array((N,3),dtype=double)
1435
1398
 
1436
1399
  """
1437
- if not inplace:
1438
- points = points.copy()
1439
- points *= self.scale_factor
1440
- points += self.origin
1441
- return points
1400
+
1401
+ return self.bounding_box.reproject(points, inplace=inplace)
1402
+
1442
1403
 
1443
1404
  # TODO move scale to bounding box/transformer
1444
- def scale(self, points: np.ndarray, inplace: bool = False) -> np.ndarray:
1405
+ def scale(self, points: np.ndarray, *, inplace: bool = False) -> np.ndarray:
1445
1406
  """Take points in UTM coordinates and reproject
1446
1407
  into scaled model space
1447
1408
 
@@ -1456,18 +1417,10 @@ class GeologicalModel:
1456
1417
  points : np.a::rray((N,3),dtype=double)
1457
1418
 
1458
1419
  """
1459
- points = np.array(points).astype(float)
1460
- if not inplace:
1461
- points = points.copy()
1462
- # if len(points.shape) == 1:
1463
- # points = points[None,:]
1464
- # if len(points.shape) != 2:
1465
- # logger.error("cannot scale array of dimensions".format(len(points.shape)))
1466
- points -= self.origin
1467
- points /= self.scale_factor
1468
- return points
1469
-
1470
- def regular_grid(self, nsteps=None, shuffle=True, rescale=False, order="C"):
1420
+ return self.bounding_box.project(np.array(points).astype(float), inplace=inplace)
1421
+
1422
+
1423
+ def regular_grid(self, *, nsteps=None, shuffle=True, rescale=False, order="C"):
1471
1424
  """
1472
1425
  Return a regular grid within the model bounding box
1473
1426
 
@@ -1483,7 +1436,7 @@ class GeologicalModel:
1483
1436
  """
1484
1437
  return self.bounding_box.regular_grid(nsteps=nsteps, shuffle=shuffle, order=order)
1485
1438
 
1486
- def evaluate_model(self, xyz: np.ndarray, scale: bool = True) -> np.ndarray:
1439
+ def evaluate_model(self, xyz: np.ndarray, *, scale: bool = True) -> np.ndarray:
1487
1440
  """Evaluate the stratigraphic id at each location
1488
1441
 
1489
1442
  Parameters
@@ -1559,7 +1512,7 @@ class GeologicalModel:
1559
1512
  logger.error(f"Model does not contain {group}")
1560
1513
  return strat_id
1561
1514
 
1562
- def evaluate_model_gradient(self, points: np.ndarray, scale: bool = True) -> np.ndarray:
1515
+ def evaluate_model_gradient(self, points: np.ndarray, *, scale: bool = True) -> np.ndarray:
1563
1516
  """Evaluate the gradient of the stratigraphic column at each location
1564
1517
 
1565
1518
  Parameters
@@ -1614,7 +1567,7 @@ class GeologicalModel:
1614
1567
  if f.type == FeatureType.FAULT:
1615
1568
  disp = f.displacementfeature.evaluate_value(points)
1616
1569
  vals[~np.isnan(disp)] += disp[~np.isnan(disp)]
1617
- return vals * -self.scale_factor # convert from restoration magnutude to displacement
1570
+ return vals # convert from restoration magnutude to displacement
1618
1571
 
1619
1572
  def get_feature_by_name(self, feature_name) -> GeologicalFeature:
1620
1573
  """Returns a feature from the mode given a name
@@ -1717,15 +1670,15 @@ class GeologicalModel:
1717
1670
  for f in self.features:
1718
1671
  if f.type == FeatureType.FAULT:
1719
1672
  nfeatures += 3
1720
- total_dof += f[0].interpolator.nx * 3
1673
+ total_dof += f[0].interpolator.dof * 3
1721
1674
  continue
1722
1675
  if isinstance(f, StructuralFrame):
1723
1676
  nfeatures += 3
1724
- total_dof += f[0].interpolator.nx * 3
1677
+ total_dof += f[0].interpolator.dof * 3
1725
1678
  continue
1726
1679
  if f.type == FeatureType.INTERPOLATED:
1727
1680
  nfeatures += 1
1728
- total_dof += f.interpolator.nx
1681
+ total_dof += f.interpolator.dof
1729
1682
  continue
1730
1683
  if verbose:
1731
1684
  print(
@@ -1783,30 +1736,15 @@ class GeologicalModel:
1783
1736
  units = []
1784
1737
  if self.stratigraphic_column is None:
1785
1738
  return []
1786
- for group in self.stratigraphic_column.keys():
1787
- if group == "faults":
1739
+ units = self.stratigraphic_column.get_isovalues()
1740
+ for name, u in units.items():
1741
+ if u['group'] not in self:
1742
+ logger.warning(f"Group {u['group']} not found in model")
1788
1743
  continue
1789
- for series in self.stratigraphic_column[group].values():
1790
- series['feature_name'] = group
1791
- units.append(series)
1792
- unit_table = pd.DataFrame(units)
1793
- for u in unit_table['feature_name'].unique():
1794
-
1795
- values = unit_table.loc[unit_table['feature_name'] == u, 'min' if bottoms else 'max']
1796
- if 'name' not in unit_table.columns:
1797
- unit_table['name'] = unit_table['feature_name']
1798
-
1799
- names = unit_table[unit_table['feature_name'] == u]['name']
1800
- values = values.loc[~np.logical_or(values == np.inf, values == -np.inf)]
1744
+ feature = self.get_feature_by_name(u['group'])
1745
+
1801
1746
  surfaces.extend(
1802
- self.get_feature_by_name(u).surfaces(
1803
- values.to_list(),
1804
- self.bounding_box,
1805
- name=names.loc[values.index].to_list(),
1806
- colours=unit_table.loc[unit_table['feature_name'] == u, 'colour'].tolist()[
1807
- 1:
1808
- ], # we don't isosurface basement, no value
1809
- )
1747
+ feature.surfaces([u['value']], self.bounding_box, name=name, colours=[u['colour']])
1810
1748
  )
1811
1749
 
1812
1750
  return surfaces