LoopStructural 1.6.14__py3-none-any.whl → 1.6.15__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 (26) hide show
  1. LoopStructural/datatypes/_bounding_box.py +22 -13
  2. LoopStructural/datatypes/_point.py +0 -1
  3. LoopStructural/export/exporters.py +2 -2
  4. LoopStructural/interpolators/__init__.py +33 -30
  5. LoopStructural/interpolators/_constant_norm.py +205 -0
  6. LoopStructural/interpolators/_discrete_interpolator.py +15 -14
  7. LoopStructural/interpolators/_finite_difference_interpolator.py +10 -10
  8. LoopStructural/interpolators/_geological_interpolator.py +1 -1
  9. LoopStructural/interpolators/_interpolatortype.py +22 -0
  10. LoopStructural/interpolators/_p1interpolator.py +6 -2
  11. LoopStructural/interpolators/_surfe_wrapper.py +4 -1
  12. LoopStructural/interpolators/supports/_2d_base_unstructured.py +1 -1
  13. LoopStructural/interpolators/supports/_2d_structured_grid.py +16 -0
  14. LoopStructural/interpolators/supports/_3d_base_structured.py +16 -0
  15. LoopStructural/interpolators/supports/_3d_structured_tetra.py +7 -3
  16. LoopStructural/modelling/core/geological_model.py +187 -234
  17. LoopStructural/modelling/features/_base_geological_feature.py +38 -2
  18. LoopStructural/modelling/features/builders/_geological_feature_builder.py +1 -1
  19. LoopStructural/modelling/intrusions/intrusion_builder.py +1 -1
  20. LoopStructural/modelling/intrusions/intrusion_frame_builder.py +1 -1
  21. LoopStructural/version.py +1 -1
  22. {loopstructural-1.6.14.dist-info → loopstructural-1.6.15.dist-info}/METADATA +2 -2
  23. {loopstructural-1.6.14.dist-info → loopstructural-1.6.15.dist-info}/RECORD +26 -24
  24. {loopstructural-1.6.14.dist-info → loopstructural-1.6.15.dist-info}/WHEEL +0 -0
  25. {loopstructural-1.6.14.dist-info → loopstructural-1.6.15.dist-info}/licenses/LICENSE +0 -0
  26. {loopstructural-1.6.14.dist-info → loopstructural-1.6.15.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
 
@@ -61,33 +61,24 @@ 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
68
  def __init__(
71
69
  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",
70
+ *args
79
71
  ):
80
72
  """
81
73
  Parameters
82
74
  ----------
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
75
+ bounding_box : BoundingBox
76
+ the bounding box of the model
77
+ origin : np.array(3,dtype=doubles)
78
+ the origin of the model
79
+ maximum : np.array(3,dtype=doubles)
80
+ the maximum of the model
81
+
91
82
  Examples
92
83
  --------
93
84
  Demo data
@@ -111,37 +102,29 @@ class GeologicalModel:
111
102
 
112
103
 
113
104
  """
114
- if logfile:
115
- self.logfile = logfile
116
- log_to_file(logfile, level=loglevel)
117
-
105
+ args = list(args)
106
+ if len(args) == 0:
107
+ raise ValueError("Must provide either bounding_box or origin and maximum")
108
+ if len(args) == 1:
109
+ bounding_box = args[0]
110
+ if not isinstance(bounding_box, BoundingBox):
111
+ raise ValueError("Must provide a bounding box")
112
+ self.bounding_box = bounding_box
113
+ if len(args) == 2:
114
+ origin = np.array(args[0])
115
+ maximum = np.array(args[1])
116
+ if not isinstance(origin, np.ndarray) or not isinstance(maximum, np.ndarray):
117
+ raise ValueError("Must provide origin and maximum as numpy arrays")
118
+ self.bounding_box = BoundingBox(
119
+ dimensions=3,
120
+ origin=np.zeros(3),
121
+ maximum=maximum - origin,
122
+ global_origin=origin,
123
+ )
118
124
  logger.info("Initialising geological model")
119
125
  self.features = []
120
126
  self.feature_name_index = {}
121
127
  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
-
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
- )
145
128
 
