|
| 1 | +from time import time |
| 2 | +import numpy as np |
| 3 | +from typing import Tuple, List, Union |
| 4 | +from scipy.fft import fftn, ifftn, fftfreq |
| 5 | +from MSUtils.general.MicrostructureImage import MicrostructureImage |
| 6 | +from MSUtils.general.h52xdmf import write_xdmf |
| 7 | + |
| 8 | + |
| 9 | +def generate_spinodal_microstructure( |
| 10 | + shape: Union[Tuple[int, int, int], List[int]], |
| 11 | + volume_fraction: float = 0.5, |
| 12 | + k0: Union[Tuple[float, float, float], List[float]] = (6.0, 6.0, 6.0), |
| 13 | + bandwidth: Union[Tuple[float, float, float], List[float]] = (0.6, 0.6, 0.6), |
| 14 | + exponent: float = 2.0, |
| 15 | + seed: int = 42, |
| 16 | + L: Union[Tuple[float, float, float], List[float]] = (1.0, 1.0, 1.0), |
| 17 | + anisotropy_axes: Union[Tuple[np.ndarray, np.ndarray], List[np.ndarray]] = ( |
| 18 | + np.array([1.0, 0.0, 0.0]), |
| 19 | + np.array([0.0, 1.0, 0.0]), |
| 20 | + ), |
| 21 | +): |
| 22 | + """Generate a binary spinodal microstructure via spectral filtering. |
| 23 | +
|
| 24 | + Parameters |
| 25 | + ---------- |
| 26 | + shape : tuple or list |
| 27 | + Grid size (nz, ny, nx). |
| 28 | + k0 : array-like, default [6.0, 6.0, 6.0] |
| 29 | + Center wavenumber in cycles per domain (not angular) for each axis [k0_v3, k0_v2, k0_v1]. |
| 30 | + bandwidth : array-like, default [0.6, 0.6, 0.6] |
| 31 | + Relative Gaussian width (fraction of k0) for each axis [bw_v3, bw_v2, bw_v1]. |
| 32 | + Use same values for isotropic bandwidth, different values for anisotropic. |
| 33 | + exponent : float |
| 34 | + High-k rolloff exponent (larger -> stronger suppression) |
| 35 | + seed : int, optional |
| 36 | + RNG seed for reproducibility |
| 37 | + L : array-like, default [1.0, 1.0, 1.0] |
| 38 | + Physical dimensions [Lz, Ly, Lx]. |
| 39 | + anisotropy_axes : 2-tuple or list of vectors, default [(1,0,0), (0,1,0)] |
| 40 | + Define a preferred coordinate system (v1, v2) for oblique orientations. |
| 41 | + v1, v2 should be orthogonal unit vectors defining preferred directions. |
| 42 | + """ |
| 43 | + np.random.seed(seed) |
| 44 | + ndim = len(shape) |
| 45 | + L = np.array(L, dtype=np.float32) |
| 46 | + dx_arr = L / np.array(shape, dtype=np.float32) |
| 47 | + k0_arr = np.array(k0, dtype=np.float32) |
| 48 | + bw_arr = np.array(bandwidth, dtype=np.float32) |
| 49 | + |
| 50 | + np.random.seed(seed) |
| 51 | + noise = np.random.randn(*shape).astype( |
| 52 | + np.float32, copy=False |
| 53 | + ) # Generate noise (resolution dependent) |
| 54 | + noise_hat = fftn(noise, workers=-1, overwrite_x=True) |
| 55 | + |
| 56 | + # Build k-space grids |
| 57 | + grids = [ |
| 58 | + fftfreq(shape[i], d=dx_arr[i]).astype(np.float32) * (2.0 * np.pi) |
| 59 | + for i in range(ndim) |
| 60 | + ] |
| 61 | + grids = np.meshgrid(*grids, indexing="ij") |
| 62 | + |
| 63 | + # Build rotation matrix: R = [v1, v2, v3]^T |
| 64 | + v1, v2 = anisotropy_axes |
| 65 | + v1 = np.array(v1, dtype=np.float32) |
| 66 | + v2 = np.array(v2, dtype=np.float32) |
| 67 | + v3 = np.cross(v1, v2) |
| 68 | + R = np.array([v1, v2, v3], dtype=np.float32) |
| 69 | + |
| 70 | + # Rotate k-space coordinates: k_rotated = R @ k_original |
| 71 | + grids = list(np.einsum("ij,jklm->iklm", R, np.array(grids))) |
| 72 | + |
| 73 | + # Compute effective radial wavenumber using ellipsoidal normalization |
| 74 | + k2_normalized = sum( |
| 75 | + (grids[i] / ((np.float32(2.0 * np.pi) * k0_arr[i]) / L[i])) ** 2 |
| 76 | + for i in range(ndim) |
| 77 | + ) |
| 78 | + k = np.sqrt(k2_normalized) |
| 79 | + k0_ref = np.float32(1.0) # Reference wavenumber is 1 after normalization |
| 80 | + |
| 81 | + del grids |
| 82 | + |
| 83 | + # Bandpass filter |
| 84 | + # Use mean bandwidth for the Gaussian envelope |
| 85 | + sigma = np.float32(np.mean(bw_arr) * k0_ref) |
| 86 | + k_diff = k - k0_ref |
| 87 | + filter_amp = np.exp(-0.5 * (k_diff / sigma) ** 2) |
| 88 | + |
| 89 | + # High-k rolloff (in-place multiplication) |
| 90 | + k_ratio = k / k0_ref |
| 91 | + filter_amp *= 1.0 / (1.0 + k_ratio**exponent) |
| 92 | + |
| 93 | + del k, k_diff, k_ratio |
| 94 | + |
| 95 | + # Apply filter in-place |
| 96 | + noise_hat *= filter_amp |
| 97 | + del filter_amp |
| 98 | + |
| 99 | + # Inverse FFT |
| 100 | + field = np.real(ifftn(noise_hat, workers=-1, overwrite_x=True)) |
| 101 | + |
| 102 | + field = (field - field.mean()) / field.std() |
| 103 | + |
| 104 | + # Threshold |
| 105 | + thr = np.quantile(field, 1.0 - volume_fraction) |
| 106 | + image = (field >= thr).astype(np.uint8) |
| 107 | + return image |
| 108 | + |
| 109 | + |
| 110 | +if __name__ == "__main__": |
| 111 | + |
| 112 | + N = [256, 256, 256] |
| 113 | + L = [1.0, 1.0, 1.0] |
| 114 | + k0 = [5.0, 20.0, 20.0] |
| 115 | + bandwidth = [0.1, 0.1, 0.1] |
| 116 | + exponent = 10.0 |
| 117 | + seed = 42 |
| 118 | + volume_fraction = 0.5 |
| 119 | + |
| 120 | + v1 = np.array([1.0, 1.0, 1.0]) |
| 121 | + v1 /= np.linalg.norm(v1) |
| 122 | + v2 = np.array([1.0, -1.0, 0.0]) |
| 123 | + v2 /= np.linalg.norm(v2) |
| 124 | + anisotropy_axes = (v1, v2) |
| 125 | + |
| 126 | + start_time = time() |
| 127 | + img = generate_spinodal_microstructure( |
| 128 | + N, |
| 129 | + volume_fraction=volume_fraction, |
| 130 | + k0=k0, |
| 131 | + bandwidth=bandwidth, |
| 132 | + exponent=exponent, |
| 133 | + seed=seed, |
| 134 | + L=L, |
| 135 | + ) |
| 136 | + end_time = time() |
| 137 | + print( |
| 138 | + f"3D spinodal microstructure generation took {end_time - start_time:.4f} seconds" |
| 139 | + ) |
| 140 | + |
| 141 | + MS = MicrostructureImage(image=img, L=L) |
| 142 | + MS.write( |
| 143 | + h5_filename="data/spinodal_spectral.h5", |
| 144 | + dset_name="/spinodal_spectral", |
| 145 | + order="zyx", |
| 146 | + ) |
| 147 | + write_xdmf( |
| 148 | + h5_filepath="data/spinodal_spectral.h5", |
| 149 | + xdmf_filepath="data/spinodal_spectral.xdmf", |
| 150 | + microstructure_length=L[::-1], |
| 151 | + ) |
0 commit comments