LoopStructural 1.6.15__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.

@@ -19,6 +19,7 @@ ch.setFormatter(formatter)
19
19
  ch.setLevel(logging.WARNING)
20
20
  loggers = {}
21
21
  from .modelling.core.geological_model import GeologicalModel
22
+ from .modelling.core.stratigraphic_column import StratigraphicColumn
22
23
  from .interpolators._api import LoopInterpolator
23
24
  from .interpolators import InterpolatorBuilder
24
25
  from .datatypes import BoundingBox
@@ -28,26 +29,43 @@ logger = getLogger(__name__)
28
29
  logger.info("Imported LoopStructural")
29
30
 
30
31
 
31
- def setLogging(level="info"):
32
+ def setLogging(level="info", handler=None):
32
33
  """
33
- Set the logging parameters for log file
34
+ Set the logging parameters for log file or custom handler
34
35
 
35
36
  Parameters
36
37
  ----------
37
- filename : string
38
- name of file or path to file
39
- level : str, optional
40
- 'info', 'warning', 'error', 'debug' mapped to logging levels, by default 'info'
38
+ level : str
39
+ 'info', 'warning', 'error', 'debug'
40
+ handler : logging.Handler, optional
41
+ A logging handler to use instead of the default StreamHandler
41
42
  """
42
43
  import LoopStructural
43
44
 
44
- logger = getLogger(__name__)
45
-
46
45
  levels = get_levels()
47
- level = levels.get(level, logging.WARNING)
48
- LoopStructural.ch.setLevel(level)
46
+ level_value = levels.get(level, logging.WARNING)
47
+
48
+ # Create default handler if none provided
49
+ if handler is None:
50
+ handler = logging.StreamHandler()
51
+
52
+ formatter = logging.Formatter(
53
+ "%(levelname)s: %(asctime)s: %(filename)s:%(lineno)d -- %(message)s"
54
+ )
55
+ handler.setFormatter(formatter)
56
+ handler.setLevel(level_value)
49
57
 
58
+ # Replace handlers in all known loggers
50
59
  for name in LoopStructural.loggers:
51
60
  logger = logging.getLogger(name)
52
- logger.setLevel(level)
53
- logger.info(f'Set logging to {level}')
61
+ logger.handlers = []
62
+ logger.addHandler(handler)
63
+ logger.setLevel(level_value)
64
+
65
+ # Also apply to main module logger
66
+ main_logger = logging.getLogger(__name__)
67
+ main_logger.handlers = []
68
+ main_logger.addHandler(handler)
69
+ main_logger.setLevel(level_value)
70
+
71
+ main_logger.info(f"Set logging to {level}")
@@ -154,16 +154,22 @@ class GeologicalInterpolator(metaclass=ABCMeta):
154
154
  ----------
155
155
  points : np.ndarray
156
156
  array containing the value constraints usually 7-8 columns.
157
- X,Y,Z,nx,ny,nz,weight
157
+ X,Y,Z,nx,ny,nz,(weight, default : 1 for each row)
158
158
 
159
159
  Returns
160
160
  -------
161
161
 
162
+ Notes
163
+ -------
164
+ If no weights are provided, w = 1 is assigned to each normal constraint.
165
+
162
166
  """
163
167
  if points.shape[1] == self.dimensions * 2:
164
168
  points = np.hstack([points, np.ones((points.shape[0], 1))])
169
+ logger.warning(f"No weight provided for normal constraints, all weights are set to 1")
170
+ raise Warning
165
171
  if points.shape[1] < self.dimensions * 2 + 1:
166
- raise ValueError("Nonrmal constraints must at least have X,Y,Z,nx,ny,nz")
172
+ raise ValueError("Normal constraints must at least have X,Y,Z,nx,ny,nz")
167
173
  self.n_n = points.shape[0]
168
174
  self.data["normal"] = points
169
175
  self.up_to_date = False
@@ -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,14 +61,11 @@ 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
-
64
+
65
65
 
66
66
  """
67
67
 
68
- def __init__(
69
- self,
70
- *args
71
- ):
68
+ def __init__(self, *args):
72
69
  """
73
70
  Parameters
74
71
  ----------
@@ -78,7 +75,7 @@ class GeologicalModel:
78
75
  the origin of the model
79
76
  maximum : np.array(3,dtype=doubles)
80
77
  the maximum of the model
81
-
78
+
82
79
  Examples
83
80
  --------
84
81
  Demo data
@@ -126,7 +123,8 @@ class GeologicalModel:
126
123
  self.feature_name_index = {}
127
124
  self._data = pd.DataFrame() # None
128
125
 
129
- self.stratigraphic_column = None
126
+ self.stratigraphic_column = StratigraphicColumn()
127
+
130
128
 
131
129
  self.tol = 1e-10 * np.max(self.bounding_box.maximum - self.bounding_box.origin)
132
130
  self._dtm = None
@@ -148,29 +146,6 @@ class GeologicalModel:
148
146
  # json["features"] = [f.to_json() for f in self.features]
149
147
  return json
150
148
 
151
- # @classmethod
152
- # def from_json(cls,json):
153
- # """
154
- # Create a geological model from a json string
155
-
156
- # Parameters
157
- # ----------
158
- # json : str
159
- # json string of the geological model
160
-
161
- # Returns
162
- # -------
163
- # model : GeologicalModel
164
- # a geological model
165
- # """
166
- # model = cls(json["model"]["origin"],json["model"]["maximum"],data=None)
167
- # model.stratigraphic_column = json["model"]["stratigraphic_column"]
168
- # model.nsteps = json["model"]["nsteps"]
169
- # model.data = pd.read_json(json["model"]["data"])
170
- # model.features = []
171
- # for feature in json["features"]:
172
- # model.features.append(GeologicalFeature.from_json(feature,model))
173
- # return model
174
149
  def __str__(self):
