Repository: idaholab/MontePy

Issue:
Write getting started and migration guide for Materials in 1.0

Discussion:

Patch:
diff --git a/doc/source/api/montepy.particle.rst b/doc/source/api/montepy.particle.rst
index 7b887d96..42d3cb64 100644
--- a/doc/source/api/montepy.particle.rst
+++ b/doc/source/api/montepy.particle.rst
@@ -4,6 +4,5 @@ montepy.particle module
 
 .. automodule:: montepy.particle
    :members:
-   :inherited-members:
    :undoc-members:
    :show-inheritance:
diff --git a/doc/source/api/montepy.surfaces.surface_type.rst b/doc/source/api/montepy.surfaces.surface_type.rst
index ec5e4d77..cbec5e51 100644
--- a/doc/source/api/montepy.surfaces.surface_type.rst
+++ b/doc/source/api/montepy.surfaces.surface_type.rst
@@ -4,6 +4,5 @@ montepy.surfaces.surface\_type module
 
 .. automodule:: montepy.surfaces.surface_type
    :members:
-   :inherited-members:
    :undoc-members:
    :show-inheritance:
diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst
index 8d810620..67e2f4b2 100644
--- a/doc/source/changelog.rst
+++ b/doc/source/changelog.rst
@@ -11,7 +11,7 @@ MontePy Changelog
 **Features Added**
 
 * Redesigned how Materials hold Material_Components. See :ref:`migrate 0 1` (:pull:`507`). 
-* Made it easier to create an Isotope, or now Nuclide: ``montepy.Nuclide("H-1.80c")`` (:issue:`505`).
+* 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`).
diff --git a/doc/source/conf.py b/doc/source/conf.py
index a410895d..cefc22e6 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -68,6 +68,8 @@ autodoc_member_order = "groupwise"
 # Display the version
 display_version = True
 
+linkcheck_ignore = ["https://nucleardata.lanl.gov/.*"]
+
 # -- External link configuration ---------------------------------------------
 UM63 = (
     "https://mcnp.lanl.gov/pdf_files/TechReport_2022_LANL_LA-UR-22-30006"
diff --git a/doc/source/dev_standards.rst b/doc/source/dev_standards.rst
index e2607f8a..3ca9da19 100644
--- a/doc/source/dev_standards.rst
+++ b/doc/source/dev_standards.rst
@@ -45,6 +45,13 @@ Design Philosophy
 #. Defer to vanilla python, and only use the standard library. Currently the only dependencies are `numpy <https://numpy.org/>`_ and `sly <https://github.com/dabeaz/sly>`_. 
    There must be good justification for breaking from this convention and complicating things for the user.
 
+Style Guide
+-----------
+
+#. Thou shall be `PEP 8 <https://peps.python.org/pep-0008/>`_, and use `black <https://black.readthedocs.io/en/stable/index.html>`_.
+#. Spaces not tabs with 4 spaces for an indent.
+#. External imports before internal imports with a blank line in between. All imports are alphabetized. 
+
 Doc Strings
 -----------
 
diff --git a/doc/source/developing.rst b/doc/source/developing.rst
index 0265bf5f..ab333a49 100644
--- a/doc/source/developing.rst
+++ b/doc/source/developing.rst
@@ -690,3 +690,18 @@ Users are more like to use this dynamic code.
 In general this philosophy is: if it's not the source of truth,
 it should be a generator.
 
