LoopStructural 1.6.16__py3-none-any.whl → 1.6.18__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.
- LoopStructural/__init__.py +1 -0
- LoopStructural/interpolators/_geological_interpolator.py +1 -2
- LoopStructural/modelling/core/fault_topology.py +234 -0
- LoopStructural/modelling/core/geological_model.py +43 -34
- LoopStructural/modelling/core/stratigraphic_column.py +129 -25
- LoopStructural/modelling/features/fold/_svariogram.py +3 -0
- LoopStructural/utils/__init__.py +1 -0
- LoopStructural/utils/_surface.py +6 -1
- LoopStructural/utils/observer.py +150 -0
- LoopStructural/version.py +1 -1
- {loopstructural-1.6.16.dist-info → loopstructural-1.6.18.dist-info}/METADATA +1 -1
- {loopstructural-1.6.16.dist-info → loopstructural-1.6.18.dist-info}/RECORD +15 -13
- {loopstructural-1.6.16.dist-info → loopstructural-1.6.18.dist-info}/WHEEL +0 -0
- {loopstructural-1.6.16.dist-info → loopstructural-1.6.18.dist-info}/licenses/LICENSE +0 -0
- {loopstructural-1.6.16.dist-info → loopstructural-1.6.18.dist-info}/top_level.txt +0 -0
LoopStructural/__init__.py
CHANGED
|
@@ -20,6 +20,7 @@ ch.setLevel(logging.WARNING)
|
|
|
20
20
|
loggers = {}
|
|
21
21
|
from .modelling.core.geological_model import GeologicalModel
|
|
22
22
|
from .modelling.core.stratigraphic_column import StratigraphicColumn
|
|
23
|
+
from .modelling.core.fault_topology import FaultTopology
|
|
23
24
|
from .interpolators._api import LoopInterpolator
|
|
24
25
|
from .interpolators import InterpolatorBuilder
|
|
25
26
|
from .datatypes import BoundingBox
|
|
@@ -166,8 +166,7 @@ class GeologicalInterpolator(metaclass=ABCMeta):
|
|
|
166
166
|
"""
|
|
167
167
|
if points.shape[1] == self.dimensions * 2:
|
|
168
168
|
points = np.hstack([points, np.ones((points.shape[0], 1))])
|
|
169
|
-
logger.
|
|
170
|
-
raise Warning
|
|
169
|
+
logger.info("No weight provided for normal constraints, all weights are set to 1")
|
|
171
170
|
if points.shape[1] < self.dimensions * 2 + 1:
|
|
172
171
|
raise ValueError("Normal constraints must at least have X,Y,Z,nx,ny,nz")
|
|
173
172
|
self.n_n = points.shape[0]
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
from ..features.fault import FaultSegment
|
|
2
|
+
from ...utils import Observable
|
|
3
|
+
from .stratigraphic_column import StratigraphicColumn
|
|
4
|
+
import enum
|
|
5
|
+
import numpy as np
|
|
6
|
+
class FaultRelationshipType(enum.Enum):
|
|
7
|
+
ABUTTING = "abutting"
|
|
8
|
+
FAULTED = "faulted"
|
|
9
|
+
NONE = "none"
|
|
10
|
+
|
|
11
|
+
class FaultTopology(Observable['FaultTopology']):
|
|
12
|
+
"""A graph representation of the relationships between faults and the
|
|
13
|
+
relationship with stratigraphic units.
|
|
14
|
+
"""
|
|
15
|
+
def __init__(self, stratigraphic_column: 'StratigraphicColumn'):
|
|
16
|
+
super().__init__()
|
|
17
|
+
self.faults = []
|
|
18
|
+
self.stratigraphic_column = stratigraphic_column
|
|
19
|
+
self.adjacency = {}
|
|
20
|
+
self.stratigraphy_fault_relationships = {}
|
|
21
|
+
def add_fault(self, fault: FaultSegment):
|
|
22
|
+
"""
|
|
23
|
+
Adds a fault to the fault topology.
|
|
24
|
+
"""
|
|
25
|
+
if not isinstance(fault, str):
|
|
26
|
+
raise TypeError("Expected a fault name.")
|
|
27
|
+
|
|
28
|
+
self.faults.append(fault)
|
|
29
|
+
self.notify('fault_added', fault=fault)
|
|
30
|
+
|
|
31
|
+
def remove_fault(self, fault: str):
|
|
32
|
+
"""
|
|
33
|
+
Removes a fault from the fault topology.
|
|
34
|
+
"""
|
|
35
|
+
if fault not in self.faults:
|
|
36
|
+
raise ValueError(f"Fault {fault} not found in the topology.")
|
|
37
|
+
|
|
38
|
+
self.faults.remove(fault)
|
|
39
|
+
# Remove any relationships involving this fault
|
|
40
|
+
self.adjacency = {k: v for k, v in self.adjacency.items() if fault not in k}
|
|
41
|
+
self.stratigraphy_fault_relationships = {
|
|
42
|
+
k: v for k, v in self.stratigraphy_fault_relationships.items() if k[1] != fault
|
|
43
|
+
}
|
|
44
|
+
self.notify('fault_removed', fault=fault)
|
|
45
|
+
|
|
46
|
+
def add_abutting_relationship(self, fault_name: str, abutting_fault: str):
|
|
47
|
+
"""
|
|
48
|
+
Adds an abutting relationship between two faults.
|
|
49
|
+
"""
|
|
50
|
+
if fault_name not in self.faults or abutting_fault not in self.faults:
|
|
51
|
+
raise ValueError("Both faults must be part of the fault topology.")
|
|
52
|
+
|
|
53
|
+
if fault_name not in self.adjacency:
|
|
54
|
+
self.adjacency[fault_name] = []
|
|
55
|
+
|
|
56
|
+
self.adjacency[(fault_name, abutting_fault)] = FaultRelationshipType.ABUTTING
|
|
57
|
+
self.notify('abutting_relationship_added', {'fault': fault_name, 'abutting_fault': abutting_fault})
|
|
58
|
+
def add_stratigraphy_fault_relationship(self, unit_name:str, fault_name: str):
|
|
59
|
+
"""
|
|
60
|
+
Adds a relationship between a stratigraphic unit and a fault.
|
|
61
|
+
"""
|
|
62
|
+
if fault_name not in self.faults:
|
|
63
|
+
raise ValueError("Fault must be part of the fault topology.")
|
|
64
|
+
|
|
65
|
+
if unit_name is None:
|
|
66
|
+
raise ValueError(f"No stratigraphic group found for unit name: {unit_name}")
|
|
67
|
+
self.stratigraphy_fault_relationships[(unit_name,fault_name)] = True
|
|
68
|
+
|
|
69
|
+
self.notify('stratigraphy_fault_relationship_added', {'unit': unit_name, 'fault': fault_name})
|
|
70
|
+
def add_faulted_relationship(self, fault_name: str, faulted_fault_name: str):
|
|
71
|
+
"""
|
|
72
|
+
Adds a faulted relationship between two faults.
|
|
73
|
+
"""
|
|
74
|
+
if fault_name not in self.faults or faulted_fault_name not in self.faults:
|
|
75
|
+
raise ValueError("Both faults must be part of the fault topology.")
|
|
76
|
+
|
|
77
|
+
if fault_name not in self.adjacency:
|
|
78
|
+
self.adjacency[fault_name] = []
|
|
79
|
+
|
|
80
|
+
self.adjacency[(fault_name, faulted_fault_name)] = FaultRelationshipType.FAULTED
|
|
81
|
+
self.notify('faulted_relationship_added', {'fault': fault_name, 'faulted_fault': faulted_fault_name})
|
|
82
|
+
def remove_fault_relationship(self, fault_name: str, related_fault_name: str):
|
|
83
|
+
"""
|
|
84
|
+
Removes a relationship between two faults.
|
|
85
|
+
"""
|
|
86
|
+
if (fault_name, related_fault_name) in self.adjacency:
|
|
87
|
+
del self.adjacency[(fault_name, related_fault_name)]
|
|
88
|
+
elif (related_fault_name, fault_name) in self.adjacency:
|
|
89
|
+
del self.adjacency[(related_fault_name, fault_name)]
|
|
90
|
+
else:
|
|
91
|
+
raise ValueError(f"No relationship found between {fault_name} and {related_fault_name}.")
|
|
92
|
+
self.notify('fault_relationship_removed', {'fault': fault_name, 'related_fault': related_fault_name})
|
|
93
|
+
def update_fault_relationship(self, fault_name: str, related_fault_name: str, new_relationship_type: FaultRelationshipType):
|
|
94
|
+
if new_relationship_type == FaultRelationshipType.NONE:
|
|
95
|
+
self.adjacency.pop((fault_name, related_fault_name), None)
|
|
96
|
+
else:
|
|
97
|
+
self.adjacency[(fault_name, related_fault_name)] = new_relationship_type
|
|
98
|
+
self.notify('fault_relationship_updated', {'fault': fault_name, 'related_fault': related_fault_name, 'new_relationship_type': new_relationship_type})
|
|
99
|
+
def change_relationship_type(self, fault_name: str, related_fault_name: str, new_relationship_type: FaultRelationshipType):
|
|
100
|
+
"""
|
|
101
|
+
Changes the relationship type between two faults.
|
|
102
|
+
"""
|
|
103
|
+
if (fault_name, related_fault_name) in self.adjacency:
|
|
104
|
+
self.adjacency[(fault_name, related_fault_name)] = new_relationship_type
|
|
105
|
+
|
|
106
|
+
else:
|
|
107
|
+
raise ValueError(f"No relationship found between {fault_name} and {related_fault_name}.")
|
|
108
|
+
self.notify('relationship_type_changed', {'fault': fault_name, 'related_fault': related_fault_name, 'new_relationship_type': new_relationship_type})
|
|
109
|
+
def get_fault_relationships(self, fault_name: str):
|
|
110
|
+
"""
|
|
111
|
+
Returns a list of relationships for a given fault.
|
|
112
|
+
"""
|
|
113
|
+
relationships = []
|
|
114
|
+
for (f1, f2), relationship_type in self.adjacency.items():
|
|
115
|
+
if f1 == fault_name or f2 == fault_name:
|
|
116
|
+
relationships.append((f1, f2, relationship_type))
|
|
117
|
+
return relationships
|
|
118
|
+
def get_fault_relationship(self, fault_name: str, related_fault_name: str):
|
|
119
|
+
"""
|
|
120
|
+
Returns the relationship type between two faults.
|
|
121
|
+
"""
|
|
122
|
+
return self.adjacency.get((fault_name, related_fault_name), FaultRelationshipType.NONE)
|
|
123
|
+
def get_faults(self):
|
|
124
|
+
"""
|
|
125
|
+
Returns a list of all faults in the topology.
|
|
126
|
+
"""
|
|
127
|
+
return self.faults
|
|
128
|
+
|
|
129
|
+
def get_stratigraphy_fault_relationships(self):
|
|
130
|
+
"""
|
|
131
|
+
Returns a dictionary of stratigraphic unit to fault relationships.
|
|
132
|
+
"""
|
|
133
|
+
return self.stratigraphy_fault_relationships
|
|
134
|
+
def get_fault_stratigraphic_unit_relationships(self):
|
|
135
|
+
units_group_pairs = self.stratigraphic_column.get_group_unit_pairs()
|
|
136
|
+
matrix = np.zeros((len(self.faults), len(units_group_pairs)), dtype=int)
|
|
137
|
+
for i, fault in enumerate(self.faults):
|
|
138
|
+
for j, (unit_name, _group) in enumerate(units_group_pairs):
|
|
139
|
+
if (unit_name, fault) in self.stratigraphy_fault_relationships:
|
|
140
|
+
matrix[i, j] = 1
|
|
141
|
+
|
|
142
|
+
return matrix
|
|
143
|
+
def get_fault_stratigraphic_relationship(self, unit_name: str, fault:str) -> bool:
|
|
144
|
+
"""
|
|
145
|
+
Returns a dictionary of fault to stratigraphic unit relationships.
|
|
146
|
+
"""
|
|
147
|
+
if unit_name is None:
|
|
148
|
+
raise ValueError(f"No stratigraphic group found for unit name: {unit_name}")
|
|
149
|
+
if (unit_name, fault) not in self.stratigraphy_fault_relationships:
|
|
150
|
+
return False
|
|
151
|
+
return self.stratigraphy_fault_relationships[(unit_name, fault)]
|
|
152
|
+
|
|
153
|
+
def update_fault_stratigraphy_relationship(self, unit_name: str, fault_name: str, flag: bool = True):
|
|
154
|
+
"""
|
|
155
|
+
Updates the relationship between a stratigraphic unit and a fault.
|
|
156
|
+
"""
|
|
157
|
+
if not flag:
|
|
158
|
+
if (unit_name, fault_name) in self.stratigraphy_fault_relationships:
|
|
159
|
+
del self.stratigraphy_fault_relationships[(unit_name, fault_name)]
|
|
160
|
+
else:
|
|
161
|
+
self.stratigraphy_fault_relationships[(unit_name, fault_name)] = flag
|
|
162
|
+
|
|
163
|
+
self.notify('stratigraphy_fault_relationship_updated', {'unit': unit_name, 'fault': fault_name})
|
|
164
|
+
|
|
165
|
+
def remove_fault_stratigraphy_relationship(self, unit_name: str, fault_name: str):
|
|
166
|
+
"""
|
|
167
|
+
Removes a relationship between a stratigraphic unit and a fault.
|
|
168
|
+
"""
|
|
169
|
+
if (unit_name, fault_name) not in self.stratigraphy_fault_relationships:
|
|
170
|
+
raise ValueError(f"No relationship found between unit {unit_name} and fault {fault_name}.")
|
|
171
|
+
else:
|
|
172
|
+
self.stratigraphy_fault_relationships.pop((unit_name, fault_name), None)
|
|
173
|
+
|
|
174
|
+
self.notify('stratigraphy_fault_relationship_removed', {'unit': unit_name, 'fault': fault_name})
|
|
175
|
+
def get_matrix(self):
|
|
176
|
+
"""
|
|
177
|
+
Returns a matrix representation of the fault relationships.
|
|
178
|
+
"""
|
|
179
|
+
matrix = np.zeros((len(self.faults), len(self.faults)), dtype=int)
|
|
180
|
+
for (fault_name, related_fault_name), relationship_type in self.adjacency.items():
|
|
181
|
+
fault_index = self.faults.index(next(f for f in self.faults if f == fault_name))
|
|
182
|
+
related_fault_index = self.faults.index(next(f for f in self.faults if f == related_fault_name))
|
|
183
|
+
if relationship_type == FaultRelationshipType.ABUTTING:
|
|
184
|
+
matrix[fault_index, related_fault_index] = 1
|
|
185
|
+
elif relationship_type == FaultRelationshipType.FAULTED:
|
|
186
|
+
matrix[fault_index, related_fault_index] = 2
|
|
187
|
+
return matrix
|
|
188
|
+
|
|
189
|
+
def to_dict(self):
|
|
190
|
+
"""
|
|
191
|
+
Returns a dictionary representation of the fault topology.
|
|
192
|
+
"""
|
|
193
|
+
return {
|
|
194
|
+
"faults": self.faults,
|
|
195
|
+
"adjacency": self.adjacency,
|
|
196
|
+
"stratigraphy_fault_relationships": self.stratigraphy_fault_relationships,
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
def update_from_dict(self, data):
|
|
200
|
+
"""
|
|
201
|
+
Updates the fault topology from a dictionary representation.
|
|
202
|
+
"""
|
|
203
|
+
with self.freeze_notifications():
|
|
204
|
+
self.faults.extend(data.get("faults", []))
|
|
205
|
+
adjacency = data.get("adjacency", {})
|
|
206
|
+
stratigraphy_fault_relationships = data.get("stratigraphy_fault_relationships", {})
|
|
207
|
+
for (fault,abutting_fault) in adjacency.values():
|
|
208
|
+
if fault not in self.faults:
|
|
209
|
+
self.add_fault(fault)
|
|
210
|
+
if abutting_fault not in self.faults:
|
|
211
|
+
self.add_fault(abutting_fault)
|
|
212
|
+
self.add_abutting_relationship(fault, abutting_fault)
|
|
213
|
+
for unit_name, fault_names in stratigraphy_fault_relationships.items():
|
|
214
|
+
for fault_name in fault_names:
|
|
215
|
+
if fault_name not in self.faults:
|
|
216
|
+
self.add_fault(fault_name)
|
|
217
|
+
self.add_stratigraphy_fault_relationship(unit_name, fault_name)
|
|
218
|
+
|
|
219
|
+
@classmethod
|
|
220
|
+
def from_dict(cls, data):
|
|
221
|
+
"""
|
|
222
|
+
Creates a FaultTopology instance from a dictionary representation.
|
|
223
|
+
"""
|
|
224
|
+
from .stratigraphic_column import StratigraphicColumn
|
|
225
|
+
stratigraphic_column = data.get("stratigraphic_column",None)
|
|
226
|
+
if not isinstance(stratigraphic_column, StratigraphicColumn):
|
|
227
|
+
if isinstance(stratigraphic_column, dict):
|
|
228
|
+
stratigraphic_column = StratigraphicColumn.from_dict(stratigraphic_column)
|
|
229
|
+
elif not isinstance(stratigraphic_column, StratigraphicColumn):
|
|
230
|
+
raise TypeError("Expected 'stratigraphic_column' to be a StratigraphicColumn instance or dict.")
|
|
231
|
+
|
|
232
|
+
topology = cls(stratigraphic_column)
|
|
233
|
+
topology.update_from_dict(data)
|
|
234
|
+
return topology
|
|
@@ -6,7 +6,7 @@ from ...utils import getLogger
|
|
|
6
6
|
|
|
7
7
|
import numpy as np
|
|
8
8
|
import pandas as pd
|
|
9
|
-
from typing import List, Optional
|
|
9
|
+
from typing import List, Optional, Union, Dict
|
|
10
10
|
import pathlib
|
|
11
11
|
from ...modelling.features.fault import FaultSegment
|
|
12
12
|
|
|
@@ -123,8 +123,7 @@ class GeologicalModel:
|
|
|
123
123
|
self.feature_name_index = {}
|
|
124
124
|
self._data = pd.DataFrame() # None
|
|
125
125
|
|
|
126
|
-
self.
|
|
127
|
-
|
|
126
|
+
self._stratigraphic_column = StratigraphicColumn()
|
|
128
127
|
|
|
129
128
|
self.tol = 1e-10 * np.max(self.bounding_box.maximum - self.bounding_box.origin)
|
|
130
129
|
self._dtm = None
|
|
@@ -187,7 +186,6 @@ class GeologicalModel:
|
|
|
187
186
|
].astype(float)
|
|
188
187
|
return data
|
|
189
188
|
|
|
190
|
-
|
|
191
189
|
if "type" in data:
|
|
192
190
|
logger.warning("'type' is deprecated replace with 'feature_name' \n")
|
|
193
191
|
data.rename(columns={"type": "feature_name"}, inplace=True)
|
|
@@ -409,7 +407,6 @@ class GeologicalModel:
|
|
|
409
407
|
"""
|
|
410
408
|
return [f.name for f in self.faults]
|
|
411
409
|
|
|
412
|
-
|
|
413
410
|
def to_file(self, file):
|
|
414
411
|
"""Save a model to a pickle file requires dill
|
|
415
412
|
|
|
@@ -506,10 +503,34 @@ class GeologicalModel:
|
|
|
506
503
|
self._data = data.copy()
|
|
507
504
|
# self._data[['X','Y','Z']] = self.bounding_box.project(self._data[['X','Y','Z']].to_numpy())
|
|
508
505
|
|
|
509
|
-
|
|
510
506
|
def set_model_data(self, data):
|
|
511
507
|
logger.warning("deprecated method. Model data can now be set using the data attribute")
|
|
512
508
|
self.data = data.copy()
|
|
509
|
+
@property
|
|
510
|
+
def stratigraphic_column(self):
|
|
511
|
+
"""Get the stratigraphic column of the model
|
|
512
|
+
|
|
513
|
+
Returns
|
|
514
|
+
-------
|
|
515
|
+
StratigraphicColumn
|
|
516
|
+
the stratigraphic column of the model
|
|
517
|
+
"""
|
|
518
|
+
return self._stratigraphic_column
|
|
519
|
+
@stratigraphic_column.setter
|
|
520
|
+
def stratigraphic_column(self, stratigraphic_column: Union[StratigraphicColumn,Dict]):
|
|
521
|
+
"""Set the stratigraphic column of the model
|
|
522
|
+
|
|
523
|
+
Parameters
|
|
524
|
+
----------
|
|
525
|
+
stratigraphic_column : StratigraphicColumn
|
|
526
|
+
the stratigraphic column to set
|
|
527
|
+
"""
|
|
528
|
+
if isinstance(stratigraphic_column, dict):
|
|
529
|
+
self.set_stratigraphic_column(stratigraphic_column)
|
|
530
|
+
return
|
|
531
|
+
elif not isinstance(stratigraphic_column, StratigraphicColumn):
|
|
532
|
+
raise ValueError("stratigraphic_column must be a StratigraphicColumn object")
|
|
533
|
+
self._stratigraphic_column = stratigraphic_column
|
|
513
534
|
|
|
514
535
|
def set_stratigraphic_column(self, stratigraphic_column, cmap="tab20"):
|
|
515
536
|
"""
|
|
@@ -539,14 +560,14 @@ class GeologicalModel:
|
|
|
539
560
|
DeprecationWarning(
|
|
540
561
|
"set_stratigraphic_column is deprecated, use model.stratigraphic_column.add_units instead"
|
|
541
562
|
)
|
|
542
|
-
for g in stratigraphic_column.keys():
|
|
563
|
+
for i, g in enumerate(stratigraphic_column.keys()):
|
|
543
564
|
for u in stratigraphic_column[g].keys():
|
|
544
565
|
thickness = 0
|
|
545
566
|
if "min" in stratigraphic_column[g][u] and "max" in stratigraphic_column[g][u]:
|
|
546
567
|
min_val = stratigraphic_column[g][u]["min"]
|
|
547
568
|
max_val = stratigraphic_column[g][u].get("max", None)
|
|
548
569
|
thickness = max_val - min_val if max_val is not None else None
|
|
549
|
-
logger.
|
|
570
|
+
logger.info(
|
|
550
571
|
f"""
|
|
551
572
|
model.stratigraphic_column.add_unit({u},
|
|
552
573
|
colour={stratigraphic_column[g][u].get("colour", None)},
|
|
@@ -557,9 +578,11 @@ class GeologicalModel:
|
|
|
557
578
|
colour=stratigraphic_column[g][u].get("colour", None),
|
|
558
579
|
thickness=thickness,
|
|
559
580
|
)
|
|
581
|
+
|
|
560
582
|
self.stratigraphic_column.add_unconformity(
|
|
561
583
|
name=''.join([g, 'unconformity']),
|
|
562
584
|
)
|
|
585
|
+
self.stratigraphic_column.group_mapping[f'Group_{i+1}'] = g
|
|
563
586
|
|
|
564
587
|
def create_and_add_foliation(
|
|
565
588
|
self,
|
|
@@ -1400,7 +1423,6 @@ class GeologicalModel:
|
|
|
1400
1423
|
|
|
1401
1424
|
return self.bounding_box.reproject(points, inplace=inplace)
|
|
1402
1425
|
|
|
1403
|
-
|
|
1404
1426
|
# TODO move scale to bounding box/transformer
|
|
1405
1427
|
def scale(self, points: np.ndarray, *, inplace: bool = False) -> np.ndarray:
|
|
1406
1428
|
"""Take points in UTM coordinates and reproject
|
|
@@ -1419,7 +1441,6 @@ class GeologicalModel:
|
|
|
1419
1441
|
"""
|
|
1420
1442
|
return self.bounding_box.project(np.array(points).astype(float), inplace=inplace)
|
|
1421
1443
|
|
|
1422
|
-
|
|
1423
1444
|
def regular_grid(self, *, nsteps=None, shuffle=True, rescale=False, order="C"):
|
|
1424
1445
|
"""
|
|
1425
1446
|
Return a regular grid within the model bounding box
|
|
@@ -1494,22 +1515,18 @@ class GeologicalModel:
|
|
|
1494
1515
|
if self.stratigraphic_column is None:
|
|
1495
1516
|
logger.warning("No stratigraphic column defined")
|
|
1496
1517
|
return strat_id
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
feature_id = self.feature_name_index.get(
|
|
1518
|
+
|
|
1519
|
+
s_id = 0
|
|
1520
|
+
for g in reversed(self.stratigraphic_column.get_groups()):
|
|
1521
|
+
feature_id = self.feature_name_index.get(g.name, -1)
|
|
1501
1522
|
if feature_id >= 0:
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
np.logical_and(
|
|
1507
|
-
vals < series.get("max", feature.max()),
|
|
1508
|
-
vals > series.get("min", feature.min()),
|
|
1509
|
-
)
|
|
1510
|
-
] = series["id"]
|
|
1523
|
+
vals = self.features[feature_id].evaluate_value(xyz)
|
|
1524
|
+
for u in g.units:
|
|
1525
|
+
strat_id[np.logical_and(vals < u.max(), vals > u.min())] = s_id
|
|
1526
|
+
s_id += 1
|
|
1511
1527
|
if feature_id == -1:
|
|
1512
|
-
logger.error(f"Model does not contain {
|
|
1528
|
+
logger.error(f"Model does not contain {g.name}")
|
|
1529
|
+
|
|
1513
1530
|
return strat_id
|
|
1514
1531
|
|
|
1515
1532
|
def evaluate_model_gradient(self, points: np.ndarray, *, scale: bool = True) -> np.ndarray:
|
|
@@ -1710,16 +1727,8 @@ class GeologicalModel:
|
|
|
1710
1727
|
ids : list
|
|
1711
1728
|
list of unique stratigraphic ids, featurename, unit name and min and max scalar values
|
|
1712
1729
|
"""
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
logger.warning('No stratigraphic column defined')
|
|
1716
|
-
return ids
|
|
1717
|
-
for group in self.stratigraphic_column.keys():
|
|
1718
|
-
if group == "faults":
|
|
1719
|
-
continue
|
|
1720
|
-
for name, series in self.stratigraphic_column[group].items():
|
|
1721
|
-
ids.append([series["id"], group, name, series['min'], series['max']])
|
|
1722
|
-
return ids
|
|
1730
|
+
return self.stratigraphic_column.get_stratigraphic_ids()
|
|
1731
|
+
|
|
1723
1732
|
|
|
1724
1733
|
def get_fault_surfaces(self, faults: List[str] = []):
|
|
1725
1734
|
surfaces = []
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import enum
|
|
2
|
-
from typing import Dict
|
|
2
|
+
from typing import Dict, Optional, List, Tuple
|
|
3
3
|
import numpy as np
|
|
4
|
-
from LoopStructural.utils import rng, getLogger
|
|
5
|
-
|
|
4
|
+
from LoopStructural.utils import rng, getLogger, Observable
|
|
6
5
|
logger = getLogger(__name__)
|
|
7
6
|
logger.info("Imported LoopStructural Stratigraphic Column module")
|
|
8
7
|
class UnconformityType(enum.Enum):
|
|
@@ -40,24 +39,59 @@ class StratigraphicColumnElement:
|
|
|
40
39
|
self.uuid = uuid
|
|
41
40
|
|
|
42
41
|
|
|
43
|
-
class StratigraphicUnit(StratigraphicColumnElement):
|
|
42
|
+
class StratigraphicUnit(StratigraphicColumnElement, Observable['StratigraphicUnit']):
|
|
44
43
|
"""
|
|
45
44
|
A class to represent a stratigraphic unit.
|
|
46
45
|
"""
|
|
47
46
|
|
|
48
|
-
def __init__(self, *, uuid=None, name=None, colour=None, thickness=None, data=None):
|
|
47
|
+
def __init__(self, *, uuid=None, name=None, colour=None, thickness=None, data=None, id=None):
|
|
49
48
|
"""
|
|
50
49
|
Initializes the StratigraphicUnit with a name and an optional description.
|
|
51
50
|
"""
|
|
52
|
-
|
|
51
|
+
StratigraphicColumnElement.__init__(self, uuid)
|
|
52
|
+
Observable.__init__(self)
|
|
53
53
|
self.name = name
|
|
54
54
|
if colour is None:
|
|
55
55
|
colour = rng.random(3)
|
|
56
56
|
self.colour = colour
|
|
57
|
-
self.
|
|
57
|
+
self._thickness = thickness
|
|
58
58
|
self.data = data
|
|
59
59
|
self.element_type = StratigraphicColumnElementType.UNIT
|
|
60
|
-
|
|
60
|
+
self._id = id
|
|
61
|
+
self.min_value = None # Minimum scalar field value for the unit
|
|
62
|
+
self.max_value = None # Maximum scalar field value for the unit
|
|
63
|
+
@property
|
|
64
|
+
def id(self):
|
|
65
|
+
return self._id
|
|
66
|
+
@property
|
|
67
|
+
def thickness(self):
|
|
68
|
+
return self._thickness
|
|
69
|
+
@thickness.setter
|
|
70
|
+
def thickness(self, value):
|
|
71
|
+
"""
|
|
72
|
+
Sets the thickness of the unit.
|
|
73
|
+
"""
|
|
74
|
+
self._thickness = value
|
|
75
|
+
self.notify('unit/thickness_updated', unit=self)
|
|
76
|
+
@id.setter
|
|
77
|
+
def id(self, value):
|
|
78
|
+
"""
|
|
79
|
+
Sets the ID of the unit.
|
|
80
|
+
"""
|
|
81
|
+
if not isinstance(value, int):
|
|
82
|
+
raise TypeError("ID must be an integer")
|
|
83
|
+
self._id = value
|
|
84
|
+
self.notify('unit/id_updated', unit=self)
|
|
85
|
+
def min(self):
|
|
86
|
+
"""
|
|
87
|
+
Returns the minimum value of the unit.
|
|
88
|
+
"""
|
|
89
|
+
return self.min_value if self.min_value is not None else 0
|
|
90
|
+
def max(self):
|
|
91
|
+
"""
|
|
92
|
+
Returns the maximum value of the unit.
|
|
93
|
+
"""
|
|
94
|
+
return self.max_value if self.max_value is not None else np.inf
|
|
61
95
|
def to_dict(self):
|
|
62
96
|
"""
|
|
63
97
|
Converts the stratigraphic unit to a dictionary representation.
|
|
@@ -154,7 +188,7 @@ class StratigraphicGroup:
|
|
|
154
188
|
self.units = units if units is not None else []
|
|
155
189
|
|
|
156
190
|
|
|
157
|
-
class StratigraphicColumn:
|
|
191
|
+
class StratigraphicColumn(Observable['StratigraphicColumn']):
|
|
158
192
|
"""
|
|
159
193
|
A class to represent a stratigraphic column, which is a vertical section of the Earth's crust
|
|
160
194
|
showing the sequence of rock layers and their relationships.
|
|
@@ -164,20 +198,38 @@ class StratigraphicColumn:
|
|
|
164
198
|
"""
|
|
165
199
|
Initializes the StratigraphicColumn with a name and a list of layers.
|
|
166
200
|
"""
|
|
167
|
-
|
|
201
|
+
super().__init__()
|
|
202
|
+
self.order = []
|
|
203
|
+
self.add_basement()
|
|
168
204
|
self.group_mapping = {}
|
|
169
|
-
|
|
205
|
+
|
|
206
|
+
def get_new_id(self):
|
|
207
|
+
"""
|
|
208
|
+
Generates a new unique ID for a stratigraphic unit.
|
|
209
|
+
"""
|
|
210
|
+
if not self.order:
|
|
211
|
+
return 0
|
|
212
|
+
return max([u.id for u in self.order if isinstance(u, StratigraphicUnit)], default=0) + 1
|
|
213
|
+
def add_basement(self):
|
|
214
|
+
self.add_unit(name='Basement', colour='grey', thickness=np.inf)
|
|
215
|
+
self.add_unconformity(
|
|
216
|
+
name='Base Unconformity', unconformity_type=UnconformityType.ERODE
|
|
217
|
+
)
|
|
218
|
+
def clear(self, basement=True):
|
|
170
219
|
"""
|
|
171
220
|
Clears the stratigraphic column, removing all elements.
|
|
172
221
|
"""
|
|
173
222
|
if basement:
|
|
174
|
-
self.
|
|
223
|
+
self.add_basement()
|
|
224
|
+
|
|
175
225
|
else:
|
|
176
226
|
self.order = []
|
|
177
227
|
self.group_mapping = {}
|
|
178
|
-
|
|
179
|
-
def add_unit(self, name,*, colour=None, thickness=None, where='top'):
|
|
180
|
-
|
|
228
|
+
self.notify('column_cleared')
|
|
229
|
+
def add_unit(self, name,*, colour=None, thickness=None, where='top',id=None):
|
|
230
|
+
if id is None:
|
|
231
|
+
id = self.get_new_id()
|
|
232
|
+
unit = StratigraphicUnit(name=name, colour=colour, thickness=thickness, id=id)
|
|
181
233
|
|
|
182
234
|
if where == 'top':
|
|
183
235
|
self.order.append(unit)
|
|
@@ -185,7 +237,9 @@ class StratigraphicColumn:
|
|
|
185
237
|
self.order.insert(0, unit)
|
|
186
238
|
else:
|
|
187
239
|
raise ValueError("Invalid 'where' argument. Use 'top' or 'bottom'.")
|
|
188
|
-
|
|
240
|
+
unit.attach(self.update_unit_values,'unit/*')
|
|
241
|
+
self.notify('unit_added', unit=unit)
|
|
242
|
+
self.update_unit_values() # Update min and max values after adding a unit
|
|
189
243
|
return unit
|
|
190
244
|
|
|
191
245
|
def remove_unit(self, uuid):
|
|
@@ -195,7 +249,9 @@ class StratigraphicColumn:
|
|
|
195
249
|
for i, element in enumerate(self.order):
|
|
196
250
|
if element.uuid == uuid:
|
|
197
251
|
del self.order[i]
|
|
252
|
+
self.notify('unit_removed', uuid=uuid)
|
|
198
253
|
return True
|
|
254
|
+
|
|
199
255
|
return False
|
|
200
256
|
|
|
201
257
|
def add_unconformity(self, name, *, unconformity_type=UnconformityType.ERODE, where='top' ):
|
|
@@ -209,6 +265,7 @@ class StratigraphicColumn:
|
|
|
209
265
|
self.order.insert(0, unconformity)
|
|
210
266
|
else:
|
|
211
267
|
raise ValueError("Invalid 'where' argument. Use 'top' or 'bottom'.")
|
|
268
|
+
self.notify('unconformity_added', unconformity=unconformity)
|
|
212
269
|
return unconformity
|
|
213
270
|
|
|
214
271
|
def get_element_by_index(self, index):
|
|
@@ -228,6 +285,7 @@ class StratigraphicColumn:
|
|
|
228
285
|
return unit
|
|
229
286
|
|
|
230
287
|
return None
|
|
288
|
+
|
|
231
289
|
def get_unconformity_by_name(self, name):
|
|
232
290
|
"""
|
|
233
291
|
Retrieves an unconformity by its name from the stratigraphic column.
|
|
@@ -245,6 +303,15 @@ class StratigraphicColumn:
|
|
|
245
303
|
if element.uuid == uuid:
|
|
246
304
|
return element
|
|
247
305
|
raise KeyError(f"No element found with uuid: {uuid}")
|
|
306
|
+
|
|
307
|
+
def get_group_for_unit_name(self, unit_name:str) -> Optional[StratigraphicGroup]:
|
|
308
|
+
"""
|
|
309
|
+
Retrieves the group for a given unit name.
|
|
310
|
+
"""
|
|
311
|
+
for group in self.get_groups():
|
|
312
|
+
if any(unit.name == unit_name for unit in group.units):
|
|
313
|
+
return group
|
|
314
|
+
return None
|
|
248
315
|
def add_element(self, element):
|
|
249
316
|
"""
|
|
250
317
|
Adds a StratigraphicColumnElement to the stratigraphic column.
|
|
@@ -287,7 +354,15 @@ class StratigraphicColumn:
|
|
|
287
354
|
if group:
|
|
288
355
|
groups.append(group)
|
|
289
356
|
return groups
|
|
357
|
+
def get_stratigraphic_ids(self) -> List[List[str]]:
|
|
358
|
+
ids = []
|
|
359
|
+
for group in self.get_groups():
|
|
360
|
+
if group == "faults":
|
|
361
|
+
continue
|
|
290
362
|
|
|
363
|
+
for unit in group.units:
|
|
364
|
+
ids.append([unit.id, group, unit.name, unit.min(), unit.max()])
|
|
365
|
+
return ids
|
|
291
366
|
def get_unitname_groups(self):
|
|
292
367
|
groups = self.get_groups()
|
|
293
368
|
groups_list = []
|
|
@@ -296,7 +371,18 @@ class StratigraphicColumn:
|
|
|
296
371
|
group = [u.name for u in g.units if isinstance(u, StratigraphicUnit)]
|
|
297
372
|
groups_list.append(group)
|
|
298
373
|
return groups_list
|
|
299
|
-
|
|
374
|
+
|
|
375
|
+
def get_group_unit_pairs(self) -> List[Tuple[str,str]]:
|
|
376
|
+
"""
|
|
377
|
+
Returns a list of tuples containing group names and unit names.
|
|
378
|
+
"""
|
|
379
|
+
groups = self.get_groups()
|
|
380
|
+
group_unit_pairs = []
|
|
381
|
+
for g in groups:
|
|
382
|
+
for u in g.units:
|
|
383
|
+
if isinstance(u, StratigraphicUnit):
|
|
384
|
+
group_unit_pairs.append((g.name, u.name))
|
|
385
|
+
return group_unit_pairs
|
|
300
386
|
|
|
301
387
|
def __getitem__(self, uuid):
|
|
302
388
|
"""
|
|
@@ -316,6 +402,21 @@ class StratigraphicColumn:
|
|
|
316
402
|
self.order = [
|
|
317
403
|
self.__getitem__(uuid) for uuid in new_order if self.__getitem__(uuid) is not None
|
|
318
404
|
]
|
|
405
|
+
self.notify('order_updated', new_order=self.order)
|
|
406
|
+
self.update_unit_values() # Update min and max values after updating the order
|
|
407
|
+
def update_unit_values(self, *, observable: Optional["Observable"] = None, event: Optional[str]= None):
|
|
408
|
+
"""
|
|
409
|
+
Updates the min and max values for each unit based on their position in the column.
|
|
410
|
+
"""
|
|
411
|
+
# If the event is not 'unit/*', skip the update
|
|
412
|
+
if event is not None and event != 'unit/*':
|
|
413
|
+
return
|
|
414
|
+
cumulative_thickness = 0
|
|
415
|
+
for element in self.order:
|
|
416
|
+
if isinstance(element, StratigraphicUnit):
|
|
417
|
+
element.min_value = cumulative_thickness
|
|
418
|
+
element.max_value = cumulative_thickness + (element.thickness or 0)
|
|
419
|
+
cumulative_thickness = element.max_value
|
|
319
420
|
|
|
320
421
|
def update_element(self, unit_data: Dict):
|
|
321
422
|
"""
|
|
@@ -334,6 +435,8 @@ class StratigraphicColumn:
|
|
|
334
435
|
element.unconformity_type = UnconformityType(
|
|
335
436
|
unit_data.get('unconformity_type', element.unconformity_type.value)
|
|
336
437
|
)
|
|
438
|
+
self.notify('element_updated', element=element)
|
|
439
|
+
self.update_unit_values() # Update min and max values after updating an element
|
|
337
440
|
|
|
338
441
|
def __str__(self):
|
|
339
442
|
"""
|
|
@@ -354,14 +457,15 @@ class StratigraphicColumn:
|
|
|
354
457
|
"""
|
|
355
458
|
if not isinstance(data, dict):
|
|
356
459
|
raise TypeError("Data must be a dictionary")
|
|
357
|
-
self.
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
460
|
+
with self.freeze_notifications():
|
|
461
|
+
self.clear(basement=False)
|
|
462
|
+
elements_data = data.get("elements", [])
|
|
463
|
+
for element_data in elements_data:
|
|
464
|
+
if "unconformity_type" in element_data:
|
|
465
|
+
element = StratigraphicUnconformity.from_dict(element_data)
|
|
466
|
+
else:
|
|
467
|
+
element = StratigraphicUnit.from_dict(element_data)
|
|
468
|
+
self.add_element(element)
|
|
365
469
|
@classmethod
|
|
366
470
|
def from_dict(cls, data):
|
|
367
471
|
"""
|
|
@@ -189,6 +189,9 @@ class SVariogram:
|
|
|
189
189
|
# find the extrema of the average curve
|
|
190
190
|
res = find_peaks_and_troughs(np.array(averagex), np.array(averagey))
|
|
191
191
|
px2, py2 = res
|
|
192
|
+
logger.info(f"Found {len(px2)} peaks and troughs in the s-variogram")
|
|
193
|
+
for i in range(len(px2)):
|
|
194
|
+
logger.info(f"Peak {i}: {px2[i]} {py2[i]}")
|
|
192
195
|
wl1 = 0.0
|
|
193
196
|
wl1py = 0.0
|
|
194
197
|
for i in range(len(px)):
|
LoopStructural/utils/__init__.py
CHANGED
LoopStructural/utils/_surface.py
CHANGED
|
@@ -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
|
)
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from contextlib import contextmanager
|
|
5
|
+
from typing import Any, Generic, Protocol, TypeVar, runtime_checkable
|
|
6
|
+
import threading
|
|
7
|
+
import weakref
|
|
8
|
+
|
|
9
|
+
__all__ = ["Observer", "Observable", "Disposable"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@runtime_checkable
|
|
13
|
+
class Observer(Protocol):
|
|
14
|
+
"""Objects implementing an *update* method can subscribe."""
|
|
15
|
+
|
|
16
|
+
def update(self, observable: "Observable", event: str, *args: Any, **kwargs: Any) -> None:
|
|
17
|
+
"""Receive a notification."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
Callback = Callable[["Observable", str, Any], None]
|
|
21
|
+
T = TypeVar("T", bound="Observable")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Disposable:
|
|
25
|
+
"""A small helper that detaches an observer when disposed."""
|
|
26
|
+
|
|
27
|
+
__slots__ = ("_detach",)
|
|
28
|
+
|
|
29
|
+
def __init__(self, detach: Callable[[], None]):
|
|
30
|
+
self._detach = detach
|
|
31
|
+
|
|
32
|
+
def dispose(self) -> None:
|
|
33
|
+
"""Detach the associated observer immediately."""
|
|
34
|
+
|
|
35
|
+
self._detach()
|
|
36
|
+
|
|
37
|
+
# Allow use as a context‑manager for temporary subscriptions
|
|
38
|
+
def __enter__(self) -> "Disposable":
|
|
39
|
+
return self
|
|
40
|
+
|
|
41
|
+
def __exit__(self, exc_type, exc, tb):
|
|
42
|
+
self.dispose()
|
|
43
|
+
return False # do not swallow exceptions
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Observable(Generic[T]):
|
|
47
|
+
"""Base‑class that provides Observer pattern plumbing."""
|
|
48
|
+
|
|
49
|
+
#: Internal storage: mapping *event* → WeakSet[Callback]
|
|
50
|
+
_observers: dict[str, weakref.WeakSet[Callback]]
|
|
51
|
+
_any_observers: weakref.WeakSet[Callback]
|
|
52
|
+
|
|
53
|
+
def __init__(self) -> None:
|
|
54
|
+
self._lock = threading.RLock()
|
|
55
|
+
self._observers = {}
|
|
56
|
+
self._any_observers = weakref.WeakSet()
|
|
57
|
+
self._frozen = 0
|
|
58
|
+
self._pending: list[tuple[str, tuple[Any, ...], dict[str, Any]]] = []
|
|
59
|
+
|
|
60
|
+
# ‑‑‑ subscription api --------------------------------------------------
|
|
61
|
+
def attach(self, listener: Observer | Callback, event: str | None = None) -> Disposable:
|
|
62
|
+
"""Register *listener* for *event* (all events if *event* is None).
|
|
63
|
+
|
|
64
|
+
Returns a :class:`Disposable` so the caller can easily detach again.
|
|
65
|
+
"""
|
|
66
|
+
callback: Callback = (
|
|
67
|
+
listener.update # type: ignore[attr‑defined]
|
|
68
|
+
if isinstance(listener, Observer) # type: ignore[misc]
|
|
69
|
+
else listener # already a callable
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
with self._lock:
|
|
73
|
+
if event is None:
|
|
74
|
+
self._any_observers.add(callback)
|
|
75
|
+
else:
|
|
76
|
+
self._observers.setdefault(event, weakref.WeakSet()).add(callback)
|
|
77
|
+
|
|
78
|
+
return Disposable(lambda: self.detach(listener, event))
|
|
79
|
+
|
|
80
|
+
def detach(self, listener: Observer | Callback, event: str | None = None) -> None:
|
|
81
|
+
"""Unregister a previously attached *listener*."""
|
|
82
|
+
|
|
83
|
+
callback: Callback = (
|
|
84
|
+
listener.update # type: ignore[attr‑defined]
|
|
85
|
+
if isinstance(listener, Observer) # type: ignore[misc]
|
|
86
|
+
else listener
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
with self._lock:
|
|
90
|
+
if event is None:
|
|
91
|
+
self._any_observers.discard(callback)
|
|
92
|
+
for s in self._observers.values():
|
|
93
|
+
s.discard(callback)
|
|
94
|
+
else:
|
|
95
|
+
self._observers.get(event, weakref.WeakSet()).discard(callback)
|
|
96
|
+
def __getstate__(self):
|
|
97
|
+
state = self.__dict__.copy()
|
|
98
|
+
state.pop('_lock', None) # RLock cannot be pickled
|
|
99
|
+
state.pop('_observers', None) # WeakSet cannot be pickled
|
|
100
|
+
state.pop('_any_observers', None)
|
|
101
|
+
return state
|
|
102
|
+
def __setstate__(self, state):
|
|
103
|
+
self.__dict__.update(state)
|
|
104
|
+
self._lock = threading.RLock()
|
|
105
|
+
self._observers = {}
|
|
106
|
+
self._any_observers = weakref.WeakSet()
|
|
107
|
+
self._frozen = 0
|
|
108
|
+
# ‑‑‑ notification api --------------------------------------------------
|
|
109
|
+
def notify(self: T, event: str, *args: Any, **kwargs: Any) -> None:
|
|
110
|
+
"""Notify observers that *event* happened."""
|
|
111
|
+
|
|
112
|
+
with self._lock:
|
|
113
|
+
if self._frozen:
|
|
114
|
+
# defer until freeze_notifications() exits
|
|
115
|
+
self._pending.append((event, args, kwargs))
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
observers = list(self._any_observers)
|
|
119
|
+
observers.extend(self._observers.get(event, ()))
|
|
120
|
+
|
|
121
|
+
# Call outside lock — prevent deadlocks if observers trigger other
|
|
122
|
+
# notifications.
|
|
123
|
+
for cb in observers:
|
|
124
|
+
try:
|
|
125
|
+
cb(self, event, *args, **kwargs)
|
|
126
|
+
except Exception: # pragma: no cover
|
|
127
|
+
# Optionally log; never allow an observer error to break flow.
|
|
128
|
+
import logging
|
|
129
|
+
|
|
130
|
+
logging.getLogger(__name__).exception(
|
|
131
|
+
"Unhandled error in observer %s for event %s", cb, event
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# ‑‑‑ batching ----------------------------------------------------------
|
|
135
|
+
@contextmanager
|
|
136
|
+
def freeze_notifications(self):
|
|
137
|
+
"""Context manager that batches notifications until exit."""
|
|
138
|
+
|
|
139
|
+
with self._lock:
|
|
140
|
+
self._frozen += 1
|
|
141
|
+
try:
|
|
142
|
+
yield self
|
|
143
|
+
finally:
|
|
144
|
+
with self._lock:
|
|
145
|
+
self._frozen -= 1
|
|
146
|
+
if self._frozen == 0 and self._pending:
|
|
147
|
+
pending = self._pending[:]
|
|
148
|
+
self._pending.clear()
|
|
149
|
+
for event, args, kw in pending: # type: ignore[has‑type]
|
|
150
|
+
self.notify(event, *args, **kw)
|
LoopStructural/version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "1.6.
|
|
1
|
+
__version__ = "1.6.18"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
LoopStructural/__init__.py,sha256=
|
|
2
|
-
LoopStructural/version.py,sha256=
|
|
1
|
+
LoopStructural/__init__.py,sha256=ZS5J2TI2OuhRE0o3EXmPbbjrvh1-Vy9RFwL7Hc1YBow,2079
|
|
2
|
+
LoopStructural/version.py,sha256=4Zzb1p1N54zNXVm2h5jVUlkFjge9TszzpDGANK9ZNPM,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=
|
|
48
|
+
LoopStructural/interpolators/_geological_interpolator.py,sha256=74tQUImZ9axkXUrWygl6vqspY3bsoHf_d3twVB30rho,11430
|
|
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,8 +71,9 @@ 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/
|
|
75
|
-
LoopStructural/modelling/core/
|
|
74
|
+
LoopStructural/modelling/core/fault_topology.py,sha256=bChp5dnfc-4GJRENWxB14mEW_13uBMh5ZYRKbLdjweE,11195
|
|
75
|
+
LoopStructural/modelling/core/geological_model.py,sha256=f6bkpXdKxr72OozUb1vdoosLcjzMIht8VNgNlONluVA,65570
|
|
76
|
+
LoopStructural/modelling/core/stratigraphic_column.py,sha256=fmT51wInY3kei7Ya01cd5P5hcnhY5TkLBLJJwkUd_lE,20807
|
|
76
77
|
LoopStructural/modelling/features/__init__.py,sha256=Vf-qd5EDBtJ1DpuXXyCcw2-wf6LWPRW5wzxDEO3vOc8,939
|
|
77
78
|
LoopStructural/modelling/features/_analytical_feature.py,sha256=U_g86LgQhYY2359rdsDqpvziYwqrWkc5EdvhJARiUWo,3597
|
|
78
79
|
LoopStructural/modelling/features/_base_geological_feature.py,sha256=kGyrbb8nNzfi-M8WSrVMEQYKtxThdcBxaji5HKXtAqw,13483
|
|
@@ -97,7 +98,7 @@ LoopStructural/modelling/features/fold/__init__.py,sha256=pOv20yQvshZozvmO_YFw2E
|
|
|
97
98
|
LoopStructural/modelling/features/fold/_fold.py,sha256=bPnnLUSiF4uoMRg8aHoOSTPRgaM0JyLoRQPu5_A-J3w,5448
|
|
98
99
|
LoopStructural/modelling/features/fold/_fold_rotation_angle_feature.py,sha256=CXLbFRQ3CrTMAcHmfdbKcmSvvLs9_6TLe0Wqi1pK2tg,892
|
|
99
100
|
LoopStructural/modelling/features/fold/_foldframe.py,sha256=Rgf5aofN0OVDTZ2pzqLzAGlJUO2rnNm3aFvLSnH77yo,7669
|
|
100
|
-
LoopStructural/modelling/features/fold/_svariogram.py,sha256=
|
|
101
|
+
LoopStructural/modelling/features/fold/_svariogram.py,sha256=uLeBWZahEmystf8mpPJH_zKxp7hPyPJh0H5ROZO0pZs,7933
|
|
101
102
|
LoopStructural/modelling/features/fold/fold_function/__init__.py,sha256=VqMjabsBd5GnPnDMXeKwXqtd0te2iXnvHxpf6jCC9YU,830
|
|
102
103
|
LoopStructural/modelling/features/fold/fold_function/_base_fold_rotation_angle.py,sha256=5Bu_5xjyu4KL7thZ4fsh938Ep3Oyh5TL7_rfz_13Qng,8047
|
|
103
104
|
LoopStructural/modelling/features/fold/fold_function/_fourier_series_fold_rotation_angle.py,sha256=Cjb6Pt6cdRoH3WGqFyJ2rHxnMe6SKvzRayA6hTuwZA8,4069
|
|
@@ -115,8 +116,8 @@ LoopStructural/modelling/intrusions/intrusion_builder.py,sha256=PtNLDreUZTGodMwt
|
|
|
115
116
|
LoopStructural/modelling/intrusions/intrusion_feature.py,sha256=ESjtikHFJQzUnowbYiY7UZ_kYdV2QHobQoRJ2far9Vc,15489
|
|
116
117
|
LoopStructural/modelling/intrusions/intrusion_frame_builder.py,sha256=YEJv2GURAL8bW6J1KscM69ZVx9yoKVQKF_gXbjB513I,40150
|
|
117
118
|
LoopStructural/modelling/intrusions/intrusion_support_functions.py,sha256=wodakheMD62WJyoKnyX8UO-C1pje0I-5kHQEoDqShzo,13951
|
|
118
|
-
LoopStructural/utils/__init__.py,sha256=
|
|
119
|
-
LoopStructural/utils/_surface.py,sha256=
|
|
119
|
+
LoopStructural/utils/__init__.py,sha256=n-3PGnV-KEy4LOJkHZ6nlnoDyKDlfG6rJhXM0OgFYm0,1004
|
|
120
|
+
LoopStructural/utils/_surface.py,sha256=M0ZYujrqu-pWz-Y8iAhZsgoDxvI5YZG4GyhtBHdbCfk,6389
|
|
120
121
|
LoopStructural/utils/_transformation.py,sha256=peuLPH3BJ5DxnPbOuNKcqK4eXhAXdbT540L1OIsO3v0,5404
|
|
121
122
|
LoopStructural/utils/colours.py,sha256=-KRf1MXKx4L8TXnwyiunmKAX4tfy0qG68fRadyfn_bM,1163
|
|
122
123
|
LoopStructural/utils/config.py,sha256=ITGOtZTo2_QBwXkG_0AFANfE90J9siCXLzxypVmg9QA,414
|
|
@@ -128,12 +129,13 @@ LoopStructural/utils/json_encoder.py,sha256=5YNouf1TlhjEqOYgthd07MRXc0JLgxern-ny
|
|
|
128
129
|
LoopStructural/utils/linalg.py,sha256=tBXyu6NXcG2AcPuzUMnkVI4ncZWtE_MPHGj2PLXRwfY,123
|
|
129
130
|
LoopStructural/utils/logging.py,sha256=dIUWEsS2lT4G1dsf4ZYXknTR7eQkrgvGA4b_E0vMIRU,2402
|
|
130
131
|
LoopStructural/utils/maths.py,sha256=KaLj9RHsxdaSkEHm4t0JEzykhiuETAV14KpjL6lknWY,10374
|
|
132
|
+
LoopStructural/utils/observer.py,sha256=etHF2zhDWbrKAaIKAktdVLosD696o_B0P9rH3G5IzV0,5305
|
|
131
133
|
LoopStructural/utils/regions.py,sha256=SjCC40GI7_n03G4mlcmvyrBgJFbxnvB3leBzXWco37o,3891
|
|
132
134
|
LoopStructural/utils/typing.py,sha256=29uVSTZdzXXH-jdlaYyBWZ1gQ2-nlZ2-XoVgG_PXNFY,157
|
|
133
135
|
LoopStructural/utils/utils.py,sha256=2Z4zVE6G752-SPmM29zebk82bROJxEwi_YiiJjcVED4,2438
|
|
134
136
|
LoopStructural/visualisation/__init__.py,sha256=5BDgKor8-ae6DrS7IZybJ3Wq_pTnCchxuY4EgzA7v1M,318
|
|
135
|
-
loopstructural-1.6.
|
|
136
|
-
loopstructural-1.6.
|
|
137
|
-
loopstructural-1.6.
|
|
138
|
-
loopstructural-1.6.
|
|
139
|
-
loopstructural-1.6.
|
|
137
|
+
loopstructural-1.6.18.dist-info/licenses/LICENSE,sha256=ZqGeNFOgmYevj7Ld7Q-kR4lAxWXuBRUdUmPC6XM_py8,1071
|
|
138
|
+
loopstructural-1.6.18.dist-info/METADATA,sha256=Ps1KAvAFlPgR11P0nbTfYrMlcPS6bX3OewD1UKRXEnU,6453
|
|
139
|
+
loopstructural-1.6.18.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
140
|
+
loopstructural-1.6.18.dist-info/top_level.txt,sha256=QtQErKzYHfg6ddxTQ1NyaTxXBVM6qAqrM_vxEPyXZLg,15
|
|
141
|
+
loopstructural-1.6.18.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|