cmflow 0.3.3.dev2__tar.gz → 0.4.1__tar.gz
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.
- {cmflow-0.3.3.dev2 → cmflow-0.4.1}/PKG-INFO +89 -1
- {cmflow-0.3.3.dev2 → cmflow-0.4.1}/README.md +88 -0
- {cmflow-0.3.3.dev2 → cmflow-0.4.1}/cmflow/conceptual_models.py +606 -6
- cmflow-0.4.1/cmflow/name_combo.py +0 -0
- {cmflow-0.3.3.dev2 → cmflow-0.4.1}/cmflow/t2data_utils.py +3 -2
- {cmflow-0.3.3.dev2 → cmflow-0.4.1}/.github/workflows/pypi.yml +0 -0
- {cmflow-0.3.3.dev2 → cmflow-0.4.1}/.github/workflows/unittest.yml +0 -0
- {cmflow-0.3.3.dev2 → cmflow-0.4.1}/.gitignore +0 -0
- {cmflow-0.3.3.dev2 → cmflow-0.4.1}/cmflow/__init__.py +0 -0
- {cmflow-0.3.3.dev2 → cmflow-0.4.1}/cmflow/conceptual_models.py.bak +0 -0
- {cmflow-0.3.3.dev2 → cmflow-0.4.1}/cmflow/geom_3dface_utils.py +0 -0
- {cmflow-0.3.3.dev2 → cmflow-0.4.1}/cmflow/geom_3dface_utils.py.bak +0 -0
- {cmflow-0.3.3.dev2 → cmflow-0.4.1}/cmflow/geom_surface_utils.py +0 -0
- {cmflow-0.3.3.dev2 → cmflow-0.4.1}/cmflow/geom_surface_utils.py.bak +0 -0
- {cmflow-0.3.3.dev2 → cmflow-0.4.1}/cmflow/t2data_utils.py.bak +0 -0
- {cmflow-0.3.3.dev2 → cmflow-0.4.1}/cmflow/test_conceptual_models.py +0 -0
- {cmflow-0.3.3.dev2 → cmflow-0.4.1}/cmflow/test_conceptual_models.py.bak +0 -0
- {cmflow-0.3.3.dev2 → cmflow-0.4.1}/cmflow/test_geom_3dface_utils.py +0 -0
- {cmflow-0.3.3.dev2 → cmflow-0.4.1}/cmflow/test_geom_3dface_utils_ex.ts +0 -0
- {cmflow-0.3.3.dev2 → cmflow-0.4.1}/cmflow/test_geom_surface_utils.py +0 -0
- {cmflow-0.3.3.dev2 → cmflow-0.4.1}/pyproject.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cmflow
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: Building TOUGH2/Waiwera models from layers of conceptual models
|
|
5
5
|
Project-URL: Homepage, https://github.com/cyeh015/cmflow
|
|
6
6
|
Project-URL: Source Code, https://github.com/cyeh015/cmflow
|
|
@@ -38,6 +38,94 @@ On Linux (Ubuntu shown here) these can be installed via apt-get:
|
|
|
38
38
|
sudo apt-get install -y python-shapely
|
|
39
39
|
sudo apt-get install -y python-rtree
|
|
40
40
|
|
|
41
|
+
# Using Leapfrog Geological Models
|
|
42
|
+
|
|
43
|
+
In Leapfrog Energy, it ispossible to generate rocktype in a Flow Model using a
|
|
44
|
+
Geological Model. This allows user to `Export Block Rock Types`, which
|
|
45
|
+
generates a .csv file, containing block by block lithology and faults. This
|
|
46
|
+
csv file can be imported into a LeapfrogGM object. These objects provide
|
|
47
|
+
useful ways to access model block's lithology and faults as generated by
|
|
48
|
+
Leapfrog Energy:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from cmflow.conceptual_models import LeapfrogGM
|
|
52
|
+
|
|
53
|
+
gm = LeapfrogGM()
|
|
54
|
+
gm.import_leapfrog_csv('gAB12345_rocktypes.csv', report=True)
|
|
55
|
+
|
|
56
|
+
litho = gm.lf_block['xyz12'] # Leapfrog assignment for block, as LeapfrogLitho
|
|
57
|
+
print(litho.litho_name) # name used in Leapfrog .csv
|
|
58
|
+
print(litho.faults) # faults that crosses the block
|
|
59
|
+
print(litho.rock) # pure lithology/formation name
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Faults from Leapfrog
|
|
64
|
+
|
|
65
|
+
It is also possible to automatically generate rocktype naming to be used in
|
|
66
|
+
AUT2/Waiwera based on certain rules. Here is an example that generate TWO
|
|
67
|
+
letter codes that represents either no fault, single fault or multiple faults
|
|
68
|
+
crossed a block. By calling `.gmf_fault_rocktype_2L()`, a rocktype naming of
|
|
69
|
+
faults are generated.
|
|
70
|
+
|
|
71
|
+
This also adds extra LeapfrogLitho object properties such as `.rocktype_fault`
|
|
72
|
+
and `.faults_sorted`. They contain the TWO letter code and list of crossing
|
|
73
|
+
faults respectively.
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
gmf_fault = gm.gmf_fault_rocktype_2L(gm_def["faults"], report=True)
|
|
77
|
+
|
|
78
|
+
code = gm.lf_block['xyz12'].rocktype_fault
|
|
79
|
+
# TWO character code that can be used as part of the rocktype naming
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The method `.gmf_fault_rocktype_2L()` also generates a `FaultRocktypes` object.
|
|
83
|
+
FaultRocktypes serves as a central registry for the fault related rocktype
|
|
84
|
+
naming convention. This can be saved and loaded for alter use. Ideally each
|
|
85
|
+
fault should have a preset direction. This can be done by `.set_directions
|
|
86
|
+
()` which expects a mapping between the original Leapfrog Fault name
|
|
87
|
+
(from .csv) and their directions.
|
|
88
|
+
|
|
89
|
+
Now the object can workout any rocktype's fault direction by working through the
|
|
90
|
+
faults exists ina particular rocktype. If all faults going through a rocktype
|
|
91
|
+
are with the same direction, that direction is used. Otherwise it returns
|
|
92
|
+
None. The `FaultRocktypes` object can keep user specified directions.
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
|
|
96
|
+
# sets single fault directions
|
|
97
|
+
gmf_fault.set_directions({
|
|
98
|
+
"F1": 1,
|
|
99
|
+
"F2": 1,
|
|
100
|
+
"F3": 1,
|
|
101
|
+
"F4": 2,
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
print(gmf_fault.rocktype_faults)
|
|
105
|
+
|
|
106
|
+
# {
|
|
107
|
+
# "A0": ("F1"),
|
|
108
|
+
# "AB": ("F1", "F2"),
|
|
109
|
+
# "AX": ("F1", "F2", "F3"),
|
|
110
|
+
# "AY": ("F1", "F2", "F4"),
|
|
111
|
+
# "D0": ("F4"),
|
|
112
|
+
# ...
|
|
113
|
+
# }
|
|
114
|
+
|
|
115
|
+
print(gmf_fault['AB'].faults) # ('F1', 'F2')
|
|
116
|
+
|
|
117
|
+
print(gmf_fault['A0'].direction) # 1
|
|
118
|
+
print(gmf_fault['AX'].direction) # 1, because all faults uses 1
|
|
119
|
+
print(gmf_fault['D0'].direction) # 2
|
|
120
|
+
print(gmf_fault['AY'].direction) # None
|
|
121
|
+
|
|
122
|
+
gmf_fault.set_rock_fault_dir('AY', 1) # user decides that this fault combo should be 1 anyways
|
|
123
|
+
|
|
124
|
+
print(gmf_fault['AY'].direction) # 1
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
|
|
41
129
|
# Example
|
|
42
130
|
|
|
43
131
|
Creates BMStats that can be used later, from Leapfrog Geology:
|
|
@@ -15,6 +15,94 @@ On Linux (Ubuntu shown here) these can be installed via apt-get:
|
|
|
15
15
|
sudo apt-get install -y python-shapely
|
|
16
16
|
sudo apt-get install -y python-rtree
|
|
17
17
|
|
|
18
|
+
# Using Leapfrog Geological Models
|
|
19
|
+
|
|
20
|
+
In Leapfrog Energy, it ispossible to generate rocktype in a Flow Model using a
|
|
21
|
+
Geological Model. This allows user to `Export Block Rock Types`, which
|
|
22
|
+
generates a .csv file, containing block by block lithology and faults. This
|
|
23
|
+
csv file can be imported into a LeapfrogGM object. These objects provide
|
|
24
|
+
useful ways to access model block's lithology and faults as generated by
|
|
25
|
+
Leapfrog Energy:
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from cmflow.conceptual_models import LeapfrogGM
|
|
29
|
+
|
|
30
|
+
gm = LeapfrogGM()
|
|
31
|
+
gm.import_leapfrog_csv('gAB12345_rocktypes.csv', report=True)
|
|
32
|
+
|
|
33
|
+
litho = gm.lf_block['xyz12'] # Leapfrog assignment for block, as LeapfrogLitho
|
|
34
|
+
print(litho.litho_name) # name used in Leapfrog .csv
|
|
35
|
+
print(litho.faults) # faults that crosses the block
|
|
36
|
+
print(litho.rock) # pure lithology/formation name
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Faults from Leapfrog
|
|
41
|
+
|
|
42
|
+
It is also possible to automatically generate rocktype naming to be used in
|
|
43
|
+
AUT2/Waiwera based on certain rules. Here is an example that generate TWO
|
|
44
|
+
letter codes that represents either no fault, single fault or multiple faults
|
|
45
|
+
crossed a block. By calling `.gmf_fault_rocktype_2L()`, a rocktype naming of
|
|
46
|
+
faults are generated.
|
|
47
|
+
|
|
48
|
+
This also adds extra LeapfrogLitho object properties such as `.rocktype_fault`
|
|
49
|
+
and `.faults_sorted`. They contain the TWO letter code and list of crossing
|
|
50
|
+
faults respectively.
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
gmf_fault = gm.gmf_fault_rocktype_2L(gm_def["faults"], report=True)
|
|
54
|
+
|
|
55
|
+
code = gm.lf_block['xyz12'].rocktype_fault
|
|
56
|
+
# TWO character code that can be used as part of the rocktype naming
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The method `.gmf_fault_rocktype_2L()` also generates a `FaultRocktypes` object.
|
|
60
|
+
FaultRocktypes serves as a central registry for the fault related rocktype
|
|
61
|
+
naming convention. This can be saved and loaded for alter use. Ideally each
|
|
62
|
+
fault should have a preset direction. This can be done by `.set_directions
|
|
63
|
+
()` which expects a mapping between the original Leapfrog Fault name
|
|
64
|
+
(from .csv) and their directions.
|
|
65
|
+
|
|
66
|
+
Now the object can workout any rocktype's fault direction by working through the
|
|
67
|
+
faults exists ina particular rocktype. If all faults going through a rocktype
|
|
68
|
+
are with the same direction, that direction is used. Otherwise it returns
|
|
69
|
+
None. The `FaultRocktypes` object can keep user specified directions.
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
|
|
73
|
+
# sets single fault directions
|
|
74
|
+
gmf_fault.set_directions({
|
|
75
|
+
"F1": 1,
|
|
76
|
+
"F2": 1,
|
|
77
|
+
"F3": 1,
|
|
78
|
+
"F4": 2,
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
print(gmf_fault.rocktype_faults)
|
|
82
|
+
|
|
83
|
+
# {
|
|
84
|
+
# "A0": ("F1"),
|
|
85
|
+
# "AB": ("F1", "F2"),
|
|
86
|
+
# "AX": ("F1", "F2", "F3"),
|
|
87
|
+
# "AY": ("F1", "F2", "F4"),
|
|
88
|
+
# "D0": ("F4"),
|
|
89
|
+
# ...
|
|
90
|
+
# }
|
|
91
|
+
|
|
92
|
+
print(gmf_fault['AB'].faults) # ('F1', 'F2')
|
|
93
|
+
|
|
94
|
+
print(gmf_fault['A0'].direction) # 1
|
|
95
|
+
print(gmf_fault['AX'].direction) # 1, because all faults uses 1
|
|
96
|
+
print(gmf_fault['D0'].direction) # 2
|
|
97
|
+
print(gmf_fault['AY'].direction) # None
|
|
98
|
+
|
|
99
|
+
gmf_fault.set_rock_fault_dir('AY', 1) # user decides that this fault combo should be 1 anyways
|
|
100
|
+
|
|
101
|
+
print(gmf_fault['AY'].direction) # 1
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
|
|
18
106
|
# Example
|
|
19
107
|
|
|
20
108
|
Creates BMStats that can be used later, from Leapfrog Geology:
|
|
@@ -12,8 +12,11 @@ from cmflow.geom_3dface_utils import Face3D
|
|
|
12
12
|
from cmflow.geom_surface_utils import get_columns_intersect_polygon
|
|
13
13
|
from cmflow.geom_surface_utils import geo_column_polygon
|
|
14
14
|
|
|
15
|
+
from dataclasses import dataclass
|
|
15
16
|
import json
|
|
16
17
|
import time
|
|
18
|
+
import string
|
|
19
|
+
import re
|
|
17
20
|
|
|
18
21
|
START = [time.time()]
|
|
19
22
|
def print_wall_time(msg, loop_stop=False, total=False):
|
|
@@ -25,14 +28,346 @@ def print_wall_time(msg, loop_stop=False, total=False):
|
|
|
25
28
|
if loop_stop:
|
|
26
29
|
START.append(t)
|
|
27
30
|
|
|
31
|
+
def assign_chars(names, chars=string.ascii_uppercase):
|
|
32
|
+
""" Assign unique characters to each name.
|
|
33
|
+
|
|
34
|
+
Returns
|
|
35
|
+
-------
|
|
36
|
+
tuple(dict, str)
|
|
37
|
+
A mapping from name to assigned character, and the remaining
|
|
38
|
+
unused characters.
|
|
39
|
+
"""
|
|
40
|
+
if len(names) > len(chars):
|
|
41
|
+
raise Exception("Not enough characters for names")
|
|
42
|
+
name_index = {n: chars[i] for i, n in enumerate(names)}
|
|
43
|
+
remaining_chars = chars[len(names):]
|
|
44
|
+
return name_index, remaining_chars
|
|
45
|
+
|
|
46
|
+
def natural_key(text):
|
|
47
|
+
""" Convert text chunks and number chunks into a tuple of string
|
|
48
|
+
and int, useful for sorting strings naturally. eg.
|
|
49
|
+
|
|
50
|
+
data = ["F24", "F8", "F2", "F10"]
|
|
51
|
+
sorted_data = sorted(data, key=natural_key)
|
|
52
|
+
print(sorted_data) # Output: ['F2', 'F8', 'F10', 'F24']
|
|
53
|
+
"""
|
|
54
|
+
return [
|
|
55
|
+
int(c) if c.isdigit() else c.lower()
|
|
56
|
+
for c in re.split(r'(\d+)', text)
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
def remove_duplicate_chars(text: str) -> str:
|
|
60
|
+
""" Removes duplicate characters while preserving their original order. """
|
|
61
|
+
return "".join(dict.fromkeys(text))
|
|
62
|
+
|
|
63
|
+
def unique_names(length, chars=None):
|
|
64
|
+
""" Generate fixed-length unique names using chars in their specified order
|
|
65
|
+
"""
|
|
66
|
+
if length < 1:
|
|
67
|
+
raise ValueError("length must be positive")
|
|
68
|
+
if not chars:
|
|
69
|
+
chars = string.digits + string.ascii_uppercase + string.ascii_lowercase
|
|
70
|
+
chars.replace('0', '')
|
|
71
|
+
|
|
72
|
+
indices = [0] * length
|
|
73
|
+
while True:
|
|
74
|
+
yield ''.join(chars[i] for i in indices)
|
|
75
|
+
|
|
76
|
+
for i in reversed(range(length)):
|
|
77
|
+
indices[i] += 1
|
|
78
|
+
if indices[i] < len(chars):
|
|
79
|
+
break
|
|
80
|
+
indices[i] = 0
|
|
81
|
+
else:
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
class LeapfrogLitho():
|
|
85
|
+
""" Block Lithology Output generated by Leapfrog Energy """
|
|
86
|
+
def __init__(self, litho_code, litho_name):
|
|
87
|
+
self.litho_code = litho_code # Leapfrog LithoCode
|
|
88
|
+
self.litho_name = litho_name # Leapfrog Lithology, exported name
|
|
89
|
+
# as parsed from .litho_name
|
|
90
|
+
self.faults = []
|
|
91
|
+
self.rock = None # pure formation/lithology name
|
|
92
|
+
self.parseLithoName()
|
|
93
|
+
|
|
94
|
+
def __repr__(self):
|
|
95
|
+
return f"LeapfrogLitho(code={self.litho_code}, name='{self.litho_name}', faults={self.faults}, rock='{self.rock}')"
|
|
96
|
+
|
|
97
|
+
def parseLithoName(self):
|
|
98
|
+
""" parse Leapfrog's Lithology names and determine the faults and pure
|
|
99
|
+
lithos
|
|
100
|
+
|
|
101
|
+
NOTE Leapfrog remove spaces from fault names, and then truncated into
|
|
102
|
+
7 characters, not counting other non-ASCII characters. Lithology
|
|
103
|
+
names remain as they are in the Leapfrog project.
|
|
104
|
+
|
|
105
|
+
Observation:
|
|
106
|
+
- a string can have two parts separated by ', '
|
|
107
|
+
- if there is only a single part, then is pure litho (to be confirmed!?)
|
|
108
|
+
- if there are two parts, then they are faults and pure litho
|
|
109
|
+
- faults are one or more faults joined by '+'
|
|
110
|
+
- fault names are shorteded by removing spaces
|
|
111
|
+
"""
|
|
112
|
+
parts = self.litho_name.split(', ')
|
|
113
|
+
if len(parts) == 1:
|
|
114
|
+
# As of 9 Aug 2026, I have never found any single part litho_name
|
|
115
|
+
# that contains a single fault by itself. It is always:
|
|
116
|
+
# - a pure rock lithi OR
|
|
117
|
+
# - multiple faults joined by '+'
|
|
118
|
+
if '+' in parts[0]:
|
|
119
|
+
self.faults = parts[0].split('+')
|
|
120
|
+
else:
|
|
121
|
+
self.rock = parts[0]
|
|
122
|
+
elif len(parts) == 2:
|
|
123
|
+
self.faults = parts[0].split('+')
|
|
124
|
+
self.rock = parts[1]
|
|
125
|
+
else:
|
|
126
|
+
raise Exception("Unexpected Lithology name format: '%s'" % lf_litho)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class MethodProxy:
|
|
130
|
+
""" A reusable proxy that maps dictionary syntax to any Python method.
|
|
131
|
+
|
|
132
|
+
eg.
|
|
133
|
+
For a class with methods get_value(key) and set_value(key, value), you
|
|
134
|
+
can create a proxy like this:
|
|
135
|
+
@property
|
|
136
|
+
def rock(self):
|
|
137
|
+
return MethodProxy(getter_method=self.get_rock,
|
|
138
|
+
setter_method=self.set_rock)
|
|
139
|
+
This enables use:
|
|
140
|
+
obj.rock['AB'] = 4
|
|
141
|
+
print(obj.rock['AB'])
|
|
142
|
+
"""
|
|
143
|
+
def __init__(self, getter_method, setter_method=None,):
|
|
144
|
+
self._getter = getter_method
|
|
145
|
+
self._setter = setter_method
|
|
146
|
+
|
|
147
|
+
def __getitem__(self, key):
|
|
148
|
+
# Calls the passed-in getter method dynamically
|
|
149
|
+
return self._getter(key)
|
|
150
|
+
|
|
151
|
+
def __setitem__(self, key, value):
|
|
152
|
+
if self._setter is None:
|
|
153
|
+
raise TypeError("This property is read-only")
|
|
154
|
+
self._setter(key, value)
|
|
155
|
+
|
|
156
|
+
@dataclass
|
|
157
|
+
class FaultRocktype:
|
|
158
|
+
name: str
|
|
159
|
+
faults: tuple[str, ...] = () # tuple of zero or more Leapfrog fault names
|
|
160
|
+
direction: int | None = None # 1 or 2 or None
|
|
161
|
+
|
|
162
|
+
class FaultRocktypes:
|
|
163
|
+
""" obj that keeps fault info of created rocktypes (usually 2 chars in GMF)
|
|
164
|
+
|
|
165
|
+
As more information is loaded, the registery becomes more useful.
|
|
166
|
+
|
|
167
|
+
leapfrog name - names Leapfrog uses in the exported rocktype .csv
|
|
168
|
+
display name - user can have custom full name for display
|
|
169
|
+
rocktype name - characters (usually 2 in GMF) is the key used as part of rocktype naming
|
|
170
|
+
|
|
171
|
+
.set_rocktypes() expects a rocktype code map to tuples of fault leapfrog names
|
|
172
|
+
.set_directions() expects a dict mapping fault leapfrog names to int direction (or None)
|
|
173
|
+
.set_display_names() expects a dict mapping fault leapfrog names to display/full names
|
|
174
|
+
"""
|
|
175
|
+
def __init__(self, leapfrog_names):
|
|
176
|
+
# initialise with a list of fault full names (as in)
|
|
177
|
+
self.lf_faults = leapfrog_names
|
|
178
|
+
|
|
179
|
+
self.display_name = {}
|
|
180
|
+
self.rocktype_faults = {}
|
|
181
|
+
self.fault_dir = {}
|
|
182
|
+
|
|
183
|
+
# normally rocktype fault direction are worked out using individual
|
|
184
|
+
# fault direction of faults in this rocktype, here user can overwrite
|
|
185
|
+
# specific rocktype direction
|
|
186
|
+
self.user_rocktype_dir = {} # user overwrite fault dir for combos
|
|
187
|
+
|
|
188
|
+
def __getitem__(self, rocktype_name):
|
|
189
|
+
""" convenience function to get a FaultRocktype object by rocktype name
|
|
190
|
+
|
|
191
|
+
Use:
|
|
192
|
+
a_faultrocktype['AB'].faults
|
|
193
|
+
a_faultrocktype['AB'].directions
|
|
194
|
+
|
|
195
|
+
"""
|
|
196
|
+
if rocktype_name not in self.rocktype_faults:
|
|
197
|
+
raise KeyError(f"Rocktype {rocktype_name} not found in FaultRocktypes.")
|
|
198
|
+
return FaultRocktype(
|
|
199
|
+
name=rocktype_name,
|
|
200
|
+
faults=self.rocktype_faults[rocktype_name],
|
|
201
|
+
direction=self.get_rock_fault_dir(rocktype_name)
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
@property
|
|
205
|
+
def faults(self):
|
|
206
|
+
""" a list of leapfrog fault names """
|
|
207
|
+
return self.lf_faults
|
|
208
|
+
|
|
209
|
+
@property
|
|
210
|
+
def rocktypes(self):
|
|
211
|
+
""" a list rocktype names """
|
|
212
|
+
return list(self.rocktype_faults.keys())
|
|
213
|
+
|
|
214
|
+
@property
|
|
215
|
+
def fault_direction(self):
|
|
216
|
+
""" a dict of all leapfrog faults and their direction """
|
|
217
|
+
return self.fault_dir
|
|
218
|
+
|
|
219
|
+
def set_display_names(self, display_name):
|
|
220
|
+
""" display_name is a dict mapping leapfrog names to display/full names
|
|
221
|
+
"""
|
|
222
|
+
self.display_name = display_name
|
|
223
|
+
|
|
224
|
+
def set_rocktypes(self, rock_table):
|
|
225
|
+
""" rock_table is dict mapping rocktype code to tuple of leapfrog fault names
|
|
226
|
+
|
|
227
|
+
This is usually constructed within something like .gmf_fault_rocktype_2L()
|
|
228
|
+
from LeapfrogGM object.
|
|
229
|
+
"""
|
|
230
|
+
self.rocktype_faults = rock_table
|
|
231
|
+
|
|
232
|
+
def set_directions(self, fault_dir):
|
|
233
|
+
""" fault_dir is a dict mapping leapfrog names to int direction (or None)
|
|
234
|
+
|
|
235
|
+
Usually user should setup these manually.
|
|
236
|
+
"""
|
|
237
|
+
for r,d in fault_dir.items():
|
|
238
|
+
if r not in self.lf_faults:
|
|
239
|
+
raise Exception(f"Fault {r} not found in FaultRocktypes.")
|
|
240
|
+
if d not in [1,2]:
|
|
241
|
+
raise Exception(f"Fault direction {d} for {r} must be 1 or 2.")
|
|
242
|
+
for f in self.lf_faults:
|
|
243
|
+
if f not in fault_dir:
|
|
244
|
+
raise Exception(f"Fault {f} not found in fault_dir, must specify direction.")
|
|
245
|
+
self.fault_dir = fault_dir
|
|
246
|
+
|
|
247
|
+
def get_rock_faults(self, rocktype_name, warn=False):
|
|
248
|
+
if not self.rocktype_faults and warn:
|
|
249
|
+
raise Exception("Fault table not set, see .set_rocktypes()")
|
|
250
|
+
return self.rocktype_faults.get(rocktype_name, None)
|
|
251
|
+
|
|
252
|
+
def set_rock_fault_dir(self, rocktype_name, direction):
|
|
253
|
+
""" update the direction of a rocktype
|
|
254
|
+
|
|
255
|
+
NOTE this is different from setting Leapfrog faults directions, see:
|
|
256
|
+
.set_directions()
|
|
257
|
+
"""
|
|
258
|
+
if rocktype_name not in self.rocktype_faults:
|
|
259
|
+
raise Exception(f"Rocktype {rocktype_name} not found in FaultRocktypes.")
|
|
260
|
+
self.user_rocktype_dir[rocktype_name] = direction
|
|
261
|
+
|
|
262
|
+
def get_rock_fault_dir(self, rocktype_name, warn=False):
|
|
263
|
+
""" return the rocktype fault direction (computed)
|
|
264
|
+
|
|
265
|
+
Working out direction if not overwritten with self.user_rocktype_dir:
|
|
266
|
+
if single fault -> fault dir
|
|
267
|
+
if all faults same dir -> fault dir
|
|
268
|
+
if some faults not specified -> None
|
|
269
|
+
if intersected by faults with multiple direction -> None
|
|
270
|
+
"""
|
|
271
|
+
if not self.fault_dir and warn:
|
|
272
|
+
raise Exception("Fault direction not set, see .set_directions()")
|
|
273
|
+
if not self.rocktype_faults and warn:
|
|
274
|
+
raise Exception("Fault table not set, see .set_rocktypes()")
|
|
275
|
+
|
|
276
|
+
# return user specified if specified
|
|
277
|
+
try:
|
|
278
|
+
return self.user_rocktype_dir[rocktype_name]
|
|
279
|
+
except KeyError:
|
|
280
|
+
pass
|
|
281
|
+
|
|
282
|
+
# otherwise work out by using individual faults
|
|
283
|
+
directions = [self.fault_dir.get(f, None) for f in self.rocktype_faults[rocktype_name]]
|
|
284
|
+
if len(directions) == 1:
|
|
285
|
+
return directions[0]
|
|
286
|
+
elif len(set(directions)) == 1:
|
|
287
|
+
return next(iter(directions))
|
|
288
|
+
else:
|
|
289
|
+
return None
|
|
290
|
+
|
|
291
|
+
def export_gmf_v0(self):
|
|
292
|
+
""" export a section of the GMF _dict_rocktypes.json that deals with
|
|
293
|
+
faults
|
|
294
|
+
"""
|
|
295
|
+
# GMF contents for _dict_rocktype.json
|
|
296
|
+
gmf1, gmf2 = {'0':''}, {'0':''}
|
|
297
|
+
for rocktype, faults in self.rocktype_faults.items():
|
|
298
|
+
if len(faults) == 1:
|
|
299
|
+
gmf1[rocktype[0]] = faults[0]
|
|
300
|
+
elif len(faults) >= 2:
|
|
301
|
+
gmf2[rocktype[1]] = "Intersection " + ", ".join(rocktype[1:])
|
|
302
|
+
# sort the dict for nicer output
|
|
303
|
+
gmf1 = dict(sorted(gmf1.items()))
|
|
304
|
+
gmf2 = dict(sorted(gmf2.items()))
|
|
305
|
+
gmf_fault = {
|
|
306
|
+
"faults": {
|
|
307
|
+
"rank": [1],
|
|
308
|
+
"legend": gmf1,
|
|
309
|
+
"direction": {f:None for f in gmf1.keys()},
|
|
310
|
+
"color": {rt[:1]: None for rt, fs in self.rocktype_faults.items() if len(fs)==1},
|
|
311
|
+
},
|
|
312
|
+
"intersections": {
|
|
313
|
+
"rank": [2],
|
|
314
|
+
"legend": gmf2,
|
|
315
|
+
"direction": {rt:self.get_rock_fault_dir(
|
|
316
|
+
rt, warn=False) for rt in self.rocktype_faults.keys()},
|
|
317
|
+
},
|
|
318
|
+
}
|
|
319
|
+
return gmf_fault
|
|
320
|
+
|
|
321
|
+
def export_gmf_convention(self, rank=[1, 2]):
|
|
322
|
+
""" export a section of the GMF _dict_rocktypes.json that deals with
|
|
323
|
+
faults
|
|
324
|
+
|
|
325
|
+
NOTE This is a proposed new _dict_rocktype.json format. I think this
|
|
326
|
+
has a few advantages over the orginal (v0) format:
|
|
327
|
+
+ less assumptions is made about convention
|
|
328
|
+
+ flexibility for change in convention less likely to break toolchain
|
|
329
|
+
+ more information can be logically stored
|
|
330
|
+
"""
|
|
331
|
+
gmf_fault = {
|
|
332
|
+
"faults": {
|
|
333
|
+
"rank": rank, # default to [1,2] ie. 2nd and 3rd character
|
|
334
|
+
# WIP
|
|
335
|
+
"data": {
|
|
336
|
+
"leapfrog_names": self.lf_faults,
|
|
337
|
+
"display_names": self.display_name,
|
|
338
|
+
"rocktype_faults": self.rocktype_faults,
|
|
339
|
+
"fault_dir": self.fault_dir,
|
|
340
|
+
},
|
|
341
|
+
# framework needs extras
|
|
342
|
+
"legend": {rt: ", ".join(fs) for rt, fs in self.rocktype_faults.items()},
|
|
343
|
+
"color": {rt: None for rt, fs in self.rocktype_faults.items() if len(fs)==1},
|
|
344
|
+
},
|
|
345
|
+
}
|
|
346
|
+
return gmf_fault
|
|
347
|
+
|
|
348
|
+
|
|
28
349
|
class LeapfrogGM(object):
|
|
350
|
+
""" Tough2 Block Lithology Output generated by Leapfrog Energy (.csv)
|
|
351
|
+
|
|
352
|
+
.lithocodes is a list of leapfrog litho codes as ordered in the .csv
|
|
353
|
+
.litholist is a list of leapfrog litho names as ordered in the .csv
|
|
354
|
+
.blocklist is a dict of blocks with value of index for accessing .litholist
|
|
355
|
+
|
|
356
|
+
"""
|
|
29
357
|
def __init__(self, geometry=''):
|
|
30
358
|
super(LeapfrogGM, self).__init__()
|
|
359
|
+
self.lithocodes = []
|
|
31
360
|
self.litholist = []
|
|
32
361
|
self.blocklitho = {}
|
|
33
362
|
self.import_from = '' # optional, if imported from leapfrog
|
|
34
363
|
self.geometry = geometry # optional, matching mulgrid geometry file
|
|
35
364
|
|
|
365
|
+
# the following provides parsed info of the Leapfrog GM
|
|
366
|
+
self.lf_lithos = [] # list of LeapfrogLitho objects (ordered as litholist)
|
|
367
|
+
self.lf_block = {} # access LeapfrogLitho objects by block name
|
|
368
|
+
self.rocks = [] # sorted pure lithos found
|
|
369
|
+
self.faults = [] # sorted faults found
|
|
370
|
+
|
|
36
371
|
def import_leapfrog_csv(self, filename, report=False):
|
|
37
372
|
""" load geology info from Leapfrog's 'Generate rock types' feature. The
|
|
38
373
|
.csv file from Leapfrog usually starts with a table of 'LithoCode,Lithology'
|
|
@@ -41,9 +376,11 @@ class LeapfrogGM(object):
|
|
|
41
376
|
import csv
|
|
42
377
|
f = open(filename,'r')
|
|
43
378
|
allrows = csv.reader(f)
|
|
44
|
-
|
|
379
|
+
self.lithocodes = []
|
|
380
|
+
self.litholist = []
|
|
45
381
|
self.blocklitho = {}
|
|
46
382
|
# TODO: this file reading is ugly, need work
|
|
383
|
+
code_index = {}
|
|
47
384
|
read_litho_name, read_block_litho = False, False
|
|
48
385
|
for row in allrows:
|
|
49
386
|
if len(row) == 1: continue
|
|
@@ -57,17 +394,275 @@ class LeapfrogGM(object):
|
|
|
57
394
|
read_block_litho = True
|
|
58
395
|
continue
|
|
59
396
|
if read_litho_name:
|
|
60
|
-
if
|
|
397
|
+
if row[0] != '#': raise Exception
|
|
398
|
+
self.lithocodes.append(row[1])
|
|
61
399
|
self.litholist.append(row[2])
|
|
62
|
-
|
|
400
|
+
code_index[int(row[1])] = len(self.litholist)-1
|
|
63
401
|
if read_block_litho:
|
|
64
402
|
if len(row) != 2: break
|
|
65
|
-
self.blocklitho[row[0]] =
|
|
403
|
+
self.blocklitho[row[0]] = code_index[int(row[1])]
|
|
66
404
|
f.close()
|
|
67
405
|
self.import_from = filename
|
|
406
|
+
|
|
407
|
+
self.parse_lithology()
|
|
408
|
+
|
|
68
409
|
if report:
|
|
69
|
-
print(
|
|
70
|
-
|
|
410
|
+
print("\n".join([
|
|
411
|
+
f"Loaded from file: {self.import_from}:",
|
|
412
|
+
f"{len(self.lf_lithos):>8} Leapfrog LithoCode",
|
|
413
|
+
f"{len(self.lf_block):>8} Blocks allocated",
|
|
414
|
+
f"{len(self.lf_faults):>8} Faults found",
|
|
415
|
+
f"{len(self.lf_rocks):>8} Pure rocks (lithos) found",
|
|
416
|
+
]) + "\n")
|
|
417
|
+
|
|
418
|
+
def parse_lithology(self):
|
|
419
|
+
""" parse Leapfrog's Lithology names and determine the faults and pure
|
|
420
|
+
lithos
|
|
421
|
+
|
|
422
|
+
Returns a list of lithology and a list of faults.
|
|
423
|
+
|
|
424
|
+
"""
|
|
425
|
+
self.lf_lithos = [] # list of LeapfrogLitho objects (ordered as litholist)
|
|
426
|
+
self.lf_block = {} # access LeapfrogLitho objects by block name
|
|
427
|
+
for code, name in zip(self.lithocodes, self.litholist):
|
|
428
|
+
lf_litho = LeapfrogLitho(code, name)
|
|
429
|
+
self.lf_lithos.append(lf_litho)
|
|
430
|
+
for block, idx in self.blocklitho.items():
|
|
431
|
+
self.lf_block[block] = self.lf_lithos[idx]
|
|
432
|
+
|
|
433
|
+
# compile lists of faults and pure (litho) rocks
|
|
434
|
+
rocks, faults = set(), set()
|
|
435
|
+
for litho in self.lf_lithos:
|
|
436
|
+
if litho.rock is not None:
|
|
437
|
+
rocks.add(litho.rock)
|
|
438
|
+
faults.update(litho.faults)
|
|
439
|
+
self.lf_rocks = sorted(rocks, key=natural_key)
|
|
440
|
+
self.lf_faults = sorted(faults, key=natural_key)
|
|
441
|
+
|
|
442
|
+
for f in self.lf_faults:
|
|
443
|
+
if f in self.lf_rocks:
|
|
444
|
+
raise Exception(f"Fault {f} also found in pure litho list")
|
|
445
|
+
for r in self.lf_rocks:
|
|
446
|
+
if r in self.lf_faults:
|
|
447
|
+
raise Exception(f"Pure litho {r} also found in fault list")
|
|
448
|
+
|
|
449
|
+
return self.lf_rocks, self.lf_faults
|
|
450
|
+
|
|
451
|
+
def gmf_litho_rocktype_1L(self, litho_codes={}, chars=None, ignore=None,
|
|
452
|
+
report=False):
|
|
453
|
+
""" Generates GMF style rocktype names (length of 1) based on lithologies.
|
|
454
|
+
Returns a dict of mapping from GMF rocktype name to Leapfrog lithology(s).
|
|
455
|
+
|
|
456
|
+
litho_codes is a user-defined dict with keys and values a single char.
|
|
457
|
+
The order is important, will affect how the final names are sorted.
|
|
458
|
+
If not specified, the names will be sorted *naturally*. Multiple litho
|
|
459
|
+
names mapping to the same code is allowed (i.e. merging rocktypes).
|
|
460
|
+
|
|
461
|
+
chars is a string containing possible characters to use. NOTE '0' is
|
|
462
|
+
special in GMF and is reserved, it will be removed from user's chars.
|
|
463
|
+
|
|
464
|
+
It is possible to ignore certain lithology by sepcifying names in ignore
|
|
465
|
+
list. Note that they will produce GMF rocktype '0'. There is a default
|
|
466
|
+
set of ignored litho names, if you want to include them, you will have
|
|
467
|
+
to set ignore=[].
|
|
468
|
+
|
|
469
|
+
Each block's rocktype code (length=1) can be accessed by:
|
|
470
|
+
GM.lf_block[block_name].rocktype_litho
|
|
471
|
+
"""
|
|
472
|
+
if chars is None:
|
|
473
|
+
chars = string.ascii_uppercase + string.ascii_lowercase + string.digits[1:]
|
|
474
|
+
else:
|
|
475
|
+
chars = chars.replace('0', '')
|
|
476
|
+
chars = remove_duplicate_chars(chars)
|
|
477
|
+
|
|
478
|
+
default_ignore = [None, 'Water', 'Unknown', 'Outside Geological Model']
|
|
479
|
+
if ignore is None:
|
|
480
|
+
ignore = [ig for ig in default_ignore if ig in self.lf_rocks]
|
|
481
|
+
else:
|
|
482
|
+
ignore = [ig for ig in ignore if ig in self.lf_rocks]
|
|
483
|
+
|
|
484
|
+
if litho_codes:
|
|
485
|
+
# some checks and remove used characters
|
|
486
|
+
for l,c in litho_codes.items():
|
|
487
|
+
if l in ignore and c != '0':
|
|
488
|
+
print(f"Warning: ignoring lithology {l}, "
|
|
489
|
+
f"overwrite user-defined litho_codes '{c}' -> '0'")
|
|
490
|
+
if len(c) != 1:
|
|
491
|
+
raise Exception(f"Lithology code for {l} must be a single character")
|
|
492
|
+
if l not in self.lf_rocks:
|
|
493
|
+
raise Exception(f"Lithology {l} not found in Leapfrog lithologies list")
|
|
494
|
+
chars = chars.replace(c, '')
|
|
495
|
+
|
|
496
|
+
for ig in ignore:
|
|
497
|
+
litho_codes[ig] = '0'
|
|
498
|
+
|
|
499
|
+
# deal with all other rocks not specified by user
|
|
500
|
+
remaining_rocks = [r for r in self.lf_rocks if r not in litho_codes]
|
|
501
|
+
new_litho_codes, chars = assign_chars(sorted(remaining_rocks, key=natural_key), chars)
|
|
502
|
+
litho_codes.update(new_litho_codes)
|
|
503
|
+
|
|
504
|
+
# keep as a property of LeapfrogLitho object
|
|
505
|
+
for litho in self.lf_lithos:
|
|
506
|
+
if litho.rock is None:
|
|
507
|
+
litho.rocktype_litho = '0'
|
|
508
|
+
else:
|
|
509
|
+
litho.rocktype_litho = litho_codes[litho.rock]
|
|
510
|
+
|
|
511
|
+
# create reverse index for GMF
|
|
512
|
+
rocktype_litho_index = {}
|
|
513
|
+
for name, rt in litho_codes.items():
|
|
514
|
+
if rt not in rocktype_litho_index:
|
|
515
|
+
rocktype_litho_index[rt] = name
|
|
516
|
+
else:
|
|
517
|
+
rocktype_litho_index[rt] += ", " + name
|
|
518
|
+
# sort the dict for nicer output
|
|
519
|
+
rocktype_litho_index = dict(sorted(rocktype_litho_index.items()))
|
|
520
|
+
|
|
521
|
+
# GMF contents for _dict_rocktype.json
|
|
522
|
+
gmf_litho = {
|
|
523
|
+
"rank": [0],
|
|
524
|
+
"legend": rocktype_litho_index,
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if report:
|
|
528
|
+
print()
|
|
529
|
+
for code, names in rocktype_litho_index.items():
|
|
530
|
+
print(f"{code} -> {names}")
|
|
531
|
+
print()
|
|
532
|
+
|
|
533
|
+
return rocktype_litho_index, gmf_litho
|
|
534
|
+
|
|
535
|
+
def gmf_fault_rocktype_2L(self, fault_codes={}, chars=None,
|
|
536
|
+
arbitrary_if_3plus=False, report=False):
|
|
537
|
+
""" Generates GMF style rocktype names (length of 2) based on faults.
|
|
538
|
+
Returns a dict of mapping from GMF rocktype name to tuple of faults.
|
|
539
|
+
|
|
540
|
+
fault_code is a user-defined dict with keys and values a single char.
|
|
541
|
+
The order is important, will affect how the final names are sorted.
|
|
542
|
+
If not specified, the names will be sorted *naturally*.
|
|
543
|
+
|
|
544
|
+
chars is a string containing possible characters to use. NOTE '0' is
|
|
545
|
+
special in GMF and is reserved, it will be removed from user's chars.
|
|
546
|
+
|
|
547
|
+
GMF uses two chars (usually 2nd and 3rd in the TOUGH-style 5 chars name)
|
|
548
|
+
- 2nd char (3rd char is zero) single fault
|
|
549
|
+
- 3rd char (combined with 2nd char as unique combination of faults)
|
|
550
|
+
|
|
551
|
+
It may be tricky to have descriptive naming when there are many
|
|
552
|
+
combinations with more than 3 faults. By default, attempt is made by
|
|
553
|
+
keeping the first character as the first fault code, and the second
|
|
554
|
+
character is a unique char that is not used by any single fault.
|
|
555
|
+
However there may not be enough available charactrers. In that case,
|
|
556
|
+
arbitrary_if_3plus can be set to True or a list of chars to use for
|
|
557
|
+
generating arbitrary 2-char unique names.
|
|
558
|
+
"""
|
|
559
|
+
if chars is None:
|
|
560
|
+
chars = string.ascii_uppercase + string.ascii_lowercase + string.digits[1:]
|
|
561
|
+
else:
|
|
562
|
+
chars = chars.replace('0', '')
|
|
563
|
+
chars = remove_duplicate_chars(chars)
|
|
564
|
+
|
|
565
|
+
if fault_codes:
|
|
566
|
+
# some checks and remove used characters
|
|
567
|
+
used = []
|
|
568
|
+
for f,c in fault_codes.items():
|
|
569
|
+
if c == '0':
|
|
570
|
+
raise Exception(f"Fault code for {f} cannot be '0'")
|
|
571
|
+
if len(c) != 1:
|
|
572
|
+
raise Exception(f"Fault code for {f} must be a single character")
|
|
573
|
+
if f not in fault_codes:
|
|
574
|
+
raise Exception(f"Fault {f} not found in Leapfrog faults list")
|
|
575
|
+
chars = chars.replace(c, '')
|
|
576
|
+
if c in used:
|
|
577
|
+
raise Exception(f"Fault code '{c}' for {f} is already used")
|
|
578
|
+
used.append(c)
|
|
579
|
+
# tolerate if user only specifies some of the faults
|
|
580
|
+
remaining_lf_faults = [f for f in self.lf_faults if f not in fault_codes]
|
|
581
|
+
new_fault_codes, chars = assign_chars(sorted(remaining_lf_faults, key=natural_key), chars=chars)
|
|
582
|
+
fault_codes.update(new_fault_codes)
|
|
583
|
+
else:
|
|
584
|
+
fault_codes, chars = assign_chars(sorted(self.lf_faults, key=natural_key), chars=chars)
|
|
585
|
+
|
|
586
|
+
name_generator = None
|
|
587
|
+
if arbitrary_if_3plus:
|
|
588
|
+
if arbitrary_if_3plus is True:
|
|
589
|
+
arbitrary_if_3plus = string.digits[1:] + string.ascii_lowercase + string.ascii_uppercase
|
|
590
|
+
arbitrary_if_3plus = arbitrary_if_3plus.replace('0', '')
|
|
591
|
+
# remove already used fault-specific codes
|
|
592
|
+
for c in fault_codes.values():
|
|
593
|
+
chars = chars.replace(c, '')
|
|
594
|
+
name_generator = unique_names(length=2, chars=arbitrary_if_3plus)
|
|
595
|
+
|
|
596
|
+
fault_rank = {name: idx for idx, name in enumerate(fault_codes)}
|
|
597
|
+
|
|
598
|
+
# sort combinations: by num of faults, then by ranking of 1st fault, then 2nd...
|
|
599
|
+
def combo_id(items):
|
|
600
|
+
# id (a tuple of faults) is also ordered by rank
|
|
601
|
+
return tuple(sorted(items, key=lambda x: fault_rank.get(x, 0)))
|
|
602
|
+
# sorting the faults to ensure unique fault sets
|
|
603
|
+
combinations = list({combo_id(c.faults) for c in self.lf_lithos})
|
|
604
|
+
# sort by combo length, then by rank sequentially in combo
|
|
605
|
+
combinations = sorted(combinations, key=lambda x: (len(x), tuple(fault_rank[y] for y in x)))
|
|
606
|
+
|
|
607
|
+
if report:
|
|
608
|
+
print("\n".join([
|
|
609
|
+
f"Generating GMF fault names:",
|
|
610
|
+
f"{len(self.lf_lithos):>8} Leapfrog LithoCode",
|
|
611
|
+
f"{len(self.lf_faults):>8} Faults found",
|
|
612
|
+
f"{len(combinations):>8} Total unique fault combinations found",
|
|
613
|
+
f"{len([c for c in combinations if len(c)==1]):>8} Single-fault combinations found",
|
|
614
|
+
f"{len([c for c in combinations if len(c)==2]):>8} Fault intersections found with 2 faults",
|
|
615
|
+
f"{len([c for c in combinations if len(c)>=3]):>8} Fault intersections found with 3 or more faults",
|
|
616
|
+
f"{max([len(c) for c in combinations]):>8} Maximum number of fault intersect in one block",
|
|
617
|
+
]) + "\n")
|
|
618
|
+
|
|
619
|
+
final_codes = {}
|
|
620
|
+
for ii,fid in enumerate(combinations):
|
|
621
|
+
if len(fid) == 0:
|
|
622
|
+
final_codes[fid] = '00'
|
|
623
|
+
elif len(fid) == 1:
|
|
624
|
+
# single item ones first -> fault code + '0'
|
|
625
|
+
final_codes[fid] = fault_codes[fid[0]] + '0'
|
|
626
|
+
elif len(fid) == 2:
|
|
627
|
+
# two items combo -> first + second
|
|
628
|
+
final_codes[fid] = ''
|
|
629
|
+
for fault in fid:
|
|
630
|
+
final_codes[fid] += fault_codes[fault]
|
|
631
|
+
else:
|
|
632
|
+
if name_generator:
|
|
633
|
+
# use arbitrary/opaque names for 3 or more faults (allow many more combos)
|
|
634
|
+
try:
|
|
635
|
+
final_codes[fid] = next(name_generator)
|
|
636
|
+
except StopIteration:
|
|
637
|
+
raise RuntimeError("Not enough characters for remaining fault combinations,")
|
|
638
|
+
else:
|
|
639
|
+
# three or more -> first fault code + nunique code
|
|
640
|
+
if not chars:
|
|
641
|
+
need_more = len(combinations) - ii
|
|
642
|
+
raise RuntimeError(f"Not enough characters for remaining fault combinations,"
|
|
643
|
+
f" need {need_more} more.")
|
|
644
|
+
code = chars[0]
|
|
645
|
+
chars = chars[1:]
|
|
646
|
+
final_codes[fid] = fault_codes[fid[0]] + code
|
|
647
|
+
|
|
648
|
+
# useful properties added to LeapfrogLitho objects
|
|
649
|
+
for litho in self.lf_lithos:
|
|
650
|
+
fid = combo_id(litho.faults)
|
|
651
|
+
litho.rocktype_fault = final_codes[fid] # eg. 'AB', 'A0', '00'
|
|
652
|
+
litho.faults_sorted = fid # eg. ('Fault1', 'Fault2')
|
|
653
|
+
|
|
654
|
+
# create reverse index for general use (key has two characters)
|
|
655
|
+
rocktype_index = {}
|
|
656
|
+
for faults, rocktype_name in final_codes.items():
|
|
657
|
+
if rocktype_name in rocktype_index:
|
|
658
|
+
raise Exception(f"Rocktype Name should be unique, {rocktype_name} repeated")
|
|
659
|
+
rocktype_index[rocktype_name] = faults
|
|
660
|
+
|
|
661
|
+
# create FaultRocktypes object to return
|
|
662
|
+
fault_rocktypes = FaultRocktypes(self.lf_faults)
|
|
663
|
+
fault_rocktypes.set_rocktypes(rocktype_index)
|
|
664
|
+
|
|
665
|
+
return fault_rocktypes
|
|
71
666
|
|
|
72
667
|
def write(self, filename):
|
|
73
668
|
with open(filename, 'w') as f:
|
|
@@ -80,6 +675,8 @@ class LeapfrogGM(object):
|
|
|
80
675
|
"geometry" : self.geometry,
|
|
81
676
|
"zones": self.litholist,
|
|
82
677
|
"blocks": self.blocklitho,
|
|
678
|
+
# Non standard field, for LeapfrogGM only
|
|
679
|
+
"import_litho_codes": self.lithocodes,
|
|
83
680
|
}
|
|
84
681
|
json.dump(data, f, indent=4, sort_keys=True)
|
|
85
682
|
|
|
@@ -90,6 +687,9 @@ class LeapfrogGM(object):
|
|
|
90
687
|
self.geometry = data['geometry']
|
|
91
688
|
self.litholist = data['zones']
|
|
92
689
|
self.blocklitho = data['blocks']
|
|
690
|
+
# Non standard field, for LeapfrogGM only
|
|
691
|
+
self.lithocodes = data["import_litho_codes"]
|
|
692
|
+
self.parse_lithology()
|
|
93
693
|
|
|
94
694
|
class CM(object):
|
|
95
695
|
def populate_model(self, np_dtype):
|
|
File without changes
|
|
@@ -296,7 +296,7 @@ def update_rocktype_bycopy(dat, blk_names, to_rocktype, convention='++***'):
|
|
|
296
296
|
else:
|
|
297
297
|
dat.grid.block[b].rocktype = dat.grid.rocktype[r_name]
|
|
298
298
|
|
|
299
|
-
def update_block_geology(dat, blk_name, rock_name):
|
|
299
|
+
def update_block_geology(dat, blk_name, rock_name, report=False):
|
|
300
300
|
""" only updates the block's rocktype name. rock_name is a 5 chars string,
|
|
301
301
|
can contain '+' character to indicate preservin that part of name. If the
|
|
302
302
|
final rocktype name does not exist in dat.grid, it will be created by
|
|
@@ -322,7 +322,8 @@ def update_block_geology(dat, blk_name, rock_name):
|
|
|
322
322
|
new_rock.name = new_rock_name
|
|
323
323
|
dat.grid.add_rocktype(new_rock)
|
|
324
324
|
dat.grid.block[blk_name].rocktype = new_rock
|
|
325
|
-
|
|
325
|
+
if report:
|
|
326
|
+
print(' new rocktype added: ', new_rock_name)
|
|
326
327
|
return new_rock_name
|
|
327
328
|
|
|
328
329
|
def setup_rockless(grid, base_rocktype=None, atm_rocktype=None):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|