Source code for pysimfrac.src.methods.spectral

import numpy as np
from pysimfrac.src.general.helper_functions import print_error

import warnings
warnings.filterwarnings("ignore", message=".*The 'nopython' keyword.*")



[docs] def initialize_spectral_parameters(self): """ Initializes parameters used in method spectral for fracture surface generation Parameters --------------- simFrac object Returns -------------- None Notes ---------------- Attaches dictionary with parameters onto the simfrac object For theoretical details see: "Brown, Simple mathematical model of a rough fracture, J. of Geophysical Research, 100 (1995): 5941-5952” “Glover et al., Synthetic rough fractures in rocks, J. of Geophysical Research, 103 (1998): 9609-9620” “Glover et al., Fluid flow in synthetic rough fractures and application to the Hachimantai geothermal hot dry rock test site, 103 (1998): 9621-9635” """ self.params = { 'H': { 'value': 0.5, 'description': "Hurst exponent. Determines fractal dimension. Range is (0, 1)" }, 'roughness': { 'value': 0.01, 'description': "Root-mean-squared value of heights [m], Range is (0, infty)" }, 'mean-aperture': { 'value': 1, 'description': "Mean Fracture Aperture" }, 'mismatch': { 'value': 0.9, 'description': "Mismatch length scale (wavelength) as a fraction of fracture size [0 < Mismatch < 1]" }, 'N': { 'value': self.nx, 'description': "Discretization of fracture self.lx/self.h" }, 'aniso': { 'value': 0, 'description': "Anisotropy Ratio [0 < Aniso < 1]. Setting to 0 is isotropic." }, 'seed': { 'value': 1, 'description': "Seed for the random number generator. Set to 0 to seed off clock." }, 'lambda_0': { 'value': 1, 'description': "(optional) 'roll-off' length scale as a fraction of fracture size [0 < lambda_0 < 1 (default)]" }, 'model': { 'value': 'linear', 'description': "(optional) Power spectral density model: 'linear' (default), 'bilinear', 'smooth'" } }
[docs] def check_spectral_parameters(self): """ Check that all required parameters for spectral field generation are provided. Exits if not. Parameters ---------------- simfrac object Returns ------------ None Notes -------------- None """ print("--> Checking spectral Method Parameters: Starting") self.check_generation_parameter("mean-aperture", 0) self.check_generation_parameter("roughness", 0) self.check_generation_parameter("mismatch", 0, 1) self.check_generation_parameter("H", 0, 1) self.check_generation_parameter("aniso", 0, 1 - 10**-16) self.check_generation_parameter("lambda_0", 0, 1) if "seed" not in self.params: self.params["seed"] = None psd_models = ['linear', 'bilinear', 'smooth'] if not self.params['model']['value'] in psd_models: print_error( f"spectral Model unknown. Value provided {self.params['model']['value']} \ \nAcceptable models: 'linear', 'bilinear', 'smooth' ") print("--> Checking spectral Method Parameters: Complete")
[docs] def create_spectral(self): """ Generate a fracture surface using the spectral method. Parameters ----------- self : simfrac object Returns -------- None Notes ------ This method initializes the spectral method parameters, checks the parameters for consistency, and generates a fracture surface using the spectral method. """ print("--> Running Fracture surface method 'spectral': Starting") ## Check parameters for model consistency self.check_spectral_parameters() ## Print method parameters to screen self.print_method_params() ## set Random number streams rand_stream1 = np.random.RandomState(self.params['seed']['value']) rand_stream2 = np.random.RandomState( rand_stream1.randint(self.params['seed']['value'])) # Derive parameters two_pi = 2.0 * np.pi Nyq = 1 / (2 * self.h) # Set different sampling of wave lengths for different facture sizes dq_x = 1 / (self.nx * self.h) dq_y = 1 / (self.ny * self.h) # Wavenumber increment wave_numbers_x = two_pi * np.linspace(-Nyq, Nyq - dq_x, self.nx) wave_numbers_y = two_pi * np.linspace(-Nyq, Nyq - dq_y, self.ny) # 2D Wavenumbers [m^-1] qx, qy = np.meshgrid(wave_numbers_x, wave_numbers_y) # Wavenumber modulus (Apply here the anisotropy conditions) q = (qx**2 + (qy / (1 - self.params['aniso']['value']))**2)**0.5 wavelength_modulus = np.zeros_like(q) idx = np.where(q != 0) wavelength_modulus[idx] = two_pi / q[idx] # for i,val in q: # if q != 0: # wavelength_modulus[i] = two_pi / val # Wavelengths modulus # Find wavelengths above the "roll-off" lengthscale roll_off = np.where(wavelength_modulus > self.params['lambda_0']['value'] * min(self.lx, self.ly)) print(f"--> Power spectral density model: {self.params['model']['value'] }") Fmodulus = np.zeros_like(q) if self.params['model']['value'] == 'linear': Fmodulus[idx] = q[idx]**(- 1 - self.params['H']['value']) Fmodulus[roll_off] = 0 elif self.params['model']['value'] == 'bilinear': Fmodulus[idx] = q[idx]**(- 1 - self.params['H']['value']) Fmodulus[roll_off] = ( two_pi / (self.params['lambda_0']['value'] * min(self.lx, self.ly)))**( -1 - self.params['H']['value']) elif self.params['model']['value'] == 'smooth': Fmodulus[idx] = ( two_pi / (self.params['lambda_0']['value'] * min(self.lx, self.ly)) + q[idx])**(-1 - self.params['H']['value']) # First surface spectrum Phase1 = two_pi * rand_stream1.rand(self.ny, self.nx) # Second surface specturm Phase2 = two_pi * rand_stream2.rand(self.ny, self.nx) # The phase spectrum of the second surface is different for short wavelengths # Find mismatching wavelengths mismatching = np.where( wavelength_modulus < self.params['mismatch']['value'] * min(self.lx, self.ly)) # Overwrite mismatching wave lengths Phase2[mismatching] = Phase1[mismatching] # Top surface in Fourier space A1 = Fmodulus * (np.cos(Phase1) + 1j * np.sin(Phase1) ) / np.abs(np.cos(Phase1) + 1j * np.sin(Phase1)) # Bottom surface in Fourier space A2 = Fmodulus * (np.cos(Phase2) + 1j * np.sin(Phase2) ) / np.abs(np.cos(Phase2) + 1j * np.sin(Phase2)) # Shift and invert FT to real space for both top and bottom # Upper surface self.top = np.fft.ifft2(np.fft.ifftshift(A1)).real # Lower surface self.bottom = np.fft.ifft2(np.fft.ifftshift(A2)).real # Roughness is used to rescale the surface variance # Divide out the variance and then multiple but the desired one. roughness_scale = self.params['roughness']['value'] / np.std(self.top) self.top *= roughness_scale roughness_scale = self.params['roughness']['value'] / np.std(self.bottom) self.bottom *= roughness_scale self.mean_aperture = self.params['mean-aperture']['value'] ## apply shear if non-zero self.top = self.top + 0.5 * self.mean_aperture self.bottom = self.bottom - 0.5 * self.mean_aperture self.aperture = self.top - self.bottom # Attached mesh to the class X = np.linspace(-0.5 * self.lx, 0.5 * self.lx, self.nx) Y = np.linspace(-0.5 * self.ly, 0.5 * self.ly, self.ny) xv, yv = np.meshgrid(X, Y) self.X = xv self.Y = yv # Apply shear if non-zero if self.shear > 0: self.apply_shear() print("--> Running Fracture surface method 'spectral': Complete")