+Constants and Meta Data Structures
+----------------------------------
+
+MontePy uses constants and data structures to utilize meta-programming
+and remove redundant code.
+Typical constants can be found in :mod:`montepy.constants`.
+
+Here are the other data structures to be aware of:
+
+* :class:`~montepy.mcnp_problem.MCNP_Problem` ``_NUMBERED_OBJ_MAP``: maps a based numbered object to its collection
+  class. This is used for loading all problem numbered object collections in an instance.
+* :func:`montepy.data_inputs.data_parser.PREFIX_MATCHES` is a set of the data object classes. The prefix is taken from
+  the classes. A data object must be a member of this class for it to automatically parse new data objects.
+* :class:`~montepy.cell.Cell` ``_INPUTS_TO_PROPERTY`` maps a cell modifier class to the attribute to load it into for a
+  cell.  The boolean is whether multiple input instances are allowed.
diff --git a/doc/source/migrations/migrate0_1.rst b/doc/source/migrations/migrate0_1.rst
index 81d4be5d..3a88c327 100644
--- a/doc/source/migrations/migrate0_1.rst
+++ b/doc/source/migrations/migrate0_1.rst
@@ -21,43 +21,47 @@ and can be advantageous.
 See :issue:`504` for more details.
 Due to this it was decided that the best way forward was to abandon the old design,
 and to create a brand new data structure.
-This means that backwards compatibility *will* be broken, 
-and so this fix is leading to a major version release.
+This means that backwards compatibility *was* broken, 
+and so this fix led to a major version release.
 
 
 Deprecations
 ------------
-The following properties and objects are currently deprecated, 
-and will be removed in MontePy 1.0.0.
+The following properties and objects are currently deprecated 
+and were removed in MontePy 1.0.0.
 
-* :func:`montepy.data_inputs.material.Material.material_components`. 
+* :func:`~montepy.data_inputs.material.Material.material_components`. 
   This is the dictionary that caused this design problem. 
 
-* :class:`montepy.data_inputs.material_components.MaterialComponents`
-  This is the class that stores information in the above dictionary. 
-  It is largely excess object wrapping, that makes the material interface 
+* :class:`~montepy.data_inputs.material_component.MaterialComponent`:
+  This was the class that stores information in the above dictionary. 
+  It was largely excess object wrapping that made the material interface 
   overly complex.
 
-* :class:`montepy.data_inputs.Isotope` will be renamed to ``Nuclide``. 
-  This is to better align with MCNP documentation,
-  and better reflect that the nuclear data for a nuclide can represent 
+* :class:`~montepy.data_inputs.isotope.Isotope` was renamed to :class:`~montepy.data_inputs.nuclide.Nuclide`. 
+  This is to better align with MCNP documentation
+  and to better reflect that the nuclear data for a nuclide can represent 
   isotopic, isomeric, or atomic data.
 
 
 New Interface & Migration
 -------------------------
 
+For more details, see the new :ref:`mat_tutorial` tutorial in the getting started guide,
+as well as the example in the :class:`~montepy.data_inputs.material.Material` documentation.
+
 .. note::
 
         This design is not finalized and is subject to change.
         This is the currently planned design for ``1.0.0a1``.
         If you have input you can `join the discussion <https://github.com/idaholab/MontePy/discussions/475>`_.
-        This is also where alpha-testing will be announced.
+        For feedback on the alpha test, please `join this discussion <https://github.com/idaholab/MontePy/discussions/604>`_.
 
 ``material_components`` removal
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
-Material composition data has moved from ``Material.material_components`` to the ``Material`` itself.
+Material composition data has moved from ``Material.material_components`` to the
+:class:`~montepy.data_inputs.material.Material` itself.
 ``Material`` is now a list-like iterable.
 It is a list of tuples which are ``(nuclide, fraction)`` pairs.
 
@@ -76,37 +80,173 @@ Searching Components
 ^^^^^^^^^^^^^^^^^^^^
 
 Finding a specific ``Nuclide`` in a ``Material`` is now much easier.
-First there will be a ``Material.find`` method that takes either a ``Nuclide`` string,
+First, there is a :func:`~montepy.data_inputs.material.Material.find` method that takes either a ``Nuclide`` string,
 or various over search criteria (e.g., ``element``),
 and creates a generator of all matching component tuples.
 
 If you want to check if a ``Material`` contains a specific ``Nuclide``
 you can simply test ``nuclide in material``.
