Repository: idaholab/MontePy

Issue:
Search for Material Nuclides containing just elemental data.
**Is your feature request related to a problem? Please describe.**

Sometimes you get a model with legacy nuclear data, e.g., `26000.50c` that you want to replace.
However, if you do `problem.materials.get_containing("26000.50c")` you will get all iron isotopes.
It's unclear right now if the library extension is being matched (needs verification).

While in some cases this is a nice behavior, its not always the best. 

**Describe the solution you'd like**

I think an `strict` parameter would be helpful.

So `problem.materials.get_containing("26000.50c", strict=True)` would exactly match the required nuclide.
This would also be helpful for finding library-less nuclides. 

**Describe alternatives you've considered**

The alternatives might be:

``` python

for material in problem.materials:
    for nuclide in material.nuclides:
        if nuclide == "2600.50c": 
             yield material
             break
```

**Additional context**
This is for version 1.0.0a1.




Discussion:

Patch:
diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst
index dc907ef1..6c16d227 100644
--- a/doc/source/changelog.rst
+++ b/doc/source/changelog.rst
@@ -14,7 +14,7 @@ MontePy Changelog
 * Made it easier to create an Isotope (now Nuclide): ``montepy.Nuclide("H-1.80c")`` (:issue:`505`).
 * When a typo in an object attribute is made an Error is raised rather than silently having no effect (:issue:`508`).
 * Improved material printing to avoid very long lists of components (:issue:`144`).
-* Allow querying for materials by components (:issue:`95`).
+* Allow querying for materials by components (:issue:`95`), either broadly or specifically (:issue:`642`).
 * Added support for getting and setting default libraries, e.g., ``nlib``, from a material (:issue:`369`).
 * Added most objects to the top level so they can be accessed like: ``montepy.Cell``.
 * Made ``Material.is_atom_fraction`` settable (:issue:`511`). 
diff --git a/doc/source/starting.rst b/doc/source/starting.rst
index 13aa37bb..e85445f3 100644
--- a/doc/source/starting.rst
+++ b/doc/source/starting.rst
@@ -409,14 +409,15 @@ For surfaces this is: :func:`~montepy.surfaces.surface_builder.parse_surface`,
 and for data inputs this is :func:`~montepy.data_inputs.data_parser.parse_data`.
 
 .. doctest::
+
    >>> surf = montepy.parse_surface("1 cz 5.0")
    >>> type(surf)
-   foo
+   <class 'montepy.surfaces.cylinder_on_axis.CylinderOnAxis'>
    >>> surf.radius
    5.0
    >>> mat = montepy.parse_data("m1 1001.80c 1")
    >>> type(mat)
-   foo
+   <class 'montepy.data_inputs.material.Material'>
 
 
 This object is still unlinked from other objects, and won't be kept with a problem.
@@ -1019,21 +1020,28 @@ on the element, not just the elemental nuclide.
     >>> montepy.Nuclide("B-0") in mat
     True
 
-For more complicated checks there is the :func:`~montepy.data_inputs.material.Material.contains`.
-This takes a plurality of nuclides as well as a threshold.
-This returns ``True`` if and only if the material contains *all* nuclides
+For more complicated checks there is the :func:`~montepy.data_inputs.material.Material.contains_all`, and 
+:func:`~montepy.data_inputs.material.Material.contains_any`.
+These functions take a plurality of nuclides as well as a threshold.
+The function ``contains_all`` returns ``True`` if and only if the material contains *all* nuclides
+with a fraction above the threshold.
+The function ``contains_any`` returns ``True`` if any of the material contains *any* nuclides
 with a fraction above the threshold.
 
 .. doctest::
 
-    >>> mat.contains("H-1.80c")
+    >>> mat.contains_all("H-1.80c")
     False
-    >>> mat.contains("U-235", "U-238", threshold=1.0)
+    >>> mat.contains_all("U-235", "U-238", threshold=1.0)
     True
-    >>> mat.contains("U-235.80c", "B-10")
+    >>> mat.contains_all("U-235.80c", "B-10")
     True
-    >>> mat.contains("U-235.80c", "B-10", threshold=1e-3)
+    >>> mat.contains_all("U-235.80c", "B-10", threshold=1e-3)
     False
+    >>> mat.contains_all("H-1.80c", "U-235.80c")
+    False
+    >>> mat.contains_any("H-1.80c", "U-235.80c")
+    True
 
 Finding Nuclides
 """"""""""""""""
diff --git a/montepy/data_inputs/material.py b/montepy/data_inputs/material.py
index f6d3a1c3..e295c405 100644
--- a/montepy/data_inputs/material.py
+++ b/montepy/data_inputs/material.py
@@ -4,6 +4,7 @@ import collections as co
 import copy
 import itertools
 import math
+import numbers
 import re
 from typing import Generator, Union
 import warnings
@@ -36,6 +37,8 @@ This is used for adding new material components.
 By default all components made from scratch are added to their own line with this many leading spaces.
 """
 
