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