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,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