146
129
  self.stratigraphic_column = None
147
130
 
@@ -160,10 +143,7 @@ class GeologicalModel:
160
143
  json = {}
161
144
  json["model"] = {}
162
145
  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
146
+ json['model']['bounding_box'] = self.bounding_box.to_dict()
167
147
  json["model"]["stratigraphic_column"] = self.stratigraphic_column
168
148
  # json["features"] = [f.to_json() for f in self.features]
169
149
  return json
@@ -192,87 +172,45 @@ class GeologicalModel:
192
172
  # model.features.append(GeologicalFeature.from_json(feature,model))
193
173
  # return model
194
174
  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
175
+ return f"GeologicalModel with {len(self.features)} features"
212
176
 
213
177
  def _ipython_key_completions_(self):
214
178
  return self.feature_name_index.keys()
215
179
 
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
238
-
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
272
-
273
- model = GeologicalModel.from_processor(processor)
274
- return model, processor
180
+ def prepare_data(self, data: pd.DataFrame) -> pd.DataFrame:
181
+ data = data.copy()
182
+ data[['X', 'Y', 'Z']] = self.bounding_box.project(data[['X', 'Y', 'Z']].to_numpy())
275
183
 
184
+ if "type" in data:
185
+ logger.warning("'type' is deprecated replace with 'feature_name' \n")
186
+ data.rename(columns={"type": "feature_name"}, inplace=True)
187
+ if "feature_name" not in data:
188
+ logger.error("Data does not contain 'feature_name' column")
189
+ raise BaseException("Cannot load data")
190
+ for h in all_heading():
191
+ if h not in data:
192
+ data[h] = np.nan
193
+ if h == "w":
194
+ data[h] = 1.0
195
+ if h == "coord":
196
+ data[h] = 0
197
+ if h == "polarity":
198
+ data[h] = 1.0
199
+ # LS wants polarity as -1 or 1, change 0 to -1
200
+ data.loc[data["polarity"] == 0, "polarity"] = -1.0
201
+ data.loc[np.isnan(data["w"]), "w"] = 1.0
202
+ if "strike" in data and "dip" in data:
203
+ logger.info("Converting strike and dip to vectors")
204
+ mask = np.all(~np.isnan(data.loc[:, ["strike", "dip"]]), axis=1)
205
+ data.loc[mask, gradient_vec_names()] = (
206
+ strikedip2vector(data.loc[mask, "strike"], data.loc[mask, "dip"])
207
+ * data.loc[mask, "polarity"].to_numpy()[:, None]
208
+ )
209
+ data.drop(["strike", "dip"], axis=1, inplace=True)
210
+ data[['X', 'Y', 'Z', 'val', 'nx', 'ny', 'nz', 'gx', 'gy', 'gz', 'tx', 'ty', 'tz']] = data[
211
+ ['X', 'Y', 'Z', 'val', 'nx', 'ny', 'nz', 'gx', 'gy', 'gz', 'tx', 'ty', 'tz']
212
+ ].astype(float)
213
+ return data
276
214
  @classmethod
277
215
  def from_processor(cls, processor):
278
216
  """Builds a model from a :class:`LoopStructural.modelling.input.ProcessInputData` object
@@ -543,14 +481,14 @@ class GeologicalModel:
543
481
  ----------
544
482
  data : pandas data frame
545
483
  with column headers corresponding to the
546
- type, X, Y, Z, nx, ny, nz, val, strike, dip, dip_dir, plunge,
484
+ feature_name, X, Y, Z, nx, ny, nz, val, strike, dip, dip_dir, plunge,
547
485
  plunge_dir, azimuth
548
486
 
549
487
  Returns
550
488
  -------
551
489
  Note
552
490
  ----
553
- Type can be any unique identifier for the feature the data point
491
+ feature_name can be any unique identifier for the feature the data point
554
492
  'eg' 'S0', 'S2', 'F1_axis'
555
493
  it is then used by the create functions to get the correct data
556
494
  """
