LoopStructural 1.6.15__py3-none-any.whl → 1.6.17__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.

@@ -0,0 +1,500 @@
1
+ import enum
2
+ from typing import Dict, Optional, List, Tuple
3
+ import numpy as np
4
+ from LoopStructural.utils import rng, getLogger, Observable
5
+ logger = getLogger(__name__)
6
+ logger.info("Imported LoopStructural Stratigraphic Column module")
7
+ class UnconformityType(enum.Enum):
8
+ """
9
+ An enumeration for different types of unconformities in a stratigraphic column.
10
+ """
11
+
12
+ ERODE = 'erode'
13
+ ONLAP = 'onlap'
14
+
15
+
16
+ class StratigraphicColumnElementType(enum.Enum):
17
+ """
18
+ An enumeration for different types of elements in a stratigraphic column.
19
+ """
20
+
21
+ UNIT = 'unit'
22
+ UNCONFORMITY = 'unconformity'
23
+
24
+
25
+ class StratigraphicColumnElement:
26
+ """
27
+ A class to represent an element in a stratigraphic column, which can be a unit or a topological object
28
+ for example unconformity.
29
+ """
30
+
31
+ def __init__(self, uuid=None):
32
+ """
33
+ Initializes the StratigraphicColumnElement with a uuid.
34
+ """
35
+ if uuid is None:
36
+ import uuid as uuid_module
37
+
38
+ uuid = str(uuid_module.uuid4())
39
+ self.uuid = uuid
40
+
41
+
42
+ class StratigraphicUnit(StratigraphicColumnElement):
43
+ """
44
+ A class to represent a stratigraphic unit.
45
+ """
46
+
47
+ def __init__(self, *, uuid=None, name=None, colour=None, thickness=None, data=None):
48
+ """
49
+ Initializes the StratigraphicUnit with a name and an optional description.
50
+ """
51
+ super().__init__(uuid)
52
+ self.name = name
53
+ if colour is None:
54
+ colour = rng.random(3)
55
+ self.colour = colour
56
+ self.thickness = thickness
57
+ self.data = data
58
+ self.element_type = StratigraphicColumnElementType.UNIT
59
+
60
+ def to_dict(self):
61
+ """
62
+ Converts the stratigraphic unit to a dictionary representation.
63
+ """
64
+ colour = self.colour
65
+ if isinstance(colour, np.ndarray):
66
+ colour = colour.astype(float).tolist()
67
+ return {"name": self.name, "colour": colour, "thickness": self.thickness, 'uuid': self.uuid}
68
+
69
+ @classmethod
70
+ def from_dict(cls, data):
71
+ """
72
+ Creates a StratigraphicUnit from a dictionary representation.
73
+ """
74
+ if not isinstance(data, dict):
75
+ raise TypeError("Data must be a dictionary")
76
+ name = data.get("name")
77
+ colour = data.get("colour")
78
+ thickness = data.get("thickness", None)
79
+ uuid = data.get("uuid", None)
80
+ return cls(uuid=uuid, name=name, colour=colour, thickness=thickness)
81
+
82
+ def __str__(self):
83
+ """
84
+ Returns a string representation of the stratigraphic unit.
85
+ """
86
+ return (
87
+ f"StratigraphicUnit(name={self.name}, colour={self.colour}, thickness={self.thickness})"
88
+ )
89
+
90
+
91
+ class StratigraphicUnconformity(StratigraphicColumnElement):
92
+ """
93
+ A class to represent a stratigraphic unconformity, which is a surface of discontinuity in the stratigraphic record.
94
+ """
95
+
96
+ def __init__(
97
+ self, *, uuid=None, name=None, unconformity_type: UnconformityType = UnconformityType.ERODE
98
+ ):
99
+ """
100
+ Initializes the StratigraphicUnconformity with a name and an optional description.
101
+ """
102
+ super().__init__(uuid)
103
+
104
+ self.name = name
105
+ if unconformity_type not in [UnconformityType.ERODE, UnconformityType.ONLAP]:
106
+ raise ValueError("Invalid unconformity type")
107
+ self.unconformity_type = unconformity_type
108
+ self.element_type = StratigraphicColumnElementType.UNCONFORMITY
109
+
110
+ def to_dict(self):
111
+ """
112
+ Converts the stratigraphic unconformity to a dictionary representation.
113
+ """
114
+ return {
115
+ "uuid": self.uuid,
116
+ "name": self.name,
117
+ "unconformity_type": self.unconformity_type.value,
118
+ }
119
+
120
+ def __str__(self):
121
+ """
122
+ Returns a string representation of the stratigraphic unconformity.
123
+ """
124
+ return (
125
+ f"StratigraphicUnconformity(name={self.name}, "
126
+ f"unconformity_type={self.unconformity_type.value})"
127
+ )
128
+
129
+ @classmethod
130
+ def from_dict(cls, data):
131
+ """
132
+ Creates a StratigraphicUnconformity from a dictionary representation.
133
+ """
134
+ if not isinstance(data, dict):
135
+ raise TypeError("Data must be a dictionary")
136
+ name = data.get("name")
137
+ unconformity_type = UnconformityType(
138
+ data.get("unconformity_type", UnconformityType.ERODE.value)
139
+ )
140
+ uuid = data.get("uuid", None)
141
+ return cls(uuid=uuid, name=name, unconformity_type=unconformity_type)
142
+ class StratigraphicGroup:
143
+ """
144
+ A class to represent a group of stratigraphic units.
145
+ This class is not fully implemented and serves as a placeholder for future development.
146
+ """
147
+
148
+ def __init__(self, name=None, units=None):
149
+ """
150
+ Initializes the StratigraphicGroup with a name and an optional list of units.
151
+ """
152
+ self.name = name
153
+ self.units = units if units is not None else []
154
+
155
+
156
+ class StratigraphicColumn(Observable['StratigraphicColumn']):
157
+ """
158
+ A class to represent a stratigraphic column, which is a vertical section of the Earth's crust
159
+ showing the sequence of rock layers and their relationships.
160
+ """
161
+
162
+ def __init__(self):
163
+ """
164
+ Initializes the StratigraphicColumn with a name and a list of layers.
165
+ """
166
+ super().__init__()
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
+ self.notify('column_cleared')
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
+ self.notify('unit_added', unit=unit)
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
+ self.notify('unit_removed', uuid=uuid)
199
+ return True
200
+
201
+ return False
202
+
203
+ def add_unconformity(self, name, *, unconformity_type=UnconformityType.ERODE, where='top' ):
204
+ unconformity = StratigraphicUnconformity(
205
+ uuid=None, name=name, unconformity_type=unconformity_type
206
+ )
207
+
208
+ if where == 'top':
209
+ self.order.append(unconformity)
210
+ elif where == 'bottom':
211
+ self.order.insert(0, unconformity)
212
+ else:
213
+ raise ValueError("Invalid 'where' argument. Use 'top' or 'bottom'.")
214
+ self.notify('unconformity_added', unconformity=unconformity)
215
+ return unconformity
216
+
217
+ def get_element_by_index(self, index):
218
+ """
219
+ Retrieves an element by its index from the stratigraphic column.
220
+ """
221
+ if index < 0 or index >= len(self.order):
222
+ raise IndexError("Index out of range")
223
+ return self.order[index]
224
+
225
+ def get_unit_by_name(self, name):
226
+ """
227
+ Retrieves a unit by its name from the stratigraphic column.
228
+ """
229
+ for unit in self.order:
230
+ if isinstance(unit, StratigraphicUnit) and unit.name == name:
231
+ return unit
232
+
233
+ return None
234
+
235
+ def get_unconformity_by_name(self, name):
236
+ """
237
+ Retrieves an unconformity by its name from the stratigraphic column.
238
+ """
239
+ for unconformity in self.order:
240
+ if isinstance(unconformity, StratigraphicUnconformity) and unconformity.name == name:
241
+ return unconformity
242
+
243
+ return None
244
+ def get_element_by_uuid(self, uuid):
245
+ """
246
+ Retrieves an element by its uuid from the stratigraphic column.
247
+ """
248
+ for element in self.order:
249
+ if element.uuid == uuid:
250
+ return element
251
+ raise KeyError(f"No element found with uuid: {uuid}")
252
+
253
+ def get_group_for_unit_name(self, unit_name:str) -> Optional[StratigraphicGroup]:
254
+ """
255
+ Retrieves the group for a given unit name.
256
+ """
257
+ for group in self.get_groups():
258
+ if any(unit.name == unit_name for unit in group.units):
259
+ return group
260
+ return None
261
+ def add_element(self, element):
262
+ """
263
+ Adds a StratigraphicColumnElement to the stratigraphic column.
264
+ """
265
+ if isinstance(element, StratigraphicColumnElement):
266
+ self.order.append(element)
267
+ else:
268
+ raise TypeError("Element must be an instance of StratigraphicColumnElement")
269
+
270
+ def get_elements(self):
271
+ """
272
+ Returns a list of all elements in the stratigraphic column.
273
+ """
274
+ return self.order
275
+
276
+ def get_groups(self):
277
+ groups = []
278
+ i=0
279
+ group = StratigraphicGroup(
280
+ name=(
281
+ f'Group_{i}'
282
+ if f'Group_{i}' not in self.group_mapping
283
+ else self.group_mapping[f'Group_{i}']
284
+ )
285
+ )
286
+ for e in reversed(self.order):
287
+ if isinstance(e, StratigraphicUnit):
288
+ group.units.append(e)
289
+ else:
290
+ if group.units:
291
+ groups.append(group)
292
+ i+=1
293
+ group = StratigraphicGroup(
294
+ name=(
295
+ f'Group_{i}'
296
+ if f'Group_{i}' not in self.group_mapping
297
+ else self.group_mapping[f'Group_{i}']
298
+ )
299
+ )
300
+ if group:
301
+ groups.append(group)
302
+ return groups
303
+
304
+ def get_unitname_groups(self):
305
+ groups = self.get_groups()
306
+ groups_list = []
307
+ group = []
308
+ for g in groups:
309
+ group = [u.name for u in g.units if isinstance(u, StratigraphicUnit)]
310
+ groups_list.append(group)
311
+ return groups_list
312
+
313
+ def get_group_unit_pairs(self) -> List[Tuple[str,str]]:
314
+ """
315
+ Returns a list of tuples containing group names and unit names.
316
+ """
317
+ groups = self.get_groups()
318
+ group_unit_pairs = []
319
+ for g in groups:
320
+ for u in g.units:
321
+ if isinstance(u, StratigraphicUnit):
322
+ group_unit_pairs.append((g.name, u.name))
323
+ return group_unit_pairs
324
+
325
+ def __getitem__(self, uuid):
326
+ """
327
+ Retrieves an element by its uuid from the stratigraphic column.
328
+ """
329
+ for element in self.order:
330
+ if element.uuid == uuid:
331
+ return element
332
+ raise KeyError(f"No element found with uuid: {uuid}")
333
+
334
+ def update_order(self, new_order):
335
+ """
336
+ Updates the order of elements in the stratigraphic column based on a new order list.
337
+ """
338
+ if not isinstance(new_order, list):
339
+ raise TypeError("New order must be a list")
340
+ self.order = [
341
+ self.__getitem__(uuid) for uuid in new_order if self.__getitem__(uuid) is not None
342
+ ]
343
+ self.notify('order_updated', new_order=self.order)
344
+
345
+ def update_element(self, unit_data: Dict):
346
+ """
347
+ Updates an existing element in the stratigraphic column with new data.
348
+ :param unit_data: A dictionary containing the updated data for the element.
349
+ """
350
+ if not isinstance(unit_data, dict):
351
+ raise TypeError("unit_data must be a dictionary")
352
+ element = self.__getitem__(unit_data['uuid'])
353
+ if isinstance(element, StratigraphicUnit):
354
+ element.name = unit_data.get('name', element.name)
355
+ element.colour = unit_data.get('colour', element.colour)
356
+ element.thickness = unit_data.get('thickness', element.thickness)
357
+ elif isinstance(element, StratigraphicUnconformity):
358
+ element.name = unit_data.get('name', element.name)
359
+ element.unconformity_type = UnconformityType(
360
+ unit_data.get('unconformity_type', element.unconformity_type.value)
361
+ )
362
+ self.notify('element_updated', element=element)
363
+
364
+ def __str__(self):
365
+ """
366
+ Returns a string representation of the stratigraphic column, listing all elements.
367
+ """
368
+ return "\n".join([f"{i+1}. {element}" for i, element in enumerate(self.order)])
369
+
370
+ def to_dict(self):
371
+ """
372
+ Converts the stratigraphic column to a dictionary representation.
373
+ """
374
+ return {
375
+ "elements": [element.to_dict() for element in self.order],
376
+ }
377
+ def update_from_dict(self, data):
378
+ """
379
+ Updates the stratigraphic column from a dictionary representation.
380
+ """
381
+ if not isinstance(data, dict):
382
+ raise TypeError("Data must be a dictionary")
383
+ with self.freeze_notifications():
384
+ self.clear(basement=False)
385
+ elements_data = data.get("elements", [])
386
+ for element_data in elements_data:
387
+ if "unconformity_type" in element_data:
388
+ element = StratigraphicUnconformity.from_dict(element_data)
389
+ else:
390
+ element = StratigraphicUnit.from_dict(element_data)
391
+ self.add_element(element)
392
+ @classmethod
393
+ def from_dict(cls, data):
394
+ """
395
+ Creates a StratigraphicColumn from a dictionary representation.
396
+ """
397
+ if not isinstance(data, dict):
398
+ raise TypeError("Data must be a dictionary")
399
+ column = cls()
400
+ column.clear(basement=False)
401
+ elements_data = data.get("elements", [])
402
+ for element_data in elements_data:
403
+ if "unconformity_type" in element_data:
404
+ element = StratigraphicUnconformity.from_dict(element_data)
405
+ else:
406
+ element = StratigraphicUnit.from_dict(element_data)
407
+ column.add_element(element)
408
+ return column
409
+
410
+ def get_isovalues(self) -> Dict[str, float]:
411
+ """
412
+ Returns a dictionary of isovalues for the stratigraphic units in the column.
413
+ """
414
+ surface_values = {}
415
+ for g in reversed(self.get_groups()):
416
+ v = 0
417
+ for u in g.units:
418
+ surface_values[u.name] = {'value':v,'group':g.name,'colour':u.colour}
419
+ v += u.thickness
420
+ return surface_values
421
+
422
+ def plot(self,*, ax=None, **kwargs):
423
+ import matplotlib.pyplot as plt
424
+ from matplotlib import cm
425
+ from matplotlib.patches import Polygon
426
+ from matplotlib.collections import PatchCollection
427
+ n_units = 0 # count how many discrete colours (number of stratigraphic units)
428
+ xmin = 0
429
+ ymin = 0
430
+ ymax = 1
431
+ xmax = 1
432
+ fig = None
433
+ if ax is None:
434
+ fig, ax = plt.subplots(figsize=(2, 10))
435
+ patches = [] # stores the individual stratigraphic unit polygons
436
+
437
+ total_height = 0
438
+ prev_coords = [0, 0]
439
+
440
+ # iterate through groups, skipping faults
441
+ for u in reversed(self.order):
442
+ if u.element_type == StratigraphicColumnElementType.UNCONFORMITY:
443
+ logger.info(f"Plotting unconformity {u.name} of type {u.unconformity_type.value}")
444
+ ax.axhline(y=total_height, linestyle='--', color='black')
445
+ ax.annotate(
446
+ getattr(u, 'name', 'Unconformity'),
447
+ xy=(xmin, total_height),
448
+ fontsize=8,
449
+ ha='left',
450
+ )
451
+
452
+ total_height -= 0.05 # Adjust height slightly for visual separation
453
+ continue
454
+
455
+ if u.element_type == StratigraphicColumnElementType.UNIT:
456
+ logger.info(f"Plotting unit {u.name} of type {u.element_type}")
457
+
458
+ n_units += 1
459
+
460
+ ymax = total_height
461
+ 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))]))
462
+
463
+ if not np.isfinite(ymin):
464
+ ymin = prev_coords[1] - (prev_coords[1] - prev_coords[0]) * (1 + rng.random())
465
+
466
+ total_height = ymin
467
+
468
+ prev_coords = (ymin, ymax)
469
+
470
+ polygon_points = np.array([[xmin, ymin], [xmax, ymin], [xmax, ymax], [xmin, ymax]])
471
+ patches.append(Polygon(polygon_points))
472
+ ax.annotate(getattr(u, 'name', 'Unknown'), xy=(xmin+(xmax-xmin)/2, (ymax-ymin)/2+ymin), fontsize=8, ha='left')
473
+
474
+ if 'cmap' not in kwargs:
475
+ import matplotlib.colors as colors
476
+
477
+ colours = []
478
+ boundaries = []
479
+ data = []
480
+ for i, u in enumerate(self.order):
481
+ if u.element_type != StratigraphicColumnElementType.UNIT:
482
+ continue
483
+ data.append((i, u.colour))
484
+ colours.append(u.colour)
485
+ boundaries.append(i) # print(u,v)
486
+ cmap = colors.ListedColormap(colours)
487
+ else:
488
+ cmap = cm.get_cmap(kwargs['cmap'], n_units - 1)
489
+ p = PatchCollection(patches, cmap=cmap)
490
+
491
+ colors = np.arange(len(patches))
492
+ p.set_array(np.array(colors))
493
+
494
+ ax.add_collection(p)
495
+
496
+ ax.set_ylim(total_height - (total_height - prev_coords[0]) * 0.1, 0)
497
+
498
+ ax.axis("off")
499
+
500
+ 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:
@@ -38,3 +38,4 @@ rng = np.random.default_rng()
38
38
 
39
39
  from ._surface import LoopIsosurfacer, surface_list
40
40
  from .colours import random_colour, random_hex_colour
41
+ from .observer import Callback, Disposable, Observable
@@ -115,12 +115,17 @@ class LoopIsosurfacer:
115
115
  values,
116
116
  )
117
117
  logger.info(f'Isosurfacing at values: {isovalues}')
118
+ individual_names = False
118
119
  if name is None:
119
120
  names = ["surface"] * len(isovalues)
120
121
  if isinstance(name, str):
121
122
  names = [name] * len(isovalues)
123
+ if len(isovalues) == 1:
124
+ individual_names = True
122
125
  if isinstance(name, list):
123
126
  names = name
127
+ if len(names) == len(isovalues):
128
+ individual_names = True
124
129
  if colours is None:
125
130
  colours = [None] * len(isovalues)
126
131
  for name, isovalue, colour in zip(names, isovalues, colours):
@@ -151,7 +156,7 @@ class LoopIsosurfacer:
151
156
  vertices=verts,
152
157
  triangles=faces,
153
158
  normals=normals,
154
- name=f"{name}_{isovalue}",
159
+ name=name if individual_names else f"{name}_{isovalue}",
155
160
  values=values,
156
161
  colour=colour,
157
162
  )