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.
Files changed (37) hide show
  1. compgen/__init__.py +29 -0
  2. compgen/exoutil.py +43 -0
  3. compgen/filters.py +120 -0
  4. compgen/gen.py +612 -0
  5. compgen/hdfwriter.py +57 -0
  6. compgen/materials.py +78 -0
  7. compgen/mesh.py +507 -0
  8. compgen/moose.py +365 -0
  9. compgen/physics/2d/elastic/settings.json +56 -0
  10. compgen/physics/2d/elastic/template.i +209 -0
  11. compgen/physics/2d/plastic/settings.json +95 -0
  12. compgen/physics/2d/plastic/template.i +558 -0
  13. compgen/physics/2d/thermal_conductivity/settings.json +49 -0
  14. compgen/physics/2d/thermal_conductivity/template.i +215 -0
  15. compgen/physics/2d/thermal_conductivity_with_interface/settings.json +53 -0
  16. compgen/physics/2d/thermal_conductivity_with_interface/template.i +229 -0
  17. compgen/physics/2d/thermal_expansion/settings.json +55 -0
  18. compgen/physics/2d/thermal_expansion/template.i +233 -0
  19. compgen/physics/3d/elastic/settings.json +68 -0
  20. compgen/physics/3d/elastic/template.i +281 -0
  21. compgen/physics/3d/plastic/settings.json +245 -0
  22. compgen/physics/3d/plastic/template.i +658 -0
  23. compgen/physics/3d/thermal_conductivity/settings.json +53 -0
  24. compgen/physics/3d/thermal_conductivity/template.i +245 -0
  25. compgen/physics/3d/thermal_conductivity_with_interface/settings.json +57 -0
  26. compgen/physics/3d/thermal_conductivity_with_interface/template.i +259 -0
  27. compgen/physics/3d/thermal_expansion/settings.json +61 -0
  28. compgen/physics/3d/thermal_expansion/template.i +276 -0
  29. compgen/runner.py +407 -0
  30. compgen/temp.py +56 -0
  31. compgen/tensors.py +65 -0
  32. compgen/utils.py +180 -0
  33. compgen-0.1.0.dist-info/METADATA +105 -0
  34. compgen-0.1.0.dist-info/RECORD +37 -0
  35. compgen-0.1.0.dist-info/WHEEL +5 -0
  36. compgen-0.1.0.dist-info/licenses/LICENSE +23 -0
  37. 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)