Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Write and read material theoretical density to/from database #1852

Merged
merged 25 commits into from
Sep 23, 2024

Conversation

drewj-tp
Copy link
Contributor

@drewj-tp drewj-tp commented Sep 3, 2024

What is the change?

Added a new component parameter theoreticalDensityFrac. When loading from blueprints, the material's theoretical density is assigned to this parameter. Since it's a parameter, it will be written to the database and then loaded onto the component when reading from db. But, we need to reassign the theoretical density to the material because a lot of Material.density calls use this value.

This PR adds logic to the database loading sequence to apply the theoretical density component parameter to the material's theoretical density.

This PR also wraps Material.density in a decorator that will prefer to call self.parent.density when applicable.

Why is the change being made?

Closes #1863

Checklist

  • The release notes have been updated if necessary.
  • The documentation is still up-to-date in the doc folder.
  • The dependencies are still up-to-date in pyproject.toml.

@drewj-tp drewj-tp self-assigned this Sep 3, 2024
@keckler
Copy link
Member

keckler commented Sep 3, 2024

I have not done a detailed review of this yet, but a few comments:

  1. I have added @john-science as a reviewer, because we really need his eyes here
  2. In some ways, this PR is very related to Refactoring Material to not subclass Composite #1062. If you haven't seen that one, I encourage you to take a look.
  3. I also do not particularly like this solution. Based on my first look at it, this whole situation is very ugly and messy, and it seems to me like a symptom of a deeper problem with how we are handling materials and their relationship with components. Perhaps it is worth thinking about this some more before we continue down this path.
  4. Is there an issue that we need to link this PR against?
  5. There are some internal materials that we will probably have to migrate.

Anyways, thank you for the PR and for the thought that you put into it. Clearly this is not the simplest problem to solve!

@drewj-tp
Copy link
Contributor Author

drewj-tp commented Sep 4, 2024

Thanks @keckler. Lots of good points here.

Definitely feel the connection to #1062, in addition to #1140 and #1819: materials are not as stateless as we want them to be. And I think that's the deeper problem you're referencing. And that's a larger effort than this symptom (td_frac) but I'm open to discussing with you, @john-science @opotowsky and @albeanth.

Is there an issue that we need to link this PR against

Yesish? #1440 touches on TD_frac as a symptom of the statefulness. This PR doesn't close that entire issue but does address a part of it.

There are some internal materials that we will probably have to migrate.

This should be transparent to materials. It just reinforces / builds the connection between BP -> material modifications -> TD frac attribute <-> component property <-> database. I'll double check

@drewj-tp
Copy link
Contributor Author

drewj-tp commented Sep 4, 2024

Chatted w/ @john-science and a maybe better solution is to have Material.getTD fall back to self.parent.p.theoreticalDensityFrac because we have reasonable confidence these materials will have a component parameters (yes they have state still but this is an okay(ish) state to have). Similarly, Material.adjustTD would be an alternate setter for the parents td frac param.

This could also coincide w/ some deprecation warnings about removing the material td frac related things. But we'd need to make sure we shift the scaling by TD from the material side to the component side. e.g., from

# usage by components
dens = self.material.density()
# materials.py
def density(...):
    return actualDensity * self.getTD()

to

# usage
dens = self.getTD() * self.material.density()
# materials.py
def density(...):
    return actualDensity

Which wouldn't be the worst change. Just would need to make sure the change got propogated well.

@drewj-tp
Copy link
Contributor Author

drewj-tp commented Sep 4, 2024

@keckler @john-science I updated the approach to have Material theoretical density fallback to self.parent.p.theoreticalDensityFrac which kind of breaks statelessness but at least lets the data be written to / loaded from database correctly.

Should be good to go!

@drewj-tp drewj-tp marked this pull request as ready for review September 4, 2024 21:51
@drewj-tp
Copy link
Contributor Author

drewj-tp commented Sep 5, 2024