@@ -565,48 +503,13 @@ class GeologicalModel:
565
503
  raise BaseException("Cannot load data")
566
504
  logger.info(f"Adding data to GeologicalModel with {len(data)} data points")
567
505
  self._data = data.copy()
506
+ # self._data[['X','Y','Z']] = self.bounding_box.project(self._data[['X','Y','Z']].to_numpy())
507
+
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
  """
@@ -654,7 +557,9 @@ class GeologicalModel:
654
557
 
655
558
  def create_and_add_foliation(
656
559
  self,
657
- series_surface_data: str,
560
+ series_surface_name: str,
561
+ *,
562
+ series_surface_data: pd.DataFrame = None,
658
563
  interpolatortype: str = "FDI",
659
564
  nelements: int = 1000,
660
565
  tol=None,
@@ -664,8 +569,18 @@ class GeologicalModel:
664
569
  """
665
570
  Parameters
666
571
  ----------
667
- series_surface_data : string
572
+ series_surface_name : string
668
573
  corresponding to the feature_name in the data
574
+ series_surface_data : pd.DataFrame, optional
575
+ data frame containing the surface data
576
+ interpolatortype : str
577
+ the type of interpolator to use, default is 'FDI'
578
+ nelements : int
579
+ the number of elements to use in the series surface
580
+ tol : float, optional
581
+ tolerance for the solver, if not specified uses the model default
582
+ faults : list, optional
583
+ list of faults to be used in the series surface, if not specified uses the model faults
669
584
  kwargs
670
585
 
671
586
  Returns
@@ -696,16 +611,18 @@ class GeologicalModel:
696
611
  bounding_box=self.bounding_box,
697
612
  interpolatortype=interpolatortype,
698
613
  nelements=nelements,
699
- name=series_surface_data,
614
+ name=series_surface_name,
700
615
  model=self,
701
616
  **kwargs,
702
617
  )
703
618
  # add data
704
- series_data = self.data[self.data["feature_name"] == series_surface_data]
705
- if series_data.shape[0] == 0:
619
+ if series_surface_data is None:
620
+ series_surface_data = self.data.loc[self.data["feature_name"] == series_surface_name]
621
+
622
+ if series_surface_data.shape[0] == 0:
706
623
  logger.warning("No data for {series_surface_data}, skipping")
707
624
  return
708
- series_builder.add_data_from_data_frame(series_data)
625
+ series_builder.add_data_from_data_frame(self.prepare_data(series_surface_data))
709
626
  self._add_faults(series_builder, features=faults)
710
627
 
711
628
  # build feature
@@ -721,7 +638,9 @@ class GeologicalModel:
721
638
 
