compgen 0.1.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.
- compgen/__init__.py +29 -0
- compgen/exoutil.py +43 -0
- compgen/filters.py +120 -0
- compgen/gen.py +612 -0
- compgen/hdfwriter.py +57 -0
- compgen/materials.py +78 -0
- compgen/mesh.py +507 -0
- compgen/moose.py +365 -0
- compgen/physics/2d/elastic/settings.json +56 -0
- compgen/physics/2d/elastic/template.i +209 -0
- compgen/physics/2d/plastic/settings.json +95 -0
- compgen/physics/2d/plastic/template.i +558 -0
- compgen/physics/2d/thermal_conductivity/settings.json +49 -0
- compgen/physics/2d/thermal_conductivity/template.i +215 -0
- compgen/physics/2d/thermal_conductivity_with_interface/settings.json +53 -0
- compgen/physics/2d/thermal_conductivity_with_interface/template.i +229 -0
- compgen/physics/2d/thermal_expansion/settings.json +55 -0
- compgen/physics/2d/thermal_expansion/template.i +233 -0
- compgen/physics/3d/elastic/settings.json +68 -0
- compgen/physics/3d/elastic/template.i +281 -0
- compgen/physics/3d/plastic/settings.json +245 -0
- compgen/physics/3d/plastic/template.i +658 -0
- compgen/physics/3d/thermal_conductivity/settings.json +53 -0
- compgen/physics/3d/thermal_conductivity/template.i +245 -0
- compgen/physics/3d/thermal_conductivity_with_interface/settings.json +57 -0
- compgen/physics/3d/thermal_conductivity_with_interface/template.i +259 -0
- compgen/physics/3d/thermal_expansion/settings.json +61 -0
- compgen/physics/3d/thermal_expansion/template.i +276 -0
- compgen/runner.py +407 -0
- compgen/temp.py +56 -0
- compgen/tensors.py +65 -0
- compgen/utils.py +180 -0
- compgen-0.1.0.dist-info/METADATA +105 -0
- compgen-0.1.0.dist-info/RECORD +37 -0
- compgen-0.1.0.dist-info/WHEEL +5 -0
- compgen-0.1.0.dist-info/licenses/LICENSE +23 -0
- compgen-0.1.0.dist-info/top_level.txt +1 -0
compgen/gen.py
ADDED
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
# Copyright 2026, UChicago Argonne, LLC
|
|
2
|
+
# All Rights Reserved
|
|
3
|
+
# Software Name: compgen
|
|
4
|
+
# By: Argonne National Laboratory
|
|
5
|
+
# OPEN SOURCE LICENSE (MIT)
|
|
6
|
+
#
|
|
7
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
# in the Software without restriction, including without limitation the rights
|
|
10
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
# furnished to do so, subject to the following conditions:
|
|
13
|
+
#
|
|
14
|
+
# The above copyright notice and this permission notice shall be included in
|
|
15
|
+
# all copies or substantial portions of the Software.
|
|
16
|
+
#
|
|
17
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
23
|
+
# THE SOFTWARE.
|
|
24
|
+
|
|
25
|
+
"""Methods for generating random composite structures."""
|
|
26
|
+
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
import importlib
|
|
29
|
+
|
|
30
|
+
import numpy as np
|
|
31
|
+
import gstools as gs
|
|
32
|
+
from scipy.optimize import curve_fit
|
|
33
|
+
from compgen.utils import standardize_image
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class DampedOscillationCovariance(gs.CovModel):
|
|
37
|
+
"""Damped Oscillation covariance model.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
dim (int): Dimension of the model.
|
|
41
|
+
var (float): Variance of the model.
|
|
42
|
+
len_scale (float): Correlation length scale.
|
|
43
|
+
freq (float): Frequency of oscillations.
|
|
44
|
+
damp (float): Damping factor.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, dim=2, var=1, len_scale=1, amp=0.1, freq=0.1, damp=0.1):
|
|
48
|
+
super().__init__(dim=dim, var=var, len_scale=len_scale)
|
|
49
|
+
self.amp = amp
|
|
50
|
+
self.freq = freq
|
|
51
|
+
self.damp = damp
|
|
52
|
+
|
|
53
|
+
def _variogram(self, h, var, len_scale, amp, freq, damp):
|
|
54
|
+
"""Actually evaluate the variogram with given parameters.
|
|
55
|
+
Args:
|
|
56
|
+
h (np.ndarray): Lag distances.
|
|
57
|
+
var (float): Variance.
|
|
58
|
+
len_scale (float): Correlation length scale.
|
|
59
|
+
amp (float): Amplitude of oscillations.
|
|
60
|
+
freq (float): Frequency of oscillations.
|
|
61
|
+
damp (float): Damping factor.
|
|
62
|
+
|
|
63
|
+
"""
|
|
64
|
+
a = var * (1.0 - np.exp(-h / len_scale))
|
|
65
|
+
b = amp * np.exp(-damp * h) * np.sin(2 * np.pi * freq * h)
|
|
66
|
+
return a + b
|
|
67
|
+
|
|
68
|
+
def variogram(self, h):
|
|
69
|
+
"""Evaluate the variogram at given lag distances.
|
|
70
|
+
Args:
|
|
71
|
+
h (np.ndarray): Lag distances.
|
|
72
|
+
"""
|
|
73
|
+
return self._variogram(
|
|
74
|
+
h, self.var, self.len_scale, self.amp, self.freq, self.damp
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def fit(self, r, c):
|
|
78
|
+
"""Fit the model to given variogram data.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
r (np.ndarray): Radii.
|
|
82
|
+
c (np.ndarray): Variogram values.
|
|
83
|
+
"""
|
|
84
|
+
popt, _ = curve_fit(
|
|
85
|
+
self._variogram,
|
|
86
|
+
r,
|
|
87
|
+
c,
|
|
88
|
+
p0=[self.var, self.len_scale, self.amp, self.freq, self.damp],
|
|
89
|
+
bounds=(0, np.inf),
|
|
90
|
+
)
|
|
91
|
+
self.var, self.len_scale, self.amp, self.freq, self.damp = popt
|
|
92
|
+
return self
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class EmpiricalCovariance(gs.CovModel):
|
|
96
|
+
"""Empirical covariance model from given data.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
dim (int): Dimension of the model.
|
|
100
|
+
r (np.ndarray): Radii.
|
|
101
|
+
c (np.ndarray): Variogram values.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
def __init__(self, dim=2, r=None, c=None):
|
|
105
|
+
super().__init__(dim=dim)
|
|
106
|
+
self.r = r
|
|
107
|
+
self.c = c
|
|
108
|
+
|
|
109
|
+
def variogram(self, h):
|
|
110
|
+
"""Evaluate the variogram at given lag distances by interpolating the empirical data.
|
|
111
|
+
Args:
|
|
112
|
+
h (np.ndarray): Lag distances.
|
|
113
|
+
"""
|
|
114
|
+
cov = np.interp(h, self.r, self.c, left=self.c[0], right=self.c[-1])
|
|
115
|
+
return cov
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class StructureGenerator:
|
|
119
|
+
"""
|
|
120
|
+
Base class for structure generators.
|
|
121
|
+
|
|
122
|
+
Keyword Args:
|
|
123
|
+
random_seed (int): Seed for the random number generator. Defaults to None.
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
def __init__(self, random_seed=None):
|
|
127
|
+
self.rng = np.random.default_rng(random_seed)
|
|
128
|
+
|
|
129
|
+
def _sample_value(self, v):
|
|
130
|
+
"""
|
|
131
|
+
Either return a value if it's a scalar or sample from a uniform distribution if it's a list.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
v (float or list): Value or range to sample
|
|
135
|
+
"""
|
|
136
|
+
if isinstance(v, list) and len(v) == 2:
|
|
137
|
+
return self.rng.uniform(v[0], v[1])
|
|
138
|
+
return v
|
|
139
|
+
|
|
140
|
+
def __iter__(self):
|
|
141
|
+
return self
|
|
142
|
+
|
|
143
|
+
def __next__(self):
|
|
144
|
+
return self.next()
|
|
145
|
+
|
|
146
|
+
def next(self):
|
|
147
|
+
"""Generate the next structure."""
|
|
148
|
+
return self.sample()
|
|
149
|
+
|
|
150
|
+
def sample(self):
|
|
151
|
+
"""Generate the next structure.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
np.ndarray: An array of shape `self.N` with random bool values.
|
|
155
|
+
"""
|
|
156
|
+
raise NotImplementedError("Subclasses must implement this method.")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class GenerativeStructureGenerator(StructureGenerator):
|
|
160
|
+
"""
|
|
161
|
+
Makes new structures from scratch
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
N (list): Size of the generated structures in each dimension
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
def __init__(self, N, random_seed=None):
|
|
168
|
+
super().__init__(random_seed=random_seed)
|
|
169
|
+
if len(N) < 1:
|
|
170
|
+
raise ValueError("N must be a list with at least one dimension.")
|
|
171
|
+
self.N = N
|
|
172
|
+
self.rng = np.random.default_rng(random_seed)
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def dim(self):
|
|
176
|
+
"""Dimension of the generated structures."""
|
|
177
|
+
return len(self.N)
|
|
178
|
+
|
|
179
|
+
@property
|
|
180
|
+
def nx(self):
|
|
181
|
+
"""Number of grid points in the x-direction."""
|
|
182
|
+
return self.N[0]
|
|
183
|
+
|
|
184
|
+
@property
|
|
185
|
+
def ny(self):
|
|
186
|
+
"""Number of grid points in the y-direction."""
|
|
187
|
+
if self.dim < 2:
|
|
188
|
+
return 0
|
|
189
|
+
return self.N[1]
|
|
190
|
+
|
|
191
|
+
@property
|
|
192
|
+
def nz(self):
|
|
193
|
+
"""Number of grid points in the z-direction."""
|
|
194
|
+
if self.dim < 3:
|
|
195
|
+
return 0
|
|
196
|
+
return self.N[2]
|
|
197
|
+
|
|
198
|
+
def sample(self):
|
|
199
|
+
"""Generate the next structure.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
np.ndarray: An array of shape `self.N` with random bool values.
|
|
203
|
+
"""
|
|
204
|
+
raise NotImplementedError("Subclasses must implement this method.")
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class TwoPointGenerator(GenerativeStructureGenerator):
|
|
208
|
+
"""Generate structures based on two point correlations.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
N (list): Size of the generated structures in each dimension.
|
|
212
|
+
reference_image (np.ndarray): 2D binary array representing the reference structure.
|
|
213
|
+
|
|
214
|
+
Keyword Args:
|
|
215
|
+
random_seed (int): Seed for the random number generator. Defaults to None.
|
|
216
|
+
l_guess (float): Initial guess for the correlation length scale. Defaults to 5.0.
|
|
217
|
+
radial_mask (float): Maximum radius to consider for fitting. Defaults to None.
|
|
218
|
+
"""
|
|
219
|
+
|
|
220
|
+
def __init__(
|
|
221
|
+
self, N, reference_image, random_seed=None, l_guess=5.0, radial_mask=None
|
|
222
|
+
):
|
|
223
|
+
super().__init__(N, random_seed=random_seed)
|
|
224
|
+
|
|
225
|
+
if self.dim != 2:
|
|
226
|
+
raise ValueError(
|
|
227
|
+
"TwoPointGenerator only supports 2D structures for the moment."
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
self.l_guess = l_guess
|
|
231
|
+
self.reference_image = standardize_image(reference_image, shape=N, dtype=bool)
|
|
232
|
+
self.acov, self.mean, self.var = self.image_statistics(self.reference_image)
|
|
233
|
+
self.r, self.c = self.radial_average_variogram(self.acov)
|
|
234
|
+
|
|
235
|
+
self.model = self.fit_model(self.r, self.c, radial_mask=radial_mask)
|
|
236
|
+
self.generator = gs.SRF(self.model)
|
|
237
|
+
|
|
238
|
+
def image_statistics(self, image):
|
|
239
|
+
"""Calculate the empirical coveriance, mean, and variance of the image.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
image (np.ndarray): 2D binary array.
|
|
243
|
+
"""
|
|
244
|
+
img = image.astype(float)
|
|
245
|
+
mean = np.mean(img)
|
|
246
|
+
F = np.fft.fft2(img - mean)
|
|
247
|
+
acov = np.fft.fftshift(
|
|
248
|
+
np.real(np.fft.ifft2(F * np.conj(F))) / (img.shape[0] * img.shape[1])
|
|
249
|
+
)
|
|
250
|
+
var = acov[img.shape[0] // 2, img.shape[1] // 2]
|
|
251
|
+
return acov, mean, var
|
|
252
|
+
|
|
253
|
+
def radial_average_variogram(self, acov):
|
|
254
|
+
"""Compute the radial average of the autocovariance and convert to variogram.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
acov (np.ndarray): 2D autocovariance array.
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
tuple: Radii and corresponding averaged values.
|
|
261
|
+
"""
|
|
262
|
+
x, y = np.indices(acov.shape)
|
|
263
|
+
center = np.array(acov.shape) // 2
|
|
264
|
+
r = np.sqrt((x - center[0]) ** 2 + (y - center[1]) ** 2)
|
|
265
|
+
r = r.astype(int)
|
|
266
|
+
|
|
267
|
+
tbin = np.bincount(r.ravel(), acov.ravel())
|
|
268
|
+
nr = np.bincount(r.ravel())
|
|
269
|
+
radialprofile = tbin / nr
|
|
270
|
+
variogram = radialprofile[0] - radialprofile
|
|
271
|
+
return np.arange(r.max() + 1), variogram
|
|
272
|
+
|
|
273
|
+
def fit_model(self, r, c, radial_mask=None):
|
|
274
|
+
"""Fit a Gaussian model to the radial average.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
r (np.ndarray): Radii.
|
|
278
|
+
c (np.ndarray): Variogram values.
|
|
279
|
+
|
|
280
|
+
Keyword Args:
|
|
281
|
+
radial_mask (float): Maximum radius to consider for fitting. Defaults to None.
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
gstools.CovModel: Fitted covariance model.
|
|
285
|
+
"""
|
|
286
|
+
# Mask off beyond some range
|
|
287
|
+
if radial_mask is not None:
|
|
288
|
+
mask = r <= radial_mask
|
|
289
|
+
r = r[mask]
|
|
290
|
+
c = c[mask]
|
|
291
|
+
|
|
292
|
+
model = DampedOscillationCovariance(dim=2)
|
|
293
|
+
model.fit(r, c)
|
|
294
|
+
|
|
295
|
+
return model
|
|
296
|
+
|
|
297
|
+
def sample(self):
|
|
298
|
+
"""Generate a new structure matching the target 2-point correlation.
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
np.ndarray: A 2D array of shape (nx, ny) with random bool values.
|
|
302
|
+
"""
|
|
303
|
+
x = np.arange(self.nx)
|
|
304
|
+
y = np.arange(self.ny)
|
|
305
|
+
field = self.generator.structured([x, y], seed=self.rng.integers(0, 1e9))
|
|
306
|
+
|
|
307
|
+
z = (field - np.mean(field)) / np.std(field)
|
|
308
|
+
thresh = np.quantile(z, 1 - self.mean)
|
|
309
|
+
|
|
310
|
+
generated = (z > thresh).astype(bool)
|
|
311
|
+
|
|
312
|
+
return generated
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
class SpinodalGenerator(GenerativeStructureGenerator):
|
|
316
|
+
"""Generate structures based on spinodal decomposition.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
N (list): Size of the generated structures in each dimension.
|
|
320
|
+
vf (float or list): Volume fraction of phase A or range for random samples.
|
|
321
|
+
l (float or list): Characteristic length scale or range for random samples.
|
|
322
|
+
bandwidth (float or list): Bandwidth of the spinodal structure or range for random samples.
|
|
323
|
+
|
|
324
|
+
Keyword Args:
|
|
325
|
+
random_seed (int): Seed for the random number generator. Defaults to None.
|
|
326
|
+
"""
|
|
327
|
+
|
|
328
|
+
def __init__(self, N, vf, l, bandwidth, random_seed=None):
|
|
329
|
+
super().__init__(N, random_seed=random_seed)
|
|
330
|
+
self.vf_input = vf
|
|
331
|
+
self.l_input = l
|
|
332
|
+
self.bandwidth_input = bandwidth
|
|
333
|
+
|
|
334
|
+
def sample(self):
|
|
335
|
+
"""Sample parameters for the next structure.
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
np.ndarray: An array of shape `self.N` with random bool values.
|
|
339
|
+
"""
|
|
340
|
+
# Sample the parameters
|
|
341
|
+
vf = self._sample_value(self.vf_input)
|
|
342
|
+
l = self._sample_value(self.l_input)
|
|
343
|
+
bandwidth = self._sample_value(self.bandwidth_input)
|
|
344
|
+
|
|
345
|
+
# Build k-vectors for each dimension and compute spectral magnitude
|
|
346
|
+
ks = [2.0 * np.pi * np.fft.fftfreq(n) for n in self.N]
|
|
347
|
+
K_components = np.meshgrid(*ks, indexing="ij")
|
|
348
|
+
K_mag = np.zeros_like(K_components[0])
|
|
349
|
+
for K in K_components:
|
|
350
|
+
K_mag = K_mag + K**2
|
|
351
|
+
K_mag = np.sqrt(K_mag)
|
|
352
|
+
|
|
353
|
+
k0 = 2.0 * np.pi / l
|
|
354
|
+
sigma_k = bandwidth * k0
|
|
355
|
+
|
|
356
|
+
assert sigma_k > 0.0
|
|
357
|
+
amplitude_spectrum = np.exp(-0.5 * ((K_mag - k0) / sigma_k) ** 2)
|
|
358
|
+
amplitude_spectrum[K_mag == 0] = 0.0
|
|
359
|
+
|
|
360
|
+
g = self.rng.normal(size=tuple(self.N))
|
|
361
|
+
g_hat = np.fft.fftn(g)
|
|
362
|
+
|
|
363
|
+
amp_filter = np.sqrt(amplitude_spectrum)
|
|
364
|
+
h_hat = g_hat * amp_filter
|
|
365
|
+
|
|
366
|
+
h = np.fft.ifftn(h_hat).real
|
|
367
|
+
h = (h - np.mean(h)) / np.std(h)
|
|
368
|
+
|
|
369
|
+
T = np.quantile(h, 1 - vf)
|
|
370
|
+
img = h > T
|
|
371
|
+
|
|
372
|
+
return img
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
class RandomGenerator(GenerativeStructureGenerator):
|
|
376
|
+
"""Generate random structures of a given shape.
|
|
377
|
+
|
|
378
|
+
Args:
|
|
379
|
+
N (list): Size of the generated structures in each dimension.
|
|
380
|
+
vf (float or list): Volume fraction of phase A or range for random samples.
|
|
381
|
+
|
|
382
|
+
Keyword Args:
|
|
383
|
+
random_seed (int): Seed for the random number generator. Defaults to None.
|
|
384
|
+
"""
|
|
385
|
+
|
|
386
|
+
def __init__(self, N, vf, random_seed=None):
|
|
387
|
+
super().__init__(N, random_seed=random_seed)
|
|
388
|
+
self.vf_input = vf
|
|
389
|
+
|
|
390
|
+
def sample(self):
|
|
391
|
+
"""Generate the next random structure via hierarchical sampling.
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
np.ndarray: An array of shape `self.N` with random bool values.
|
|
395
|
+
"""
|
|
396
|
+
# First select the volume fraction of phase A
|
|
397
|
+
vf = self._sample_value(self.vf_input)
|
|
398
|
+
total = int(np.prod(self.N))
|
|
399
|
+
nf = int(total * vf)
|
|
400
|
+
# Handle trivial cases explicitly
|
|
401
|
+
if nf <= 0:
|
|
402
|
+
return np.zeros(tuple(self.N), dtype=bool)
|
|
403
|
+
if nf >= total:
|
|
404
|
+
return np.ones(tuple(self.N), dtype=bool)
|
|
405
|
+
|
|
406
|
+
# Then sample a hierarchical random structure across arbitrary dimensions
|
|
407
|
+
inds = self.rng.choice(total, size=nf, replace=False)
|
|
408
|
+
img = np.zeros((total,), dtype=bool)
|
|
409
|
+
img[inds] = True
|
|
410
|
+
return img.reshape(tuple(self.N))
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
class FromNumpyGenerator(StructureGenerator):
|
|
414
|
+
"""
|
|
415
|
+
Just read a npy file. The assumption is the first axis is the number of
|
|
416
|
+
structures and then the rest are the dimensions.
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
filename (str): Path to the .npy file containing the structures.
|
|
420
|
+
"""
|
|
421
|
+
|
|
422
|
+
def __init__(self, filename, random_seed=None):
|
|
423
|
+
super().__init__(random_seed=random_seed)
|
|
424
|
+
self.data = np.load(filename)
|
|
425
|
+
self.index = 0
|
|
426
|
+
self.total = self.data.shape[0]
|
|
427
|
+
|
|
428
|
+
def sample(self):
|
|
429
|
+
"""Generate the next structure from the loaded data.
|
|
430
|
+
|
|
431
|
+
Returns:
|
|
432
|
+
np.ndarray: An array of shape corresponding to the loaded structures.
|
|
433
|
+
"""
|
|
434
|
+
if self.index >= self.total:
|
|
435
|
+
self.index = 0 # Reset to the beginning if we reach the end
|
|
436
|
+
generated = self.data[self.index]
|
|
437
|
+
self.index += 1
|
|
438
|
+
return generated
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
# pylint: disable=too-many-instance-attributes
|
|
442
|
+
class Reconstruction2D(GenerativeStructureGenerator):
|
|
443
|
+
"""Reconstruction-based generator using the MCRpy library.
|
|
444
|
+
|
|
445
|
+
Args:
|
|
446
|
+
N (list): Desired output size [nx, ny].
|
|
447
|
+
target_path (str or Path): Path to a target `.npy` file, or a folder
|
|
448
|
+
containing multiple `.npy` files.
|
|
449
|
+
|
|
450
|
+
Keyword Args:
|
|
451
|
+
random_seed (int): Seed for the random number generator.
|
|
452
|
+
descriptors (dict[str, float]): Mapping from descriptor name to weight.
|
|
453
|
+
Example: {"VolumeFractions": 5.0, "Correlations": 1.0}
|
|
454
|
+
Default is {"Correlations": 1.0} (recommended)
|
|
455
|
+
limit_to (int): Limit for descriptor convolutions.
|
|
456
|
+
"""
|
|
457
|
+
|
|
458
|
+
def __init__(
|
|
459
|
+
self,
|
|
460
|
+
N,
|
|
461
|
+
target_path,
|
|
462
|
+
random_seed=None,
|
|
463
|
+
descriptors=None,
|
|
464
|
+
limit_to=16,
|
|
465
|
+
):
|
|
466
|
+
super().__init__(N, random_seed=random_seed)
|
|
467
|
+
|
|
468
|
+
if self.dim != 2:
|
|
469
|
+
raise ValueError(
|
|
470
|
+
"Reconstruction2D only supports 2D structures for the moment."
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
# mcrpy required only if this generator is used
|
|
474
|
+
try:
|
|
475
|
+
self.mcrpy = importlib.import_module("mcrpy")
|
|
476
|
+
except ImportError as e:
|
|
477
|
+
raise RuntimeError(
|
|
478
|
+
"Reconstruction2D requires the MCRpy library. "
|
|
479
|
+
"To install in the active python environment:"
|
|
480
|
+
" pip install mcrpy"
|
|
481
|
+
) from e
|
|
482
|
+
|
|
483
|
+
# mcrpy expects np.product
|
|
484
|
+
if not hasattr(np, "product"):
|
|
485
|
+
np.product = np.prod
|
|
486
|
+
|
|
487
|
+
self.target_path = Path(target_path)
|
|
488
|
+
if not self.target_path.exists():
|
|
489
|
+
raise FileNotFoundError(f"Target file/folder not found: {self.target_path}")
|
|
490
|
+
|
|
491
|
+
if self.target_path.is_file():
|
|
492
|
+
self.target_files = [self.target_path]
|
|
493
|
+
elif self.target_path.is_dir():
|
|
494
|
+
self.target_files = sorted(self.target_path.glob("*.npy"))
|
|
495
|
+
if len(self.target_files) == 0:
|
|
496
|
+
raise FileNotFoundError(
|
|
497
|
+
f"No .npy files found in target folder: {self.target_path}"
|
|
498
|
+
)
|
|
499
|
+
else:
|
|
500
|
+
raise ValueError(
|
|
501
|
+
f"target_path must be a file or directory, got: {self.target_path}"
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
self.targets = []
|
|
505
|
+
common_shape = None
|
|
506
|
+
for f in self.target_files:
|
|
507
|
+
arr = np.asarray(np.load(f))
|
|
508
|
+
arr = np.squeeze(arr)
|
|
509
|
+
if arr.ndim != 2:
|
|
510
|
+
raise ValueError(f"Target '{f}' must be 2D, got shape {arr.shape}")
|
|
511
|
+
arr_bin = standardize_image(arr, dtype=int)
|
|
512
|
+
if common_shape is None:
|
|
513
|
+
common_shape = arr_bin.shape
|
|
514
|
+
else:
|
|
515
|
+
if arr_bin.shape != common_shape:
|
|
516
|
+
raise ValueError("All target arrays must have the same shape. ")
|
|
517
|
+
self.targets.append(arr_bin)
|
|
518
|
+
|
|
519
|
+
if len(self.targets) == 0:
|
|
520
|
+
raise RuntimeError("No valid targets were loaded.")
|
|
521
|
+
|
|
522
|
+
self.n_targets = len(self.targets)
|
|
523
|
+
self.target_shape = common_shape
|
|
524
|
+
self.target_vf = float(np.mean([np.mean(t) for t in self.targets]))
|
|
525
|
+
self.targets_ms = [self._array_to_microstructure(t) for t in self.targets]
|
|
526
|
+
|
|
527
|
+
# mcrpy settings
|
|
528
|
+
if descriptors is None:
|
|
529
|
+
descriptors = {"Correlations": 1.0}
|
|
530
|
+
allowed_descriptors = {
|
|
531
|
+
"VolumeFractions",
|
|
532
|
+
"Variation",
|
|
533
|
+
"TwoPointCorrelations",
|
|
534
|
+
"Correlations",
|
|
535
|
+
"FFTCorrelations",
|
|
536
|
+
"LinealPathApproximation",
|
|
537
|
+
}
|
|
538
|
+
invalid_keys = set(descriptors.keys()) - allowed_descriptors
|
|
539
|
+
if invalid_keys:
|
|
540
|
+
raise ValueError(
|
|
541
|
+
f"Invalid descriptor keys: {invalid_keys}. "
|
|
542
|
+
f"Allowed keys are: {allowed_descriptors}"
|
|
543
|
+
)
|
|
544
|
+
self.descriptor_types = list(descriptors.keys())
|
|
545
|
+
self.descriptor_weights = list(descriptors.values())
|
|
546
|
+
self.limit_to = limit_to
|
|
547
|
+
self.char_settings = self.mcrpy.CharacterizationSettings(
|
|
548
|
+
descriptor_types=self.descriptor_types,
|
|
549
|
+
limit_to=self.limit_to,
|
|
550
|
+
)
|
|
551
|
+
self.recon_settings = self.mcrpy.ReconstructionSettings(
|
|
552
|
+
descriptor_types=self.descriptor_types,
|
|
553
|
+
descriptor_weights=self.descriptor_weights,
|
|
554
|
+
limit_to=self.limit_to,
|
|
555
|
+
use_multigrid_reconstruction=True,
|
|
556
|
+
max_iter=200,
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
def _array_to_microstructure(self, arr):
|
|
560
|
+
ms = self.mcrpy.Microstructure(
|
|
561
|
+
arr,
|
|
562
|
+
use_multiphase=False,
|
|
563
|
+
)
|
|
564
|
+
return ms
|
|
565
|
+
|
|
566
|
+
def _make_descriptor(self):
|
|
567
|
+
"""Build a single descriptor dictionary consumable by MCRpy."""
|
|
568
|
+
descs = [
|
|
569
|
+
self.mcrpy.characterize(ms, self.char_settings) for ms in self.targets_ms
|
|
570
|
+
]
|
|
571
|
+
|
|
572
|
+
# If there is only one target, no averaging is needed.
|
|
573
|
+
if len(descs) == 1:
|
|
574
|
+
return descs[0]
|
|
575
|
+
|
|
576
|
+
# Build new descriptor by averaging the descriptors across all targets while
|
|
577
|
+
# keeping the settings from the first one (they are the same across all targets).
|
|
578
|
+
descriptor_dict = {}
|
|
579
|
+
for k in descs[0]:
|
|
580
|
+
if k == "settings":
|
|
581
|
+
descriptor_dict[k] = descs[0][k]
|
|
582
|
+
else:
|
|
583
|
+
descriptor_dict[k] = sum(d[k] for d in descs) / len(descs)
|
|
584
|
+
|
|
585
|
+
return descriptor_dict
|
|
586
|
+
|
|
587
|
+
def reconstruct(self, initial_array):
|
|
588
|
+
"""Run MCRpy reconstruction starting from `initial_array`."""
|
|
589
|
+
|
|
590
|
+
descriptor_dict = self._make_descriptor()
|
|
591
|
+
initial_ms = self._array_to_microstructure(initial_array)
|
|
592
|
+
|
|
593
|
+
_, reconstructed_ms = self.mcrpy.reconstruct(
|
|
594
|
+
descriptor_dict=descriptor_dict,
|
|
595
|
+
desired_shape=self.target_shape,
|
|
596
|
+
settings=self.recon_settings,
|
|
597
|
+
initial_microstructure=initial_ms,
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
generated = reconstructed_ms.decode_phases(raw=True)
|
|
601
|
+
generated = np.asarray(generated)
|
|
602
|
+
generated = np.squeeze(generated)
|
|
603
|
+
return generated
|
|
604
|
+
|
|
605
|
+
def sample(self):
|
|
606
|
+
|
|
607
|
+
# Control first guess for reproducibility
|
|
608
|
+
first_guess = self.rng.random(self.target_shape) < self.target_vf
|
|
609
|
+
|
|
610
|
+
generated = self.reconstruct(first_guess)
|
|
611
|
+
generated = standardize_image(generated, shape=(self.nx, self.ny), dtype=bool)
|
|
612
|
+
return generated
|
compgen/hdfwriter.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Copyright 2026, UChicago Argonne, LLC
|
|
2
|
+
# All Rights Reserved
|
|
3
|
+
# Software Name: compgen
|
|
4
|
+
# By: Argonne National Laboratory
|
|
5
|
+
# OPEN SOURCE LICENSE (MIT)
|
|
6
|
+
#
|
|
7
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
# in the Software without restriction, including without limitation the rights
|
|
10
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
# furnished to do so, subject to the following conditions:
|
|
13
|
+
#
|
|
14
|
+
# The above copyright notice and this permission notice shall be included in
|
|
15
|
+
# all copies or substantial portions of the Software.
|
|
16
|
+
#
|
|
17
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
23
|
+
# THE SOFTWARE.
|
|
24
|
+
|
|
25
|
+
"""Utilities for helping read/write HDF5 files."""
|
|
26
|
+
|
|
27
|
+
import h5py
|
|
28
|
+
import numpy as np
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def write_hdf5(filename, data_dict):
|
|
32
|
+
"""Write a dictionary of data to an HDF5 file.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
filename (str): Path to the output HDF5 file.
|
|
36
|
+
data_dict (dict): Dictionary containing data to write.
|
|
37
|
+
"""
|
|
38
|
+
with h5py.File(filename, "w") as hdf:
|
|
39
|
+
|
|
40
|
+
def recursive_write(group, data):
|
|
41
|
+
for key, value in data.items():
|
|
42
|
+
if isinstance(value, dict):
|
|
43
|
+
subgroup = group.create_group(key)
|
|
44
|
+
recursive_write(subgroup, value)
|
|
45
|
+
elif isinstance(value, list):
|
|
46
|
+
subgroup = group.create_group(key)
|
|
47
|
+
for i, item in enumerate(value):
|
|
48
|
+
subgroup.attrs[str(i)] = item
|
|
49
|
+
elif isinstance(value, (int, float, str)):
|
|
50
|
+
group.attrs[key] = value
|
|
51
|
+
else:
|
|
52
|
+
if isinstance(value, tuple):
|
|
53
|
+
group.create_dataset(key, data=np.array(value, dtype=int))
|
|
54
|
+
else:
|
|
55
|
+
group.create_dataset(key, data=value)
|
|
56
|
+
|
|
57
|
+
recursive_write(hdf, data_dict)
|