Had a chat with @keckler this morning and we both agree the recent changes here are better but still have some yuck. His main critique (correct me if I'm wrong) is that we have multiple sources of truth for the density. And Component.density() will diverge from Component.material.density() for reasons beyond theoretical density (depletion updates composition, thermal expansion). And that Component.density`, being derived from the compositions, is correctly written to / read from the database.

Therefore, if you were to examine a "from blueprints" reactor with a modified theoretical density compared to a "from database" reactor at the same timestep,

  1. Component.density would agree for the same component, but
  2. Component.material.density would not agree (due to the TD frac)

A solution we are circling around involves discouraging calls to Material.density except for when freshly creating a component. Or if there is a parent attached to the material. If we can find a way to enforce this (docs, warnings, maybe exceptions (later?)) then users doing restart analysis would see something like

>>> b = r.core[0][0]
>>> c = b[0]
>>> dens = c.material.density(...)
[warn] The density of a component is correct only from that component, not its material

Where the warning is logged if the density of a material with a parent is called.

We'd probably want to do something to consume this warning when we are initializing the component and/or for the first time. Somewhere around

def density(self):
"""Returns the mass density of the object in g/cc."""
density = composites.Composite.density(self)
if not density:
# possible that there are no nuclides in this component yet. In that case, defer to Material.
density = self.material.density(Tc=self.temperatureInC)
return density

But we also have a non-zero number of tests that look at the agreement between Component.density(...) and Component.material.density(...)

And, what do we do about methods like

def densityTimesHeatCapacity(self, Tk: float = None, Tc: float = None) -> float:
"""
Return heat capacity * density at a temperature.
Parameters
----------
Tk : float, optional
Temperature in Kelvin.
Tc : float, optional
Temperature in degrees Celsius
Returns
-------
rhoCP : float
Calculated value for the HT9 density* heat capacity
unit (J/m^3-K)
"""
Tc = getTc(Tc, Tk)
rhoCp = self.density(Tc=Tc) * 1000.0 * self.heatCapacity(Tc=Tc)
return rhoCp

that are convenience methods but still subject to this problem

We'd also need to make sure internal projects (and external users!) are avoiding calls to Component.material.density when appropriate.

@drewj-tp
Copy link
Contributor Author

drewj-tp commented Sep 5, 2024

Related to #1436

@john-science john-science added the enhancement New feature or request label Sep 5, 2024
@drewj-tp
Copy link
Contributor Author

drewj-tp commented Sep 9, 2024

PR builds are passing but push PRs are failing?

both failures are due to xs library read/write?

FAILED armi/nuclearDataIO/tests/test_xsLibraries.py::Isotxs_merge_Tests::test_cannotMergeXSLibWithSameNuclideNames - OSError: Failed to read/write <IsotxsIO /Users/runner/work/armi/armi/armi/nuclearDataIO/tests/fixtures/ISOAB> 


Traceback (most recent call last):
  File "/Users/runner/work/armi/armi/armi/nuclearDataIO/cccc/isotxs.py", line 329, in readWrite
    nuclideIO.rwNuclide()
  File "/Users/runner/work/armi/armi/armi/nuclearDataIO/cccc/isotxs.py", line 483, in rwNuclide
    self._rw7DRecord(blockNumIndex, subBlock)
  File "/Users/runner/work/armi/armi/armi/nuclearDataIO/cccc/isotxs.py", line 686, in _rw7DRecord
    dataVals.append(record.rwFloat(0.0))
                    ^^^^^^^^^^^^^^^^^^^
  File "/Users/runner/work/armi/armi/armi/nuclearDataIO/cccc/cccc.py", line 381, in rwFloat
    (f,) = struct.unpack("f", self._stream.read(self._floatSize))
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
struct.error: unpack requires a buffer of 4 bytes

🤷
Going to re-roll tests

@john-science
Copy link
Member

PR builds are passing but push PRs are failing?
🤷 Going to re-roll tests

Well, we can't have tests that only pass some of the time. That's no good. Maybe we'll run them 10 more times and see if they pass.

@john-science
Copy link
Member

A Linux unit test just failed:

FAILED armi/nuclearDataIO/tests/test_xsLibraries.py::Isotxs_merge_Tests::test_canRemoveIsotopes - OSError: Failed to read/write <IsotxsIO /home/runner/work/armi/armi/armi/nuclearDataIO/tests/fixtures/ISOAB> 


Traceback (most recent call last):
  File "/home/runner/work/armi/armi/armi/nuclearDataIO/cccc/isotxs.py", line 329, in readWrite
    nuclideIO.rwNuclide()
  File "/home/runner/work/armi/armi/armi/nuclearDataIO/cccc/isotxs.py", line 483, in rwNuclide
    self._rw7DRecord(blockNumIndex, subBlock)
  File "/home/runner/work/armi/armi/armi/nuclearDataIO/cccc/isotxs.py", line 686, in _rw7DRecord
    dataVals.append(record.rwFloat(0.0))
  File "/home/runner/work/armi/armi/armi/nuclearDataIO/cccc/cccc.py", line 381, in rwFloat
    (f,) = struct.unpack("f", self._stream.read(self._floatSize))
struct.error: unpack requires a buffer of 4 bytes
===== 1 failed, 2273 passed, 19 skipped, 26 warnings in 264.15s (0:04:24) ======

Interesting.

@drewj-tp
Copy link
Contributor Author

A Linux unit test just failed:

FAILED armi/nuclearDataIO/tests/test_xsLibraries.py::Isotxs_merge_Tests::test_canRemoveIsotopes - OSError: Failed to read/write <IsotxsIO /home/runner/work/armi/armi/armi/nuclearDataIO/tests/fixtures/ISOAB> 


Traceback (most recent call last):
  File "/home/runner/work/armi/armi/armi/nuclearDataIO/cccc/isotxs.py", line 329, in readWrite
    nuclideIO.rwNuclide()
  File "/home/runner/work/armi/armi/armi/nuclearDataIO/cccc/isotxs.py", line 483, in rwNuclide
    self._rw7DRecord(blockNumIndex, subBlock)
  File "/home/runner/work/armi/armi/armi/nuclearDataIO/cccc/isotxs.py", line 686, in _rw7DRecord
    dataVals.append(record.rwFloat(0.0))
  File "/home/runner/work/armi/armi/armi/nuclearDataIO/cccc/cccc.py", line 381, in rwFloat
    (f,) = struct.unpack("f", self._stream.read(self._floatSize))
struct.error: unpack requires a buffer of 4 bytes
===== 1 failed, 2273 passed, 19 skipped, 26 warnings in 264.15s (0:04:24) ======

Interesting.

Very interesting. Same errors I saw in #1852 (comment)

Maybe it's a race condition w/ the parallel tests? https://github.com/terrapower/armi/actions/runs/10797243037/job/29948075116?pr=1852#step:6:409

OSError: Failed to read/write <IsotxsIO /home/runner/work/armi/armi/armi/nuclearDataIO/tests/fixtures/ISOAB>

Defaults to one and will need to be updated in at least two modes

1. Creating from blueprints needs to pull from material,
2. Loading from database needs to assign to material.

Unfortunatly, the `setter` function does not provide access to the
`Component` so we can't do `self.component.material.assignTD` when
assigning a new value to the parameter.
A zero theoretical density might be something to avoid though. It's
non-physical except for a void material where there is literally
nothing...
I spent a while trying to figure out why the armi object attached to a parameter
collection was not the same as the object that held the parameter
collection. Or, why `self.p._ArmiObject is not self` evaluates to False
but `isinstance(self, self.p._ArmiObject)` does evalute to True. The
comment attached to this attribute mentions it is a class but so much
of armi revolves around this `node.parent` tree-like behavior that I felt
or wanted to have this tree-behavior extend to parameter collections.

This would make some work on the TD fraction easier because the
parameter setter would have easier access to the `ArmiObject`
**instance** that held that collection.
@drewj-tp
Copy link
Contributor Author

In the failed instances of this test, the following have all failed

  • FAILED armi/nuclearDataIO/tests/test_xsLibraries.py::Isotxs_merge_Tests::test_canRemoveIsotopes - OSError: Failed to read/write <IsotxsIO /home/runner/work/armi/armi/armi/nuclearDataIO/tests/fixtures/ISOAA>
  • FAILED armi/nuclearDataIO/tests/test_xsLibraries.py::Isotxs_merge_Tests::test_canRemoveIsotopes - OSError: Failed to read/write <IsotxsIO D:\a\armi\armi\armi\nuclearDataIO\tests\fixtures\ISOAB>
  • FAILED armi/nuclearDataIO/tests/test_xsLibraries.py::Isotxs_merge_Tests::test_cannotMergeXSLibWithSameNuclideNames - OSError: Failed to read/write <IsotxsIO /Users/runner/work/armi/armi/armi/nuclearDataIO/tests/fixtures/ISOAB>
  • FAILED armi/nuclearDataIO/tests/test_xsLibraries.py::Isotxs_merge_Tests::test_canRemoveIsotopes - OSError: Failed to read/write <IsotxsIO D:\a\armi\armi\armi\nuclearDataIO\tests\fixtures\ISOAA>

What can we learn from this?

  1. Confirming operating system agnostic - D:\a\armi\... and /home/runner/... and /Users/runner/...
  2. Two fixture files nuclearDataIO/tests/fixtures/ISOAA and nuclearDataIO/tests/fixtures/ISOAB
  3. Occurs in Isotxs_merge_Tests::test_canRemoveIsotopes and Isotxs_merge_Tests::test_cannotMergeXSLibWithSameNuclideNames

It's possible that separate threads are trying to read from the same fixture resource at the same time. But that should be okay. I've launched a bunch of separate threads all reading from the same file on linux and there are no errors with multiple processes reading the same file.

I updated the script to have some processes open the file for writing and some for reading. That also seems to be working, at least in terms of "the operating system does not yell at me."

So if multiple parallel workers are trying to read from these fixtures at the same time, we should be good.

If, and this is an unfounded if, some worker is writing to the fixture file while another process is reading it, and the fixture file is not fully complete by the time the reader process tries to read, then we could see these errors about not getting all the right bytes for a float that we see.

But these fixtures are tracked in the repository and should not be edited during tests. The tests in question create separate empty library objects and do merge actions on those empty libraries with fixture libraries.

The error traceback sure looks like we're trying to write a value no matter what in

Traceback (most recent call last):
  File "/Users/runner/work/armi/armi/armi/nuclearDataIO/cccc/isotxs.py", line 329, in readWrite
    nuclideIO.rwNuclide()
  File "/Users/runner/work/armi/armi/armi/nuclearDataIO/cccc/isotxs.py", line 483, in rwNuclide
    self._rw7DRecord(blockNumIndex, subBlock)
  File "/Users/runner/work/armi/armi/armi/nuclearDataIO/cccc/isotxs.py", line 686, in _rw7DRecord
    dataVals.append(record.rwFloat(0.0))
                    ^^^^^^^^^^^^^^^^^^^
  File "/Users/runner/work/armi/armi/armi/nuclearDataIO/cccc/cccc.py", line 381, in rwFloat
    (f,) = struct.unpack("f", self._stream.read(self._floatSize))

for _ in range(bandWidth):
dataVals.append(record.rwFloat(0.0))

despite the docstring saying it's only trying to read the record

def _rw7DRecord(self, blockNumIndex, subBlock):
"""
Read scatter matrix.

The files are also opened for reading in binary only during the well named isotxs.readBinary

Test class set up

This might be a red herring. But the test setup for the library merging tests are strange?

@classmethod
def setUpClass(cls):
cls.libAA = None
cls.libAB = None
cls.libCombined = None
cls.libLumped = None
@classmethod
def tearDownClass(cls):
cls.libAA = None
cls.libAB = None
cls.libCombined = None
cls.libLumped = None
del cls.libAA
del cls.libAB
del cls.libCombined
del cls.libLumped
def setUp(self):
TempFileMixin.setUp(self)
# load a library that is in the ARMI tree. This should
# be a small library with LFPs, Actinides, structure, and coolant
for attrName, path in [
("libAA", self.getLibAAPath),
("libAB", self.getLibABPath),
("libCombined", self.getLibAA_ABPath),
("libLumped", self.getLibLumpedPath),
]:
if getattr(self.__class__, attrName) is None:
setattr(self.__class__, attrName, self.getReadFunc()(path()))

There's a setUpClass that unsets class attributes. So only called once per process. That's good.

But then the setUp method for each instance on the test tries to set class attributes?

@john-science
Copy link
Member

Okay, we believe the strange test errors were not related to this branch. And we have fixed them in main.

doc/release/0.4.rst Outdated Show resolved Hide resolved
@drewj-tp
Copy link
Contributor Author

Test cases found the warning message produced a quarter of a million log entries. Draft while I 1) reduce that, and 2) find a more helpful way to track down calls to Material.denstiy

@drewj-tp drewj-tp marked this pull request as draft September 19, 2024 22:59
Using traceback, we can find the thing that called `Material.density`
when the material is attached to a `Component`. This information,
specifically the file name and line number, is used in the warning
message. The file name and line number is used as the label in the
warning so multiple locations that invoke this behavior will show up
as one logged message. But their number of invocations will also be
counted.
@drewj-tp
Copy link
Contributor Author

@john-science @keckler 8d6f624 introduced a mechanism to both reduce the number of log entries and provide a way to track down offending calls

[warn] Found call to Material.density in /path/to/a.py at line 126. Calls to Material.density when attached to a component have the potential to induce subtle differences as Component.density and Material.density can diverge.
[warn] Found call to Material.density in /path/to/b.py at line 237. Calls to Material.density when attached to a component have the potential to induce subtle differences as Component.density and Material.density can diverge.
[warn] Found call to Material.density in /path/to/b.py at line 238. Calls to Material.density when attached to a component have the potential to induce subtle differences as Component.density and Material.density can diverge.
[warn] Found call to Material.density in /path/to/c.py at line 189. Calls to Material.density when attached to a component have the potential to induce subtle differences as Component.density and Material.density can diverge.
[warn] Found call to Material.density in /path/to/c.py at line 207. Calls to Material.density when attached to a component have the potential to induce subtle differences as Component.density and Material.density can diverge.
[warn] Found call to Material.density in /path/to/c.py at line 215. Calls to Material.density when attached to a component have the potential to induce subtle differences as Component.density and Material.density can diverge.
[warn] Found call to Material.density in /path/to/c.py at line 74. Calls to Material.density when attached to a component have the potential to induce subtle differences as Component.density and Material.density can diverge.

[info] ----- Final Warning Count --------
[info]     COUNT                LABEL

[info]      222       Found call to Material.density in /path/to/a.py at line 126
[info]      1830      Found call to Material.density in /path/to/b.py at line 237
[info]      1830      Found call to Material.density in /path/to/b.py at line 238
[info]      3498      Found call to Material.density in /path/to/b.py at line 134
[info]      3498      Found call to Material.density in /path/to/b.py at line 135
[info]     13992      Found call to Material.density in /path/to/c.py at line 74
[info]     474765     Found call to Material.density in /path/to/c.py at line 189
[info]     474765     Found call to Material.density in /path/to/c.py at line 207
[info]     474765     Found call to Material.density in /path/to/c.py at line 215

@drewj-tp drewj-tp marked this pull request as ready for review September 20, 2024 14:58
return design.assemblies["fuel a"]
return design.assemblies[assem]

def loadB4CAssembly(self, materialModifications: str):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not convinced this method should exist. It's a one-line method, that just calls another method, and it is only used once.


def loadUZrAssembly(self, materialModifications):
yamlString = self.uZrInput + "\n" + materialModifications
return self._loadAssembly(self.uZrInput, materialModifications, "fuel a")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not convinced this method should exist. It's a one-line method, that just calls another method, and it is only used once.

Copy link
Member

@john-science john-science left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work!

@john-science john-science merged commit 136cb3a into main Sep 23, 2024
19 checks passed
@john-science john-science deleted the drewj/serialize-td-frac/1440 branch September 23, 2024 15:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Material theoretical density is not written to database and can be lost in restart runs
3 participants