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.
- forgeo/gmlib/GeologicalModel3D.py +758 -0
- forgeo/gmlib/__init__.py +9 -0
- forgeo/gmlib/_version.py +34 -0
- forgeo/gmlib/architecture/__init__.py +1 -0
- forgeo/gmlib/architecture/core.py +130 -0
- forgeo/gmlib/common.pyd +0 -0
- forgeo/gmlib/fault_network.py +171 -0
- forgeo/gmlib/geomodeller_data.py +101 -0
- forgeo/gmlib/geomodeller_project.py +396 -0
- forgeo/gmlib/myxmltools.py +30 -0
- forgeo/gmlib/pypotential2D.pyd +0 -0
- forgeo/gmlib/pypotential3D.pyd +0 -0
- forgeo/gmlib/tesselate.py +236 -0
- forgeo/gmlib/tesselate_deprecated.py +249 -0
- forgeo/gmlib/topography_reader.py +198 -0
- forgeo/gmlib/utils/__init__.py +0 -0
- forgeo/gmlib/utils/append_data.py +508 -0
- forgeo/gmlib/utils/export.py +45 -0
- forgeo/gmlib/utils/normalized_gradient.py +40 -0
- forgeo/gmlib/utils/tools.py +35 -0
- forgeo_gmlib-0.6.2.dist-info/METADATA +23 -0
- forgeo_gmlib-0.6.2.dist-info/RECORD +24 -0
- forgeo_gmlib-0.6.2.dist-info/WHEEL +5 -0
- forgeo_gmlib-0.6.2.dist-info/licenses/LICENSE +661 -0
|
@@ -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}")
|