Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/serpentTools/parsers/depmatrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,12 @@ def _read(self):
self.deltaT = float(line.split()[-1][:-1])

# process initial isotopics
# Serpent 2.2.3+ with ``set depmtx 1`` emits extra scalar lines
# (e.g. ``flx = 2;``, ``N0 = zeros(n, 1);``) between the time
# header and the first isotope entry. Advance past them.
line = stream.readline()
while line and not NDENS_REGEX.search(line):
line = stream.readline()
match = self._getMatch(line, NDENS_REGEX, 'n0 vector')
line = _parseIsoBlock(stream, tempN0, match, line, NDENS_REGEX)
numIso = len(tempN0)
Expand Down
35 changes: 27 additions & 8 deletions src/serpentTools/parsers/results.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
from collections import OrderedDict
from numbers import Real
import re
import warnings

from numpy import array, empty, asarray
from numpy import array, empty, asarray, object_ as np_object
from cycler import cycler
from matplotlib import rcParams
from matplotlib.pyplot import gca
Expand Down Expand Up @@ -155,18 +156,26 @@ class ListOfArrays(list):
----------
A : numpy.ndarray
Array where each row ``A[i]`` is the ``i``-th row or
sub-array that was appended
sub-array that was appended. If rows have inconsistent shapes
(e.g. Serpent 2.2.3+ mixes vector and scalar ``BURN_STEP``
entries for burnup and decay steps), this returns an object array.
"""

def __init__(self, values=None):
super().__init__()
self._shape = None
self._dtype = None
self._ragged = False
if values is not None:
self.append(values)

@property
def A(self):
if self._ragged:
out = empty(len(self), dtype=np_object)
for i, v in enumerate(self):
out[i] = v
return out
return array(self)

def append(self, value):
Expand All @@ -177,18 +186,28 @@ def append(self, value):
value : numpy.ndarray or array-like
Some object that can be coerced to a numpy.ndarray.
Must have same shape and data type (e.g. float) as
previously appended rows
previously appended rows. If shapes disagree (e.g. Serpent
2.2.3+ ``BURN_STEP`` mixes a 3-element vector for burnup
steps with a scalar for decay steps), a ``UserWarning`` is
issued and the list switches to ragged (object) mode.

"""
value = asarray(value)
if not self:
self._shape = value.shape
self._dtype = value.dtype
elif self._shape != value.shape:
raise ValueError(
"Shapes do not agree: Current {} vs. incoming {}".format(
self._shape, value.shape))
elif self._dtype != value.dtype:
elif not self._ragged and self._shape != value.shape:
warnings.warn(
"Shapes do not agree: Current {} vs. incoming {}. "
"This can occur when Serpent 2.2.3+ mixes vector and scalar "
"forms for a variable (e.g. BURN_STEP). "
"Switching to ragged (object) storage.".format(
self._shape, value.shape),
UserWarning,
stacklevel=3,
)
self._ragged = True
elif not self._ragged and self._dtype != value.dtype:
raise TypeError(
"Types do not agree: Current {} vs. incoming {}".format(
self._dtype, value.dtype))
Expand Down
27 changes: 27 additions & 0 deletions tests/test_depmtx.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,3 +324,30 @@ class SparseReadDepmtxFuncTester(ReadDepmtxFuncTesterBase, DenseTesterMixin):


del DepmtxTestHelper, DepmtxReaderTesterBase, ReadDepmtxFuncTesterBase


def test_depmtx_extra_header_lines(tmp_path):
"""Serpent 2.2.3 with ``set depmtx 1`` emits extra scalar/declaration
lines (e.g. ``flx = 2;``, ``N0 = zeros(n, 1);``) immediately after the
time header and before the N0 isotope data. The reader must skip these
and still parse the file correctly. Regression test for issue #533.
"""
ref_text = open(TEST_FILE).read()
# Insert extra lines after the first line (t = ...)
first_line, rest = ref_text.split('\n', 1)
extra = 'flx = 2;\nN0 = zeros(74, 1);\n'
modified_text = first_line + '\n' + extra + rest

modified_file = tmp_path / "depmtx_extra.m"
modified_file.write_text(modified_text)

reader_ref = DepmtxReader(TEST_FILE, sparse=False)
reader_ref.read()

reader_mod = DepmtxReader(str(modified_file), sparse=False)
reader_mod.read()

assert_array_equal(reader_mod.n0, reader_ref.n0)
assert_array_equal(reader_mod.n1, reader_ref.n1)
assert_array_equal(reader_mod.zai, reader_ref.zai)
assert reader_mod.deltaT == reader_ref.deltaT
45 changes: 45 additions & 0 deletions tests/test_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,48 @@ def test_nowarns_220(fake220File: str, logInterceptor: LoggerMixin):
rc["serpentVersion"] = "2.2.0"
serpentTools.read(fake220File, "results")
assert not logInterceptor.handler.logMessages


# --- Tests for ListOfArrays ragged handling (issue #537) ---

import warnings as _warnings
import numpy as _np
from serpentTools.parsers.results import ListOfArrays


def test_list_of_arrays_consistent_shapes():
"""Consistent shapes produce a stacked 2D numpy array."""
loa = ListOfArrays(_np.array([0.0, 1.0, 0.0]))
loa.append(_np.array([0.0, 2.0, 0.0]))
assert not loa._ragged
assert loa.A.shape == (2, 3)


def test_list_of_arrays_ragged_warns_and_stores():
"""Mixed shapes (e.g. Serpent 2.2.3 BURN_STEP) issue a UserWarning
and switch to object-dtype storage instead of raising ValueError."""
loa = ListOfArrays(_np.array([0.0, 1.0, 0.0])) # (3,) burnup step
with _warnings.catch_warnings(record=True) as w:
_warnings.simplefilter("always")
loa.append(_np.array([19.0])) # (1,) decay step
assert len(w) == 1
assert issubclass(w[0].category, UserWarning)
assert "Serpent 2.2.3+" in str(w[0].message)
assert loa._ragged
A = loa.A
assert A.dtype == object
assert A.shape == (2,)
assert _np.array_equal(A[0], _np.array([0.0, 1.0, 0.0]))
assert _np.array_equal(A[1], _np.array([19.0]))


def test_list_of_arrays_ragged_no_extra_warning():
"""Once in ragged mode, subsequent appends do not emit additional warnings."""
loa = ListOfArrays(_np.array([0.0, 1.0, 0.0]))
with _warnings.catch_warnings(record=True):
_warnings.simplefilter("always")
loa.append(_np.array([19.0])) # triggers ragged mode
with _warnings.catch_warnings(record=True) as w:
_warnings.simplefilter("always")
loa.append(_np.array([1.0, 0.0, 0.0])) # no extra warning
assert len(w) == 0
Loading