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.
@@ -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