-The ``Material.contains`` function will provide more options,
-such as setting a minimum threshold, and testing for multiple nuclides at once.
+The :func:`~montepy.data_inputs.material.Material.contains` function will provide more options,
+such as setting a minimum threshold and testing for multiple nuclides at once.
 
 Adding Nuclides
 ^^^^^^^^^^^^^^^
-Adding a new nuclide will be easiest with the ``add_nuclide`` function.
 
-Editing Nuclide Compositon
-^^^^^^^^^^^^^^^^^^^^^^^^^^
+Adding a new nuclide is easiest with the :func:`~montepy.data_inputs.material.Material.add_nuclide` function.
+
+Editing Nuclide Composition
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
 Editing a material composition will be very similar to editing a ``list``.
 Existing components can be set to a nuclide component nuclide.
 Also existing components can be deleted with ``del``. 
+For just editing the fractions or nuclides the functions:
+:func:`~montepy.data_inputs.material.Material.nuclides`
+and :func:`~montepy.data_inputs.material.Material.values` provide the easiest interface.
 
 
 ``Isotope`` Deprecation and Removal
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
-The decision was made to remove the name ``Isotope``.
+The decision was made to remove the name :class:`montepy.data_inputs.isotope.Isotope`.
 This is because not all material components are an isotope,
 they may be an isomer, or event an element.
-Rather the MCNP generalized terminology of ``Nuclide`` was adopted.
+Rather the MCNP generalized terminology of :class:`montepy.data_inputs.nuclide.Nuclide` was adopted.
 The idea of a specific nuclide, e.g., ``H-1`` was separated from an
 MCNP material component e.g., ``1001.80c``. 
-The actual ``Nuclide`` information was moved to a new class: ``Nucleus``,
+The actual ``Nuclide`` information was moved to a new class: :class:`~montepy.data_inputs.nuclide.Nucleus`
 that is immutable. 
-The ``Nuclide`` wraps this and adds a ``Library`` object to specify the nuclear data that is used.
+The :class:`~montepy.data_inputs.nuclide.Nuclide` wraps this and adds a :class:`~montepy.data_inputs.nuclide.Library` object to specify the nuclear data that is used.
 It makes sense to be able to change a library.
 It does not make sense to change the intrinsic properties of a nuclide (i.e., ``Z``, ``A``, etc.).
