diff --git a/armi/reactor/blueprints/componentBlueprint.py b/armi/reactor/blueprints/componentBlueprint.py index 66caae6e3..f6b85228e 100644 --- a/armi/reactor/blueprints/componentBlueprint.py +++ b/armi/reactor/blueprints/componentBlueprint.py @@ -225,8 +225,66 @@ class can then use to modify the isotopics as necessary. constructedObject = components.factory(shape, [], kwargs) _setComponentFlags(constructedObject, self.flags, blueprint) insertDepletableNuclideKeys(constructedObject, blueprint) + + # set the custom density for non-custom material components after construction + self.setCustomDensity(constructedObject, blueprint, matMods) + return constructedObject + def setCustomDensity(self, constructedComponent, blueprint, matMods): + """Apply a custom density to a material with custom isotopics but not a 'custom material'.""" + if self.isotopics is None: + # No custom isotopics specified + return + + density = blueprint.customIsotopics[self.isotopics].density + if density is None: + # Nothing to do + return + + if density <= 0: + runLog.error( + "A zero or negative density was specified in a custom isotopics input. " + "This is not permitted, if a 0 density material is needed, use 'Void'. " + "The component is {} and the isotopics entry is {}.".format( + constructedComponent, self.isotopics + ) + ) + raise ValueError( + "A zero or negative density was specified in the custom isotopics for a component" + ) + + mat = materials.resolveMaterialClassByName(self.material)() + if not isinstance(mat, materials.Custom): + # check for some problem cases + if "TD_frac" in matMods.keys(): + runLog.warning( + "Both TD_frac and a custom density (custom isotopics) has been specified for " + "material {}. The custom density will override the density calculated using " + "TD_frac.".format(self.material) + ) + if not mat.density(Tc=self.Tinput) > 0: + runLog.error( + "A custom density has been assigned to material '{}', which has no baseline " + "density. Only materials with a starting density may be assigned a density. " + "This comes up e.g. if isotopics are assigned to 'Void'.".format( + self.material + ) + ) + raise ValueError( + "Cannot apply custom densities to materials without density." + ) + + densityRatio = density / constructedComponent.density() + constructedComponent.changeNDensByFactor(densityRatio) + + runLog.important( + "A custom material density was specified in the custom isotopics for non-custom " + "material {}. The component density has been altered to " + "{}.".format(mat, constructedComponent.density()), + single=True, + ) + def _conformKwargs(self, blueprint, matMods): """This method gets the relevant kwargs to construct the component.""" kwargs = {"mergeWith": self.mergeWith or "", "isotopics": self.isotopics or ""} diff --git a/armi/reactor/blueprints/isotopicOptions.py b/armi/reactor/blueprints/isotopicOptions.py index 077db9f21..1ce62c520 100644 --- a/armi/reactor/blueprints/isotopicOptions.py +++ b/armi/reactor/blueprints/isotopicOptions.py @@ -393,15 +393,12 @@ def apply(self, material): material.massFrac = dict(self.massFracs) if self.density is not None: if not isinstance(material, materials.Custom): - runLog.warning( - "You either specified a custom mass density or number densities " - "(which implies a mass density) on `{}` with custom isotopics `{}`. " - "This has no effect on this Material class; you can only " - "override mass density on `Custom` " - "materials. Consider switching to number fraction input. " - "Continuing to use {} mass density.".format( - material, self.name, material - ) + runLog.important( + "A custom density or number densities has been specified for non-custom " + "material {}. The material object's density will not be updated to prevent unintentional " + "density changes across the model. Only custom materials may have a density " + "specified.".format(material), + single=True, ) # specifically, non-Custom materials only use refDensity and dLL, mat.customDensity has no effect return diff --git a/armi/reactor/blueprints/tests/test_customIsotopics.py b/armi/reactor/blueprints/tests/test_customIsotopics.py index 3366a3c36..4c341b45b 100644 --- a/armi/reactor/blueprints/tests/test_customIsotopics.py +++ b/armi/reactor/blueprints/tests/test_customIsotopics.py @@ -14,6 +14,7 @@ """Unit test custom isotopics.""" import unittest +from logging import DEBUG import yamlize @@ -22,10 +23,12 @@ from armi.reactor import blueprints from armi.reactor.blueprints import isotopicOptions from armi.reactor.flags import Flags +from armi import runLog +from armi.tests import mockRunLogs class TestCustomIsotopics(unittest.TestCase): - yamlString = r""" + yamlPreamble = r""" nuclide flags: U238: {burn: true, xs: true} U235: {burn: true, xs: true} @@ -75,15 +78,6 @@ class TestCustomIsotopics(unittest.TestCase): U234: 0.000054 density: 19.1 - # >>> from armi.nucDirectory import elements, nuclideBases - # >>> import numpy - # >>> u = elements.bySymbol['U'] - # >>> w_i = numpy.array([n.abundance for n in u.getNaturalIsotopics()]) - # >>> Mi = numpy.array([n.weight for n in u.getNaturalIsotopics()]) - # >>> Ni = w_i * 19.1 * 6.0221e23 / Mi - # >>> N_norm = Ni / sum(Ni) - # >>> N_norm.round(6) - # array([ 5.50000000e-05, 7.29500000e-03, 9.92650000e-01]) uranium isotopic number fractions: input format: number fractions U238: 0.992650 @@ -91,21 +85,26 @@ class TestCustomIsotopics(unittest.TestCase): U234: 0.000055 density: 19.1 - # >>> from armi.nucDirectory import elements, nuclideBases - # >>> import numpy - # >>> u = elements.bySymbol['U'] - # >>> Mi = numpy.array([n.weight for n in u.getNaturalIsotopics()]) - # >>> w_i = numpy.array([n.abundance for n in u.getNaturalIsotopics()]) - # >>> Ni = 19.1 * w_i * 6.0221e23 / Mi - # array([ 2.65398007e+18, 3.52549755e+20, 4.79692055e+22]) - # >>> for n, ni in zip(u.getNaturalIsotopics(), Ni): - # >>> print ' {}: {:.7e}'.format(n.name, ni) # requires 7 decimal places! uranium isotopic number densities: &u_isotopics input format: number densities U234: 2.6539102e-06 U235: 3.5254048e-04 U238: 4.7967943e-02 + bad uranium isotopic mass fractions: + input format: mass fractions + U238: 0.992742 + U235: 0.007204 + U234: 0.000054 + density: 0 + + negative uranium isotopic mass fractions: + input format: mass fractions + U238: 0.992742 + U235: 0.007204 + U234: 0.000054 + density: -1 + linked uranium number densities: *u_isotopics steel: @@ -114,6 +113,9 @@ class TestCustomIsotopics(unittest.TestCase): C: 0.3 density: 7.0 +""" + + yamlGoodBlocks = r""" blocks: uzr fuel: &block_0 fuel: &basic_fuel @@ -163,7 +165,72 @@ class TestCustomIsotopics(unittest.TestCase): material: Custom isotopics: linked uranium number densities - steel: &block_6 + fuel with no modifications: &block_6 # after a custom density has been set + fuel: + <<: *basic_fuel + + overspecified fuel: &block_7 + fuel: + <<: *basic_fuel + material: UraniumOxide + isotopics: uranium isotopic number densities + + density set via number density: &block_8 + fuel: + <<: *basic_fuel + isotopics: uranium isotopic number densities + + steel: &block_9 + clad: + shape: Hexagon + material: Custom + isotopics: steel + Tinput: 25.0 + Thot: 600.0 + ip: 0.0 + mult: 169.0 + op: 0.86602 + +assemblies: + fuel a: &assembly_a + specifier: IC + blocks: [*block_0, *block_1, *block_2, *block_3, *block_4, *block_5, *block_6, *block_7, *block_8, *block_9] + height: [10, 10, 10, 10, 10, 10, 10, 10, 10, 10] + axial mesh points: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] + xs types: [A, A, A, A, A, A, A, A, A, A] + material modifications: + TD_frac: ["", "", "", "", "", "", "", 0.1, "", ""] + +""" + + yamlBadBlocks = r""" +blocks: + uzr fuel: &block_0 + fuel: &basic_fuel + shape: Hexagon + material: UZr + Tinput: 25.0 + Thot: 600.0 + ip: 0.0 + mult: 1.0 + op: 10.0 + + clad: + shape: Circle + material: HT9 + Tinput: 25.0 + Thot: 600.0 + id: 0.0 + mult: 1.0 + od: 10.0 + + custom void: &block_1 + fuel: + <<: *basic_fuel + material: Void + isotopics: uranium isotopic number densities + + steel: &block_2 clad: shape: Hexagon material: Custom @@ -174,16 +241,65 @@ class TestCustomIsotopics(unittest.TestCase): mult: 169.0 op: 0.86602 + no density uo2: &block_3 + fuel: + <<: *basic_fuel + material: UraniumOxide + isotopics: uranium isotopic number densities + + no density uo2: &block_4 + fuel: + <<: *basic_fuel + material: UraniumOxide + isotopics: bad uranium isotopic mass fractions + + no density uo2: &block_5 + fuel: + <<: *basic_fuel + material: UraniumOxide + isotopics: bad uranium isotopic mass fractions assemblies: fuel a: &assembly_a specifier: IC - blocks: [*block_0, *block_1, *block_2, *block_3, *block_4, *block_5, *block_6] - height: [10, 10, 10, 10, 10, 10,10] - axial mesh points: [1, 1, 1, 1, 1, 1,1] - xs types: [A, A, A, A, A, A,A] + blocks: [*block_0, *block_1, *block_2] + height: [10, 10, 10] + axial mesh points: [1, 1, 1] + xs types: [A, A, A] + material modifications: + TD_frac: ["", "", ""] + + fuel b: &assembly_b + specifier: IC + blocks: [*block_0, *block_3, *block_2] + height: [10, 10, 10] + axial mesh points: [1, 1, 1] + xs types: [A, A, A] + material modifications: + TD_frac: ["", "0.0", ""] # set density to 0 to cause error in custom density + + fuel c: &assembly_c + specifier: IC + blocks: [*block_0, *block_4, *block_2] + height: [10, 10, 10] + axial mesh points: [1, 1, 1] + xs types: [A, A, A] + + fuel d: &assembly_d + specifier: IC + blocks: [*block_0, *block_5, *block_2] + height: [10, 10, 10] + axial mesh points: [1, 1, 1] + xs types: [A, A, A] + """ + + # this yaml is supposed to successfully build + yamlString = yamlPreamble + yamlGoodBlocks + + # This yaml is designed to raise an error when built + yamlStringWithError = yamlPreamble + yamlBadBlocks """:meta hide-value:""" @classmethod @@ -216,18 +332,89 @@ def test_massFractionsAreApplied(self): :id: T_ARMI_MAT_USER_INPUT3 :tests: R_ARMI_MAT_USER_INPUT """ - fuel0 = self.a[0].getComponent(Flags.FUEL) fuel1 = self.a[1].getComponent(Flags.FUEL) fuel2 = self.a[2].getComponent(Flags.FUEL) self.assertEqual(self.numCustomNuclides, len(fuel1.p.numberDensities)) self.assertAlmostEqual(19.1, fuel1.density()) - # density only works with a Custom material type. - self.assertAlmostEqual(fuel0.density(), fuel2.density()) self.assertEqual( set(fuel2.p.numberDensities.keys()), set(fuel1.p.numberDensities.keys()) ) # keys are same + def test_densitiesAppliedToNonCustomMaterials(self): + """Ensure that a density can be set in custom isotopics for components using library materials.""" + # The template block + fuel0 = self.a[0].getComponent(Flags.FUEL) + # The block with custom density but not the 'Custom' material + fuel2 = self.a[2].getComponent(Flags.FUEL) + # A block like the template block, but made after the custom block + fuel6 = self.a[6].getComponent(Flags.FUEL) + # A block with custom density set via number density + fuel8 = self.a[8].getComponent(Flags.FUEL) + + # Check that the density is set correctly on the custom density block, + # and that it is not the same as the original + self.assertAlmostEqual(19.1, fuel2.density()) + self.assertNotAlmostEqual(fuel0.density(), fuel2.density(), places=2) + # Check that the custom density block has the correct material + self.assertEqual("UZr", fuel2.material.name) + # Check that the block with only number densities set has a new density + self.assertAlmostEqual(19.1, fuel8.density()) + # original material density should not be changed after setting a custom density component, + # so a new block without custom isotopics and density should have the same density as the original + self.assertAlmostEqual(fuel6.density(), fuel0.density()) + self.assertEqual(fuel6.material.name, fuel0.material.name) + self.assertEqual("UZr", fuel0.material.name) + + def test_customDensityLogsAndErrors(self): + """Test that the right warning messages and errors are emitted when applying custom densities.""" + # Check for warnings when specifying both TD_frac and custom isotopics + with mockRunLogs.BufferLog() as mockLog: + # we should start with a clean slate + self.assertEqual("", mockLog.getStdout()) + runLog.LOG.startLog("test_customDensityLogsAndErrors") + runLog.LOG.setVerbosity(DEBUG) + + # rebuild the input to capture the logs + cs = settings.Settings() + cs = cs.modified(newSettings={CONF_XS_KERNEL: "MC2v2"}) + bp = blueprints.Blueprints.load(self.yamlString) + bp.constructAssem(cs, name="fuel a") + + # Check for log messages + streamVal = mockLog.getStdout() + self.assertIn("Both TD_frac and a custom density", streamVal, msg=streamVal) + self.assertIn( + "A custom material density was specified", streamVal, msg=streamVal + ) + self.assertIn( + "A custom density or number densities has been specified", + streamVal, + msg=streamVal, + ) + + # Check that assigning a custom density to the Void material fails + cs = settings.Settings() + cs = cs.modified(newSettings={CONF_XS_KERNEL: "MC2v2"}) + bp = blueprints.Blueprints.load(self.yamlStringWithError) + # Ensure we have some Void + self.assertEqual(bp.blockDesigns["custom void"]["fuel"].material, "Void") + # Can't have stuff in Void + with self.assertRaises(ValueError): + bp.constructAssem(cs, name="fuel a") + + # Try making a 0 density non-Void material by setting TD_frac to 0.0 + with self.assertRaises(ValueError): + bp.constructAssem(cs, name="fuel b") + + # Try making a material with mass fractions with a density of 0 + with self.assertRaises(ValueError): + bp.constructAssem(cs, name="fuel c") + + # Try making a material with mass fractions with a negative density + with self.assertRaises(ValueError): + bp.constructAssem(cs, name="fuel d") + def test_numberFractions(self): """Ensure that the custom isotopics can be specified via number fractions. diff --git a/armi/tests/test_lwrInputs.py b/armi/tests/test_lwrInputs.py index 93c10a111..b2e1934d9 100644 --- a/armi/tests/test_lwrInputs.py +++ b/armi/tests/test_lwrInputs.py @@ -61,8 +61,6 @@ def test_loadC5G7(self): # test warnings are being logged for malformed isotopics info in the settings file streamVal = mock.getStdout() - self.assertGreater(streamVal.count("[warn]"), 32, msg=streamVal) - self.assertGreater(streamVal.count("custom isotopics"), 32, msg=streamVal) self.assertIn("UraniumOxide", streamVal, msg=streamVal) self.assertIn("SaturatedWater", streamVal, msg=streamVal) self.assertIn("invalid settings: fakeBad", streamVal, msg=streamVal) diff --git a/armi/tests/tutorials/c5g7-blueprints.yaml b/armi/tests/tutorials/c5g7-blueprints.yaml index 29d8a9b26..0a4770719 100644 --- a/armi/tests/tutorials/c5g7-blueprints.yaml +++ b/armi/tests/tutorials/c5g7-blueprints.yaml @@ -162,7 +162,6 @@ blocks: # has same od as fuel, so there needs to be something in the gap. shape: Circle material: Void - isotopics: moderator Tinput: 25.0 Thot: 25.0 id: fission chamber.od diff --git a/doc/release/0.3.rst b/doc/release/0.3.rst index dbff151f5..ca6fdaf8b 100644 --- a/doc/release/0.3.rst +++ b/doc/release/0.3.rst @@ -13,6 +13,7 @@ New Features #. System information is now also logged on Linux. (`PR#1689 `_) #. Reset ``Reactor`` data on worker processors after every interaction to free memory from state distribution. (`PR#1729 `_ and `PR#1750 `_) +#. Density can be specified for components via ``custom isotopics`` in the blueprints. (`PR#1745 `_) #. TBD API Changes diff --git a/doc/user/inputs.rst b/doc/user/inputs.rst index 4d69b7cd3..ea5216540 100644 --- a/doc/user/inputs.rst +++ b/doc/user/inputs.rst @@ -1081,6 +1081,30 @@ nuclear data library). The (mass) ``density`` input is invalid when specifying ``number densities``; the code will present an error message. +Material density may be specified in custom isotopics either explicitly in a ``mass fractions`` input +format (shown above) or implicitly with ``number densities``. This is fairly straightforward for the +``Custom`` material, as it has no baseline density. Density may also be specified for components using +materials which have entries in the materials library. Users should be aware of the following interactions +when specifying a custom density for components using a library material: + + 1. The library material density will not be changed. Only the component(s) with the custom isotopics + entry will have the density modification. + + 2. Density specified by custom isotopics will override all other density modifications in the component + construction phase (e.g. ``TD_frac`` entries). + + 3. Only the component density is changed, not other material properties are altered to account for the + change in composition/density. + + 4. Density can only be specified using custom isotopics for non- ``Custom`` materials that have some + initial density. Don't try to make ``Void`` have mass! + +Densities specified using ``Custom Isotopics`` are applied in component construction, and should be specified +at the input temperature for the component. Note that when overriding the density of a library material, all +other properties of that material (e.g. expansion coefficients) will continue to be used as if the component +consisted of the library material. In other words, ARMI will still think the component is made out of the +original material! + Advanced topics ---------------