+NuclideLike = Union[Nuclide, Nucleus, Element, str, numbers.Integral]
+
 
 class _DefaultLibraries:
     """
@@ -603,11 +606,11 @@ See <https://www.montepy.org/migrations/migrate0_1.html> for more information ""
         del self._components[idx]
 
     def __contains__(self, nuclide):
-        if not isinstance(nuclide, (Nuclide, Nucleus, Element, str)):
+        if not isinstance(nuclide, (Nuclide, Nucleus, Element, str, numbers.Integral)):
             raise TypeError(
                 f"Can only check if a Nuclide, Nucleus, Element, or str is in a material. {nuclide} given."
             )
-        if isinstance(nuclide, str):
+        if isinstance(nuclide, (str, numbers.Integral)):
             nuclide = Nuclide(nuclide)
         # switch to elemental
         if isinstance(nuclide, (Nucleus, Nuclide)) and nuclide.A == 0:
@@ -669,7 +672,7 @@ See <https://www.montepy.org/migrations/migrate0_1.html> for more information ""
         for nuclide, _ in self:
             nuclide.library = new_library
 
-    def add_nuclide(self, nuclide: Union[Nuclide, str, int], fraction: float):
+    def add_nuclide(self, nuclide: NuclideLike, fraction: float):
         """
         Add a new component to this material of the given nuclide, and fraction.
 
@@ -684,24 +687,19 @@ See <https://www.montepy.org/migrations/migrate0_1.html> for more information ""
             raise TypeError(
                 f"Nuclide must of type Nuclide, str, or int. {nuclide} of type {type(nuclide)} given."
             )
-        if not isinstance(fraction, (float, int)):
-            raise TypeError(
-                f"Fraction must be a numerical value. {fraction} of type {type(fraction)}"
-            )
-        if isinstance(nuclide, (str, int)):
-            nuclide = Nuclide(nuclide)
+        nuclide = self._promote_nuclide(nuclide, True)
         self.append((nuclide, fraction))
 