722
639
  def create_and_add_fold_frame(
723
640
  self,
724
- foldframe_data,
641
+ fold_frame_name:str,
642
+ *,
643
+ fold_frame_data=None,
725
644
  interpolatortype="FDI",
726
645
  nelements=1000,
727
646
  tol=None,
@@ -731,10 +650,24 @@ class GeologicalModel:
731
650
  """
732
651
  Parameters
733
652
  ----------
734
- foldframe_data : string
653
+ fold_frame_name : string
735
654
  unique string in feature_name column
736
-
737
- kwargs
655
+ fold_frame_data : pandas data frame
656
+ if not specified uses the model data
657
+ interpolatortype : str
658
+ the type of interpolator to use, default is 'FDI'
659
+ nelements : int
660
+ the number of elements to use in the fold frame
661
+ tol : float, optional
662
+ tolerance for the solver
663
+ buffer : float
664
+ buffer to add to the bounding box of the fold frame
665
+ **kwargs : dict
666
+ additional parameters to be passed to the
667
+ :class:`LoopStructural.modelling.features.builders.StructuralFrameBuilder`
668
+ and :meth:`LoopStructural.modelling.features.builders.StructuralFrameBuilder.setup`
669
+ and the interpolator, such as `domain` or `tol`
670
+
738
671
 
739
672
  Returns
740
673
  -------
@@ -751,15 +684,19 @@ class GeologicalModel:
751
684
  fold_frame_builder = StructuralFrameBuilder(
752
685
  interpolatortype=interpolatortype,
753
686
  bounding_box=self.bounding_box.with_buffer(buffer),
754
- name=foldframe_data,
687
+ name=fold_frame_name,
755
688
  frame=FoldFrame,
756
689
  nelements=nelements,
757
690
  model=self,
758
691
  **kwargs,
759
692
  )
760
693
  # 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)
694
+ if fold_frame_data is None:
695
+ fold_frame_data = self.data.loc[self.data["feature_name"] == fold_frame_name]
696
+ if fold_frame_data.shape[0] == 0:
697
+ logger.warning(f"No data for {fold_frame_name}, skipping")
698
+ return
699
+ fold_frame_builder.add_data_from_data_frame(self.prepare_data(fold_frame_data))
763
700
  self._add_faults(fold_frame_builder[0])
764
701
  self._add_faults(fold_frame_builder[1])
765
702
  self._add_faults(fold_frame_builder[2])
@@ -775,7 +712,9 @@ class GeologicalModel:
775
712
 
776
713
  def create_and_add_folded_foliation(
777
714
  self,
778
- foliation_data,
715
+ foliation_name,
716
+ *,
717
+ foliation_data=None,
779
718
  interpolatortype="DFI",
780
719
  nelements=10000,
781
720
  buffer=0.1,
@@ -832,14 +771,20 @@ class GeologicalModel:
832
771
  bounding_box=self.bounding_box.with_buffer(buffer),
833
772
  nelements=nelements,
834
773
  fold=fold,
835
- name=foliation_data,
774
+ name=foliation_name,
836
775
  svario=svario,
837
776
  model=self,
838
777
  **kwargs,
839
778
  )
840
-
779
+ if foliation_data is None:
780
+ foliation_data = self.data.loc[self.data["feature_name"] == foliation_name]
781
+ if foliation_data.shape[0] == 0:
782
+ logger.warning(f"No data for {foliation_name}, skipping")
783
+ return
841
784
  series_builder.add_data_from_data_frame(
842
- self.data[self.data["feature_name"] == foliation_data]
785
+ self.prepare_data(
786
+ foliation_data
787
+ )
843
788
  )
844
789
  self._add_faults(series_builder)
845
790
  # series_builder.add_data_to_interpolator(True)
@@ -858,7 +803,9 @@ class GeologicalModel:
858
803
 
859
804
  def create_and_add_folded_fold_frame(
860
805
  self,
861
- fold_frame_data,
806
+ fold_frame_name: str,
807
+ *,
808
+ fold_frame_data: Optional[pd.DataFrame] = None,
862
809
  interpolatortype="FDI",
863
810
  nelements=10000,
864
811
  fold_frame=None,
@@ -869,14 +816,22 @@ class GeologicalModel:
869
816
 
870
817
  Parameters
871
818
  ----------
872
- fold_frame_data : string
819
+ fold_frame_name : string
873
820
  name of the feature to be added
874
-
821
+ fold_frame_data : pandas data frame, optional
822
+ data frame containing the fold frame data, if not specified uses the model data
823
+ interpolatortype : str
824
+ the type of interpolator to use, default is 'FDI' (unused) 5/6/2025
875
825
  fold_frame : StructuralFrame, optional
876
826
  the fold frame for the fold if not specified uses last feature added
877
-
878
- kwargs : dict
879
- parameters passed to child functions
827
+ nelements : int
828
+ the number of elements to use in the fold frame
829
+ tol : float, optional
830
+ tolerance for the solver, if not specified uses the model default
831
+ **kwargs : dict
832
+ additional parameters to be passed to the
833
+ :class:`LoopStructural.modelling.features.builders.StructuralFrameBuilder`
834
+ and :meth:`LoopStructural.modelling.features.builders.StructuralFrameBuilder.setup`
880
835
 
881
836
  Returns
882
837
  -------
@@ -917,15 +872,15 @@ class GeologicalModel:
917
872
  interpolatortype=interpolatortypes,
918
873
  bounding_box=self.bounding_box.with_buffer(kwargs.get("buffer", 0.1)),
919
874
  nelements=[nelements, nelements, nelements],
920
- name=fold_frame_data,
875
+ name=fold_frame_name,
921
876
  fold=fold,
922
877
  frame=FoldFrame,
923
878
  model=self,
924
879
  **kwargs,
925
880
  )
926
- fold_frame_builder.add_data_from_data_frame(
927
- self.data[self.data["feature_name"] == fold_frame_data]
928
- )
881
+ if fold_frame_data is None:
882
+ fold_frame_data = self.data[self.data["feature_name"] == fold_frame_name]
883
+ fold_frame_builder.add_data_from_data_frame(self.prepare_data(fold_frame_data))
929
884
 
930
885
  for i in range(3):
931
886
  self._add_faults(fold_frame_builder[i])
@@ -947,6 +902,7 @@ class GeologicalModel:
947
902
  self,
948
903
  intrusion_name,
949
904
  intrusion_frame_name,
905
+ *,
950
906
  intrusion_frame_parameters={},
951
907
  intrusion_lateral_extent_model=None,
952
908
  intrusion_vertical_extent_model=None,
@@ -1224,7 +1180,7 @@ class GeologicalModel:
1224
1180
  return uc_feature
1225
1181
 
1226
1182
  def create_and_add_domain_fault(
1227
- self, fault_surface_data, nelements=10000, interpolatortype="FDI", **kwargs
1183
+ self, fault_surface_data,*, nelements=10000, interpolatortype="FDI", **kwargs
1228
1184
  ):
1229
1185
  """
1230
1186
  Parameters
@@ -1252,7 +1208,7 @@ class GeologicalModel:
1252
1208
  )