175
150
  return f"GeologicalModel with {len(self.features)} features"
176
151
 
@@ -181,6 +156,38 @@ class GeologicalModel:
181
156
  data = data.copy()
182
157
  data[['X', 'Y', 'Z']] = self.bounding_box.project(data[['X', 'Y', 'Z']].to_numpy())
183
158
 
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
189
+
190
+
184
191
  if "type" in data:
185
192
  logger.warning("'type' is deprecated replace with 'feature_name' \n")
186
193
  data.rename(columns={"type": "feature_name"}, inplace=True)
@@ -402,12 +409,6 @@ class GeologicalModel:
402
409
  """
403
410
  return [f.name for f in self.faults]
404
411
 
405
- def check_inialisation(self):
406
- if self.data is None:
407
- logger.error("Data not associated with GeologicalModel. Run set_data")
408
- return False
409
- if self.data.shape[0] > 0:
410
- return True
411
412
 
412
413
  def to_file(self, file):
413
414
  """Save a model to a pickle file requires dill
@@ -506,7 +507,6 @@ class GeologicalModel:
506
507
  # self._data[['X','Y','Z']] = self.bounding_box.project(self._data[['X','Y','Z']].to_numpy())
507
508
 
508
509
 
509
-
510
510
  def set_model_data(self, data):
511
511
  logger.warning("deprecated method. Model data can now be set using the data attribute")
512
512
  self.data = data.copy()
@@ -532,28 +532,34 @@ class GeologicalModel:
532
532
  }
533
533
 
534
534
  """
535
+ self.stratigraphic_column.clear()
535
536
  # if the colour for a unit hasn't been specified we can just sample from
536
537
  # a colour map e.g. tab20
537
538
  logger.info("Adding stratigraphic column to model")
538
- random_colour = True
539
- n_units = 0
539
+ DeprecationWarning(
540
+ "set_stratigraphic_column is deprecated, use model.stratigraphic_column.add_units instead"
541
+ )
540
542
  for g in stratigraphic_column.keys():
541
543
  for u in stratigraphic_column[g].keys():
542
- if "colour" in stratigraphic_column[g][u]:
543
- random_colour = False
544
- break
545
- n_units += 1
546
- if random_colour:
547
- import matplotlib.cm as cm
548
-
549
- cmap = cm.get_cmap(cmap, n_units)
550
- cmap_colours = cmap.colors
551
- ci = 0
552
- for g in stratigraphic_column.keys():
553
- for u in stratigraphic_column[g].keys():
554
- stratigraphic_column[g][u]["colour"] = cmap_colours[ci, :]
555
- ci += 1
556
- 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
+ )
557
563
 
558
564
  def create_and_add_foliation(
559
565
  self,
@@ -600,9 +606,7 @@ class GeologicalModel:
600
606
  An interpolator will be chosen by calling :meth:`LoopStructural.GeologicalModel.get_interpolator`
601
607
 
602
608
  """
603
- if not self.check_inialisation():
604
- logger.warning(f"{series_surface_data} not added, model not initialised")
605
- return
609
+
606
610
  # if tol is not specified use the model default
607
611
  if tol is None:
608
612
  tol = self.tol
@@ -638,7 +642,7 @@ class GeologicalModel:
638
642
 