-    def contains(
+    def contains_all(
         self,
-        nuclide: Union[Nuclide, Nucleus, Element, str, int],
-        *args: Union[Nuclide, Nucleus, Element, str, int],
+        *nuclides: NuclideLike,
         threshold: float = 0.0,
+        strict: bool = False,
     ) -> bool:
         """
-        Checks if this material contains multiple nuclides.
+        Checks if this material contains of all of the given nuclides.
 
-        A boolean and is used for this comparison.
+        A boolean "and" is used for this comparison.
         That is this material must contain all nuclides at or above the given threshold
         in order to return true.
 
@@ -715,33 +713,86 @@ See <https://www.montepy.org/migrations/migrate0_1.html> for more information ""
 
             # try to find LEU materials
             for mat in problem.materials:
-                if mat.contains("U-235", threshold=0.02):
+                if mat.contains_all("U-235", threshold=0.02):
                     # your code here
                     pass
 
-            # try to find any fissile materials
+            # try to find a uranium
             for mat in problem.materials:
-                if mat.contains("U-235", "U-233", "Pu-239", threshold=1e-6):
+                if mat.contains_all("U"):
                     pass
 
-            # try to find a uranium
+        .. note::
+
+            The difference between :func:`contains_all` and :func:`contains_any` is only for how they
+            handle being given multiple nuclides. This does not impact how given Elements will match
+            daughter Nuclides. This is handled instead by ``strict``.
+
+        .. note::
+
+            For details on how to use the ``strict`` argument see the examples in: :func:`find`.
+
+        .. versionadded:: 1.0.0
+
+        :param nuclides: a plurality of nuclides to check for.
+        :type nuclides: Union[Nuclide, Nucleus, Element, str, int]
+        :param threshold: the minimum concentration of a nuclide to be considered. The material components are not
+            first normalized.
+        :type threshold: float
+        :param strict: If True this does not let an elemental nuclide match all child isotopes, isomers, nor will an isotope
+            match all isomers, nor will a blank library match all libraries.
+        :type strict: bool
+
+        :return: whether or not this material contains all components given above the threshold.
+        :rtype: bool
+
+        :raises TypeError: if any argument is of the wrong type.
+        :raises ValueError: if the fraction is not positive or zero, or if nuclide cannot be interpreted as a Nuclide.
+
+        """
+        return self._contains_arb(
+            *nuclides, bool_func=all, threshold=threshold, strict=strict
+        )
+
+    def contains_any(
+        self,
+        *nuclides: NuclideLike,
+        threshold: float = 0.0,
+        strict: bool = False,
+    ) -> bool:
+        """
+        Checks if this material contains any of the given nuclide.
+
+        A boolean "or" is used for this comparison.
+        That is, this material must contain any nuclides at or above the given threshold
+        in order to return true.
+
+        Examples
+        ^^^^^^^^
+
+        .. testcode::
+
+            import montepy
+            problem = montepy.read_input("tests/inputs/test.imcnp")
+
+            # try to find any fissile materials
             for mat in problem.materials:
-                if mat.contains("U"):
+                if mat.contains_any("U-235", "U-233", "Pu-239", threshold=1e-6):
                     pass
-
         .. note::
 
-            If a nuclide is in a material multiple times, and cumulatively exceeds the threshold,
-            but for each instance it appears it is below the threshold this method will return False.
+            For details on how to use the ``strict`` argument see the examples in: :func:`find`.
+
         .. versionadded:: 1.0.0
 
-        :param nuclide: the first nuclide to check for.
-        :type nuclide: Union[Nuclide, Nucleus, Element, str, int]
-        :param args: a plurality of other nuclides to check for.
-        :type args: Union[Nuclide, Nucleus, Element, str, int]
+        :param nuclides: a plurality of nuclides to check for.
+        :type nuclides: Union[Nuclide, Nucleus, Element, str, int]
         :param threshold: the minimum concentration of a nuclide to be considered. The material components are not
             first normalized.
         :type threshold: float
+        :param strict: If True this does not let an elemental nuclide match all child isotopes, isomers, nor will an isotope
+            match all isomers, nor will a blank library match all libraries.
+        :type strict: bool
 
         :return: whether or not this material contains all components given above the threshold.
         :rtype: bool
@@ -750,59 +801,79 @@ See <https://www.montepy.org/migrations/migrate0_1.html> for more information ""
         :raises ValueError: if the fraction is not positive or zero, or if nuclide cannot be interpreted as a Nuclide.
 
         """
-        nuclides = []
-        for nuclide in [nuclide] + list(args):
-            if not isinstance(nuclide, (str, int, Element, Nucleus, Nuclide)):
-                raise TypeError(
-                    f"Nuclide must be a type that can be converted to a Nuclide. The allowed types are: "
-                    f"Nuclide, Nucleus, str, int. {nuclide} given."
-                )
-            if isinstance(nuclide, (str, int)):
-                nuclide = Nuclide(nuclide)
-            # treat elemental as element
-            if isinstance(nuclide, (Nucleus, Nuclide)) and nuclide.A == 0:
-                nuclide = nuclide.element
-            if isinstance(nuclide, Nuclide) and not str(nuclide.library):
-                nuclide = nuclide.nucleus
-            nuclides.append(nuclide)
-
-        if not isinstance(threshold, float):
+        return self._contains_arb(
+            *nuclides, bool_func=any, threshold=threshold, strict=strict
+        )
+
+    @staticmethod
+    def _promote_nuclide(nuclide, strict):
+        # This is necessary for python 3.9
+        if not isinstance(nuclide, (Nuclide, Nucleus, Element, str, numbers.Integral)):
+            raise TypeError(
+                f"Nuclide must be a type that can be converted to a Nuclide. The allowed types are: "
+                f"Nuclide, Nucleus, str, int. {nuclide} given."
+            )
+        if isinstance(nuclide, (str, int)):
+            nuclide = Nuclide(nuclide)
+        # treat elemental as element
+        if isinstance(nuclide, (Nucleus, Nuclide)) and nuclide.A == 0 and not strict:
+            nuclide = nuclide.element
+        if isinstance(nuclide, Nuclide) and not str(nuclide.library) and not strict:
+            nuclide = nuclide.nucleus
+        return nuclide
+
+    def _contains_arb(
+        self,
+        *nuclides: Union[Nuclide, Nucleus, Element, str, int],
+        bool_func: co.abc.Callable[co.abc.Iterable[bool]] = None,
+        threshold: float = 0.0,
+        strict: bool = False,
+    ) -> bool:
+        nuclide_finders = []
+        if not isinstance(threshold, numbers.Real):
             raise TypeError(
                 f"Threshold must be a float. {threshold} of type: {type(threshold)} given"
             )
         if threshold < 0.0:
             raise ValueError(f"Threshold must be positive or zero. {threshold} given.")
+        if not isinstance(strict, bool):
+            raise TypeError(
+                f"Strict must be bool. {strict} of type: {type(strict)} given."
+            )
+        for nuclide in nuclides:
+            nuclide_finders.append(self._promote_nuclide(nuclide, strict))
 
         # fail fast
-        for nuclide in nuclides:
-            if nuclide not in self:
-                return False
+        if bool_func == all:
+            for nuclide in nuclide_finders:
+                if nuclide not in self and bool_func == all:
+                    return False
 
         nuclides_search = {}
         nuclei_search = {}
         element_search = {}
-        for nuclide in nuclides:
+        for nuclide in nuclide_finders:
             if isinstance(nuclide, Element):
-                element_search[nuclide] = False
+                element_search[nuclide] = 0.0
             if isinstance(nuclide, Nucleus):
-                nuclei_search[nuclide] = False
+                nuclei_search[nuclide] = 0.0
             if isinstance(nuclide, Nuclide):
-                nuclides_search[str(nuclide).lower()] = False
+                nuclides_search[str(nuclide).lower()] = 0.0
 
         for nuclide, fraction in self:
-            if fraction < threshold:
-                continue
             if str(nuclide).lower() in nuclides_search:
-                nuclides_search[str(nuclide).lower()] = True
+                nuclides_search[str(nuclide).lower()] += fraction
             if nuclide.nucleus in nuclei_search:
-                nuclei_search[nuclide.nucleus] = True
+                nuclei_search[nuclide.nucleus] += fraction
             if nuclide.element in element_search:
-                element_search[nuclide.element] = True
-        return all(
+                element_search[nuclide.element] += fraction
+
+        threshold_check = lambda x: x > threshold
+        return bool_func(
             (
-                all(nuclides_search.values()),
-                all(nuclei_search.values()),
-                all(element_search.values()),
+                bool_func(map(threshold_check, nuclides_search.values())),
+                bool_func(map(threshold_check, nuclei_search.values())),
+                bool_func(map(threshold_check, element_search.values())),
             )
         )
 
@@ -875,7 +946,7 @@ See <https://www.montepy.org/migrations/migrate0_1.html> for more information ""
         """
 
         def setter(old_val, new_val):
-            if not isinstance(new_val, float):
+            if not isinstance(new_val, numbers.Real):
                 raise TypeError(
                     f"Value must be set to a float. {new_val} of type {type(new_val)} given."
                 )
@@ -999,6 +1070,7 @@ See <https://www.montepy.org/migrations/migrate0_1.html> for more information ""
         A: Union[int, slice] = None,
         meta_state: Union[int, slice] = None,
         library: Union[str, slice] = None,
+        strict: bool = False,
     ) -> Generator[tuple[int, tuple[Nuclide, float]]]:
         """
         Finds all components that meet the given criteria.
@@ -1011,7 +1083,7 @@ See <https://www.montepy.org/migrations/migrate0_1.html> for more information ""
         For the library the slicing is done using string comparisons.
 
         Examples
-        ^^^^^^^^
+        --------
 
         .. testcode::
 
@@ -1020,7 +1092,7 @@ See <https://www.montepy.org/migrations/migrate0_1.html> for more information ""
             mat.number = 1
 
             # make non-sense material
-            for nuclide in ["U-235.80c", "U-238.70c", "Pu-239.00c", "O-16.00c"]:
+            for nuclide in ["U-235.80c", "U-238.70c", "Pu-239.00c", "O-16.00c", "C-0", "C-12.00c", "Fe-56"]:
                 mat.add_nuclide(nuclide, 0.1)
 
             print("Get all uranium nuclides.")
@@ -1041,7 +1113,50 @@ See <https://www.montepy.org/migrations/migrate0_1.html> for more information ""
             Get all transuranics
             [(0, (Nuclide('U-235.80c'), 0.1)), (1, (Nuclide('U-238.70c'), 0.1)), (2, (Nuclide('Pu-239.00c'), 0.1))]
             Get all ENDF/B-VIII.0
-            [(2, (Nuclide('Pu-239.00c'), 0.1)), (3, (Nuclide('O-16.00c'), 0.1))]
+            [(2, (Nuclide('Pu-239.00c'), 0.1)), (3, (Nuclide('O-16.00c'), 0.1)), (5, (Nuclide('C-12.00c'), 0.1))]
+
+
+
+        Strict (Explicit) Matching
+        ^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+        Generally this functions treats ambiguity implicitly, and will match as many nuclides as possible.
+        This is generally useful, but not always.
+        By default when only an element is given all daughter nuclides match, as seen above.
+        However, MCNP does provide some "natural" or "elemental" nuclear data,
+        and it would be helpful to find these sometimes.
+        For instance, you may want to find all instances of elemental nuclides,
+        and replace them with explicit isotopes (for instance migrating from ENDF/B-VII.1 to ENDF/B-VIII).
+        In these cases the ``strict`` argument is needed.
+        When ``strict`` is True an ambiguous ``A`` will only match elemental data:
+
+        .. testcode::
+
+            print("Strict: False", list(mat.find(element="C")))
+            print("Strict: True", list(mat.find(element="C", strict=True)))
+
+        will print:
+
+        .. testoutput::
+
+            Strict: False [(4, (Nuclide('C-0'), 0.1)), (5, (Nuclide('C-12.00c'), 0.1))]
+            Strict: True [(4, (Nuclide('C-0'), 0.1))]
+
+
+        Similarly to find nuclides with no library defined you can use strict:
+
+        .. testcode::
+
+            print("Strict: False", list(mat.find(library=None)))
+            print("Strict: True", list(mat.find(library=None, strict=True)))
+
+        This would print:
+
+        .. testoutput::
+
+            Strict: False [(0, (Nuclide('U-235.80c'), 0.1)), (1, (Nuclide('U-238.70c'), 0.1)), (2, (Nuclide('Pu-239.00c'), 0.1)), (3, (Nuclide('O-16.00c'), 0.1)), (4, (Nuclide('C-0'), 0.1)), (5, (Nuclide('C-12.00c'), 0.1)), (6, (Nuclide('Fe-56'), 0.1))]
+            Strict: True [(4, (Nuclide('C-0'), 0.1)), (6, (Nuclide('Fe-56'), 0.1))]
+
 
         .. versionadded:: 1.0.0
 
@@ -1057,6 +1172,9 @@ See <https://www.montepy.org/migrations/migrate0_1.html> for more information ""
         :type meta_state: int, slice
         :param library: the libraries to limit the search to.
         :type library: str, slice
+        :param strict: When true this will strictly match elements as only elements (when no A is given), and only match blank
+            libraries when no library is given.
+        :type strict: bool
 
         :returns: a generator of all matching nuclides, as their index and then a tuple of their nuclide, and fraction pairs that match.
         :rtype: Generator[tuple[int, tuple[Nuclide, float]]]
@@ -1078,9 +1196,13 @@ See <https://www.montepy.org/migrations/migrate0_1.html> for more information ""
             raise TypeError(
                 f"library must a str or a slice. {library} of type {type(library)} given."
             )
+        if not isinstance(strict, bool):
+            raise TypeError(
+                f"strict must be a bool. {strict} of type {type(strict)} given."
+            )
         if name:
             fancy_nuclide = Nuclide(name)
-            if fancy_nuclide.A == 0:
+            if fancy_nuclide.A == 0 and not strict:
                 element = fancy_nuclide.element
                 fancy_nuclide = None
         else:
@@ -1090,6 +1212,13 @@ See <https://www.montepy.org/migrations/migrate0_1.html> for more information ""
         else:
             first_filter = self.__prep_filter(fancy_nuclide)
 
+        # create filter for defaults if strict
+        if strict:
+            # if strict and element switch to A=0
+            if element and A is None:
+                A = 0
+            if library is None:
+                library = ""
         filters = [
             first_filter,
             self.__prep_element_filter(element),
@@ -1112,6 +1241,7 @@ See <https://www.montepy.org/migrations/migrate0_1.html> for more information ""
         A: Union[int, slice] = None,
         meta_state: Union[int, slice] = None,
         library: Union[str, slice] = None,
+        strict: bool = False,
     ) -> Generator[float]:
         """
         A wrapper for :func:`find` that only returns the fractions of the components.
@@ -1154,11 +1284,16 @@ See <https://www.montepy.org/migrations/migrate0_1.html> for more information ""
         :type meta_state: int, slice
         :param library: the libraries to limit the search to.
         :type library: str, slice
+        :param strict: whether to strictly match elements as only elements (when no A is given), and only match blank
+            libraries when no library is given.
+        :type strict: bool
 
         :returns: a generator of fractions whose nuclide matches the criteria.
         :rtype: Generator[float]
         """
-        for _, (_, fraction) in self.find(name, element, A, meta_state, library):
+        for _, (_, fraction) in self.find(
+            name, element, A, meta_state, library, strict
+        ):
             yield fraction
 
     def __bool__(self):
diff --git a/montepy/materials.py b/montepy/materials.py
index 406f1147..36281d6a 100644
--- a/montepy/materials.py
+++ b/montepy/materials.py
@@ -1,6 +1,7 @@
 # Copyright 2024, Battelle Energy Alliance, LLC All Rights Reserved.
 
 from __future__ import annotations
+import collections as co
 import copy
 from typing import Generator, Union
 
@@ -30,16 +31,64 @@ class Materials(NumberedDataObjectCollection):
     def __init__(self, objects=None, problem=None):
         super().__init__(Material, objects, problem)
 
-    def get_containing(
+    def get_containing_any(
         self,
-        nuclide: Union[
+        *nuclides: Union[
             montepy.data_inputs.nuclide.Nuclide,
             montepy.data_inputs.nuclide.Nucleus,
             montepy.Element,
             str,
             int,
         ],
-        *args: Union[
+        threshold: float = 0.0,
+        strict: bool = False,
+    ) -> Generator[Material]:
+        """
+        Get all materials that contain any of these these nuclides.
+
+        This uses :func:`~montepy.data_inputs.material.Material.contains` under the hood.
+        See that documentation for more guidance.
+
+        Examples
+        ^^^^^^^^
+
+        One example would to be find all water bearing materials:
+
+        .. testcode::
+
+            import montepy
+            problem = montepy.read_input("foo.imcnp")
+            for mat in problem.materials.get_containing_any("H-1", "U-235", threshold = 0.3):
+                print(mat)
+
+        .. testoutput::
+
+            MATERIAL: 1, ['hydrogen', 'oxygen']
+
+        .. versionadded:: 1.0.0
+
+        :param nuclides: a plurality of nuclides to check for.
+        :type nuclides: Union[Nuclide, Nucleus, Element, str, int]
+        :param threshold: the minimum concentration of a nuclide to be considered. The material components are not
+            first normalized.
+        :type threshold: float
+        :param strict: If True this does not let an elemental nuclide match all child isotopes, isomers, nor will an isotope
+            match all isomers, nor will a blank library match all libraries.
+        :type strict: bool
+
+        :return: A generator of all matching materials
+        :rtype: Generator[Material]
+
+        :raises TypeError: if any argument is of the wrong type.
+        :raises ValueError: if the fraction is not positive or zero, or if nuclide cannot be interpreted as a Nuclide.
+        """
+        return self._contains_arb(
+            *nuclides, bool_func=any, threshold=threshold, strict=strict
+        )
+
+    def get_containing_all(
+        self,
+        *nuclides: Union[
             montepy.data_inputs.nuclide.Nuclide,
             montepy.data_inputs.nuclide.Nucleus,
             montepy.Element,
@@ -47,9 +96,10 @@ class Materials(NumberedDataObjectCollection):
             int,
         ],
         threshold: float = 0.0,
+        strict: bool = False,
     ) -> Generator[Material]:
         """
-        Get all materials that contain these nuclides.
+        Get all materials that contain all of these nuclides.
 
         This uses :func:`~montepy.data_inputs.material.Material.contains` under the hood.
         See that documentation for more guidance.
@@ -63,7 +113,7 @@ class Materials(NumberedDataObjectCollection):
 
             import montepy
             problem = montepy.read_input("foo.imcnp")
-            for mat in problem.materials.get_containing("H-1", "O-16", threshold = 0.3):
+            for mat in problem.materials.get_containing_all("H-1", "O-16", threshold = 0.3):
                 print(mat)
 
         .. testoutput::
@@ -72,13 +122,14 @@ class Materials(NumberedDataObjectCollection):
 
         .. versionadded:: 1.0.0
 
-        :param nuclide: the first nuclide to check for.
-        :type nuclide: Union[Nuclide, Nucleus, Element, str, int]
-        :param args: a plurality of other nuclides to check for.
-        :type args: Union[Nuclide, Nucleus, Element, str, int]
+        :param nuclides: a plurality of nuclides to check for.
+        :type nuclides: Union[Nuclide, Nucleus, Element, str, int]
         :param threshold: the minimum concentration of a nuclide to be considered. The material components are not
             first normalized.
         :type threshold: float
+        :param strict: If True this does not let an elemental nuclide match all child isotopes, isomers, nor will an isotope
+            match all isomers, nor will a blank library match all libraries.
+        :type strict: bool
 
         :return: A generator of all matching materials
         :rtype: Generator[Material]
@@ -86,25 +137,26 @@ class Materials(NumberedDataObjectCollection):
         :raises TypeError: if any argument is of the wrong type.
         :raises ValueError: if the fraction is not positive or zero, or if nuclide cannot be interpreted as a Nuclide.
         """
-        nuclides = []
-        for nuclide in [nuclide] + list(args):
-            if not isinstance(
-                nuclide,
-                (
-                    str,
-                    int,
-                    montepy.Element,
-                    montepy.data_inputs.nuclide.Nucleus,
-                    montepy.Nuclide,
-                ),
-            ):
-                raise TypeError(
-                    f"nuclide must be of type str, int, Element, Nucleus, or Nuclide. "
-                    f"{nuclide} of type {type(nuclide)} given."
-                )
-            if isinstance(nuclide, (str, int)):
-                nuclide = montepy.Nuclide(nuclide)
-            nuclides.append(nuclide)
+        return self._contains_arb(
+            *nuclides, bool_func=all, threshold=threshold, strict=strict
+        )
+
+    def _contains_arb(
+        self,
+        *nuclides: Union[
+            montepy.data_inputs.nuclide.Nuclide,
+            montepy.data_inputs.nuclide.Nucleus,
+            montepy.Element,
+            str,
+            int,
+        ],
+        bool_func: co.abc.Callable[co.abc.Iterable[bool]],
+        threshold: float = 0.0,
+        strict: bool = False,
+    ) -> Generator[Material]:
+        nuclide_finders = []
+        for nuclide in nuclides:
+            nuclide_finders.append(Material._promote_nuclide(nuclide, strict))
 
         def sort_by_type(nuclide):
             type_map = {
@@ -115,9 +167,14 @@ class Materials(NumberedDataObjectCollection):
             return type_map[type(nuclide)]
 
         # optimize by most hashable and fail fast
-        nuclides = sorted(nuclides, key=sort_by_type)
+        nuclide_finders = sorted(nuclide_finders, key=sort_by_type)
         for material in self:
-            if material.contains(*nuclides, threshold=threshold):
+            if material._contains_arb(
+                *nuclide_finders,
+                bool_func=bool_func,
+                threshold=threshold,
+                strict=strict,
+            ):
                 # maybe? Maybe not?
                 # should Materials act like a set?
                 yield material


Test patch:
diff --git a/tests/test_material.py b/tests/test_material.py
index 71010351..b0b7db91 100644
--- a/tests/test_material.py
+++ b/tests/test_material.py
@@ -35,7 +35,9 @@ class TestMaterial:
             "u238",
             "am242",
             "am242m1",
+            "Co60m2.50c",
             "Pu239",
+            "Ni-0.60c",
         ]
         mat = Material()
         mat.number = 1
@@ -246,41 +248,64 @@ class TestMaterial:
             mat.append((Nuclide("1001.80c"), -1.0))
 
     @pytest.mark.parametrize(
-        "content, is_in",
+        "content, strict, is_in",
         [
-            ("1001.80c", True),
-            ("H-1", True),
-            (Element(1), True),
-            (Nucleus(Element(1), 1), True),
-            (Element(43), False),
-            ("B-10.00c", False),
-            ("H", True),
-            (Nucleus(Element(5), 10), False),
+            ("1001.80c", False, True),
+            (1001, False, True),
+            ("H-1", False, True),
+            (Element(1), False, True),
+            (Nucleus(Element(1), 1), False, True),
+            (Element(43), False, False),
+            ("B-10.00c", False, False),
+            ("H", False, True),
+            ("H", True, False),
+            (Nucleus(Element(5), 10), False, False),
+            ("Ni", False, True),
+            ("Ni", True, False),  # test wrong library
+            ("Ni-0.60c", True, True),
+            ("Co-60", False, False),
+            ("Co-60", True, False),
+            ("Co-60m2.50c", True, True),
         ],
     )
-    def test_material_contains(_, big_material, content, is_in):
-        assert is_in == (content in big_material), "Contains didn't work properly"
-        assert is_in == big_material.contains(content)
+    def test_material_contains(_, big_material, content, strict, is_in):
+        if not strict:
+            assert is_in == (content in big_material), "Contains didn't work properly"
+        assert is_in == big_material.contains_all(content, strict=strict)
+        assert is_in == big_material.contains_any(content, strict=strict)
         with pytest.raises(TypeError):
-            5 in big_material
+            {} in big_material
+        with pytest.raises(TypeError):
+            big_material.contains_all("H", strict=5)
+        with pytest.raises(TypeError):
+            big_material.contains_any("H", strict=5)
 
     def test_material_multi_contains(_, big_material):
-        assert big_material.contains("1001", "U-235", "Pu-239", threshold=0.01)
-        assert not big_material.contains("1001", "U-235", "Pu-239", threshold=0.07)
-        assert not big_material.contains("U-235", "B-10")
+        # contains all
+        assert big_material.contains_all("1001", "U-235", "Pu-239", threshold=0.01)
+        assert not big_material.contains_all(
+            "1001", "U-235", "Pu-239", threshold=0.01, strict=True
+        )
+        assert not big_material.contains_all("1001", "U-235", "Pu-239", threshold=0.07)
+        assert not big_material.contains_all("U-235", "B-10")
+        # contains any
+        assert not big_material.contains_any("C", "B", "F")
+        print("sadness")
+        assert big_material.contains_any("h-1", "C", "B")
 
     def test_material_contains_bad(_):
         mat = Material()
-        with pytest.raises(TypeError):
-            mat.contains(mat)
-        with pytest.raises(TypeError):
-            mat.contains("1001", mat)
-        with pytest.raises(ValueError):
-            mat.contains("hi")
-        with pytest.raises(TypeError):
-            mat.contains("1001", threshold="hi")
-        with pytest.raises(ValueError):
-            mat.contains("1001", threshold=-1.0)
+        for method in [mat.contains_all, mat.contains_any]:
+            with pytest.raises(TypeError):
+                method(mat)
+            with pytest.raises(TypeError):
+                method("1001", mat)
+            with pytest.raises(ValueError):
+                method("hi")
+            with pytest.raises(TypeError):
+                method("1001", threshold="hi")
+            with pytest.raises(ValueError):
+                method("1001", threshold=-1.0)
 
     def test_material_normalize(_, big_material):
         # make sure it's not an invalid starting condition
@@ -300,14 +325,25 @@ class TestMaterial:
             ({"name": "U235m1"}, 1),
             ({"element": Element(1)}, 6),
             ({"element": "H"}, 6),
+            ({"element": "H", "strict": True}, 0),
+            ({"element": "H", "A": 1, "strict": True}, 0),
+            (
+                {
+                    "element": "H",
+                    "A": 1,
+                    "library": slice("00c", "05c"),
+                    "strict": True,
+                },
+                2,
+            ),
             ({"element": slice(92, 95)}, 5),
             ({"A": 1}, 4),
             ({"A": slice(235, 240)}, 5),
             ({"A": slice(232, 243, 2)}, 5),
-            ({"A": slice(None)}, 15),
-            ({"meta_state": 0}, 13),
+            ({"A": slice(None)}, 17),
+            ({"meta_state": 0}, 14),
             ({"meta_state": 1}, 2),
-            ({"meta_state": slice(0, 2)}, 15),
+            ({"meta_state": slice(0, 2)}, 16),
             ({"library": "80c"}, 3),
             ({"library": slice("00c", "10c")}, 2),
         ],
@@ -337,6 +373,8 @@ class TestMaterial:
             list(big_material.find(element=1.23))
         with pytest.raises(TypeError):
             list(big_material.find(library=5))
+        with pytest.raises(TypeError):
+            list(big_material.find(strict=5))
 
     def test_material_str(_):
         in_str = "M20 1001.80c 0.5 8016.80c 0.4 94239.80c 0.1"
diff --git a/tests/test_numbered_collection.py b/tests/test_numbered_collection.py
index 48a58447..3b47c699 100644
--- a/tests/test_numbered_collection.py
+++ b/tests/test_numbered_collection.py
@@ -736,13 +736,30 @@ class TestMaterials:
             (("B",), 1.0, 0),
         ],
     )
-    def test_get_containing(_, m0_prob, nuclides, threshold, num):
-        ret = list(m0_prob.materials.get_containing(*nuclides, threshold=threshold))
+    def test_get_containing_all(_, m0_prob, nuclides, threshold, num):
+        ret = list(m0_prob.materials.get_containing_all(*nuclides, threshold=threshold))
         assert len(ret) == num
         for mat in ret:
             assert isinstance(mat, montepy.Material)
         with pytest.raises(TypeError):
-            next(m0_prob.materials.get_containing(m0_prob))
+            next(m0_prob.materials.get_containing_all(m0_prob))
+
+    @pytest.mark.parametrize(
+        "nuclides, threshold, num",
+        [
+            (("26054", "26056"), 1.0, 1),
+            ((montepy.Nuclide("H-1"),), 0.0, 1),
+            (("B",), 1.0, 0),
+            (("U-235", "H-1"), 0.0, 2),
+        ],
+    )
+    def test_get_containing_any(_, m0_prob, nuclides, threshold, num):
+        ret = list(m0_prob.materials.get_containing_any(*nuclides, threshold=threshold))
+        assert len(ret) == num
+        for mat in ret:
+            assert isinstance(mat, montepy.Material)
+        with pytest.raises(TypeError):
+            next(m0_prob.materials.get_containing_any(m0_prob))
 
     @pytest.fixture
     def h2o(_):