forgeo-gmlib 0.6.2__cp312-cp312-win_amd64.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.
@@ -0,0 +1,396 @@
1
+ #
2
+ # This file is part of gmlib. It is free software.
3
+ # You can redistribute it and/or modify it under the terms of the GNU Affero General Public License version 3.
4
+ #
5
+
6
+
7
+ import os
8
+ from collections import namedtuple
9
+
10
+ import numpy as np
11
+ from lxml.builder import ElementMaker
12
+
13
+ from forgeo.gmlib import myxmltools as mx
14
+ from forgeo.gmlib import topography_reader
15
+ from forgeo.gmlib.geomodeller_data import (
16
+ CovarianceModel,
17
+ FaultData,
18
+ GradientData,
19
+ Pile,
20
+ PotentialData,
21
+ SeriesData,
22
+ )
23
+
24
+ Formation = namedtuple("Formation", ["name", "color", "is_dummy"])
25
+ GmlCoord = namedtuple("GmlCoord", ["x", "y", "z"])
26
+ DrillHole = namedtuple("DrillHole", ["name", "total_depth", "collar"])
27
+
28
+ # FIXME: this sould not be static
29
+ nsmap = {"geo": "http://www.geomodeller.com/geo", "gml": "http://www.opengis.net/gml"}
30
+ geo = ElementMaker(namespace=nsmap["geo"])
31
+ gml = ElementMaker(namespace=nsmap["gml"])
32
+
33
+
34
+ def extract_coord(node, tag):
35
+ pts = []
36
+ coords = node.find(tag)
37
+ if coords is not None:
38
+ for coord in coords:
39
+ P = GmlCoord(
40
+ *tuple(
41
+ np.double(coord.find(axis).text)
42
+ for axis in (gml.X().tag, gml.Y().tag, gml.Z().tag)
43
+ )
44
+ )
45
+ pts.append(P)
46
+ return pts
47
+
48
+
49
+ def extract_crs(xml_root):
50
+ assert xml_root.tag == geo.Project3DEdit().tag
51
+ try:
52
+ gmcrs = xml_root.attrib["CoordSystem"]
53
+ except KeyError:
54
+ gmcrs = None
55
+ try:
56
+ qgiscrs = xml_root.attrib["QGisCRS"]
57
+ except KeyError:
58
+ qgiscrs = None
59
+ return gmcrs, qgiscrs
60
+
61
+
62
+ def read_box(xml_root):
63
+ extent = xml_root.find(geo.Extent3DOfProject().tag)
64
+ extentbox = extent.find(geo.ExtentBox3D().tag)
65
+ extent3D = extentbox.find(geo.Extent3D().tag)
66
+ xy = extent3D.find(geo.ExtentXY().tag)
67
+ limits = {}
68
+ for s, val in xy.items():
69
+ limits[s] = float(val)
70
+ z = extent3D.find(geo.ExtentZ().tag)
71
+ for s, val in z.items():
72
+ limits[s] = float(val)
73
+ return limits
74
+
75
+
76
+ def read_color(root):
77
+ color = None
78
+ graphic = root.find(geo.Graphic().tag)
79
+ if graphic is not None:
80
+ shading = graphic.find(geo.ColorShading().tag)
81
+ color = tuple(
82
+ int(shading.attrib[name]) / 255.0 # convert to float RGB
83
+ for name in ("Red", "Green", "Blue")
84
+ )
85
+ return color
86
+
87
+
88
+ def read_formations(xml_root):
89
+ formations = xml_root.find(geo.Formations().tag)
90
+ result = []
91
+
92
+ def extract_formation(formation, is_dummy=False):
93
+ name = formation.attrib["Name"]
94
+ color = read_color(formation)
95
+ return Formation(name, color, is_dummy)
96
+
97
+ if formations is not None:
98
+ for formation in formations.findall(geo.Formation().tag):
99
+ result.append(extract_formation(formation))
100
+ for formation in formations.findall(geo.DummyFormation().tag):
101
+ result.append(extract_formation(formation, True))
102
+ return result
103
+
104
+
105
+ def find_topography_sections(xml_root):
106
+ topography_sections = []
107
+ sections = xml_root.find(geo.Sections().tag)
108
+ if sections is not None:
109
+ for section in sections.findall(geo.Section().tag):
110
+ if "IsTopography" in section:
111
+ if section.attrib["IsTopography"] == "true":
112
+ topography_sections.append(section)
113
+ return topography_sections
114
+
115
+
116
+ def extract_drillholes(xml_root):
117
+ drillholes = []
118
+ dhs = xml_root.find(geo.DrillHoles().tag)
119
+ if dhs is not None:
120
+ for dh in dhs.findall(geo.DrillholeWithFields().tag):
121
+ name = dh.attrib["Name"]
122
+ total_depth = float(dh.attrib["TotalDepth"])
123
+ collar_pos = extract_coord(dh, geo.Collar().tag)
124
+ assert len(collar_pos) == 1
125
+ drillholes.append(DrillHole(name, total_depth, collar_pos[0]))
126
+ return drillholes
127
+
128
+
129
+ def read_topography_info(xml_root):
130
+ topography_sections = find_topography_sections(xml_root)
131
+ if not topography_sections:
132
+ return None
133
+ assert len(topography_sections) == 1 # only one topography section!!!
134
+ section = topography_sections[0]
135
+ shape = section.find(geo.Shape3DOfSection().tag)
136
+ return section.attrib["Name"], shape.attrib["FileName"]
137
+
138
+
139
+ def read_data(xml_root):
140
+ data = xml_root.findall(geo.Data().tag)
141
+ return [d.attrib["Name"] for d in data]
142
+
143
+
144
+ def read_potential_data(xml_root, box, scalardt, maximum_number_of_interfaces=None):
145
+ field = xml_root.find(geo.PotentialField().tag)
146
+ if field is None:
147
+ return None
148
+ glocs = []
149
+ gvals = []
150
+ interfaces = []
151
+ covariance_model = CovarianceModel(field.find(geo.covariance().tag), box)
152
+ constraints = field.find(geo.Constraints().tag)
153
+ if constraints is not None:
154
+ if any(constraint.attrib["value"] != "0" for constraint in constraints):
155
+ mess = "inequality constraints are not handled for now"
156
+ raise NotImplementedError(mess)
157
+ gradients = field.find(geo.Gradients().tag)
158
+ if gradients is not None:
159
+ for gradient in gradients.iterfind(geo.Gradient().tag):
160
+ V = tuple(scalardt.type(gradient.attrib[s]) for s in ("Gx", "Gy", "Gz"))
161
+ gvals.append(V)
162
+ P = tuple(scalardt.type(gradient.attrib[s]) for s in ("XGr", "YGr", "ZGr"))
163
+ glocs.append(P)
164
+ pts = extract_coord(field, geo.Points().tag)
165
+ interface_points = field.find(geo.InterfacePoints().tag)
166
+ for interface in interface_points:
167
+
168
+ def attr_as_int(s):
169
+ return int(interface.attrib[s])
170
+
171
+ interfaces.append((attr_as_int("pnt"), attr_as_int("npnt")))
172
+ if maximum_number_of_interfaces is not None:
173
+ assert len(interfaces) >= maximum_number_of_interfaces, (
174
+ "Not enough interfaces read."
175
+ )
176
+ del interfaces[maximum_number_of_interfaces:]
177
+
178
+ def array_of_scalars(v):
179
+ return np.array(v, dtype=scalardt)
180
+
181
+ potdata = PotentialData()
182
+ potdata.covariance_model = covariance_model
183
+ potdata.gradients = GradientData(array_of_scalars(glocs), array_of_scalars(gvals))
184
+ pts = array_of_scalars(pts)
185
+ potdata.interfaces = [pts[start : start + nb] for start, nb in interfaces]
186
+ return potdata
187
+
188
+
189
+ def extract_raw_faults_data(root, scalardt):
190
+ faults_data = {}
191
+ faults = root.find(geo.Faults().tag)
192
+ if faults is not None:
193
+ for fault in faults.findall(geo.Fault().tag):
194
+ data = FaultData(fault.attrib["Name"])
195
+ geology = fault.find(geo.FaultGeology().tag).attrib
196
+ if int(geology["FAULT_TYPE"]) == 1: # finite fault
197
+ data.infinite = False
198
+ data.center_type = None
199
+ center_type = geology["CENTER_TYPE"]
200
+ if center_type == "0":
201
+ data.center_type = "mean_center"
202
+ elif center_type == "1":
203
+ data.center_type = "databox_center"
204
+ else:
205
+ assert center_type == "2"
206
+ data.center_type = (
207
+ scalardt.type(geology["CENTERX"]),
208
+ scalardt.type(geology["CENTERY"]),
209
+ scalardt.type(geology["CENTERZ"]),
210
+ )
211
+ data.lateral_extent = scalardt.type(geology["LATERAL_EXTENT"])
212
+ data.vertical_extent = scalardt.type(geology["VERTICAL_EXTENT"])
213
+ data.influence_radius = scalardt.type(geology["RADIUS_OF_INFLUENCE"])
214
+ for other in fault.iterfind(geo.StopsOnFault().tag):
215
+ data.stops_on.append(other.attrib["Name"])
216
+ data.color = read_color(fault)
217
+ faults_data[data.name] = data
218
+ return faults_data
219
+
220
+
221
+ def read_modeled_faults_data(root, box, scalardt):
222
+ faults_data = extract_raw_faults_data(root, scalardt)
223
+ if faults_data:
224
+ model = root.find(geo.GeologicalModel().tag)
225
+ if model is not None:
226
+ modelfaults = model.find(geo.ModelFaults().tag)
227
+ if modelfaults is not None:
228
+ faultpotentials = modelfaults.findall(geo.PotentialFault().tag)
229
+ for fault in faultpotentials:
230
+ data = read_data(fault)
231
+ assert len(data) == 1
232
+ name = data[0]
233
+ potdata = read_potential_data(fault, box, scalardt)
234
+ faults_data[name].potential_data = potdata
235
+ deleted_faults = [
236
+ name for name, data in faults_data.items() if data.potential_data is None
237
+ ]
238
+ for name in deleted_faults:
239
+ del faults_data[name]
240
+ return faults_data
241
+
242
+
243
+ def read_pile(root, box, scalardt):
244
+ pile = None
245
+ model = root.find(geo.GeologicalModel().tag)
246
+ if model is not None:
247
+ project_column = model.find(geo.ProjectStratigraphicColumn().tag)
248
+ # modelseries = model.find(geo.ModelStratigraphicColumn().tag)
249
+ # for serie in modelseries:
250
+ # modelseries.find('geo:Data',ns)
251
+ # name = serie.attrib['Name']
252
+ # print (name)
253
+
254
+ reference = {"true": "base", "false": "top"}[project_column.attrib["IsBase"]]
255
+ pile = Pile(reference)
256
+ all_series = []
257
+ for serie in project_column:
258
+ # read data
259
+ name = serie.attrib["name"]
260
+ serie_data = SeriesData(name)
261
+ serie_data.relation = {"1": "onlap", "2": "erode"}[serie.attrib["relation"]]
262
+ serie_data.formations = read_data(serie)
263
+ all_influencing_faults = serie.findall(geo.InfluencedByFault().tag)
264
+ influenced_by_fault = [f.attrib["Name"] for f in all_influencing_faults]
265
+ if influenced_by_fault:
266
+ serie_data.influenced_by_fault = influenced_by_fault
267
+ serie_data.potential_data = read_potential_data(serie, box, scalardt)
268
+ if serie_data.potential_data is not None:
269
+ if len(serie_data.potential_data.interfaces) > len(
270
+ serie_data.formations
271
+ ):
272
+ if len(serie_data.formations) == 1:
273
+ pass
274
+ else:
275
+ pass
276
+ serie_data.potential_data = read_potential_data(
277
+ serie, box, scalardt, len(serie_data.formations)
278
+ )
279
+ assert serie_data.potential_data is None or len(
280
+ serie_data.potential_data.interfaces
281
+ ) == len(serie_data.formations), f"Inconsitent {name} series."
282
+ all_series.append(serie_data)
283
+ model_column = model.find(geo.ModelStratigraphicColumn().tag)
284
+ if model_column is not None:
285
+ selection = [data.attrib["Name"] for data in model_column]
286
+ series_selection = [
287
+ serie for serie in all_series if serie.name in selection
288
+ ]
289
+ for ref, loc in (("base", 0), ("top", -1)):
290
+ if pile.reference == ref:
291
+ dummy_serie = all_series[loc]
292
+ if (
293
+ dummy_serie.name not in selection
294
+ and dummy_serie.potential_data is None
295
+ ):
296
+ series_selection.insert(loc, dummy_serie)
297
+ all_series = series_selection
298
+ pile.all_series = all_series
299
+ return pile
300
+
301
+
302
+ def extract_tree(filepath):
303
+ if os.path.islink(filepath):
304
+ filepath = os.readlink(filepath)
305
+ filepath = os.path.realpath(filepath)
306
+ tree = mx.parse(filepath)
307
+ root = tree.getroot()
308
+ for ns, uri in nsmap.items():
309
+ assert ns in root.nsmap
310
+ assert root.nsmap[ns] == uri
311
+ return tree
312
+
313
+
314
+ def extract_project_data(filepath, scalardt=np.dtype("d"), skip_topography=False):
315
+ root = extract_tree(filepath).getroot()
316
+ crs = extract_crs(root)
317
+ box = read_box(root)
318
+ faults_data = read_modeled_faults_data(root, box, scalardt)
319
+ pile = read_pile(root, box, scalardt)
320
+ topography_info = None if skip_topography else read_topography_info(root)
321
+ if topography_info is None:
322
+ topography = topography_reader.ImplicitHorizontalPlane(box["Zmax"])
323
+ else:
324
+ _name, filename = topography_info
325
+ project_directory = os.path.split(filepath)[0]
326
+ topography = topography_reader.sec_extract(
327
+ os.path.join(project_directory, filename)
328
+ )
329
+ formations = read_formations(root)
330
+ return {
331
+ "box": box,
332
+ "crs": crs,
333
+ "pile": pile,
334
+ "faults_data": faults_data,
335
+ "topography": topography,
336
+ "formations": formations,
337
+ }
338
+
339
+
340
+ def extract_project_drillholes(filepath):
341
+ root = extract_tree(filepath).getroot()
342
+ return extract_drillholes(root)
343
+
344
+
345
+ def make_center(aSerie, xmin, xmax, ymin, ymax, zmin, zmax):
346
+ cx = (xmin + xmax) / 2
347
+ cy = (ymin + ymax) / 2
348
+ cz = (zmin + zmax) / 2
349
+ Themax = xmax - xmin
350
+ pts = aSerie.potential_data.interfaces.points
351
+ # interfaces
352
+ for p in pts:
353
+ x = 0.5 + (p[0] - cx) / Themax
354
+ y = 0.5 + (p[1] - cy) / Themax
355
+ z = 0.5 + (p[2] - cz) / Themax
356
+ p[0] = x
357
+ p[1] = y
358
+ p[2] = z
359
+
360
+ gpts = aSerie.potential_data.gradients.locations
361
+ # gradients
362
+ for p in gpts:
363
+ x = 0.5 + (p[0] - cx) / Themax
364
+ y = 0.5 + (p[1] - cy) / Themax
365
+ z = 0.5 + (p[2] - cz) / Themax
366
+ p[0] = x
367
+ p[1] = y
368
+ p[2] = z
369
+
370
+ cov = aSerie.potential_data.covariance_model
371
+ cov.range = cov.range / Themax
372
+
373
+
374
+ if __name__ == "__main__":
375
+ import sys
376
+
377
+ from matplotlib import pyplot as plt
378
+
379
+ if len(sys.argv) > 1:
380
+ projectfile = sys.argv[1]
381
+ if os.path.exists(projectfile):
382
+ assert not os.path.islink(projectfile)
383
+ data = extract_project_data(projectfile)
384
+ box, pile, faults_data, topography, fcolors = data
385
+ nx, ny = 200, 200
386
+ xmin, xmax = box["Xmin"], box["Xmax"]
387
+ ymin, ymax = box["Ymin"], box["Ymax"]
388
+ xy = np.meshgrid(
389
+ np.linspace(xmin, xmax, nx), np.linspace(ymin, ymax, ny), indexing="ij"
390
+ )
391
+ x, y = (a.ravel() for a in xy)
392
+ z = np.array([topography.evaluate_z((xi, yi)) for xi, yi in zip(x, y)])
393
+ z.shape = nx, ny
394
+ plt.gca().set_aspect("equal")
395
+ box = (xmin, xmax, ymin, ymax)
396
+ plt.imshow(np.transpose(z)[::-1], extent=box)
@@ -0,0 +1,30 @@
1
+ #
2
+ # This file is part of gmlib. It is free software.
3
+ # You can redistribute it and/or modify it under the terms of the GNU Affero General Public License version 3.
4
+ #
5
+
6
+ """
7
+ Created on Mon Jul 18 12:12:46 2016
8
+
9
+ @author: lopez
10
+ """
11
+
12
+ import os
13
+
14
+ from lxml import etree
15
+
16
+
17
+ def parse(filename):
18
+ assert os.path.exists(filename)
19
+ # cf. https://lxml.de/FAQ.html#why-doesn-t-the-pretty-print-option-reformat-my-xml-output
20
+ parser = etree.XMLParser(remove_blank_text=True)
21
+ return etree.parse(filename, parser)
22
+
23
+
24
+ def create_child_if_needed(parent, tag):
25
+ """The parent namespace will be used."""
26
+ qname = etree.QName(etree.QName(parent).namespace, tag)
27
+ child = parent.find(qname)
28
+ if child is None:
29
+ child = etree.SubElement(parent, qname)
30
+ return child
Binary file
Binary file
@@ -0,0 +1,236 @@
1
+ import contextlib
2
+ from collections import defaultdict
3
+ from pathlib import Path
4
+
5
+ import numpy as np
6
+ from pycgal.Polygon_mesh_processing import (
7
+ border_halfedges,
8
+ connected_components,
9
+ extract_zmap_corners_and_borders,
10
+ isotropic_remeshing,
11
+ remove_connected_components,
12
+ split,
13
+ split_long_edges,
14
+ triangulate_faces,
15
+ )
16
+ from pycgal.Surface_mesh import Edges, Surface_mesh
17
+ from skimage.measure import marching_cubes
18
+
19
+ from .GeologicalModel3D import GeologicalModel
20
+
21
+
22
+ def _remove_unvalid_side(S, limit, valid_side):
23
+ connected_components(S, "f:component")
24
+ components = S.face_property("f:component")
25
+ values = set()
26
+ faces = []
27
+ for f in S.faces():
28
+ compf = components[f]
29
+ if compf not in values:
30
+ values.add(compf)
31
+ face_center = np.array(S.centroid(f), copy=False)[None, :]
32
+ if valid_side * limit(face_center) <= 0:
33
+ faces.append(f)
34
+ remove_connected_components(S, faces)
35
+
36
+
37
+ def _split_border_edges(S, ds2):
38
+ """
39
+ Check that border edges are compliant for isotropic remeshing.
40
+ """
41
+ border_edges = Edges(S, border_halfedges(S))
42
+ split_long_edges(S, (4 / 3) * ds2, border_edges)
43
+
44
+
45
+ def _tesselate_horizontal_plane(box, z, target_squared_edge=None):
46
+ if box.zmin > z or box.zmax < z:
47
+ return Surface_mesh()
48
+ vertices = np.array(
49
+ [
50
+ (box.xmin, box.ymin, box.zmax),
51
+ (box.xmax, box.ymin, box.zmax),
52
+ (box.xmax, box.ymax, box.zmax),
53
+ (box.xmin, box.ymax, box.zmax),
54
+ ]
55
+ )
56
+ square = np.array([[0, 1, 2, 3]])
57
+ mesh = Surface_mesh(vertices, square)
58
+ corners, _borders = extract_zmap_corners_and_borders(mesh)
59
+ assert len(corners) == 4
60
+ triangulate_faces(mesh)
61
+ if target_squared_edge:
62
+ isotropic_remeshing(mesh, target_squared_edge, constrained_vertices=corners)
63
+ return mesh
64
+
65
+
66
+ class FaultTesselator:
67
+ def __init__(self, box, shape, model):
68
+ self.shape = nx, ny, nz = shape
69
+ self.ds2 = min(
70
+ (box.xmax - box.xmin) / nx,
71
+ (box.ymax - box.ymin) / ny,
72
+ (box.zmax - box.zmin) / nz,
73
+ )
74
+ steps = (
75
+ np.linspace(box.xmin, box.xmax, nx),
76
+ np.linspace(box.ymin, box.ymax, ny),
77
+ np.linspace(box.zmin, box.zmax, nz),
78
+ )
79
+ coordinates = np.meshgrid(*steps, indexing="ij")
80
+ grid_points = np.stack(coordinates, axis=-1)
81
+ grid_points.shape = (-1, 3)
82
+ self.model = model
83
+ self.box = box
84
+ self.grid_points = grid_points
85
+ self.fault_surfaces = None
86
+ self.tesselate()
87
+
88
+ def rescale_into_box(self, pts):
89
+ box = self.box
90
+ nx, ny, nz = self.shape
91
+ return pts * np.array(
92
+ [
93
+ (box.xmax - box.xmin) / (nx - 1),
94
+ (box.ymax - box.ymin) / (ny - 1),
95
+ (box.zmax - box.zmin) / (nz - 1),
96
+ ]
97
+ ) + np.array([box.xmin, box.ymin, box.zmin])
98
+
99
+ def tesselate_functor(self, f, target_level=0):
100
+ """Will tesellate the target_level isolevel surface of the scalar functor f
101
+ using the discretization parameters of the self instance (grid_points, ds2...).
102
+ :param target_level: defaults to 0
103
+ """
104
+ field_value = f(self.grid_points)
105
+ if np.all(field_value < target_level) or np.all(field_value > target_level):
106
+ return Surface_mesh()
107
+ field_value.shape = self.shape
108
+ isocontour = marching_cubes(field_value, level=target_level)
109
+ vertices, faces, *_ = isocontour
110
+ vertices = self.rescale_into_box(vertices)
111
+ S = Surface_mesh(vertices, faces)
112
+ _split_border_edges(S, self.ds2)
113
+ isotropic_remeshing(S, self.ds2, do_project=True, protect_constraints=True)
114
+ return S
115
+
116
+ def sort_faults(self):
117
+ """
118
+ Will sort faults from the most subordinated to the major ones.
119
+ There is only a partial order relation between faults.
120
+ """
121
+ # find the faults that a given fault limits (reverse of stops on)
122
+ fault_limits = defaultdict(set)
123
+ model = self.model
124
+ for name, fault_data in model.faults_data.items():
125
+ fault_limits[name].update(set())
126
+ for limit in fault_data.stops_on:
127
+ fault_limits[limit].add(name)
128
+ sorted_faults = []
129
+ faults = set(model.faults.keys())
130
+ while len(faults) > 0:
131
+ subordinated_faults = {
132
+ fault for fault, limits in fault_limits.items() if len(limits) == 0
133
+ }
134
+ for limits in fault_limits.values():
135
+ limits -= subordinated_faults
136
+ for fault in subordinated_faults:
137
+ fault_limits.pop(fault)
138
+ sorted_faults.extend(subordinated_faults)
139
+ faults -= subordinated_faults
140
+ return sorted_faults
141
+
142
+ def tesselate_topography(self):
143
+ model = self.model
144
+ # Retrieve the exact DTM
145
+ # topography = Surface_mesh(*model.topography.underlying_dtm())
146
+ # or use a topography approximation
147
+ try:
148
+ z_plane = model.topography.z
149
+ except AttributeError:
150
+ z_plane = None
151
+ if type(z_plane) is float:
152
+ topography = _tesselate_horizontal_plane(self.box, z_plane, self.ds2)
153
+ else:
154
+ topography = self.tesselate_functor(model.topography)
155
+ self.topography = topography
156
+
157
+ def tesselate(self):
158
+ self.tesselate_topography()
159
+ topography = self.topography
160
+ model = self.model
161
+
162
+ fault_surfaces = {}
163
+
164
+ for name, field in model.faults.items():
165
+ fault_surfaces[name] = self.tesselate_functor(field)
166
+
167
+ for name in self.sort_faults():
168
+ fault_data = model.faults_data[name]
169
+ S = fault_surfaces[name]
170
+ if S.is_empty():
171
+ continue
172
+ points = fault_data.potential_data.interfaces[0]
173
+ # Clip with topography
174
+ split(S, topography)
175
+ # FIXME: as defined here the depth functor maybe inaccurate
176
+ # we should project the point onto the tesselated DTM surface
177
+ _remove_unvalid_side(S, model.topography, -1)
178
+ # Clip with limiting faults
179
+ for limit in fault_data.stops_on:
180
+ FL = model.faults[limit]
181
+ SL = fault_surfaces[limit]
182
+ if SL.is_empty():
183
+ continue
184
+ split(S, SL)
185
+ _remove_unvalid_side(S, FL, np.mean(FL(points)))
186
+ # Clip finite faults at the end
187
+ # that makes that the *infinite extension of finite faults*
188
+ # can limit other faults
189
+ if not fault_data.infinite:
190
+ assert model.is_finite_fault(name)
191
+ ellipsoid = model.fault_ellipsoids[name]
192
+ E = self.tesselate_functor(ellipsoid)
193
+ if not E.is_empty():
194
+ split(S, E)
195
+ _remove_unvalid_side(S, ellipsoid, -1)
196
+
197
+ self.fault_surfaces = fault_surfaces
198
+
199
+
200
+ def tesselate_faults(box, shape, model, with_topography=False):
201
+ tesselator = FaultTesselator(box, shape, model)
202
+ if with_topography:
203
+ return tesselator.fault_surfaces, tesselator.topography
204
+ return tesselator.fault_surfaces
205
+
206
+
207
+ if __name__ == "__main__":
208
+ import sys
209
+
210
+ filepath = Path(sys.argv[1])
211
+ if filepath.exists():
212
+ model = GeologicalModel(str(filepath.absolute()))
213
+ else:
214
+ msg = f"{filepath} not found!"
215
+ raise OSError(msg)
216
+
217
+ shape = (20,) * 3
218
+ fault_surfaces, topography = tesselate_faults(
219
+ model.getbox(), shape, model, with_topography=True
220
+ )
221
+
222
+ vtkw = None
223
+ with contextlib.suppress(ModuleNotFoundError):
224
+ import vtkwriters as vtkw
225
+
226
+ if vtkw is not None:
227
+
228
+ def to_vtu(S, name):
229
+ if S.number_of_faces() == 0:
230
+ return
231
+ v, t = S.as_arrays()
232
+ vtkw.write_vtu(vtkw.vtu_doc(v, t[0]), f"{name}.vtu")
233
+
234
+ to_vtu(topography, f"{filepath.stem}_topography")
235
+ for name, S in fault_surfaces.items():
236
+ to_vtu(S, f"{filepath.stem}_fault_surface_{name}")