GeoDFN 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- GeoDFN/Classes/DFNGenerator.py +678 -0
- GeoDFN/Classes/DFNGeneratorWithSeed.py +714 -0
- GeoDFN/Classes/DFNGeneratorWithSeedAndExclusion.py +739 -0
- GeoDFN/Classes/__init__.py +5 -0
- GeoDFN/Classes/_validation.py +151 -0
- GeoDFN/Classes/apertureCalculator.py +164 -0
- GeoDFN/Classes/bufferZoneCalculator.py +14 -0
- GeoDFN/Classes/fractureLengthPDFs.py +54 -0
- GeoDFN/Classes/orientationPDFs.py +32 -0
- GeoDFN/Classes/spatialDistributionPDFs.py +53 -0
- GeoDFN/Example-BrazilAperture.py +74 -0
- GeoDFN/Example-BrazilFixedSeeds.py +90 -0
- GeoDFN/Example-BrazilFixedSeedsAndExclusion.py +90 -0
- GeoDFN/Example-BrazilRandomSeeds.py +87 -0
- GeoDFN/__init__.py +5 -0
- geodfn-2.0.0.dist-info/METADATA +249 -0
- geodfn-2.0.0.dist-info/RECORD +20 -0
- geodfn-2.0.0.dist-info/WHEEL +5 -0
- geodfn-2.0.0.dist-info/licenses/LICENSE +21 -0
- geodfn-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,678 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import random
|
|
3
|
+
import json
|
|
4
|
+
import numpy as np
|
|
5
|
+
import matplotlib.pyplot as plt
|
|
6
|
+
import math
|
|
7
|
+
import os
|
|
8
|
+
from .apertureCalculator import apertureCalculator
|
|
9
|
+
from .bufferZoneCalculator import bufferZoneCalculator
|
|
10
|
+
from .fractureLengthPDFs import fractureLengthPDFs
|
|
11
|
+
from .spatialDistributionPDFs import SpatialDistributionPDFs
|
|
12
|
+
from .orientationPDFs import OrientationPDFs
|
|
13
|
+
from ._validation import validate_inputs
|
|
14
|
+
|
|
15
|
+
import matplotlib.colors as mcolors
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DFNGenerator:
|
|
21
|
+
|
|
22
|
+
def __init__(self, domainLengthX, domainLengthY, sets, apertureCalculationParameters, DFNName,
|
|
23
|
+
numOfRealizations=1, IsMultipleStressAzimuths=False, stressAzimuth=None, savePic=True,
|
|
24
|
+
output_dir='DFNs', progress_callback=None):
|
|
25
|
+
validate_inputs(domainLengthX, domainLengthY, sets, apertureCalculationParameters, numOfRealizations)
|
|
26
|
+
self.maxtries = []
|
|
27
|
+
self.xmax = domainLengthX
|
|
28
|
+
self.ymax = domainLengthY
|
|
29
|
+
self.outputDir = os.path.join(output_dir, str(DFNName))
|
|
30
|
+
self.apertureCalculation = apertureCalculator(apertureCalculationParameters, stage='first')
|
|
31
|
+
self.numberOfMaxTries = 400000
|
|
32
|
+
self._sets = sets
|
|
33
|
+
self._apertureCalculationParameters = apertureCalculationParameters
|
|
34
|
+
self._numOfRealizations = numOfRealizations
|
|
35
|
+
self._IsMultipleStressAzimuths = IsMultipleStressAzimuths
|
|
36
|
+
self._stressAzimuth = stressAzimuth
|
|
37
|
+
self._savePic = savePic
|
|
38
|
+
self._progress_callback = progress_callback
|
|
39
|
+
self.generate()
|
|
40
|
+
|
|
41
|
+
def generate(self):
|
|
42
|
+
sets = self._sets
|
|
43
|
+
apertureCalculationParameters = self._apertureCalculationParameters
|
|
44
|
+
IsMultipleStressAzimuths = self._IsMultipleStressAzimuths
|
|
45
|
+
stressAzimuth = self._stressAzimuth
|
|
46
|
+
savePic = self._savePic
|
|
47
|
+
self.realizations = []
|
|
48
|
+
|
|
49
|
+
for i in range(self._numOfRealizations):
|
|
50
|
+
logger.info('Section A: generate fractures')
|
|
51
|
+
self.maxtries = []
|
|
52
|
+
allFractureSets = []
|
|
53
|
+
self.setNumber = 1
|
|
54
|
+
for setConfig in sets:
|
|
55
|
+
fractureSet, setConfig = self._generate_fractures(setConfig)
|
|
56
|
+
fractureSet = self._sort_fractures(fractureSet)
|
|
57
|
+
allFractureSets.append((fractureSet, setConfig))
|
|
58
|
+
self.setNumber = self.setNumber + 1
|
|
59
|
+
|
|
60
|
+
logger.info('Section B: placing the fractures')
|
|
61
|
+
allProcessedFractureSets = []
|
|
62
|
+
for fractureSet, setConfig in allFractureSets:
|
|
63
|
+
logger.debug('bufferZone: %s', setConfig['bufferZone'])
|
|
64
|
+
self.bufferZoneCalculation = bufferZoneCalculator(setConfig['bufferZone'])
|
|
65
|
+
fractureSet = self.bufferZoneCalculation.calculate(fractureSet)
|
|
66
|
+
processedFractureSet = self.place_fractures(fractureSet, setConfig)
|
|
67
|
+
allProcessedFractureSets.append(processedFractureSet)
|
|
68
|
+
|
|
69
|
+
logger.info('maxtries= %s', self.maxtries)
|
|
70
|
+
|
|
71
|
+
maxtriesDir = os.path.join(self.outputDir, 'tries')
|
|
72
|
+
os.makedirs(maxtriesDir, exist_ok=True)
|
|
73
|
+
maxtriesFile = os.path.join(maxtriesDir, 'tries.txt')
|
|
74
|
+
with open(maxtriesFile, 'w') as fileID:
|
|
75
|
+
fileID.write(f"Number of iterations for each set: {self.maxtries}\n")
|
|
76
|
+
|
|
77
|
+
if not any(t > self.numberOfMaxTries for t in self.maxtries):
|
|
78
|
+
self.realizations.append(allProcessedFractureSets)
|
|
79
|
+
if IsMultipleStressAzimuths:
|
|
80
|
+
self._stressAzimuth = stressAzimuth
|
|
81
|
+
for azimuth in stressAzimuth:
|
|
82
|
+
apertureCalculationParameters["strike"] = azimuth
|
|
83
|
+
self.apertureCalculation = apertureCalculator(apertureCalculationParameters, stage='second')
|
|
84
|
+
for fracture_set in allProcessedFractureSets:
|
|
85
|
+
fracture_set = self.apertureCalculation.get_calculator(fracture_set)
|
|
86
|
+
|
|
87
|
+
logger.info('Section D: Generating the outputs')
|
|
88
|
+
|
|
89
|
+
if savePic:
|
|
90
|
+
self._plot_fractures(allProcessedFractureSets, name='DFNPic', number=i)
|
|
91
|
+
|
|
92
|
+
self._write_output_properties_per_set('outputPropertiesPerSet', allProcessedFractureSets, number=i)
|
|
93
|
+
self._write_overall_properties('outputPropertiesTotal', allProcessedFractureSets, number=i)
|
|
94
|
+
self._write_fracture_coordinates('fractureCoordinates', allProcessedFractureSets, number=i)
|
|
95
|
+
self._write_fracture_set('fractureSet', allProcessedFractureSets, number=i)
|
|
96
|
+
self._write_fracture_apertures('aperture', allProcessedFractureSets, number=i)
|
|
97
|
+
self._write_input_properties('inputProperties', apertureCalculationParameters, sets, stressAzimuth, number=i)
|
|
98
|
+
self._plot_orientation_stereographic('orientationStereographic', allProcessedFractureSets, number=i)
|
|
99
|
+
if IsMultipleStressAzimuths:
|
|
100
|
+
self._write_corrected_apertures('correlatedAperture', allProcessedFractureSets, number=i)
|
|
101
|
+
self._plot_corrected_apertures('aperturePerStrikeTotal', allProcessedFractureSets, number=i)
|
|
102
|
+
|
|
103
|
+
if self._progress_callback:
|
|
104
|
+
self._progress_callback(i + 1, self._numOfRealizations)
|
|
105
|
+
|
|
106
|
+
def _place_longest_fracture(self, longestFracture):
|
|
107
|
+
referenceWithinDomain = False
|
|
108
|
+
number = 0
|
|
109
|
+
tries = 0
|
|
110
|
+
logger.debug('fracture length %s', longestFracture['fracture length'])
|
|
111
|
+
while not referenceWithinDomain:
|
|
112
|
+
seed_x = random.uniform(0, self.xmax)
|
|
113
|
+
seed_y = random.uniform(0, self.ymax)
|
|
114
|
+
(new_x_start, new_y_start), (new_x_end, new_y_end) = self._fracture_coordinate(
|
|
115
|
+
longestFracture, longestFracture['theta'], seed_x, seed_y)
|
|
116
|
+
if self._is_within_domain(new_x_start, new_y_start) and self._is_within_domain(new_x_end, new_y_end):
|
|
117
|
+
referenceWithinDomain = True
|
|
118
|
+
addedFracture = {
|
|
119
|
+
'number': number,
|
|
120
|
+
'x_start': new_x_start,
|
|
121
|
+
'y_start': new_y_start,
|
|
122
|
+
'x_end': new_x_end,
|
|
123
|
+
'y_end': new_y_end,
|
|
124
|
+
'fracture length': longestFracture['fracture length'],
|
|
125
|
+
'set number': longestFracture['set number'],
|
|
126
|
+
'fracture spacing': longestFracture['fracture spacing'],
|
|
127
|
+
'theta': longestFracture['theta'],
|
|
128
|
+
}
|
|
129
|
+
else:
|
|
130
|
+
tries += 1
|
|
131
|
+
if tries > 500:
|
|
132
|
+
return False
|
|
133
|
+
return (addedFracture, seed_x, seed_y)
|
|
134
|
+
|
|
135
|
+
# Keep old name as alias for backward compatibility
|
|
136
|
+
def placeLongestFracture(self, longestFracture):
|
|
137
|
+
return self._place_longest_fracture(longestFracture)
|
|
138
|
+
|
|
139
|
+
def place_fractures(self, fractures, setConfig):
|
|
140
|
+
spatialDistributionPDF = SpatialDistributionPDFs(
|
|
141
|
+
setConfig['spatialDistributionPDF'], setConfig['spatialDistributionPDFParams'])
|
|
142
|
+
spatialDistributionPDFMode = spatialDistributionPDF.compute_mode()
|
|
143
|
+
|
|
144
|
+
processedFractures = []
|
|
145
|
+
logger.debug('placing the longest fracture')
|
|
146
|
+
added = False
|
|
147
|
+
while not added:
|
|
148
|
+
added = self._place_longest_fracture(fractures[0])
|
|
149
|
+
|
|
150
|
+
(addedFracture, seed_x, seed_y) = added
|
|
151
|
+
processedFractures.append(addedFracture)
|
|
152
|
+
logger.debug('the longest fracture added')
|
|
153
|
+
numberOfTries = 0
|
|
154
|
+
maxTriesReached = False
|
|
155
|
+
|
|
156
|
+
number = 0
|
|
157
|
+
logger.debug('placing rest of fractures')
|
|
158
|
+
for fracture in fractures[1:]:
|
|
159
|
+
theta = fracture['theta']
|
|
160
|
+
if theta < 0:
|
|
161
|
+
theta += 360
|
|
162
|
+
if maxTriesReached:
|
|
163
|
+
break
|
|
164
|
+
isNewFractureAdded = False
|
|
165
|
+
|
|
166
|
+
while not isNewFractureAdded:
|
|
167
|
+
if numberOfTries > self.numberOfMaxTries:
|
|
168
|
+
logger.info("Global max retries reached, stopping all fracture placements.")
|
|
169
|
+
maxTriesReached = True
|
|
170
|
+
break
|
|
171
|
+
|
|
172
|
+
referenceWithinDomain = False
|
|
173
|
+
while not referenceWithinDomain:
|
|
174
|
+
if numberOfTries > self.numberOfMaxTries:
|
|
175
|
+
logger.info("Global max retries reached during domain validation, stopping all fracture placements.")
|
|
176
|
+
maxTriesReached = True
|
|
177
|
+
break
|
|
178
|
+
|
|
179
|
+
distance = (spatialDistributionPDF.get_value() - spatialDistributionPDFMode) * random.choice([-1, 1])
|
|
180
|
+
angle = np.random.uniform(0, 2 * np.pi)
|
|
181
|
+
new_x_mid = seed_x + distance * np.cos(angle)
|
|
182
|
+
new_y_mid = seed_y + distance * np.sin(angle)
|
|
183
|
+
|
|
184
|
+
(new_x_start, new_y_start), (new_x_end, new_y_end) = self._fracture_coordinate(
|
|
185
|
+
fracture, theta, new_x_mid, new_y_mid)
|
|
186
|
+
if self._is_within_domain(new_x_start, new_y_start) and self._is_within_domain(new_x_end, new_y_end):
|
|
187
|
+
referenceWithinDomain = True
|
|
188
|
+
else:
|
|
189
|
+
numberOfTries += 1
|
|
190
|
+
|
|
191
|
+
if maxTriesReached:
|
|
192
|
+
break
|
|
193
|
+
too_close = False
|
|
194
|
+
for existing_frac in processedFractures:
|
|
195
|
+
existing_coords = ((existing_frac['x_start'], existing_frac['y_start']),
|
|
196
|
+
(existing_frac['x_end'], existing_frac['y_end']))
|
|
197
|
+
new_coords = ((new_x_start, new_y_start), (new_x_end, new_y_end))
|
|
198
|
+
if segment_to_segment_distance(new_coords, existing_coords) < existing_frac['fracture spacing'] + \
|
|
199
|
+
fracture['fracture spacing']:
|
|
200
|
+
too_close = True
|
|
201
|
+
numberOfTries += 1
|
|
202
|
+
break
|
|
203
|
+
|
|
204
|
+
if numberOfTries > self.numberOfMaxTries:
|
|
205
|
+
logger.info("Global max retries reached, stopping all fracture placements.")
|
|
206
|
+
maxTriesReached = True
|
|
207
|
+
break
|
|
208
|
+
if not too_close:
|
|
209
|
+
isNewFractureAdded = True
|
|
210
|
+
number += 1
|
|
211
|
+
addedFracture = {
|
|
212
|
+
'number': number,
|
|
213
|
+
'x_start': new_x_start,
|
|
214
|
+
'y_start': new_y_start,
|
|
215
|
+
'x_end': new_x_end,
|
|
216
|
+
'y_end': new_y_end,
|
|
217
|
+
'fracture length': fracture['fracture length'],
|
|
218
|
+
'fracture spacing': fracture['fracture spacing'],
|
|
219
|
+
'set number': fracture['set number'],
|
|
220
|
+
'theta': fracture['theta'],
|
|
221
|
+
'number of tries': numberOfTries,
|
|
222
|
+
}
|
|
223
|
+
processedFractures.append(addedFracture)
|
|
224
|
+
if maxTriesReached:
|
|
225
|
+
break
|
|
226
|
+
|
|
227
|
+
self.maxtries.append(numberOfTries)
|
|
228
|
+
processedFractures = self.apertureCalculation.get_calculator(processedFractures)
|
|
229
|
+
return processedFractures
|
|
230
|
+
|
|
231
|
+
def _generate_fractures(self, setConfig):
|
|
232
|
+
orientationDistributionPDF = OrientationPDFs(
|
|
233
|
+
setConfig['orientationDistributionPDF'], setConfig['orientationDistributionPDFParams'])
|
|
234
|
+
|
|
235
|
+
logger.debug('fractureLengthPDFParams["Lmax"]= %s', setConfig['fractureLengthPDFParams']["Lmax"])
|
|
236
|
+
|
|
237
|
+
if setConfig['fractureLengthPDF'] == 'Constant':
|
|
238
|
+
logger.debug('Fracture length PDF is constant')
|
|
239
|
+
n = math.ceil(setConfig['I'] * self.ymax * self.xmax / setConfig['fractureLengthPDFParams']['L'])
|
|
240
|
+
fractures = []
|
|
241
|
+
for _ in range(n):
|
|
242
|
+
newFrac = {'fracture length': setConfig['fractureLengthPDFParams']['L']}
|
|
243
|
+
newFrac['theta'] = orientationDistributionPDF.get_value()
|
|
244
|
+
newFrac['set number'] = self.setNumber
|
|
245
|
+
fractures.append(newFrac)
|
|
246
|
+
else:
|
|
247
|
+
fractureLengthPDF = fractureLengthPDFs(setConfig['fractureLengthPDF'], setConfig['fractureLengthPDFParams'])
|
|
248
|
+
fractures = []
|
|
249
|
+
newFrac = {}
|
|
250
|
+
newFrac['fracture length'] = fractureLengthPDF.get_value()
|
|
251
|
+
newFrac['theta'] = orientationDistributionPDF.get_value()
|
|
252
|
+
newFrac['set number'] = self.setNumber
|
|
253
|
+
fractures.append(newFrac)
|
|
254
|
+
while self._compute_intensity(fractures) < setConfig['I']:
|
|
255
|
+
newFrac = {}
|
|
256
|
+
newFrac['fracture length'] = fractureLengthPDF.get_value()
|
|
257
|
+
newFrac['theta'] = orientationDistributionPDF.get_value()
|
|
258
|
+
newFrac['set number'] = self.setNumber
|
|
259
|
+
fractures.append(newFrac)
|
|
260
|
+
|
|
261
|
+
return fractures, setConfig
|
|
262
|
+
|
|
263
|
+
# Keep old name as alias
|
|
264
|
+
def generateFractures(self, setConfig):
|
|
265
|
+
return self._generate_fractures(setConfig)
|
|
266
|
+
|
|
267
|
+
def _compute_intensity(self, fractures):
|
|
268
|
+
total_length = sum([fracture['fracture length'] for fracture in fractures])
|
|
269
|
+
area = self.xmax * self.ymax
|
|
270
|
+
return total_length / area
|
|
271
|
+
|
|
272
|
+
def computeIntensity(self, fractures):
|
|
273
|
+
return self._compute_intensity(fractures)
|
|
274
|
+
|
|
275
|
+
def _sort_fractures(self, fractures):
|
|
276
|
+
fractures.sort(key=lambda x: x['fracture length'], reverse=True)
|
|
277
|
+
for i, fracture in enumerate(fractures, start=0):
|
|
278
|
+
fracture['number'] = i
|
|
279
|
+
return fractures
|
|
280
|
+
|
|
281
|
+
def sortFractures(self, fractures):
|
|
282
|
+
return self._sort_fractures(fractures)
|
|
283
|
+
|
|
284
|
+
def _fracture_coordinate(self, fracture, theta, midX, midY):
|
|
285
|
+
half_length = fracture['fracture length'] / 2
|
|
286
|
+
theta_adjusted = 90 - theta
|
|
287
|
+
new_x_start = midX - half_length * np.cos(np.radians(theta_adjusted))
|
|
288
|
+
new_y_start = midY - half_length * np.sin(np.radians(theta_adjusted))
|
|
289
|
+
new_x_end = midX + half_length * np.cos(np.radians(theta_adjusted))
|
|
290
|
+
new_y_end = midY + half_length * np.sin(np.radians(theta_adjusted))
|
|
291
|
+
return (new_x_start, new_y_start), (new_x_end, new_y_end)
|
|
292
|
+
|
|
293
|
+
def fractureCoordinate(self, fracture, theta, midX, midY):
|
|
294
|
+
return self._fracture_coordinate(fracture, theta, midX, midY)
|
|
295
|
+
|
|
296
|
+
def _is_within_domain(self, x, y):
|
|
297
|
+
return 0 <= x <= self.xmax and 0 <= y <= self.ymax
|
|
298
|
+
|
|
299
|
+
def is_within_domain(self, x, y):
|
|
300
|
+
return self._is_within_domain(x, y)
|
|
301
|
+
|
|
302
|
+
def _distance_of_well_from_closest_fracture(self, fractures, wellLocation):
|
|
303
|
+
min_distance = float('inf')
|
|
304
|
+
for frac in fractures:
|
|
305
|
+
x_start, y_start = frac['x_start'], frac['y_start']
|
|
306
|
+
x_end, y_end = frac['x_end'], frac['y_end']
|
|
307
|
+
distance = point_to_segment_distance(
|
|
308
|
+
np.array(wellLocation), np.array([x_start, y_start]), np.array([x_end, y_end]))
|
|
309
|
+
min_distance = min(min_distance, distance)
|
|
310
|
+
return min_distance
|
|
311
|
+
|
|
312
|
+
def distanceOfWellFromClosetFracture(self, fractures, wellLocation):
|
|
313
|
+
return self._distance_of_well_from_closest_fracture(fractures, wellLocation)
|
|
314
|
+
|
|
315
|
+
def _write_input_properties(self, name, apertureCalculationParameters, fractureSets, stressAzimuth, number=0):
|
|
316
|
+
InputPropertiesDir = os.path.join(self.outputDir, name)
|
|
317
|
+
os.makedirs(InputPropertiesDir, exist_ok=True)
|
|
318
|
+
InputPropertiesFile = os.path.join(InputPropertiesDir, f"{number + 1:03}{name}.txt")
|
|
319
|
+
with open(InputPropertiesFile, 'w') as f:
|
|
320
|
+
f.write("domainLengthX: " + str(self.xmax) + '\n')
|
|
321
|
+
f.write("domainLengthY: " + str(self.ymax) + '\n')
|
|
322
|
+
i = 1
|
|
323
|
+
for fracture_set in fractureSets:
|
|
324
|
+
if isinstance(fracture_set, dict):
|
|
325
|
+
f.write("fractureSet " + str(i) + " : " + json.dumps(fracture_set, indent=4) + '\n')
|
|
326
|
+
else:
|
|
327
|
+
f.write("fractureSet " + str(i) + " : " + '\n')
|
|
328
|
+
i += 1
|
|
329
|
+
f.write("stressAzimuth: " + str(stressAzimuth) + '\n')
|
|
330
|
+
f.write("fracture aperture " + str(i) + " : " + json.dumps(apertureCalculationParameters, indent=4) + '\n')
|
|
331
|
+
|
|
332
|
+
def generateInputPropertiesFile(self, name, apertureCalculationParameters, fractureSets, stressAzimuth, number=0):
|
|
333
|
+
return self._write_input_properties(name, apertureCalculationParameters, fractureSets, stressAzimuth, number)
|
|
334
|
+
|
|
335
|
+
def _write_fracture_coordinates(self, name, fractures, number=0):
|
|
336
|
+
coordinatesOutputDir = os.path.join(self.outputDir, name)
|
|
337
|
+
os.makedirs(coordinatesOutputDir, exist_ok=True)
|
|
338
|
+
InputPropertiesFile = os.path.join(coordinatesOutputDir, f"{number + 1:03}{name}.txt")
|
|
339
|
+
with open(InputPropertiesFile, 'w') as fileID:
|
|
340
|
+
for fracture_set in fractures:
|
|
341
|
+
for frac in fracture_set:
|
|
342
|
+
fileID.write(
|
|
343
|
+
f"{frac['x_start']:.4f} {frac['y_start']:.4f} {frac['x_end']:.4f} {frac['y_end']:.4f}\n")
|
|
344
|
+
|
|
345
|
+
def generateTextFileForFractureCoordinates(self, name, fractures, number=0):
|
|
346
|
+
return self._write_fracture_coordinates(name, fractures, number)
|
|
347
|
+
|
|
348
|
+
def _write_fracture_set(self, name, fractures, number=0):
|
|
349
|
+
setOutputDir = os.path.join(self.outputDir, name)
|
|
350
|
+
os.makedirs(setOutputDir, exist_ok=True)
|
|
351
|
+
InputPropertiesFile = os.path.join(setOutputDir, f"{number + 1:03}{name}.txt")
|
|
352
|
+
with open(InputPropertiesFile, 'w') as fileID:
|
|
353
|
+
for fracture_set in fractures:
|
|
354
|
+
for frac in fracture_set:
|
|
355
|
+
fileID.write(f"{frac['set number']}\n")
|
|
356
|
+
|
|
357
|
+
def generateTextFileForFractureSet(self, name, fractures, number=0):
|
|
358
|
+
return self._write_fracture_set(name, fractures, number)
|
|
359
|
+
|
|
360
|
+
def _write_fracture_apertures(self, name, fractures, number=0):
|
|
361
|
+
apertureOutputDir = os.path.join(self.outputDir, name)
|
|
362
|
+
os.makedirs(apertureOutputDir, exist_ok=True)
|
|
363
|
+
InputPropertiesFile = os.path.join(apertureOutputDir, f"{number + 1:03}{name}.txt")
|
|
364
|
+
with open(InputPropertiesFile, 'w') as fileID:
|
|
365
|
+
for fracture_set in fractures:
|
|
366
|
+
for frac in fracture_set:
|
|
367
|
+
fileID.write(f"{frac['fracture aperture']:.7f}\n")
|
|
368
|
+
|
|
369
|
+
def generateTextFileForFractureApertures(self, name, fractures, number=0):
|
|
370
|
+
return self._write_fracture_apertures(name, fractures, number)
|
|
371
|
+
|
|
372
|
+
def _write_corrected_apertures(self, name, fractures, number=0):
|
|
373
|
+
correctedApertureDir = os.path.join(self.outputDir, str(name))
|
|
374
|
+
os.makedirs(correctedApertureDir, exist_ok=True)
|
|
375
|
+
file_handles = {}
|
|
376
|
+
try:
|
|
377
|
+
for set_index, fracture_set in enumerate(fractures):
|
|
378
|
+
for fracture in fracture_set:
|
|
379
|
+
for key, value in fracture.items():
|
|
380
|
+
if key.startswith('correctedAperture'):
|
|
381
|
+
if key not in file_handles:
|
|
382
|
+
filePath = os.path.join(correctedApertureDir, f"{key}_{number + 1:03}.txt")
|
|
383
|
+
file_handles[key] = open(filePath, 'w')
|
|
384
|
+
file_handles[key].write(f"{value:.7f}\n")
|
|
385
|
+
finally:
|
|
386
|
+
for file in file_handles.values():
|
|
387
|
+
file.close()
|
|
388
|
+
|
|
389
|
+
def generateTextFilesForCorrectedApertures(self, name, fractures, number=0):
|
|
390
|
+
return self._write_corrected_apertures(name, fractures, number)
|
|
391
|
+
|
|
392
|
+
def _write_output_properties_per_set(self, name, fractureSets, number=0):
|
|
393
|
+
outputFileForOutputPropertiesDir = os.path.join(self.outputDir, name)
|
|
394
|
+
os.makedirs(outputFileForOutputPropertiesDir, exist_ok=True)
|
|
395
|
+
outputPropertiesFile = os.path.join(outputFileForOutputPropertiesDir, f"{number + 1:03}{name}.txt")
|
|
396
|
+
|
|
397
|
+
with open(outputPropertiesFile, 'w') as fileID:
|
|
398
|
+
for i, fractures in enumerate(fractureSets):
|
|
399
|
+
intensity = self._compute_intensity(fractures)
|
|
400
|
+
lengths = [frac['fracture length'] for frac in fractures]
|
|
401
|
+
apertures = [frac['fracture aperture'] for frac in fractures]
|
|
402
|
+
minL = min(lengths)
|
|
403
|
+
maxL = max(lengths)
|
|
404
|
+
avgL = sum(lengths) / len(lengths)
|
|
405
|
+
minAperture = min(apertures)
|
|
406
|
+
maxAperture = max(apertures)
|
|
407
|
+
avgAperture = sum(apertures) / len(apertures)
|
|
408
|
+
setProperties = {
|
|
409
|
+
'intensity': intensity,
|
|
410
|
+
'minLength': minL,
|
|
411
|
+
'maxLength': maxL,
|
|
412
|
+
'avgLength': avgL,
|
|
413
|
+
'minAperture': minAperture,
|
|
414
|
+
'maxAperture': maxAperture,
|
|
415
|
+
'avgAperture': avgAperture
|
|
416
|
+
}
|
|
417
|
+
fileID.write(f"Properties for set{i + 1}:\n")
|
|
418
|
+
for key, value in setProperties.items():
|
|
419
|
+
if value > 1:
|
|
420
|
+
fileID.write(f"{key} : {value:.3f}\n")
|
|
421
|
+
else:
|
|
422
|
+
fileID.write(f"{key} : {value:.3e}\n")
|
|
423
|
+
stressAzimuths = set()
|
|
424
|
+
for frac in fractures:
|
|
425
|
+
for key in frac:
|
|
426
|
+
if key.startswith('correctedAperture'):
|
|
427
|
+
stressAzimuths.add(key)
|
|
428
|
+
for azimuth in sorted(stressAzimuths):
|
|
429
|
+
avgAperture = sum(frac[azimuth] for frac in fractures if azimuth in frac) / len(fractures)
|
|
430
|
+
maxAperture = max(frac[azimuth] for frac in fractures if azimuth in frac)
|
|
431
|
+
minAperture = min(frac[azimuth] for frac in fractures if azimuth in frac)
|
|
432
|
+
if avgAperture > 1:
|
|
433
|
+
fileID.write(f"{azimuth} Average: {avgAperture:.3f}\n")
|
|
434
|
+
fileID.write(f"{azimuth} Max: {maxAperture:.3f}\n")
|
|
435
|
+
fileID.write(f"{azimuth} Min: {minAperture:.3f}\n")
|
|
436
|
+
else:
|
|
437
|
+
fileID.write(f"{azimuth} Average: {avgAperture:.3e}\n")
|
|
438
|
+
fileID.write(f"{azimuth} Max: {maxAperture:.3e}\n")
|
|
439
|
+
fileID.write(f"{azimuth} Min: {minAperture:.3e}\n")
|
|
440
|
+
fileID.write("\n")
|
|
441
|
+
|
|
442
|
+
def generateOutputFileForOutputPropertiesPerSet(self, name, fractureSets, number=0):
|
|
443
|
+
return self._write_output_properties_per_set(name, fractureSets, number)
|
|
444
|
+
|
|
445
|
+
def _write_overall_properties(self, name, fractureSets, number=0):
|
|
446
|
+
outputFileForOverallPropertiesDir = os.path.join(self.outputDir, name)
|
|
447
|
+
os.makedirs(outputFileForOverallPropertiesDir, exist_ok=True)
|
|
448
|
+
outputPropertiesFile = os.path.join(outputFileForOverallPropertiesDir, f"{number + 1:03}{name}.txt")
|
|
449
|
+
|
|
450
|
+
allFractures = [frac for fractures in fractureSets for frac in fractures]
|
|
451
|
+
totalCountFrac = len(allFractures)
|
|
452
|
+
|
|
453
|
+
totalIntersection = 0
|
|
454
|
+
for i, set1 in enumerate(fractureSets):
|
|
455
|
+
for set2 in fractureSets[i + 1:]:
|
|
456
|
+
for frac1 in set1:
|
|
457
|
+
for frac2 in set2:
|
|
458
|
+
line1 = ((frac1['x_start'], frac1['y_start']), (frac1['x_end'], frac1['y_end']))
|
|
459
|
+
line2 = ((frac2['x_start'], frac2['y_start']), (frac2['x_end'], frac2['y_end']))
|
|
460
|
+
intersect = line_intersection(line1, line2)
|
|
461
|
+
if intersect:
|
|
462
|
+
totalIntersection += 1
|
|
463
|
+
|
|
464
|
+
areaRock = self.xmax * self.ymax
|
|
465
|
+
connectivity = totalIntersection / (areaRock * totalCountFrac)
|
|
466
|
+
totalIntensity = self._compute_intensity(allFractures)
|
|
467
|
+
lengths = [frac['fracture length'] for frac in allFractures]
|
|
468
|
+
apertures = [frac['fracture aperture'] for frac in allFractures]
|
|
469
|
+
minLength = min(lengths)
|
|
470
|
+
maxLength = max(lengths)
|
|
471
|
+
avgLength = sum(lengths) / totalCountFrac
|
|
472
|
+
minAperture = min(apertures)
|
|
473
|
+
maxAperture = max(apertures)
|
|
474
|
+
avgAperture = sum(apertures) / totalCountFrac
|
|
475
|
+
|
|
476
|
+
wellLocation = (self.xmax / 2, self.ymax / 2)
|
|
477
|
+
minDistanceFromWell = self._distance_of_well_from_closest_fracture(allFractures, wellLocation)
|
|
478
|
+
|
|
479
|
+
stressAzimuths = set()
|
|
480
|
+
for frac in allFractures:
|
|
481
|
+
for key in frac:
|
|
482
|
+
if key.startswith('correctedAperture'):
|
|
483
|
+
stressAzimuths.add(key)
|
|
484
|
+
|
|
485
|
+
with open(outputPropertiesFile, 'w') as fileID:
|
|
486
|
+
fileID.write(f"Total number of fractures: {totalCountFrac}\n"
|
|
487
|
+
f"Total intensity: {totalIntensity:.3f}\n"
|
|
488
|
+
f"Minimum fracture length: {minLength:.3f}\n"
|
|
489
|
+
f"Maximum fracture length: {maxLength:.3f}\n"
|
|
490
|
+
f"Average fracture length: {avgLength:.3f}\n"
|
|
491
|
+
f"Average aperture: {avgAperture:.3e}\n"
|
|
492
|
+
f"Maximum aperture: {maxAperture:.3e}\n"
|
|
493
|
+
f"Minimum aperture: {minAperture:.3e}\n"
|
|
494
|
+
f"connectivity= {connectivity:.3e}\n"
|
|
495
|
+
f"wellLocation= {wellLocation}\n"
|
|
496
|
+
f"minDistanceFromWell= {minDistanceFromWell}\n")
|
|
497
|
+
for azimuth in sorted(stressAzimuths):
|
|
498
|
+
avgAperture = sum(frac[azimuth] for frac in allFractures if azimuth in frac) / len(allFractures)
|
|
499
|
+
maxAperture = max(frac[azimuth] for frac in allFractures if azimuth in frac)
|
|
500
|
+
minAperture = min(frac[azimuth] for frac in allFractures if azimuth in frac)
|
|
501
|
+
fileID.write(f"{azimuth} Average: {avgAperture:.3e}\n")
|
|
502
|
+
fileID.write(f"{azimuth} Max: {maxAperture:.3e}\n")
|
|
503
|
+
fileID.write(f"{azimuth} Min: {minAperture:.3e}\n")
|
|
504
|
+
|
|
505
|
+
def generateOutputFileForOverallProperties(self, name, fractureSets, number=0):
|
|
506
|
+
return self._write_overall_properties(name, fractureSets, number)
|
|
507
|
+
|
|
508
|
+
def _plot_fractures(self, fractureSets, name, number=0):
|
|
509
|
+
figDir = os.path.join(self.outputDir, 'pics')
|
|
510
|
+
os.makedirs(figDir, exist_ok=True)
|
|
511
|
+
figDirFile = os.path.join(figDir, f"{number + 1:03}{name}.png")
|
|
512
|
+
|
|
513
|
+
ratio = self.ymax / self.xmax
|
|
514
|
+
plt.figure(figsize=(10, 10 * ratio))
|
|
515
|
+
colors = ['red', 'blue', 'green', 'purple', 'orange', 'brown']
|
|
516
|
+
setLabels = [f'Set {i + 1}' for i in range(len(fractureSets))]
|
|
517
|
+
|
|
518
|
+
for fractures, color, label in zip(fractureSets, colors, setLabels):
|
|
519
|
+
for fracture in fractures:
|
|
520
|
+
x_start, y_start = fracture['x_start'], fracture['y_start']
|
|
521
|
+
x_end, y_end = fracture['x_end'], fracture['y_end']
|
|
522
|
+
plt.plot([x_start, x_end], [y_start, y_end], color=color, label=label)
|
|
523
|
+
label = "_nolegend_"
|
|
524
|
+
|
|
525
|
+
plt.annotate('N', xy=(1.02, 1.00), xycoords='axes fraction', fontsize=20, ha='center', va='center')
|
|
526
|
+
plt.annotate('', xy=(1.02, 0.98), xytext=(1.02, 0.9), xycoords='axes fraction', textcoords='axes fraction',
|
|
527
|
+
arrowprops=dict(facecolor='black', shrink=0.05), ha='center', va='center')
|
|
528
|
+
plt.xlabel('X Coordinate')
|
|
529
|
+
plt.ylabel('Y Coordinate')
|
|
530
|
+
plt.title('DFN Visualization')
|
|
531
|
+
plt.grid(True)
|
|
532
|
+
plt.xlim(0, self.xmax)
|
|
533
|
+
plt.ylim(0, self.ymax)
|
|
534
|
+
plt.legend(loc='upper center', bbox_to_anchor=(0.5, -0.07), fancybox=True, shadow=True, ncol=len(fractureSets))
|
|
535
|
+
plt.savefig(figDirFile, bbox_inches='tight', format='png', dpi=300)
|
|
536
|
+
|
|
537
|
+
def plotFractures(self, fractureSets, name, number=0):
|
|
538
|
+
return self._plot_fractures(fractureSets, name, number)
|
|
539
|
+
|
|
540
|
+
def _plot_corrected_apertures(self, name, fractureSets, number=0):
|
|
541
|
+
figDir = os.path.join(self.outputDir, 'stressDependency')
|
|
542
|
+
os.makedirs(figDir, exist_ok=True)
|
|
543
|
+
figDirFile = os.path.join(figDir, f"{number + 1:03}{name}.png")
|
|
544
|
+
|
|
545
|
+
stress_azimuths = [key for key in fractureSets[0][0].keys() if key.startswith('correctedAperture')]
|
|
546
|
+
|
|
547
|
+
num_rows = 2
|
|
548
|
+
num_cols = (len(stress_azimuths) + num_rows - 1) // num_rows
|
|
549
|
+
fig, axes = plt.subplots(num_rows, num_cols, figsize=(12, 18), constrained_layout=True)
|
|
550
|
+
|
|
551
|
+
all_apertures = [fracture[key] for fracture_set in fractureSets for fracture in fracture_set for key in stress_azimuths]
|
|
552
|
+
min_aperture, max_aperture = min(all_apertures), max(all_apertures)
|
|
553
|
+
|
|
554
|
+
norm = mcolors.Normalize(vmin=min_aperture, vmax=max_aperture)
|
|
555
|
+
cmap = plt.get_cmap('viridis')
|
|
556
|
+
|
|
557
|
+
ax = axes.flat[0]
|
|
558
|
+
ax.set_aspect('equal', adjustable='box')
|
|
559
|
+
for set_index, fractures in enumerate(fractureSets):
|
|
560
|
+
for fracture in fractures:
|
|
561
|
+
corrected_aperture = fracture['initial aperture']
|
|
562
|
+
color = cmap(norm(corrected_aperture))
|
|
563
|
+
x_start, y_start = fracture['x_start'], fracture['y_start']
|
|
564
|
+
x_end, y_end = fracture['x_end'], fracture['y_end']
|
|
565
|
+
ax.plot([x_start, x_end], [y_start, y_end], color=color)
|
|
566
|
+
ax.set_xlabel('X Coordinate')
|
|
567
|
+
ax.set_ylabel('Y Coordinate')
|
|
568
|
+
ax.set_title('Initial aperture')
|
|
569
|
+
ax.grid(True)
|
|
570
|
+
ax.set_xlim(0, self.xmax)
|
|
571
|
+
ax.set_ylim(0, self.ymax)
|
|
572
|
+
|
|
573
|
+
for ax_idx, azimuth in enumerate(stress_azimuths):
|
|
574
|
+
ax = axes.flat[ax_idx + 1]
|
|
575
|
+
ax.set_aspect('equal', adjustable='box')
|
|
576
|
+
for set_index, fractures in enumerate(fractureSets):
|
|
577
|
+
for fracture in fractures:
|
|
578
|
+
corrected_aperture = fracture[azimuth]
|
|
579
|
+
color = cmap(norm(corrected_aperture))
|
|
580
|
+
x_start, y_start = fracture['x_start'], fracture['y_start']
|
|
581
|
+
x_end, y_end = fracture['x_end'], fracture['y_end']
|
|
582
|
+
ax.plot([x_start, x_end], [y_start, y_end], color=color)
|
|
583
|
+
ax.set_xlabel('X Coordinate')
|
|
584
|
+
ax.set_ylabel('Y Coordinate')
|
|
585
|
+
ax.set_title(f'{azimuth.replace("correctedAperture", "Stress orientation = ")} ')
|
|
586
|
+
ax.grid(True)
|
|
587
|
+
ax.set_xlim(0, self.xmax)
|
|
588
|
+
ax.set_ylim(0, self.ymax)
|
|
589
|
+
|
|
590
|
+
sm = plt.cm.ScalarMappable(cmap=cmap, norm=norm)
|
|
591
|
+
sm.set_array([])
|
|
592
|
+
cbar = fig.colorbar(sm, ax=axes.ravel().tolist(), orientation='vertical', pad=0.01)
|
|
593
|
+
cbar.set_label('Corrected Aperture Value', rotation=270, labelpad=15)
|
|
594
|
+
plt.savefig(figDirFile, format='png', dpi=300)
|
|
595
|
+
plt.close(fig)
|
|
596
|
+
|
|
597
|
+
def plotCorrectedApertures(self, name, fractureSets, number=0):
|
|
598
|
+
return self._plot_corrected_apertures(name, fractureSets, number)
|
|
599
|
+
|
|
600
|
+
def _plot_orientation_stereographic(self, name, fractureSets, number=0):
|
|
601
|
+
orientationStereographicDir = os.path.join(self.outputDir, name)
|
|
602
|
+
os.makedirs(orientationStereographicDir, exist_ok=True)
|
|
603
|
+
orientationStereographicDirFile = os.path.join(orientationStereographicDir, f"{number + 1:03}{name}.png")
|
|
604
|
+
colors = ['b', 'g', 'r', 'c', 'm', 'y', 'k']
|
|
605
|
+
fig, ax = plt.subplots(subplot_kw={'polar': True})
|
|
606
|
+
for i, fracture_set in enumerate(fractureSets):
|
|
607
|
+
theta_values = [frac['theta'] for frac in fracture_set]
|
|
608
|
+
theta_radians = np.radians(theta_values)
|
|
609
|
+
ax.hist(theta_radians, bins=36, density=True, alpha=0.75, color=colors[i % len(colors)],
|
|
610
|
+
label=f'Set {i + 1}')
|
|
611
|
+
ax.set_theta_zero_location('N')
|
|
612
|
+
ax.set_theta_direction(-1)
|
|
613
|
+
plt.legend()
|
|
614
|
+
plt.savefig(orientationStereographicDirFile, format='png', dpi=300)
|
|
615
|
+
|
|
616
|
+
def plotOrientationStereographic(self, name, fractureSets, number=0):
|
|
617
|
+
return self._plot_orientation_stereographic(name, fractureSets, number)
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def point_to_segment_distance(p, a, b):
|
|
621
|
+
if np.all(a == b):
|
|
622
|
+
return np.linalg.norm(p - a)
|
|
623
|
+
v = b - a
|
|
624
|
+
w = p - a
|
|
625
|
+
c1 = np.dot(w, v)
|
|
626
|
+
if c1 <= 0:
|
|
627
|
+
return np.linalg.norm(p - a)
|
|
628
|
+
c2 = np.dot(v, v)
|
|
629
|
+
if c2 <= c1:
|
|
630
|
+
return np.linalg.norm(p - b)
|
|
631
|
+
b = c1 / c2
|
|
632
|
+
pb = a + b * v
|
|
633
|
+
return np.linalg.norm(p - pb)
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
def segment_to_segment_distance(s1, s2):
|
|
637
|
+
intersect, _, _ = line_intersection(s1, s2)
|
|
638
|
+
if intersect:
|
|
639
|
+
return 0
|
|
640
|
+
else:
|
|
641
|
+
s1_start, s1_end = s1
|
|
642
|
+
s2_start, s2_end = s2
|
|
643
|
+
distances = [
|
|
644
|
+
point_to_segment_distance(np.array(s1_start), np.array(s2_start), np.array(s2_end)),
|
|
645
|
+
point_to_segment_distance(np.array(s1_end), np.array(s2_start), np.array(s2_end)),
|
|
646
|
+
point_to_segment_distance(np.array(s2_start), np.array(s1_start), np.array(s1_end)),
|
|
647
|
+
point_to_segment_distance(np.array(s2_end), np.array(s1_start), np.array(s1_end))
|
|
648
|
+
]
|
|
649
|
+
return min(distances)
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def line_intersection(line1, line2):
|
|
653
|
+
((x1, y1), (x2, y2)) = line1
|
|
654
|
+
((x3, y3), (x4, y4)) = line2
|
|
655
|
+
|
|
656
|
+
A1 = y2 - y1
|
|
657
|
+
B1 = x1 - x2
|
|
658
|
+
C1 = A1 * x1 + B1 * y1
|
|
659
|
+
|
|
660
|
+
A2 = y4 - y3
|
|
661
|
+
B2 = x3 - x4
|
|
662
|
+
C2 = A2 * x3 + B2 * y3
|
|
663
|
+
|
|
664
|
+
determinant = A1 * B2 - A2 * B1
|
|
665
|
+
|
|
666
|
+
if determinant == 0:
|
|
667
|
+
return False, None, None
|
|
668
|
+
|
|
669
|
+
x = (B2 * C1 - B1 * C2) / determinant
|
|
670
|
+
y = (A1 * C2 - A2 * C1) / determinant
|
|
671
|
+
|
|
672
|
+
if (min(x1, x2) <= x <= max(x1, x2) and
|
|
673
|
+
min(y1, y2) <= y <= max(y1, y2) and
|
|
674
|
+
min(x3, x4) <= x <= max(x3, x4) and
|
|
675
|
+
min(y3, y4) <= y <= max(y3, y4)):
|
|
676
|
+
return True, x, y
|
|
677
|
+
else:
|
|
678
|
+
return False, x, y
|