Skip to content

Commit 37e2feb

Browse files
yrrepyGuySten
andauthored
Flux Energy Group Conversion using lethargy-weighted redistribution (#3705)
Co-authored-by: GuySten <[email protected]>
1 parent 8c8867e commit 37e2feb

File tree

5 files changed

+209
-1
lines changed

5 files changed

+209
-1
lines changed

docs/source/pythonapi/mgxs.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,16 @@ Module Variables
1111
.. autodata:: openmc.mgxs.GROUP_STRUCTURES
1212
:annotation:
1313

14+
Functions
15+
+++++++++
16+
17+
.. autosummary::
18+
:toctree: generated
19+
:nosignatures:
20+
:template: myfunction.rst
21+
22+
openmc.mgxs.convert_flux_groups
23+
1424
Classes
1525
+++++++
1626

openmc/mgxs/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import numpy as np
22

3-
from openmc.mgxs.groups import EnergyGroups
3+
from openmc.mgxs.groups import EnergyGroups, convert_flux_groups
44
from openmc.mgxs.library import Library
55
from openmc.mgxs.mgxs import *
66
from openmc.mgxs.mdgxs import *

openmc/mgxs/groups.py

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,3 +300,139 @@ def merge(self, other):
300300
# Assign merged edges to merged groups
301301
merged_groups.group_edges = list(merged_edges)
302302
return merged_groups
303+
304+
305+
def convert_flux_groups(flux, source_groups, target_groups):
306+
"""Convert flux spectrum between energy group structures.
307+
308+
Uses flux-per-unit-lethargy conservation, which assumes constant flux per
309+
unit lethargy within each source group and distributes flux to target
310+
groups proportionally to their lethargy width.
311+
312+
.. versionadded:: 0.15.4
313+
314+
Parameters
315+
----------
316+
flux : Iterable of float
317+
Flux values for source groups. Length must equal
318+
source_groups.num_groups.
319+
source_groups : EnergyGroups or str
320+
Energy group structure of the input flux with boundaries in [eV].
321+
Can be an EnergyGroups instance or the name of a group structure
322+
(e.g., 'CCFE-709').
323+
target_groups : EnergyGroups or str
324+
Target energy group structure with boundaries in [eV]. Can be an
325+
EnergyGroups instance or the name of a group structure
326+
(e.g., 'UKAEA-1102').
327+
328+
Returns
329+
-------
330+
numpy.ndarray
331+
Flux values for target groups. Total flux is conserved for
332+
overlapping energy regions.
333+
334+
Raises
335+
------
336+
TypeError
337+
If source_groups or target_groups is not EnergyGroups or str
338+
ValueError
339+
If flux length doesn't match source_groups, or flux contains
340+
negative, NaN, or infinite values
341+
342+
See Also
343+
--------
344+
EnergyGroups : Energy group structure class
345+
346+
Notes
347+
-----
348+
The assumption of constant flux per unit lethargy within each source
349+
group is physically reasonable for most reactor spectra but is not
350+
exact. For best accuracy, use source spectra with sufficiently fine
351+
energy resolution.
352+
353+
Examples
354+
--------
355+
Convert FNS 709-group flux to UKAEA-1102 structure:
356+
357+
>>> import numpy as np
358+
>>> flux_709 = np.load('tests/fns_flux_709.npy')
359+
>>> flux_1102 = openmc.mgxs.convert_flux_groups(flux_709, 'CCFE-709', 'UKAEA-1102')
360+
361+
Convert using EnergyGroups instances:
362+
363+
>>> source = openmc.mgxs.EnergyGroups([1.0, 10.0, 100.0])
364+
>>> target = openmc.mgxs.EnergyGroups([1.0, 5.0, 10.0, 50.0, 100.0])
365+
>>> flux_target = openmc.mgxs.convert_flux_groups([1e8, 2e8], source, target)
366+
367+
References
368+
----------
369+
.. [1] J. J. Duderstadt and L. J. Hamilton, "Nuclear Reactor Analysis,"
370+
John Wiley & Sons, 1976.
371+
.. [2] M. Fleming and J.-Ch. Sublet, "FISPACT-II User Manual,"
372+
UKAEA-R(18)001, UK Atomic Energy Authority, 2018. See GRPCONVERT keyword.
373+
374+
"""
375+
# Handle string group structure names
376+
if isinstance(source_groups, str):
377+
source_groups = EnergyGroups(source_groups)
378+
if isinstance(target_groups, str):
379+
target_groups = EnergyGroups(target_groups)
380+
381+
# Type validation
382+
cv.check_type('source_groups', source_groups, EnergyGroups)
383+
cv.check_type('target_groups', target_groups, EnergyGroups)
384+
385+
# Convert flux to numpy array
386+
flux = np.asarray(flux, dtype=np.float64)
387+
if flux.ndim != 1:
388+
raise ValueError(f'flux must be 1-dimensional, got shape {flux.shape}')
389+
390+
# Validate flux length matches source groups
391+
if len(flux) != source_groups.num_groups:
392+
raise ValueError(
393+
f'Length of flux ({len(flux)}) must equal number of source '
394+
f'groups ({source_groups.num_groups})'
395+
)
396+
397+
# Check for invalid flux values
398+
if np.any(np.isnan(flux)):
399+
raise ValueError('flux contains NaN values')
400+
if np.any(np.isinf(flux)):
401+
raise ValueError('flux contains infinite values')
402+
if np.any(flux < 0):
403+
raise ValueError('flux values must be non-negative')
404+
405+
# Get energy edges
406+
source_edges = source_groups.group_edges
407+
target_edges = target_groups.group_edges
408+
num_target = target_groups.num_groups
409+
410+
# Initialize output array
411+
flux_target = np.zeros(num_target)
412+
413+
# Main conversion loop: distribute flux using lethargy weighting
414+
for idx_src, flux_src in enumerate(flux):
415+
if flux_src == 0:
416+
continue
417+
418+
e_low_src = source_edges[idx_src]
419+
e_high_src = source_edges[idx_src + 1]
420+
lethargy_src = np.log(e_high_src / e_low_src)
421+
422+
for idx_tgt in range(num_target):
423+
e_low_tgt = target_edges[idx_tgt]
424+
e_high_tgt = target_edges[idx_tgt + 1]
425+
426+
# Skip non-overlapping groups
427+
if e_high_tgt <= e_low_src or e_low_tgt >= e_high_src:
428+
continue
429+
430+
# Calculate overlap region
431+
e_low_overlap = max(e_low_src, e_low_tgt)
432+
e_high_overlap = min(e_high_src, e_high_tgt)
433+
lethargy_overlap = np.log(e_high_overlap / e_low_overlap)
434+
435+
# Distribute flux proportionally to lethargy fraction
436+
flux_target[idx_tgt] += flux_src * (lethargy_overlap / lethargy_src)
437+
438+
return flux_target

tests/fns_flux_709.npy

5.66 KB
Binary file not shown.
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"""Tests for openmc.mgxs.convert_flux_groups function."""
2+
3+
import numpy as np
4+
import pytest
5+
from pytest import approx
6+
7+
import openmc.mgxs
8+
9+
10+
def test_coarse_to_fine():
11+
"""Test coarse to fine conversion with flux conservation."""
12+
source = openmc.mgxs.EnergyGroups([1.0, 10.0, 100.0])
13+
target = openmc.mgxs.EnergyGroups([1.0, 5.0, 10.0, 50.0, 100.0])
14+
flux_source = np.array([1e8, 2e8])
15+
16+
flux_target = openmc.mgxs.convert_flux_groups(flux_source, source, target)
17+
18+
# Check conservation
19+
assert np.sum(flux_target) == approx(np.sum(flux_source))
20+
assert len(flux_target) == 4
21+
assert np.all(flux_target >= 0)
22+
23+
24+
def test_fine_to_coarse():
25+
"""Test fine to coarse conversion (reverse direction)."""
26+
source = openmc.mgxs.EnergyGroups([1.0, 5.0, 10.0, 50.0, 100.0])
27+
target = openmc.mgxs.EnergyGroups([1.0, 10.0, 100.0])
28+
flux_source = np.array([1e7, 2e7, 3e7, 4e7])
29+
30+
flux_target = openmc.mgxs.convert_flux_groups(flux_source, source, target)
31+
32+
assert np.sum(flux_target) == approx(np.sum(flux_source))
33+
assert len(flux_target) == 2
34+
35+
36+
def test_lethargy_distribution():
37+
"""Test that flux is distributed by lethargy, not linear energy."""
38+
# Single group from 1 to 100 eV
39+
source = openmc.mgxs.EnergyGroups([1.0, 100.0])
40+
# Split into two groups: 1-10 eV and 10-100 eV
41+
target = openmc.mgxs.EnergyGroups([1.0, 10.0, 100.0])
42+
flux_source = np.array([1e8])
43+
44+
flux_target = openmc.mgxs.convert_flux_groups(flux_source, source, target)
45+
46+
# Each target group spans one decade (ln(10) lethargy each)
47+
# So flux should be split 50/50 by lethargy
48+
assert flux_target[0] == approx(5e7)
49+
assert flux_target[1] == approx(5e7)
50+
51+
52+
def test_fns_ccfe709_to_ukaea1102():
53+
"""Test CCFE-709 to UKAEA-1102 conversion with real FNS flux spectrum."""
54+
from pathlib import Path
55+
flux_file = Path(__file__).parent.parent / 'fns_flux_709.npy'
56+
fns_flux_709 = np.load(flux_file)
57+
58+
flux_1102 = openmc.mgxs.convert_flux_groups(fns_flux_709, 'CCFE-709', 'UKAEA-1102')
59+
60+
assert len(flux_1102) == 1102
61+
assert np.sum(flux_1102) == approx(np.sum(fns_flux_709), rel=1e-10)
62+
assert np.all(flux_1102 >= 0)

0 commit comments

Comments
 (0)