+
+
+Code Comparison between 0.x and 1.x
+-----------------------------------
+ 
+Here are some example code blocks of various material operations in both versions.
+
+Iterating over Material Components
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+In MontePy 0.x
+""""""""""""""
+
+.. testcode::
+   :skipif: True
+   
+   import montepy
+   problem = montepy.read_input("foo.imcnp")
+   mat = problem.materials[1]
+   for component in mat.material_components.values():
+        print(component.fraction, component.isotope)
+
+This would print:
+
+.. testoutput::
+
+   2.0  H-1     (80c)
+   1.0  O-16    (80c)
+
+In MontePy 1.x
+""""""""""""""
+
+.. testcode::
+
+   import montepy
+   problem = montepy.read_input("foo.imcnp")
+   mat = problem.materials[1]
+   for nuclide, fraction in mat:
+        print(fraction, nuclide)
+
+Would print:
+
+.. testoutput::
+
+   2.0  H-1     (80c)
+   1.0  O-16    (80c)
+
+Adding Material Components
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Appending and editing the material components in a material in MontePy 0.x
+was rather clunky. That was a large part of the motivation for this release.
+
+In MontePy 0.x
+""""""""""""""
+.. testcode::
+   :skipif: True
+
+   from montepy.data_inputs.isotope import Isotope
+   from montepy.data_inputs.material_component import MaterialComponent
+   #construct new isotope
+   new_isotope = Isotope("5010.80c")
+   # construct new component
+   comp = MaterialComponent(new_isotope, 1e-6)
+   # add actual component to material
+   mat.material_components[new_isotope] = comp
+
+In MontePy 1.x
+""""""""""""""
+
+.. testcode::
+   
+   mat.add_nuclide("B-10.80c", 1e-6)
+
+
+Finding, Editing, and Deleting a Component
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Accessing a specific component is another reason for this release.
+As you may have noticed, ``material_components`` is a dictionary with the keys being an ``Isotope``.
+Due to a bug in MontePy 0.x, the exact instance of an Isotope must be passed as the key to access that item.
+
+In MontePy 0.x
+""""""""""""""
+
+.. testcode::
+   :skipif: True
+
+   target_isotope = Isotope("5010.80c")
+   key = None
+   for isotope in mat.material_components:
+        if isotope.element == target_isotope.element and isotope.A == target_isotope.A:
+            key = isotope
+            break
+   # get material component object
+   comp = mat[key]
+   # edit it. This will update the material because everything is a pointer 
+   comp.fraction = 2e-6
+   # delete the component
+   del mat[key]
+
+
+In MontePy 1.x
+""""""""""""""
+
+.. testcode::
+
+   target_isotope = montepy.Nuclide("B-10.80c")
+   for comp_idx, (nuc, fraction) in mat.find(target_isotope):
+        break
+   # update fraction
+   mat.values[comp_idx] = 2e-6
+   # delete component
+   del mat[comp_idx]
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/doc/source/starting.rst b/doc/source/starting.rst
index f0873b28..13aa37bb 100644
--- a/doc/source/starting.rst
+++ b/doc/source/starting.rst
@@ -439,7 +439,7 @@ Surfaces
 
 The most important unsung heroes of an MCNP problem are the surfaces.
 They may be tedious to work with but you can't get anything done without them.
-MCNP supports *alot* of types of surfaces, and all of them are special in their own way.
+MCNP supports *a lot* of types of surfaces, and all of them are special in their own way.
 You can see all the surface types here: :class:`~montepy.surfaces.surface_type.SurfaceType`.
 By default all surfaces are an instance of :class:`~montepy.surfaces.surface.Surface`.
 They will always have the properties: ``surface_type``, and ``surface_constants``.
@@ -705,7 +705,7 @@ This will completely redefine the cell's geometry. You can also modify the geome
     fuel_cyl.number = 20
     fuel_cyl.radius = 1.20
     other_fuel_region = -fuel_cyl
-    fuel_cell.geometry |= other_fuel_region 
+    fuel_cell.geometry |= other_fuel_region #|| 
 
 .. warning:: 
 
@@ -784,6 +784,344 @@ For example:
     >>> new_cell.material.number 
     100
 