639
643
  def create_and_add_fold_frame(
640
644
  self,
641
- fold_frame_name:str,
645
+ fold_frame_name: str,
642
646
  *,
643
647
  fold_frame_data=None,
644
648
  interpolatortype="FDI",
@@ -667,15 +671,14 @@ class GeologicalModel:
667
671
  :class:`LoopStructural.modelling.features.builders.StructuralFrameBuilder`
668
672
  and :meth:`LoopStructural.modelling.features.builders.StructuralFrameBuilder.setup`
669
673
  and the interpolator, such as `domain` or `tol`
670
-
674
+
671
675
 
672
676
  Returns
673
677
  -------
674
678
  fold_frame : FoldFrame
675
679
  the created fold frame
676
680
  """
677
- if not self.check_inialisation():
678
- return False
681
+
679
682
  if tol is None:
680
683
  tol = self.tol
681
684
 
@@ -751,8 +754,7 @@ class GeologicalModel:
751
754
  :class:`LoopStructural.modelling.features.builders.FoldedFeatureBuilder`
752
755
 
753
756
  """
754
- if not self.check_inialisation():
755
- return False
757
+
756
758
  if tol is None:
757
759
  tol = self.tol
758
760
 
@@ -781,11 +783,8 @@ class GeologicalModel:
781
783
  if foliation_data.shape[0] == 0:
782
784
  logger.warning(f"No data for {foliation_name}, skipping")
783
785
  return
784
- series_builder.add_data_from_data_frame(
785
- self.prepare_data(
786
- foliation_data
787
- )
788
- )
786
+ series_builder.add_data_from_data_frame(self.prepare_data(foliation_data))
787
+
789
788
  self._add_faults(series_builder)
790
789
  # series_builder.add_data_to_interpolator(True)
791
790
  # build feature
@@ -852,8 +851,7 @@ class GeologicalModel:
852
851
  see :class:`LoopStructural.modelling.features.fold.FoldEvent`,
853
852
  :class:`LoopStructural.modelling.features.builders.FoldedFeatureBuilder`
854
853
  """
855
- if not self.check_inialisation():
856
- return False
854
+
857
855
  if tol is None:
858
856
  tol = self.tol
859
857
 
@@ -1180,7 +1178,7 @@ class GeologicalModel:
1180
1178
  return uc_feature
1181
1179
 
1182
1180
  def create_and_add_domain_fault(
1183
- self, fault_surface_data,*, nelements=10000, interpolatortype="FDI", **kwargs
1181
+ self, fault_surface_data, *, nelements=10000, interpolatortype="FDI", **kwargs
1184
1182
  ):
1185
1183
  """
1186
1184
  Parameters
@@ -1234,7 +1232,7 @@ class GeologicalModel:
1234
1232
  fault_name: str,
1235
1233
  displacement: float,
1236
1234
  *,
1237
- fault_data:Optional[pd.DataFrame] = None,
1235
+ fault_data: Optional[pd.DataFrame] = None,
1238
1236
  interpolatortype="FDI",
1239
1237
  tol=None,
1240
1238
  fault_slip_vector=None,
@@ -1319,7 +1317,7 @@ class GeologicalModel:
1319
1317
  if "data_region" in kwargs:
1320
1318
  kwargs.pop("data_region")
1321
1319
  logger.error("kwarg data_region currently not supported, disabling")
1322
- displacement_scaled = displacement
1320
+ displacement_scaled = displacement
1323
1321
  fault_frame_builder = FaultBuilder(
1324
1322
  interpolatortype,
1325
1323
  bounding_box=self.bounding_box,
@@ -1340,11 +1338,11 @@ class GeologicalModel:
1340
1338
  if fault_center is not None and ~np.isnan(fault_center).any():
1341
1339
  fault_center = self.scale(fault_center, inplace=False)
1342
1340
  if minor_axis:
1343
- minor_axis = minor_axis
1341
+ minor_axis = minor_axis
1344
1342
  if major_axis:
1345
- major_axis = major_axis
1343
+ major_axis = major_axis
1346
1344
  if intermediate_axis:
1347
- intermediate_axis = intermediate_axis
1345
+ intermediate_axis = intermediate_axis
1348
1346
  fault_frame_builder.create_data_from_geometry(
1349
1347
  fault_frame_data=self.prepare_data(fault_data),
1350
1348
  fault_center=fault_center,
@@ -1400,7 +1398,8 @@ class GeologicalModel:
1400
1398
 
1401
1399
  """
1402
1400
 
1403
- return self.bounding_box.reproject(points,inplace=inplace)
1401
+ return self.bounding_box.reproject(points, inplace=inplace)
1402
+
1404
1403
 
1405
1404
  # TODO move scale to bounding box/transformer
1406
1405
  def scale(self, points: np.ndarray, *, inplace: bool = False) -> np.ndarray:
@@ -1418,7 +1417,8 @@ class GeologicalModel:
1418
1417
  points : np.a::rray((N,3),dtype=double)
1419
1418
 
1420
1419
  """
1421
- return self.bounding_box.project(np.array(points).astype(float),inplace=inplace)
1420
+ return self.bounding_box.project(np.array(points).astype(float), inplace=inplace)
1421
+
1422
1422
 
1423
1423
  def regular_grid(self, *, nsteps=None, shuffle=True, rescale=False, order="C"):
1424
1424
  """
@@ -1567,7 +1567,7 @@ class GeologicalModel:
1567
1567
  if f.type == FeatureType.FAULT:
1568
1568
  disp = f.displacementfeature.evaluate_value(points)
1569
1569
  vals[~np.isnan(disp)] += disp[~np.isnan(disp)]
1570
- return vals # convert from restoration magnutude to displacement
1570
+ return vals # convert from restoration magnutude to displacement
1571
1571
 
1572
1572
  def get_feature_by_name(self, feature_name) -> GeologicalFeature:
1573
1573
  """Returns a feature from the mode given a name
@@ -1736,30 +1736,15 @@ class GeologicalModel:
1736
1736
  units = []
1737
1737
  if self.stratigraphic_column is None:
1738
1738
  return []
1739
- for group in self.stratigraphic_column.keys():
1740
- 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")
1741
1743
  continue
1742
- for series in self.stratigraphic_column[group].values():
1743
- series['feature_name'] = group
1744
- units.append(series)
1745
- unit_table = pd.DataFrame(units)
1746
- for u in unit_table['feature_name'].unique():
1747
-
1748
- values = unit_table.loc[unit_table['feature_name'] == u, 'min' if bottoms else 'max']
1749
- if 'name' not in unit_table.columns:
1750
- unit_table['name'] = unit_table['feature_name']
1751
-
1752
- names = unit_table[unit_table['feature_name'] == u]['name']
1753
- values = values.loc[~np.logical_or(values == np.inf, values == -np.inf)]
1744
+ feature = self.get_feature_by_name(u['group'])
1745
+
1754
1746
  surfaces.extend(
1755
- self.get_feature_by_name(u).surfaces(
1756
- values.to_list(),
1757
- self.bounding_box,
1758
- name=names.loc[values.index].to_list(),
1759
- colours=unit_table.loc[unit_table['feature_name'] == u, 'colour'].tolist()[
1760
- 1:
1761
- ], # we don't isosurface basement, no value
1762
- )
1747
+ feature.surfaces([u['value']], self.bounding_box, name=name, colours=[u['colour']])
1763
1748
  )
1764
1749
 
1765
1750
  return surfaces
@@ -0,0 +1,473 @@
1
+ import enum
2
+ from typing import Dict
3
+ import numpy as np
4
+ from LoopStructural.utils import rng, getLogger
5
+
6
+ logger = getLogger(__name__)
7
+ logger.info("Imported LoopStructural Stratigraphic Column module")
8
+ class UnconformityType(enum.Enum):
9
+ """
10
+ An enumeration for different types of unconformities in a stratigraphic column.
11
+ """
12
+
13
+ ERODE = 'erode'
14
+ ONLAP = 'onlap'
15
+
16
+
17
+ class StratigraphicColumnElementType(enum.Enum):
18
+ """
19
+ An enumeration for different types of elements in a stratigraphic column.
20
+ """
21
+
22
+ UNIT = 'unit'
23
+ UNCONFORMITY = 'unconformity'
24
+
25
+
26
+ class StratigraphicColumnElement:
27
+ """
28
+ A class to represent an element in a stratigraphic column, which can be a unit or a topological object
29
+ for example unconformity.
30
+ """
31
+
32
+ def __init__(self, uuid=None):
33
+ """
34
+ Initializes the StratigraphicColumnElement with a uuid.
35
+ """
36
+ if uuid is None:
37
+ import uuid as uuid_module
38
+
39
+ uuid = str(uuid_module.uuid4())
40
+ self.uuid = uuid
41
+
42
+
43
+ class StratigraphicUnit(StratigraphicColumnElement):
44
+ """
45
+ A class to represent a stratigraphic unit.
46
+ """
47
+
48
+ def __init__(self, *, uuid=None, name=None, colour=None, thickness=None, data=None):
49
+ """
50
+ Initializes the StratigraphicUnit with a name and an optional description.
51
+ """
52
+ super().__init__(uuid)
53
+ self.name = name
54
+ if colour is None:
55
+ colour = rng.random(3)
56
+ self.colour = colour
57
+ self.thickness = thickness
58
+ self.data = data
59
+ self.element_type = StratigraphicColumnElementType.UNIT
60
+
61
+ def to_dict(self):
62
+ """
63
+ Converts the stratigraphic unit to a dictionary representation.
64
+ """
65
+ colour = self.colour
66
+ if isinstance(colour, np.ndarray):
67
+ colour = colour.astype(float).tolist()
68
+ return {"name": self.name, "colour": colour, "thickness": self.thickness, 'uuid': self.uuid}
69
+
70
+ @classmethod
71
+ def from_dict(cls, data):
72
+ """
73
+ Creates a StratigraphicUnit from a dictionary representation.
74
+ """
75
+ if not isinstance(data, dict):
76
+ raise TypeError("Data must be a dictionary")
77
+ name = data.get("name")
78
+ colour = data.get("colour")
79
+ thickness = data.get("thickness", None)
80
+ uuid = data.get("uuid", None)
81
+ return cls(uuid=uuid, name=name, colour=colour, thickness=thickness)
82
+
83
+ def __str__(self):
84
+ """
85
+ Returns a string representation of the stratigraphic unit.
86
+ """
87
+ return (
88
+ f"StratigraphicUnit(name={self.name}, colour={self.colour}, thickness={self.thickness})"
89
+ )
90
+
91
+
92
+ class StratigraphicUnconformity(StratigraphicColumnElement):
93
+ """
94
+ A class to represent a stratigraphic unconformity, which is a surface of discontinuity in the stratigraphic record.
95
+ """
96
+
97
+ def __init__(
98
+ self, *, uuid=None, name=None, unconformity_type: UnconformityType = UnconformityType.ERODE
99
+ ):
100
+ """
101
+ Initializes the StratigraphicUnconformity with a name and an optional description.
102
+ """
103
+ super().__init__(uuid)
104
+
105
+ self.name = name
106
+ if unconformity_type not in [UnconformityType.ERODE, UnconformityType.ONLAP]:
107
+ raise ValueError("Invalid unconformity type")
108
+ self.unconformity_type = unconformity_type
109
+ self.element_type = StratigraphicColumnElementType.UNCONFORMITY
110
+
111
+ def to_dict(self):
112
+ """
113
+ Converts the stratigraphic unconformity to a dictionary representation.
114
+ """
115
+ return {
116
+ "uuid": self.uuid,
117
+ "name": self.name,
118
+ "unconformity_type": self.unconformity_type.value,
119
+ }
120
+
121
+ def __str__(self):
122
+ """
123
+ Returns a string representation of the stratigraphic unconformity.
124
+ """
125
+ return (
126
+ f"StratigraphicUnconformity(name={self.name}, "
127
+ f"unconformity_type={self.unconformity_type.value})"
128
+ )
129
+
130
+ @classmethod
131
+ def from_dict(cls, data):
132
+ """
133
+ Creates a StratigraphicUnconformity from a dictionary representation.
134
+ """
135
+ if not isinstance(data, dict):
136
+ raise TypeError("Data must be a dictionary")
137
+ name = data.get("name")
138
+ unconformity_type = UnconformityType(
139
+ data.get("unconformity_type", UnconformityType.ERODE.value)
140
+ )
141
+ uuid = data.get("uuid", None)
142
+ return cls(uuid=uuid, name=name, unconformity_type=unconformity_type)
143
+ class StratigraphicGroup:
144
+ """
145
+ A class to represent a group of stratigraphic units.
146
+ This class is not fully implemented and serves as a placeholder for future development.
147
+ """
148
+
149
+ def __init__(self, name=None, units=None):
150
+ """
151
+ Initializes the StratigraphicGroup with a name and an optional list of units.
152
+ """
153
+ self.name = name
154
+ self.units = units if units is not None else []
155
+
156
+
157
+ class StratigraphicColumn:
158
+ """
159
+ A class to represent a stratigraphic column, which is a vertical section of the Earth's crust
160
+ showing the sequence of rock layers and their relationships.
161
+ """
162
+
163
+ def __init__(self):
164
+ """
165
+ Initializes the StratigraphicColumn with a name and a list of layers.
166
+ """
167
+ self.order = [StratigraphicUnit(name='Basement', colour='grey', thickness=np.inf),StratigraphicUnconformity(name='Base Unconformity', unconformity_type=UnconformityType.ERODE)]
168
+ self.group_mapping = {}
169
+ def clear(self,basement=True):
170
+ """
171
+ Clears the stratigraphic column, removing all elements.
172
+ """
173
+ if basement:
174
+ self.order = [StratigraphicUnit(name='Basement', colour='grey', thickness=np.inf),StratigraphicUnconformity(name='Base Unconformity', unconformity_type=UnconformityType.ERODE)]
175
+ else:
176
+ self.order = []
177
+ self.group_mapping = {}
178
+
179
+ def add_unit(self, name,*, colour=None, thickness=None, where='top'):
180
+ unit = StratigraphicUnit(name=name, colour=colour, thickness=thickness)
181
+
182
+ if where == 'top':
183
+ self.order.append(unit)
184
+ elif where == 'bottom':
185
+ self.order.insert(0, unit)
186
+ else:
187
+ raise ValueError("Invalid 'where' argument. Use 'top' or 'bottom'.")
188
+
189
+ return unit
190
+
191
+ def remove_unit(self, uuid):
192
+ """
193
+ Removes a unit or unconformity from the stratigraphic column by its uuid.
194
+ """
195
+ for i, element in enumerate(self.order):
196
+ if element.uuid == uuid:
197
+ del self.order[i]
198
+ return True
199
+ return False
200
+
201
+ def add_unconformity(self, name, *, unconformity_type=UnconformityType.ERODE, where='top' ):
202
+ unconformity = StratigraphicUnconformity(
203
+ uuid=None, name=name, unconformity_type=unconformity_type
204
+ )
205
+
206
+ if where == 'top':
207
+ self.order.append(unconformity)
208
+ elif where == 'bottom':
209
+ self.order.insert(0, unconformity)
210
+ else:
211
+ raise ValueError("Invalid 'where' argument. Use 'top' or 'bottom'.")
212
+ return unconformity
213
+
214
+ def get_element_by_index(self, index):
215
+ """
216
+ Retrieves an element by its index from the stratigraphic column.
217
+ """
218
+ if index < 0 or index >= len(self.order):
219
+ raise IndexError("Index out of range")
220
+ return self.order[index]
221
+
222
+ def get_unit_by_name(self, name):
223
+ """
224
+ Retrieves a unit by its name from the stratigraphic column.
225
+ """
226
+ for unit in self.order:
227
+ if isinstance(unit, StratigraphicUnit) and unit.name == name:
228
+ return unit
229
+
230
+ return None
231
+ def get_unconformity_by_name(self, name):
232
+ """
233
+ Retrieves an unconformity by its name from the stratigraphic column.
234
+ """
235
+ for unconformity in self.order:
236
+ if isinstance(unconformity, StratigraphicUnconformity) and unconformity.name == name:
237
+ return unconformity
238
+
239
+ return None
240
+ def get_element_by_uuid(self, uuid):
241
+ """
242
+ Retrieves an element by its uuid from the stratigraphic column.
243
+ """
244
+ for element in self.order:
245
+ if element.uuid == uuid:
246
+ return element
247
+ raise KeyError(f"No element found with uuid: {uuid}")
248
+ def add_element(self, element):
249
+ """
250
+ Adds a StratigraphicColumnElement to the stratigraphic column.
251
+ """
252
+ if isinstance(element, StratigraphicColumnElement):
253
+ self.order.append(element)
254
+ else:
255
+ raise TypeError("Element must be an instance of StratigraphicColumnElement")
256
+
257
+ def get_elements(self):
258
+ """
259
+ Returns a list of all elements in the stratigraphic column.
260
+ """
261
+ return self.order
262
+
263
+ def get_groups(self):
264
+ groups = []
265
+ i=0
266
+ group = StratigraphicGroup(
267
+ name=(
268
+ f'Group_{i}'
269
+ if f'Group_{i}' not in self.group_mapping
270
+ else self.group_mapping[f'Group_{i}']
271
+ )
272
+ )
273
+ for e in reversed(self.order):
274
+ if isinstance(e, StratigraphicUnit):
275
+ group.units.append(e)
276
+ else:
277
+ if group.units:
278
+ groups.append(group)
279
+ i+=1
280
+ group = StratigraphicGroup(
281
+ name=(
282
+ f'Group_{i}'
283
+ if f'Group_{i}' not in self.group_mapping
284
+ else self.group_mapping[f'Group_{i}']
285
+ )
286
+ )
287
+ if group:
288
+ groups.append(group)
289
+ return groups
290
+
291
+ def get_unitname_groups(self):
292
+ groups = self.get_groups()
293
+ groups_list = []
294
+ group = []
295
+ for g in groups:
296
+ group = [u.name for u in g.units if isinstance(u, StratigraphicUnit)]
297
+ groups_list.append(group)
298
+ return groups_list
299
+
300
+
301
+ def __getitem__(self, uuid):
302
+ """
303
+ Retrieves an element by its uuid from the stratigraphic column.
304
+ """
305
+ for element in self.order:
306
+ if element.uuid == uuid:
307
+ return element
308
+ raise KeyError(f"No element found with uuid: {uuid}")
309
+
310
+ def update_order(self, new_order):
311
+ """
312
+ Updates the order of elements in the stratigraphic column based on a new order list.
313
+ """
314
+ if not isinstance(new_order, list):
315
+ raise TypeError("New order must be a list")
316
+ self.order = [
317
+ self.__getitem__(uuid) for uuid in new_order if self.__getitem__(uuid) is not None
318
+ ]
319
+
320
+ def update_element(self, unit_data: Dict):
321
+ """
322
+ Updates an existing element in the stratigraphic column with new data.
323
+ :param unit_data: A dictionary containing the updated data for the element.
324
+ """
325
+ if not isinstance(unit_data, dict):
326
+ raise TypeError("unit_data must be a dictionary")
327
+ element = self.__getitem__(unit_data['uuid'])
328
+ if isinstance(element, StratigraphicUnit):
329
+ element.name = unit_data.get('name', element.name)
330
+ element.colour = unit_data.get('colour', element.colour)
331
+ element.thickness = unit_data.get('thickness', element.thickness)
332
+ elif isinstance(element, StratigraphicUnconformity):
333
+ element.name = unit_data.get('name', element.name)
334
+ element.unconformity_type = UnconformityType(
335
+ unit_data.get('unconformity_type', element.unconformity_type.value)
336
+ )
337
+
338
+ def __str__(self):
339
+ """
340
+ Returns a string representation of the stratigraphic column, listing all elements.
341
+ """
342
+ return "\n".join([f"{i+1}. {element}" for i, element in enumerate(self.order)])
343
+
344
+ def to_dict(self):
345
+ """
346
+ Converts the stratigraphic column to a dictionary representation.
347
+ """
348
+ return {
349
+ "elements": [element.to_dict() for element in self.order],
350
+ }
351
+ def update_from_dict(self, data):
352
+ """
353
+ Updates the stratigraphic column from a dictionary representation.
354
+ """
355
+ if not isinstance(data, dict):
356
+ raise TypeError("Data must be a dictionary")
357
+ self.clear(basement=False)
358
+ elements_data = data.get("elements", [])
359
+ for element_data in elements_data:
360
+ if "unconformity_type" in element_data:
361
+ element = StratigraphicUnconformity.from_dict(element_data)
362
+ else:
363
+ element = StratigraphicUnit.from_dict(element_data)
364
+ self.add_element(element)
365
+ @classmethod
366
+ def from_dict(cls, data):
367
+ """
368
+ Creates a StratigraphicColumn from a dictionary representation.
369
+ """
370
+ if not isinstance(data, dict):
371
+ raise TypeError("Data must be a dictionary")
372
+ column = cls()
373
+ column.clear(basement=False)
374
+ elements_data = data.get("elements", [])
375
+ for element_data in elements_data:
376
+ if "unconformity_type" in element_data:
377
+ element = StratigraphicUnconformity.from_dict(element_data)
378
+ else:
379
+ element = StratigraphicUnit.from_dict(element_data)
380
+ column.add_element(element)
381
+ return column
382
+
383
+ def get_isovalues(self) -> Dict[str, float]:
384
+ """
385
+ Returns a dictionary of isovalues for the stratigraphic units in the column.
386
+ """
387
+ surface_values = {}
388
+ for g in reversed(self.get_groups()):
389
+ v = 0
390
+ for u in g.units:
391
+ surface_values[u.name] = {'value':v,'group':g.name,'colour':u.colour}
392
+ v += u.thickness
393
+ return surface_values
394
+
395
+ def plot(self,*, ax=None, **kwargs):
396
+ import matplotlib.pyplot as plt
397
+ from matplotlib import cm
398
+ from matplotlib.patches import Polygon
399
+ from matplotlib.collections import PatchCollection
400
+ n_units = 0 # count how many discrete colours (number of stratigraphic units)
401
+ xmin = 0
402
+ ymin = 0
403
+ ymax = 1
404
+ xmax = 1
405
+ fig = None
406
+ if ax is None:
407
+ fig, ax = plt.subplots(figsize=(2, 10))
408
+ patches = [] # stores the individual stratigraphic unit polygons
409
+
410
+ total_height = 0
411
+ prev_coords = [0, 0]
412
+
413
+ # iterate through groups, skipping faults
414
+ for u in reversed(self.order):
415
+ if u.element_type == StratigraphicColumnElementType.UNCONFORMITY:
416
+ logger.info(f"Plotting unconformity {u.name} of type {u.unconformity_type.value}")
417
+ ax.axhline(y=total_height, linestyle='--', color='black')
418
+ ax.annotate(
419
+ getattr(u, 'name', 'Unconformity'),
420
+ xy=(xmin, total_height),
421
+ fontsize=8,
422
+ ha='left',
423
+ )
424
+
425
+ total_height -= 0.05 # Adjust height slightly for visual separation
426
+ continue
427
+
428
+ if u.element_type == StratigraphicColumnElementType.UNIT:
429
+ logger.info(f"Plotting unit {u.name} of type {u.element_type}")
430
+
431
+ n_units += 1
432
+
433
+ ymax = total_height
434
+ ymin = ymax - (getattr(u, 'thickness', np.nan) if not np.isinf(getattr(u, 'thickness', np.nan)) else np.nanmean([getattr(e, 'thickness', np.nan) for e in self.order if not np.isinf(getattr(e, 'thickness', np.nan))]))
435
+
436
+ if not np.isfinite(ymin):
437
+ ymin = prev_coords[1] - (prev_coords[1] - prev_coords[0]) * (1 + rng.random())
438
+
439
+ total_height = ymin
440
+
441
+ prev_coords = (ymin, ymax)
442
+
443
+ polygon_points = np.array([[xmin, ymin], [xmax, ymin], [xmax, ymax], [xmin, ymax]])
444
+ patches.append(Polygon(polygon_points))
445
+ ax.annotate(getattr(u, 'name', 'Unknown'), xy=(xmin+(xmax-xmin)/2, (ymax-ymin)/2+ymin), fontsize=8, ha='left')
446
+
447
+ if 'cmap' not in kwargs:
448
+ import matplotlib.colors as colors
449
+
450
+ colours = []
451
+ boundaries = []
452
+ data = []
453
+ for i, u in enumerate(self.order):
454
+ if u.element_type != StratigraphicColumnElementType.UNIT:
455
+ continue
456
+ data.append((i, u.colour))
457
+ colours.append(u.colour)
458
+ boundaries.append(i) # print(u,v)
459
+ cmap = colors.ListedColormap(colours)
460
+ else:
461
+ cmap = cm.get_cmap(kwargs['cmap'], n_units - 1)
462
+ p = PatchCollection(patches, cmap=cmap)
463
+
464
+ colors = np.arange(len(patches))
465
+ p.set_array(np.array(colors))
466
+
467
+ ax.add_collection(p)
468
+
469
+ ax.set_ylim(total_height - (total_height - prev_coords[0]) * 0.1, 0)
470
+
471
+ ax.axis("off")
472
+
473
+ return fig
@@ -150,6 +150,7 @@ class FaultBuilder(StructuralFrameBuilder):
150
150
  np.logical_and(fault_frame_data["coord"] == 0, fault_frame_data["val"] == 0),
151
151
  ["X", "Y"],
152
152
  ].to_numpy()
153
+ self.fault_dip = fault_dip
153
154
  if fault_normal_vector is None:
154
155
  if fault_frame_data.loc[
155
156
  np.logical_and(fault_frame_data["coord"] == 0, fault_frame_data["nx"].notna())].shape[0]>0:
@@ -104,7 +104,7 @@ class FaultSegment(StructuralFrame):
104
104
  if self.builder is None:
105
105
  raise ValueError("Fault builder not set")
106
106
  return self.builder.fault_major_axis
107
-
107
+
108
108
  @property
109
109
  def fault_intermediate_axis(self):
110
110
  if self.builder is None:
LoopStructural/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "1.6.15"
1
+ __version__ = "1.6.16"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: LoopStructural
3
- Version: 1.6.15
3
+ Version: 1.6.16
4
4
  Summary: 3D geological modelling
5
5
  Author-email: Lachlan Grose <lachlan.grose@monash.edu>
6
6
  License: MIT
@@ -1,5 +1,5 @@
1
- LoopStructural/__init__.py,sha256=fg_Vm1aMDYIf_CffTFopLsTx21u6deLaI7JMVpRYdOI,1378
2
- LoopStructural/version.py,sha256=Pie9fpkEEQtWduVE5VudTkNuOKHw2-cSLQQzkG-8ENk,23
1
+ LoopStructural/__init__.py,sha256=3yLT09qS3rOrQtyxsECOTh98B3sKv2IrE_DIfTSu930,2022
2
+ LoopStructural/version.py,sha256=DGM49nQ783oxeemzhU9PZifWC3GA5fiS9MnhU4zNfso,23
3
3
  LoopStructural/datasets/__init__.py,sha256=ylb7fzJU_DyQ73LlwQos7VamqkDSGITbbnoKg7KAOmE,677
4
4
  LoopStructural/datasets/_base.py,sha256=FB_D5ybBYHoaNbycdkpZcRffzjrrL1xp9X0k-pyob9Y,7618
5
5
  LoopStructural/datasets/_example_models.py,sha256=Zg33IeUyh4C-lC0DRMLqCDP2IrX8L-gNV1WxJwBGjzM,113
@@ -45,7 +45,7 @@ LoopStructural/interpolators/_constant_norm.py,sha256=gGaDGDoEzfnL4b6386YwInCxIA
45
45
  LoopStructural/interpolators/_discrete_fold_interpolator.py,sha256=eDe0R1lcQ0AuMcv7zlpu5c-soCv7AybIqQAuN2vFE3M,6542
46
46
  LoopStructural/interpolators/_discrete_interpolator.py,sha256=bPGJ1CrvLmz3m86JkXAiw7WbfbGEeGXR5cklDX54PQU,26083
47
47
  LoopStructural/interpolators/_finite_difference_interpolator.py,sha256=qc7zpqJka16I7yv-GigjQxF0hWRRHyWpHm8dHersy_8,18712
48
- LoopStructural/interpolators/_geological_interpolator.py,sha256=BXUJD1OrSbgZbJwe884FDkew0m1XxiG1mtY5Og_CFJE,11196
48
+ LoopStructural/interpolators/_geological_interpolator.py,sha256=hdi8mFwHp4G3Tv3lXDvQey8QrRIHuk3KRbJcKtBh918,11460
49
49
  LoopStructural/interpolators/_interpolator_builder.py,sha256=Z8bhmco5aSQX19A8It2SB_rG61wnlyshWfp3ivm8rU0,4586
50
50
  LoopStructural/interpolators/_interpolator_factory.py,sha256=fbjebXSe5IgTol1tnBlnsw9gD426v-TGkX3gquIg7LI,2782
51
51
  LoopStructural/interpolators/_interpolatortype.py,sha256=q8U9JGyFpO2FBA9XsMI5ojv3TV1LYqyvYHzLAbHcj9A,593
@@ -71,7 +71,8 @@ LoopStructural/interpolators/supports/_face_table.py,sha256=Hyj4Io63NkPRN8ab9uDH
71
71
  LoopStructural/interpolators/supports/_support_factory.py,sha256=XNAxnr-JS3KEhdsoZeJ-VaLTJwlvxgBuRMCqYrCDW18,1485
72
72
  LoopStructural/modelling/__init__.py,sha256=a-bq2gDhyUlcky5l9kl_IP3ExMdohkgYjQz2V8madQE,902
73
73
  LoopStructural/modelling/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
74
- LoopStructural/modelling/core/geological_model.py,sha256=2NIVrPHu_8rx3Cgp9oPgFazBWHUI0dLs7vWFngfN3g0,65272
74
+ LoopStructural/modelling/core/geological_model.py,sha256=asAyjVkO-uILX8QQgM8oqdkBQQfCjFDpuXyy2wn8lwc,65150
75
+ LoopStructural/modelling/core/stratigraphic_column.py,sha256=ZHquRb6S97KwB8K8pqEGRSDah0-FjU2uOqdor8NMuY4,16828
75
76
  LoopStructural/modelling/features/__init__.py,sha256=Vf-qd5EDBtJ1DpuXXyCcw2-wf6LWPRW5wzxDEO3vOc8,939
76
77
  LoopStructural/modelling/features/_analytical_feature.py,sha256=U_g86LgQhYY2359rdsDqpvziYwqrWkc5EdvhJARiUWo,3597
77
78
  LoopStructural/modelling/features/_base_geological_feature.py,sha256=kGyrbb8nNzfi-M8WSrVMEQYKtxThdcBxaji5HKXtAqw,13483
@@ -84,14 +85,14 @@ LoopStructural/modelling/features/_structural_frame.py,sha256=e3QmNHLwuZc5PX3rLa
84
85
  LoopStructural/modelling/features/_unconformity_feature.py,sha256=2Bx0BI38YLdcNvDWuP9E1pKFN4orEUq9aC8b5xG1UVk,2362
85
86
  LoopStructural/modelling/features/builders/__init__.py,sha256=Gqld1C-PcaXfJ8vpkWMDCmehmd3hZNYQk1knPtl59Bk,266
86
87
  LoopStructural/modelling/features/builders/_base_builder.py,sha256=N3txGC98V08A8-k2TLdoIWgWLfblZ91kaTvciPq_QVM,3750
87
- LoopStructural/modelling/features/builders/_fault_builder.py,sha256=CeQnvgDrgMIbyPV6nB0qnpY5PJG1OYTJIukRXv4df1E,25324
88
+ LoopStructural/modelling/features/builders/_fault_builder.py,sha256=_DZ0Hy_-jjm2fFU-5lY60zGisixdUWbAjsOQzMFKigY,25359
88
89
  LoopStructural/modelling/features/builders/_folded_feature_builder.py,sha256=1_0BVTzcvmFl6K3_lX-jF0tiMFPmS8j6vPeSLn9MbrE,6607
89
90
  LoopStructural/modelling/features/builders/_geological_feature_builder.py,sha256=tQJJol1U5wH6V0Rw3OgigCFPssv8uOPQ5jdwdLFg3cc,22015
90
91
  LoopStructural/modelling/features/builders/_structural_frame_builder.py,sha256=ms3-fuFpDEarjzYU5W499TquOIlTwHPUibVxIypfmWY,8019
91
92
  LoopStructural/modelling/features/fault/__init__.py,sha256=4u0KfYzmoO-ddFGo9qd9ov0gBoLqBiPAUsaw5zhEOAQ,189
92
93
  LoopStructural/modelling/features/fault/_fault_function.py,sha256=QEPh2jIvgD68hEJc5SM5xuMzZw-93V1me1ZbK9G2TB0,12655
93
94
  LoopStructural/modelling/features/fault/_fault_function_feature.py,sha256=4m0jVNx7ewrVI0pECI1wNciv8Cy8FzhZrYDjKJ_e2GU,2558
94
- LoopStructural/modelling/features/fault/_fault_segment.py,sha256=dNTCY0ZyC8krrL1suSnhywSE_i5V_VZ4DJ2BieirkhI,18305
95
+ LoopStructural/modelling/features/fault/_fault_segment.py,sha256=BEIVAY_-iQYYuoyIj1doq_cDLgmMpY0PDYBiuBXOjN8,18309
95
96
  LoopStructural/modelling/features/fold/__init__.py,sha256=pOv20yQvshZozvmO_YFw2E7Prp9DExlm855N-0SnxbQ,175
96
97
  LoopStructural/modelling/features/fold/_fold.py,sha256=bPnnLUSiF4uoMRg8aHoOSTPRgaM0JyLoRQPu5_A-J3w,5448
97
98
  LoopStructural/modelling/features/fold/_fold_rotation_angle_feature.py,sha256=CXLbFRQ3CrTMAcHmfdbKcmSvvLs9_6TLe0Wqi1pK2tg,892
@@ -131,8 +132,8 @@ LoopStructural/utils/regions.py,sha256=SjCC40GI7_n03G4mlcmvyrBgJFbxnvB3leBzXWco3
131
132
  LoopStructural/utils/typing.py,sha256=29uVSTZdzXXH-jdlaYyBWZ1gQ2-nlZ2-XoVgG_PXNFY,157
132
133
  LoopStructural/utils/utils.py,sha256=2Z4zVE6G752-SPmM29zebk82bROJxEwi_YiiJjcVED4,2438
133
134
  LoopStructural/visualisation/__init__.py,sha256=5BDgKor8-ae6DrS7IZybJ3Wq_pTnCchxuY4EgzA7v1M,318
134
- loopstructural-1.6.15.dist-info/licenses/LICENSE,sha256=ZqGeNFOgmYevj7Ld7Q-kR4lAxWXuBRUdUmPC6XM_py8,1071
135
- loopstructural-1.6.15.dist-info/METADATA,sha256=2aOi3Osgig1KIQcpmKAyTDOJbo1DksD-mA3SICfAbI0,6453
136
- loopstructural-1.6.15.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
137
- loopstructural-1.6.15.dist-info/top_level.txt,sha256=QtQErKzYHfg6ddxTQ1NyaTxXBVM6qAqrM_vxEPyXZLg,15
138
- loopstructural-1.6.15.dist-info/RECORD,,
135
+ loopstructural-1.6.16.dist-info/licenses/LICENSE,sha256=ZqGeNFOgmYevj7Ld7Q-kR4lAxWXuBRUdUmPC6XM_py8,1071
136
+ loopstructural-1.6.16.dist-info/METADATA,sha256=uvSdqdLD2MRCeE972hyitE5L5UfIne1o5uVRXzIPIPU,6453
137
+ loopstructural-1.6.16.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
138
+ loopstructural-1.6.16.dist-info/top_level.txt,sha256=QtQErKzYHfg6ddxTQ1NyaTxXBVM6qAqrM_vxEPyXZLg,15
139
+ loopstructural-1.6.16.dist-info/RECORD,,