Repository: openmc-dev/openmc

Issue:
Add material nuclides using a dictionary.
At the PHYSOR '22 workshop, an attendee mentioned that it would be useful to specify nuclide by passing in a dictionary with nuclide strings as keys and tuples for the atom/weight percent or atom/b-cm. Something like:

```python
mat_dict = {'H1': (2.0, 'ao'),
            'O16': (1.0, 'ao')}
material.add_nuclides(mat_dict)
```




Discussion:

Resolving patch:
diff --git a/openmc/material.py b/openmc/material.py
--- a/openmc/material.py
+++ b/openmc/material.py
@@ -34,7 +34,9 @@ class Material(IDManagerMixin):
     To create a material, one should create an instance of this class, add
     nuclides or elements with :meth:`Material.add_nuclide` or
     :meth:`Material.add_element`, respectively, and set the total material
-    density with :meth:`Material.set_density()`. The material can then be
+    density with :meth:`Material.set_density()`. Alternatively, you can
+    use :meth:`Material.add_components()` to pass a dictionary
+    containing all the component information. The material can then be
     assigned to a cell using the :attr:`Cell.fill` attribute.
 
     Parameters
@@ -403,6 +405,54 @@ def add_nuclide(self, nuclide: str, percent: float, percent_type: str = 'ao'):
 
         self._nuclides.append(NuclideTuple(nuclide, percent, percent_type))
 
+    def add_components(self, components: dict, percent_type: str = 'ao'):
+        """ Add multiple elements or nuclides to a material
+
+        .. versionadded:: 0.13.1
+
+        Parameters
+        ----------
+        components : dict of str to float or dict
+            Dictionary mapping element or nuclide names to their atom or weight
+            percent. To specify enrichment of an element, the entry of
+            ``components`` for that element must instead be a dictionary
+            containing the keyword arguments as well as a value for
+            ``'percent'``
+        percent_type : {'ao', 'wo'}
+            'ao' for atom percent and 'wo' for weight percent
+
+        Examples
+        --------
+        >>> mat = openmc.Material()
+        >>> components  = {'Li': {'percent': 1.0,
+        >>>                       'enrichment': 60.0,
+        >>>                       'enrichment_target': 'Li7'},
+        >>>                'Fl': 1.0,
+        >>>                'Be6': 0.5}
+        >>> mat.add_components(components)
+
+        """
+
+        for component, params in components.items():
+            cv.check_type('component', component, str)
+            if isinstance(params, float):
+                params = {'percent': params}
+
+            else:
+                cv.check_type('params', params, dict)
+                if 'percent' not in params:
+                    raise ValueError("An entry in the dictionary does not have "
+                                     "a required key: 'percent'")
+
+            params['percent_type'] = percent_type
+
+            ## check if nuclide
+            if str.isdigit(component[-1]):
+                self.add_nuclide(component, **params)
+            else: # is element
+                kwargs = params
+                self.add_element(component, **params)
+
     def remove_nuclide(self, nuclide: str):
         """Remove a nuclide from the material
 


Test patch:
diff --git a/tests/unit_tests/test_material.py b/tests/unit_tests/test_material.py
--- a/tests/unit_tests/test_material.py
+++ b/tests/unit_tests/test_material.py
@@ -25,6 +25,52 @@ def test_add_nuclide():
     with pytest.raises(ValueError):
         m.add_nuclide('H1', 1.0, 'oa')
 
+def test_add_components():
+    """Test adding multipe elements or nuclides at once"""
+    m = openmc.Material()
+    components = {'H1': 2.0,
+                  'O16': 1.0,
+                  'Zr': 1.0,
+                  'O': 1.0,
+                  'U': {'percent': 1.0,
+                        'enrichment': 4.5},
+                  'Li': {'percent': 1.0,
+                         'enrichment': 60.0,
+                         'enrichment_target': 'Li7'},
+                  'H': {'percent': 1.0,
+                        'enrichment': 50.0,
+                        'enrichment_target': 'H2',
+                        'enrichment_type': 'wo'}}
+    m.add_components(components)
+    with pytest.raises(ValueError):
+        m.add_components({'U': {'percent': 1.0,
+                                'enrichment': 100.0}})
+    with pytest.raises(ValueError):
+        m.add_components({'Pu': {'percent': 1.0,
+                                 'enrichment': 3.0}})
+    with pytest.raises(ValueError):
+        m.add_components({'U': {'percent': 1.0,
+                                'enrichment': 70.0,
+                                'enrichment_target':'U235'}})
+    with pytest.raises(ValueError):
+        m.add_components({'He': {'percent': 1.0,
+                                 'enrichment': 17.0,
+                                 'enrichment_target': 'He6'}})
+    with pytest.raises(ValueError):
+        m.add_components({'li': 1.0})  # should fail as 1st char is lowercase
+    with pytest.raises(ValueError):
+        m.add_components({'LI': 1.0})  # should fail as 2nd char is uppercase
+    with pytest.raises(ValueError):
+        m.add_components({'Xx': 1.0})  # should fail as Xx is not an element
+    with pytest.raises(ValueError):
+        m.add_components({'n': 1.0})  # check to avoid n for neutron being accepted
+    with pytest.raises(TypeError):
+        m.add_components({'H1': '1.0'})
+    with pytest.raises(TypeError):
+        m.add_components({1.0: 'H1'}, percent_type = 'wo')
+    with pytest.raises(ValueError):
+        m.add_components({'H1': 1.0}, percent_type = 'oa')
+
 
 def test_remove_nuclide():
     """Test removing nuclides."""
@@ -49,7 +95,7 @@ def test_remove_elements():
     assert m.nuclides[0].percent == 1.0
 
 
-def test_elements():
+def test_add_element():
     """Test adding elements."""
     m = openmc.Material()
     m.add_element('Zr', 1.0)
@@ -74,7 +120,6 @@ def test_elements():
     with pytest.raises(ValueError):
         m.add_element('n', 1.0)  # check to avoid n for neutron being accepted
 
-
 def test_elements_by_name():
     """Test adding elements by name"""
     m = openmc.Material()
@@ -441,7 +486,7 @@ def test_activity_of_tritium():
     m1.add_nuclide("H3", 1)
     m1.set_density('g/cm3', 1)
     m1.volume = 1
-    assert pytest.approx(m1.activity) == 3.559778e14 
+    assert pytest.approx(m1.activity) == 3.559778e14
 
 
 def test_activity_of_metastable():