1253
1209
 
1254
1210
  # add data
1255
- unconformity_data = self.data[self.data["feature_name"] == fault_surface_data]
1211
+ unconformity_data = self.data.loc[self.data["feature_name"] == fault_surface_data]
1256
1212
 
1257
1213
  domain_fault_feature_builder.add_data_from_data_frame(unconformity_data)
1258
1214
  # look through existing features if there is a fault before an
@@ -1275,8 +1231,10 @@ class GeologicalModel:
1275
1231
 
1276
1232
  def create_and_add_fault(
1277
1233
  self,
1278
- fault_surface_data,
1279
- displacement,
1234
+ fault_name: str,
1235
+ displacement: float,
1236
+ *,
1237
+ fault_data:Optional[pd.DataFrame] = None,
1280
1238
  interpolatortype="FDI",
1281
1239
  tol=None,
1282
1240
  fault_slip_vector=None,
@@ -1299,9 +1257,12 @@ class GeologicalModel:
1299
1257
  """
1300
1258
  Parameters
1301
1259
  ----------
1302
- fault_surface_data : string
1260
+ fault_name : string
1303
1261
  name of the fault surface data in the dataframe
1304
1262
  displacement : displacement magnitude
1263
+ displacement magnitude of the fault, in model units
1264
+ fault_data : pd.DataFrame, optional
1265
+ data frame containing the fault data, if not specified uses the model data
1305
1266
  major_axis : [type], optional
1306
1267
  [description], by default None
1307
1268
  minor_axis : [type], optional
@@ -1328,7 +1289,7 @@ class GeologicalModel:
1328
1289
  if "fault_vectical_radius" in kwargs and intermediate_axis is None:
1329
1290
  intermediate_axis = kwargs["fault_vectical_radius"]
1330
1291
 
1331
- logger.info(f'Creating fault "{fault_surface_data}"')
1292
+ logger.info(f'Creating fault "{fault_name}"')
1332
1293
  logger.info(f"Displacement: {displacement}")
1333
1294
  logger.info(f"Tolerance: {tol}")
1334
1295
  logger.info(f"Fault function: {faultfunction}")
@@ -1353,35 +1314,39 @@ class GeologicalModel:
1353
1314
  # tol *= 0.1*minor_axis
1354
1315
 
1355
1316
  if displacement == 0:
1356
- logger.warning(f"{fault_surface_data} displacement is 0")
1317
+ logger.warning(f"{fault_name} displacement is 0")
1357
1318
 
1358
1319
  if "data_region" in kwargs:
1359
1320
  kwargs.pop("data_region")
1360
1321
  logger.error("kwarg data_region currently not supported, disabling")
1361
- displacement_scaled = displacement / self.scale_factor
1322
+ displacement_scaled = displacement
1362
1323
  fault_frame_builder = FaultBuilder(
1363
1324
  interpolatortype,
1364
1325
  bounding_box=self.bounding_box,
1365
1326
  nelements=kwargs.pop("nelements", 1e4),
1366
- name=fault_surface_data,
1327
+ name=fault_name,
1367
1328
  model=self,
1368
1329
  **kwargs,
1369
1330
  )
1370
- fault_frame_data = self.data.loc[self.data["feature_name"] == fault_surface_data].copy()
1331
+ if fault_data is None:
1332
+ fault_data = self.data.loc[self.data["feature_name"] == fault_name]
1333
+ if fault_data.shape[0] == 0:
1334
+ logger.warning(f"No data for {fault_name}, skipping")
1335
+ return
1336
+
1371
1337
  self._add_faults(fault_frame_builder, features=faults)
1372
1338
  # add data
1373
- fault_frame_data = self.data.loc[self.data["feature_name"] == fault_surface_data].copy()
1374
1339
 
1375
1340
  if fault_center is not None and ~np.isnan(fault_center).any():
1376
1341
  fault_center = self.scale(fault_center, inplace=False)
1377
1342
  if minor_axis:
1378
- minor_axis = minor_axis / self.scale_factor
1343
+ minor_axis = minor_axis
1379
1344
  if major_axis:
1380
- major_axis = major_axis / self.scale_factor
1345
+ major_axis = major_axis
1381
1346
  if intermediate_axis:
1382
- intermediate_axis = intermediate_axis / self.scale_factor
1347
+ intermediate_axis = intermediate_axis
1383
1348
  fault_frame_builder.create_data_from_geometry(
1384
- fault_frame_data=fault_frame_data,
1349
+ fault_frame_data=self.prepare_data(fault_data),
1385
1350
  fault_center=fault_center,
1386
1351
  fault_normal_vector=fault_normal_vector,
1387
1352
  fault_slip_vector=fault_slip_vector,
@@ -1418,7 +1383,7 @@ class GeologicalModel:
1418
1383
  return fault
1419
1384
 
1420
1385
  # TODO move rescale to bounding box/transformer
1421
- def rescale(self, points: np.ndarray, inplace: bool = False) -> np.ndarray:
1386
+ def rescale(self, points: np.ndarray, *, inplace: bool = False) -> np.ndarray:
1422
1387
  """
1423
1388
  Convert from model scale to real world scale - in the future this
1424
1389
  should also do transformations?
@@ -1434,14 +1399,11 @@ class GeologicalModel:
1434
1399
  points : np.array((N,3),dtype=double)
1435
1400
 
1436
1401
  """
1437
- if not inplace:
1438
- points = points.copy()
1439
- points *= self.scale_factor
1440
- points += self.origin
1441
- return points
1402
+
1403
+ return self.bounding_box.reproject(points,inplace=inplace)
1442
1404
 
1443
1405
  # TODO move scale to bounding box/transformer
1444
- def scale(self, points: np.ndarray, inplace: bool = False) -> np.ndarray:
1406
+ def scale(self, points: np.ndarray, *, inplace: bool = False) -> np.ndarray:
1445
1407
  """Take points in UTM coordinates and reproject
1446
1408
  into scaled model space
1447
1409
 
@@ -1456,18 +1418,9 @@ class GeologicalModel:
1456
1418
  points : np.a::rray((N,3),dtype=double)
1457
1419
 
1458
1420
  """
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"):
1421
+ return self.bounding_box.project(np.array(points).astype(float),inplace=inplace)
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(