+
+.. _mat_tutorial:
+
+Materials
+---------
+
+Materials are how the nuclide concentrations in cells are specified.
+MontePy has always supported materials, but since version 1.0.0,
+the design of the interface has significantly improved.
+
+Specifying Nuclides 
+^^^^^^^^^^^^^^^^^^^
+
+To specify a material, one needs to be able to specify the nuclides that are contained in it.
+This is done through :class:`~montepy.data_inputs.nuclide.Nuclide` objects.
+This actually a wrapper of a :class:`~montepy.data_inputs.nuclide.Nucleus` and a :class:`~montepy.data_inputs.nuclide.Library` object.
+Users should rarely need to interact with the latter two objects, but it is good to be aware of them.
+The general idea is that a ``Nuclide`` instance represents a specific set of ACE data that for a ``Nucleus``, 
+which represents only a physical nuclide with a given ``Library``.
+
+The easiest way to specify a Nuclide is by its string name. 
+MontePy supports all valid MCNP ZAIDs for MCNP 6.2, and MCNP 6.3.0.
+See :class:`~montepy.data_inputs.nuclide.Nuclide` for how metastable isomers are handled.
+However, ZAIDs (like many things in MCNP) are cumbersome.
+Therefore, MontePy also supports its own nuclide names as well, which are meant to be more intuitive.
+These are very similar to the names introduced with MCNP 6.3.1 (section 1.2.2): this follows:
+
+.. code-block::
+
+   Nn[-A][mS][.library]
+
+Where:
+
+* ``Nn`` is the atomic symbol of the nuclide, case insensitive. This is required.
+* ``A`` is the atomic mass. Zero-padding is not needed. Optional.
+* ``S`` is the metastable isomeric state. Only states 1 - 4 are allowed. Optional.
+* ``library`` is the library extension of the nuclide. This only supports MCNP 6.2, 6.3 formatting, i.e., 2 - 3 digits followed by a single letter. Optional. 
+
+The following are all valid ways to specify a nuclide:
+
+.. doctest::
+
+   >>> import montepy
+   >>> montepy.Nuclide("1001.80c")
+   Nuclide('H-1.80c')
+   >>> montepy.Nuclide("H-1.80c")
+   Nuclide('H-1.80c')
+   >>> montepy.Nuclide("H-1.710nc")
+   Nuclide('H-1.710nc')
+   >>> montepy.Nuclide("H")
+   Nuclide('H-0')
+   >>> montepy.Nuclide("Co-60m1")
+   Nuclide('Co-60m1')
+   >>> montepy.Nuclide("Co")
+   Nuclide('Co-0')
+
+
+.. note::
+
+   The new SZAID and Name syntax for nuclides introduced with MCNP 6.3.1 is not currently supported by MontePy.
+   This support likely will be added soon, but probably not prior to MCNP 6.3.1 being available on RSICC. 
+
+
+Working with Material Components
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Iterating over Material Components
+""""""""""""""""""""""""""""""""""
+
+Materials are list-like iterables of tuples.
+
+.. testcode:: 
+
+    mat = problem.materials[1]
+
+    for comp in mat:
+        print(comp)
+
+This shows:
+
+.. testoutput::
+
+    (Nuclide('U-235.80c'), 5.0)
+    (Nuclide('U-238.80c'), 95.0)
+
+If you need just the nuclide or just the fractions, these are accessible by:
+:func:`~montepy.data_inputs.material.Material.nuclides` and 
+:func:`~montepy.data_inputs.material.Material.values`, respectively.
+
+.. testcode::
+
+    for nuclide in mat.nuclides:
+        print(repr(nuclide))
+    for fraction in mat.values:
+        print(fraction)
+
+shows:
+
+.. testoutput::
+
+    Nuclide('U-235.80c')
+    Nuclide('U-238.80c')
+    5.0
+    95.0
+
+Updating Components of Materials
+""""""""""""""""""""""""""""""""
+
+Materials are also list-like in that they are settable by index.
+The material must always be set to a tuple of a nuclide and a fraction.
+
+For instance:
+
+.. testcode::
+
+    nuclide = mat[0][0]
+    mat[0] = (nuclide, 4.0)
+
+Generally this is pretty clunky, so 
+:func:`~montepy.data_inputs.material.Material.nuclides` and 
+:func:`~montepy.data_inputs.material.Material.values` are also settable.
+To undo the previous changes:
+
+.. testcode::
+
+    mat.values[0] = 5.0
+    print(mat[0])
+
+This outputs: 
+
+.. testoutput::
+
+    (Nuclide('U-235.80c'), 5.0)
+
+Adding Components to a Material
+"""""""""""""""""""""""""""""""
+
+To add components to a material use either
+:func:`~montepy.data_inputs.material.Material.add_nuclide`, or
+:func:`~montepy.data_inputs.material.Material.append`.
+:func:`~montepy.data_inputs.material.Material.add_nuclide` is generally the easier method to use.
+It accepts a nuclide or the name of a nuclide, and its fraction.
+
+.. note::
+
+    When adding a new component it is not possible to change whether the fraction is in atom fraction 
+    or mass fraction.
+    This is settable through :func:`~montepy.data_inputs.material.Material.is_atom_fraction`.
+
+.. testcode::
+
+    mat.add_nuclide("B-10.80c", 1e-6)
+    for comp in mat:
+        print(comp)
+
+.. testoutput::
+
+    (Nuclide('U-235.80c'), 5.0)
+    (Nuclide('U-238.80c'), 95.0)
+    (Nuclide('B-10.80c'), 1e-06)
+
+
+Libraries
+^^^^^^^^^
+
+MCNP nuclear data comes pre-packaged in multiple different libraries that come from different nuclear data sources
+(e.g., ENDF/B-VIII.0),
+at different temperatures, 
+and for different data needs, e.g., neutron data vs. photo-atomic data.
+For more details see `LA-UR-17-20709 <https://www.osti.gov/biblio/1342828>`_, or 
+`LANL's nuclear data libraries <https://nucleardata.lanl.gov/>`_. 
+
+All :class:`~montepy.data_inputs.nuclide.Nuclide` have a :class:`~montepy.data_inputs.nuclide.Nuclide.library`,
+though it may be just ``""``. 
+These can be manually set for each nuclide.
+If you wish to change all of the components in a material to use the same library you can use
+:func:`~montepy.data_inputs.material.Material.change_libraries`.
+
+MCNP has a precedence system for determining which library use in a specific instance.
+This precedence order is:
+
+#. The library specified with the nuclide e.g., ``80c`` in ``1001.80c``.
+#. The library specified as default for the material e.g., ``nlib = 80c``.
+#. The library specified as default in the default material, ``M0``. 
+#. The first matching entry in the ``XSDIR`` file.
+
+.. note::
+
+    MontePy currently does not support reading an ``XSDIR`` file. It will not provide information for 
+    that final step.
+
+Which library will be used for a given nuclide, material, and problem can be checked with:
+:func:`~montepy.data_inputs.material.Material.get_nuclide_library`.
+
+.. seealso::
+
+    * :manual63:`5.6.1`
+    * :manual62:`108`
+
+
+Finding Materials and Nuclides
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Next, we will cover how to find if
+
+* a nuclide is in a material
+* multiple nuclides are in a material
+* a range of nuclides (e.g., transuranics) is in a material
+* specific materials are in a problem.
+
+Check if Nuclide in Material
+""""""""""""""""""""""""""""
+
+First, you can test if a :class:`~montepy.data_inputs.nuclide.Nuclide` 
+(or :class:`~montepy.data_inputs.nuclide.Nucleus`, or :class:`~montepy.data_inputs.element.Element`, or ``str``),
+is in a material.
+This is generally interpreted broadly rather than explicitly.
+For instance, if the test nuclide has no library this will match
+for all libraries, not just the empty library.
+Similarly, an elemental nuclide, e.g., ``H-0``, will match all nuclides based
+on the element, not just the elemental nuclide.
+
+.. doctest::
+
+    >>> montepy.Nuclide('H-1.80c') in mat
+    False
+    >>> montepy.Element(92) in mat
+    True
+    >>> "U-235" in mat
+    True
+    >>> "U-235.70c" in mat
+    False
+    >>> 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
+with a fraction above the threshold.
+
+.. doctest::
+
+    >>> mat.contains("H-1.80c")
+    False
+    >>> mat.contains("U-235", "U-238", threshold=1.0)
+    True
+    >>> mat.contains("U-235.80c", "B-10")
+    True
+    >>> mat.contains("U-235.80c", "B-10", threshold=1e-3)
+    False
+
+Finding Nuclides
+""""""""""""""""
+
+Often you may need to only work a subset of the components in a material.
+:func:`~montepy.data_inputs.material.Material.find`.
+This returns a Generator of the index of the matching component, and then the component tuple.
+
+.. testcode::
+
+    # find all uraium nuclides
+    for idx, (nuclide, fraction) in mat.find("U"):
+        print(idx, nuclide, fraction)
+
+.. testoutput::
+
+    0  U-235   (80c) 5.0
+    1  U-238   (80c) 95.0
+
+There are also other fancy ways to pass slices, for instance to find all transuranics.
+See the examples in :func:`~montepy.data_inputs.material.Material.find` for more details.
+
+There is a related function as well :func:`~montepy.data_inputs.material.Material.find_vals`,
+which accepts the same arguments but only returns the matching fractions.
+This is great for instance to calculate the heavy metal fraction of a fuel:
+
+.. testcode::
+
+    # get all heavy metal fractions
+    hm_fraction = sum(mat.find_vals(element=slice(90,None))) # slice is requires an end value to accept a start
+    print(hm_fraction)
+
+Shows:
+
+.. testoutput::
+
+    100.0
+
+Finding Materials
+"""""""""""""""""
+
+There are a lot of cases where you may want to find specific materials in a problem,
+for instance getting all steels in a problem.
+This is done with the function :func:`~montepy.materials.Materials.get_containing`
+of :class:`~montepy.materials.Materials`.
+It takes the same arguments as :func:`~montepy.data_inputs.material.Material.contains` 
+previously discussed.
+
+Mixing Materials
+^^^^^^^^^^^^^^^^
+
+Commonly materials are a mixture of other materials.
+For instance a good idea for defining structural materials might be to create a new material for each element,
+that adds the naturally occurring nuclides of the element,
+and then mixing those elements together to make steel, zircaloy, etc.
+This mixing is done with :class:`~montepy.materials.Materials.mix`.
+Note this is a method of ``Materials`` and not ``Material``.
+
+.. note::
+
+    Materials can only be combined if they are all atom fraction or mass fraction.
+
+.. note::
+
+    The materials being mixed will be normalized prior to mixing (the original materials are unaffected).
+
+.. testcode::
+
+    mats = problem.materials
+    h2o = montepy.Material()
+    h2o.number = 1
+    h2o.add_nuclide("1001.80c", 2.0)
+    h2o.add_nuclide("8016.80c", 1.0)
+
+    boric_acid = montepy.Material()
+    boric_acid.number = 2
+    for nuclide, fraction in {
+        "1001.80c": 3.0,
+        "B-10.80c": 1.0 * 0.189,
+        "B-11.80c": 1.0 * 0.796,
+        "O-16.80c": 3.0
+    }.items():
+        boric_acid.add_nuclide(nuclide, fraction)
+
+    # boric acid concentration
+    boron_conc = 100e-6 # 100 ppm
+    borated_water = mats.mix([h2o, boric_acid], [1 - boron_conc, boron_conc])
+
 Universes
 ---------
 
