Iterative Source Reweighting#
This one-group fixed-source example demonstrates partial model updates across
several runs of one mcdc.Simulation. A homogeneous slab contains
symmetric left and right sources. Each iteration changes only their relative
probabilities, compiles a complete new snapshot, and writes a separate output
file.
The three source mixtures are 20/80, 50/50, and 80/20. Because the geometry and material are symmetric, the 20/80 and 80/20 flux profiles should approximately mirror one another, while the 50/50 profile should be approximately symmetric about the slab midpoint.
Full Input#
1import numpy as np
2
3import mcdc
4
5simulation = mcdc.Simulation("Iterative source reweighting")
6
7# Homogeneous one-group slab
8material = mcdc.MaterialMG(
9 capture=np.array([0.2]),
10 scatter=np.array([[0.8]]),
11)
12left_boundary = mcdc.Surface.PlaneZ(z=0.0, boundary_condition="vacuum")
13right_boundary = mcdc.Surface.PlaneZ(z=10.0, boundary_condition="vacuum")
14slab = mcdc.Cell(
15 region=+left_boundary & -right_boundary,
16 fill=material,
17)
18simulation.set_model([slab])
19
20# Symmetric source regions
21source_left = mcdc.Source(
22 name="Left source",
23 z=[1.0, 2.0],
24 isotropic=True,
25 energy_group=0,
26)
27source_right = mcdc.Source(
28 name="Right source",
29 z=[8.0, 9.0],
30 isotropic=True,
31 energy_group=0,
32)
33simulation.set_sources([source_left, source_right])
34
35# One tally is reused by every iteration
36mesh = mcdc.MeshStructured(z=np.linspace(0.0, 10.0, 51))
37flux_tally = mcdc.Tally(
38 name="source_mix_flux",
39 mesh=mesh,
40 scores=["flux"],
41)
42simulation.set_tallies([flux_tally])
43
44simulation.settings.N_particle = 20_000
45simulation.settings.N_batch = 10
46
47# Partially update the same owned model and compile a fresh snapshot each time.
48source_mixes = (
49 ("left_20", 0.2),
50 ("left_50", 0.5),
51 ("left_80", 0.8),
52)
53
54for case_name, left_fraction in source_mixes:
55 source_left.probability = left_fraction
56 source_right.probability = 1.0 - left_fraction
57 simulation.settings.output_name = f"source_mix_{case_name}"
58
59 simulation.compile()
60 simulation.run()
Post-processing#
1from pathlib import Path
2
3import h5py
4import matplotlib.pyplot as plt
5import numpy as np
6
7cases = (
8 ("left_20", "20% left / 80% right"),
9 ("left_50", "50% left / 50% right"),
10 ("left_80", "80% left / 20% right"),
11)
12
13profiles = {}
14uncertainties = {}
15z = None
16
17for case_name, _ in cases:
18 output_path = Path(f"source_mix_{case_name}.h5")
19 with h5py.File(output_path, "r") as output:
20 tally = output["tallies/source_mix_flux"]
21 case_z = tally["grid/z"][:]
22 flux = tally["flux/mean"][:]
23 flux_sdev = tally["flux/sdev"][:]
24
25 if z is None:
26 z = case_z
27 elif not np.array_equal(z, case_z):
28 raise ValueError(f"Inconsistent tally grid in {output_path}")
29
30 dz = case_z[1:] - case_z[:-1]
31 profiles[case_name] = flux / dz
32 uncertainties[case_name] = flux_sdev / dz
33
34z_mid = 0.5 * (z[:-1] + z[1:])
35dz = z[1:] - z[:-1]
36left_half = z_mid < 5.0
37right_half = ~left_half
38
39print("Integrated flux comparison")
40print("--------------------------")
41for case_name, label in cases:
42 profile = profiles[case_name]
43 left_flux = np.sum(profile[left_half] * dz[left_half])
44 right_flux = np.sum(profile[right_half] * dz[right_half])
45 print(
46 f"{label:24s} left={left_flux:.6e} right={right_flux:.6e} "
47 f"left/right={left_flux / right_flux:.4f}"
48 )
49
50mirror_difference = np.linalg.norm(profiles["left_20"] - profiles["left_80"][::-1])
51mirror_scale = np.linalg.norm(0.5 * (profiles["left_20"] + profiles["left_80"][::-1]))
52balanced_difference = np.linalg.norm(profiles["left_50"] - profiles["left_50"][::-1])
53balanced_scale = np.linalg.norm(profiles["left_50"])
54
55print()
56print(
57 "20/80 versus mirrored 80/20 relative RMS difference: "
58 f"{mirror_difference / mirror_scale:.4e}"
59)
60print(
61 "50/50 profile relative left-right asymmetry: "
62 f"{balanced_difference / balanced_scale:.4e}"
63)
64
65figure, axis = plt.subplots()
66for case_name, label in cases:
67 profile = profiles[case_name]
68 uncertainty = uncertainties[case_name]
69 axis.plot(z_mid, profile, label=label)
70 axis.fill_between(
71 z_mid,
72 profile - uncertainty,
73 profile + uncertainty,
74 alpha=0.15,
75 )
76
77axis.axvline(5.0, color="black", linestyle="--", linewidth=1.0)
78axis.set_xlabel("z [cm]")
79axis.set_ylabel("Flux")
80axis.grid()
81axis.legend()
82figure.tight_layout()
83figure.savefig("iterative_source_comparison.png", dpi=150)
84plt.close(figure)
The post-processing script overlays the three spatial flux profiles, prints the integrated flux in each half of the slab, and reports two symmetry comparisons.
How to Run#
From inside examples/iterative_source_reweighting:
python input.py
python process-output.py
The calculation writes source_mix_left_20.h5,
source_mix_left_50.h5, and source_mix_left_80.h5. Post-processing
writes iterative_source_comparison.png.