diff --git a/montepy/constants.py b/montepy/constants.py
index 069691bb..abe1091f 100644
--- a/montepy/constants.py
+++ b/montepy/constants.py
@@ -37,10 +37,10 @@ The number of characters allowed in a line for each MCNP version.
 
 Citations:
 
-* 5.1.60 and 6.1.0: Section 2.6.2 of LA-UR-18-20808
-* 6.2.0: Section 1.1.1 of LA-UR-17-29981
-* 6.3.0: Section 3.2.2 of LA-UR-22-30006
-* 6.3.1: Section 3.2.2 of LA-UR-24-24602
+* 5.1.60 and 6.1.0: Section 2.6.2 of `LA-UR-18-20808 <https://www.osti.gov/biblio/1419730>`_
+* 6.2.0: Section 1.1.1: :manual62:`13`
+* 6.3.0: :manual63:`3.2.2`
+* 6.3.1: Section 3.2.2 of `LA-UR-24-24602 <https://www.osti.gov/biblio/2372634>`_ 
 """
 
 DEFAULT_VERSION = (6, 3, 0)
diff --git a/montepy/input_parser/syntax_node.py b/montepy/input_parser/syntax_node.py
index af8d6c78..bdae6f87 100644
--- a/montepy/input_parser/syntax_node.py
+++ b/montepy/input_parser/syntax_node.py
@@ -730,7 +730,7 @@ class PaddingNode(SyntaxNodeBase):
 
     def __eq__(self, other):
         if not isinstance(other, (type(self), str)):
-            raise "PaddingNode can only be compared to PaddingNode or str"
+            return False
         if isinstance(other, type(self)):
             other = other.format()
         return self.format() == other
@@ -1379,9 +1379,7 @@ class ValueNode(SyntaxNodeBase):
 
     def __eq__(self, other):
         if not isinstance(other, (type(self), str, int, float)):
-            raise TypeError(
-                f"ValueNode can't be equal to {type(other)} type. {other} given."
-            )
+            return False
         if isinstance(other, ValueNode):
             other_val = other.value
             if self.type != other.type:
@@ -1764,9 +1762,7 @@ class ListNode(SyntaxNodeBase):
 
     def __eq__(self, other):
         if not isinstance(other, (type(self), list)):
-            raise TypeError(
-                f"ListNode can only be compared to a ListNode or List. {other} given."
-            )
+            return False
         if len(self) != len(other):
             return False
         for lhs, rhs in zip(self, other):
diff --git a/montepy/particle.py b/montepy/particle.py
index 6e52f9dd..2c9b6ca7 100644
--- a/montepy/particle.py
+++ b/montepy/particle.py
@@ -7,7 +7,7 @@ class Particle(str, Enum):
     """
     Supported MCNP supported particles.
 
-    Taken from Table 2-2 of LA-UR-17-29981.
+    Taken from :manual62:`46`.
     """
 
     NEUTRON = "N"
@@ -68,7 +68,7 @@ class LibraryType(str, Enum):
 
     .. versionadded:: 1.0.0
 
-    Taken from section of 5.6.1 of LA-UR-22-30006
+    Taken from :manual63:`5.6.1`.
     """
 
     def __new__(cls, value, particle=None):


Test patch:
diff --git a/tests/test_syntax_parsing.py b/tests/test_syntax_parsing.py
index 67c5e5fa..8d03f7f0 100644
--- a/tests/test_syntax_parsing.py
+++ b/tests/test_syntax_parsing.py
@@ -361,8 +361,7 @@ class TestValueNode(TestCase):
     def test_value_equality(self):
         value_node1 = syntax_node.ValueNode("1", int)
         self.assertTrue(value_node1 == value_node1)
-        with self.assertRaises(TypeError):
-            value_node1 == syntax_node.PaddingNode("")
+        assert not value_node1 == syntax_node.PaddingNode("")
         value_node2 = syntax_node.ValueNode("2", int)
         self.assertTrue(value_node1 != value_node2)
         value_node3 = syntax_node.ValueNode("hi", str)
@@ -570,8 +569,7 @@ class TestPaddingNode(TestCase):
         self.assertTrue(pad != " hi ")
         pad1 = syntax_node.PaddingNode(" ")
         self.assertTrue(pad == pad1)
-        with self.assertRaises(TypeError):
-            pad == 1
+        assert not pad == 1
 
     def test_comment_init(self):
         comment = syntax_node.CommentNode("$ hi")
@@ -722,8 +720,7 @@ class TestListNode(TestCase):
         list_node1 = syntax_node.ListNode("list")
         for i in range(20):
             list_node1.append(syntax_node.ValueNode("1.0", float))
-        with self.assertRaises(TypeError):
-            list_node1 == "hi"
+        assert not list_node1 == "hi"
         list2 = [syntax_node.ValueNode("1.0", float)] * 19
         self.assertTrue(not list_node1 == list2)
         list2 = [syntax_node.ValueNode("1.0", float)] * 20