Repository: pynucastro/pynucastro

Issue:
add unit test for the RateFilter stuff
We should add unit tests to test the new RateFilter and Library classes.

Discussion:

Patch:
diff --git a/examples/pynucastro-examples.ipynb b/examples/pynucastro-examples.ipynb
index 6c9d6342..65202098 100644
--- a/examples/pynucastro-examples.ipynb
+++ b/examples/pynucastro-examples.ipynb
@@ -196,7 +196,7 @@
     {
      "data": {
       "text/plain": [
-       "pynucastro.rates.rate.Nucleus"
+       "pynucastro.nucleus.nucleus.Nucleus"
       ]
      },
      "execution_count": 8,
@@ -559,7 +559,7 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "79e5f2f2f68d425db37d3531bded5436",
+       "model_id": "19bbbe6428624ce99c3b13327f3a0bbb",
        "version_major": 2,
        "version_minor": 0
       },
diff --git a/pynucastro/__init__.py b/pynucastro/__init__.py
index f0e4ad1b..23169502 100644
--- a/pynucastro/__init__.py
+++ b/pynucastro/__init__.py
@@ -26,6 +26,11 @@ be in the Reaclib 1 format.
 nucdata
 -------
 
+nucdata provides:
+
+* Nucleus : a single nucleus, with a descriptive name and its
+  properties.
+
 nucdata provides tables of binding energy per nucleon in MeV,
 partition function and the number of spin states for nuclides specified
 by their number of neutrons N and atomic number Z.
@@ -76,9 +81,6 @@ rates
 rates provides classes and functions for interpreting individual
 reaction rates, including:
 
-* Nucleus : a single nucleus, with a descriptive name and its
-  properties.
-
 * Rate : a single Reaclib rate, with methods for plotting and
   evaluating it.
 
@@ -123,6 +125,9 @@ from ._version import version
 
 __version__ = version
 
+from pynucastro.nucleus import \
+    Nucleus
+
 from pynucastro.networks import \
     RateCollection, \
     Composition, \
@@ -136,7 +141,6 @@ from pynucastro.networks import \
 
 from pynucastro.rates import \
     Tfactors, \
-    Nucleus, \
     Rate, \
     RateFilter, \
     Library, \
diff --git a/pynucastro/networks/base_cxx_network.py b/pynucastro/networks/base_cxx_network.py
index cc26216b..280256b8 100644
--- a/pynucastro/networks/base_cxx_network.py
+++ b/pynucastro/networks/base_cxx_network.py
@@ -9,7 +9,6 @@ import os
 import shutil
 import sys
 import re
-from collections import OrderedDict
 from abc import ABC, abstractmethod
 import random
 import string
@@ -48,7 +47,7 @@ class BaseCxxNetwork(ABC, RateCollection):
 
         # a dictionary of functions to call to handle specific parts
         # of the C++ template
-        self.ftags = OrderedDict()
+        self.ftags = {}
         self.ftags['<nrat_reaclib>'] = self._nrat_reaclib
         self.ftags['<nrat_tabular>'] = self._nrat_tabular
         self.ftags['<nrxn>'] = self._nrxn
diff --git a/pynucastro/networks/base_fortran_network.py b/pynucastro/networks/base_fortran_network.py
index 40e4f4f2..f129f904 100644
--- a/pynucastro/networks/base_fortran_network.py
+++ b/pynucastro/networks/base_fortran_network.py
@@ -9,7 +9,6 @@ import os
 import shutil
 import sys
 import re
-from collections import OrderedDict
 from abc import ABC, abstractmethod
 import random
 import string
@@ -48,7 +47,7 @@ class BaseFortranNetwork(ABC, RateCollection):
 
         # a dictionary of functions to call to handle specific parts
         # of the Fortran template
-        self.ftags = OrderedDict()
+        self.ftags = {}
         self.ftags['<nrates>'] = self._nrates
         self.ftags['<nrat_reaclib>'] = self._nrat_reaclib
         self.ftags['<nrat_tabular>'] = self._nrat_tabular
diff --git a/pynucastro/networks/rate_collection.py b/pynucastro/networks/rate_collection.py
index c66cf7ff..1b66f4bb 100644
--- a/pynucastro/networks/rate_collection.py
+++ b/pynucastro/networks/rate_collection.py
@@ -19,7 +19,8 @@ from matplotlib.ticker import MaxNLocator
 import networkx as nx
 
 # Import Rate
-from pynucastro.rates import Rate, RatePair, Nucleus, Library
+from pynucastro.nucleus import Nucleus
+from pynucastro.rates import Rate, RatePair, Library
 
 mpl.rcParams['figure.dpi'] = 100
 
diff --git a/pynucastro/networks/sympy_network_support.py b/pynucastro/networks/sympy_network_support.py
index a9e08db8..7134eece 100644
--- a/pynucastro/networks/sympy_network_support.py
+++ b/pynucastro/networks/sympy_network_support.py
@@ -2,7 +2,6 @@
 through sympy"""
 
 import re
-from collections import OrderedDict
 
 import sympy
 
@@ -13,7 +12,7 @@ class SympyRates:
 
         self.ctype = ctype
 
-        self.symbol_ludict = OrderedDict()  # Symbol lookup dictionary
+        self.symbol_ludict = {}  # Symbol lookup dictionary
 
         if self.ctype == "Fortran":
             self.name_density = 'state % rho'
diff --git a/pynucastro/nucdata/__init__.py b/pynucastro/nucdata/__init__.py
index d46b2236..0e2a4b35 100644
--- a/pynucastro/nucdata/__init__.py
+++ b/pynucastro/nucdata/__init__.py
@@ -8,5 +8,5 @@ particular, the binding energies)
 from .binding_nuclide import BindingNuclide
 from .binding_table import BindingTable
 from .elements import Element, UnidentifiedElement, PeriodicTable
-from .partition_function import PartitionFunction, PartitionFunctionCollection
+from .partition_function import PartitionFunction, PartitionFunctionTable, PartitionFunctionCollection
 from .spin_nuclide import SpinNuclide, SpinTable
diff --git a/pynucastro/nucdata/partition_function.py b/pynucastro/nucdata/partition_function.py
index bda0c12f..b219e748 100644
--- a/pynucastro/nucdata/partition_function.py
+++ b/pynucastro/nucdata/partition_function.py
@@ -115,6 +115,11 @@ class PartitionFunction:
 
         return newpf
 
+    def __eq__(self, other):
+
+        return (np.all(self.partition_function == other.partition_function) and
+                np.all(self.temperature == other.temperature))
+
     def construct_spline_interpolant(self, order=3):
         """
         Construct an interpolating univariate spline of order >= 1 and
@@ -163,7 +168,7 @@ class PartitionFunctionTable:
         self._partition_function = {}
         self.name = None
         self.temperatures = None
-        self.read_table(file_name)
+        self._read_table(file_name)
 
     def _add_nuclide_pfun(self, nuc, pfun):
         assert isinstance(nuc, str)
@@ -183,7 +188,7 @@ class PartitionFunctionTable:
         if str(nuc) in self._partition_function.keys():
             return self._partition_function[nuc]
 
-    def read_table(self, file_name):
+    def _read_table(self, file_name):
         with open(file_name, 'r') as fin:
 
             # get headers name
@@ -229,7 +234,7 @@ class PartitionFunctionCollection:
     def __init__(self, use_high_temperatures=True, use_set='frdm'):
         self._partition_function_tables = {}
         self.use_high_temperatures = use_high_temperatures
-        self.use_set = 'frdm'
+        self.use_set = use_set
         self._read_collection()
 
     def _add_table(self, table):
diff --git a/pynucastro/nucdata/spin_nuclide.py b/pynucastro/nucdata/spin_nuclide.py
index 95a22207..7a53cfb3 100644
--- a/pynucastro/nucdata/spin_nuclide.py
+++ b/pynucastro/nucdata/spin_nuclide.py
@@ -26,6 +26,10 @@ class SpinNuclide:
 
         return rep
 
+    def __eq__(self, other):
+
+        return self.a == other.a and self.z == other.z
+
 
 class SpinTable:
     """
diff --git a/pynucastro/nucleus/__init__.py b/pynucastro/nucleus/__init__.py
new file mode 100644
index 00000000..44b8796a
--- /dev/null
+++ b/pynucastro/nucleus/__init__.py
@@ -0,0 +1,8 @@
+"""Properties of nuclei needed in construction reaction rates (in
+particular, the binding energies)
+
+"""
+
+__all__ = ["nucleus"]
+
+from .nucleus import Nucleus, UnsupportedNucleus
diff --git a/pynucastro/nucleus/nucleus.py b/pynucastro/nucleus/nucleus.py
new file mode 100644
index 00000000..3e2dbbd2
--- /dev/null
+++ b/pynucastro/nucleus/nucleus.py
@@ -0,0 +1,172 @@
+"""
+Classes and methods to interface with files storing rate data.
+"""
+
+import os
+import re
+
+from pynucastro.nucdata import UnidentifiedElement, PeriodicTable, PartitionFunctionCollection, BindingTable, SpinTable
+
+_pynucastro_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
+_pynucastro_rates_dir = os.path.join(_pynucastro_dir, 'library')
+_pynucastro_tabular_dir = os.path.join(_pynucastro_rates_dir, 'tabular')
+
+
+#read the spin table once and store it at the module-level
+_spin_table = SpinTable(set_double_gs=False)
+
+# read the binding energy table once and store it at the module-level
+_binding_table = BindingTable()
+
+
+_pcollection = PartitionFunctionCollection(use_high_temperatures=True, use_set='frdm')
+
+
+class UnsupportedNucleus(BaseException):
+    def __init__(self):
+        return
+
+
+class Nucleus:
+    """
+    a nucleus that participates in a reaction -- we store it in a
+    class to hold its properties, define a sorting, and give it a
+    pretty printing string.
+
+    :var Z:               atomic number
+    :var N:               neutron number
+    :var A:               atomic mass
+    :var nucbind:         nuclear binding energy (MeV / nucleon)
+    :var short_spec_name: nucleus abbrevation (e.g. "he4")
+    :var caps_name:       capitalized short species name (e.g. "He4")
+    :var el:              element name (e.g. "he")
+    :var pretty:          LaTeX formatted version of the nucleus name
+
+    """
+    _cache = {}
+
+    def __init__(self, name, dummy=False):
+        name = name.lower()
+        self.raw = name
+
+        # a dummy nucleus is one that we can use where a nucleus is needed
+        # but it is not considered to be part of the network
+        self.dummy = dummy
+
+        # element symbol and atomic weight
+        if name == "p":
+            self.el = "h"
+            self.A = 1
+            self.short_spec_name = "h1"
+            self.caps_name = "H1"
+        elif name == "d":
+            self.el = "h"
+            self.A = 2
+            self.short_spec_name = "h2"
+            self.caps_name = "H2"
+        elif name == "t":
+            self.el = "h"
+            self.A = 3
+            self.short_spec_name = "h3"
+            self.caps_name = "H3"
+        elif name == "a":
+            #this is a convenience, enabling the use of a commonly-used alias:
+            #    He4 --> \alpha --> "a" , e.g. c12(a,g)o16
+            self.el = "he"
+            self.A = 4
+            self.short_spec_name = "he4"
+            self.raw = "he4"
+            self.caps_name = "He4"
+        elif name == "n":
+            self.el = "n"
+            self.A = 1
+            self.Z = 0
+            self.N = 1
+            self.short_spec_name = "n"
+            self.spec_name = "neutron"
+            self.pretty = fr"\mathrm{{{self.el}}}"
+            self.caps_name = "N"
+        else:
+            e = re.match(r"([a-zA-Z]*)(\d*)", name)
+            self.el = e.group(1).title()  # chemical symbol
+            assert self.el
+            try:
+                self.A = int(e.group(2))
+            except (TypeError, ValueError):
+                if (name.strip() == 'al-6' or
+                    name.strip() == 'al*6'):
+                    raise UnsupportedNucleus()
+                else:
+                    raise
+            assert self.A >= 0
+            self.short_spec_name = name
+            self.caps_name = name.capitalize()
+
+        # set the number of spin states
+        try:
+            self.spin_states = _spin_table.get_spin_nuclide(self.short_spec_name).spin_states
+        except NotImplementedError:
+            self.spin_states = None
+
+        # use lowercase element abbreviation regardless the case of the input
+        self.el = self.el.lower()
+
+        # set a partition function object to every nucleus
+        self.partition_function = _pcollection.get_partition_function(self.short_spec_name)
+
+        # atomic number comes from periodic table
+        if name != "n":
+            try:
+                i = PeriodicTable.lookup_abbreviation(self.el)
+            except UnidentifiedElement:
+                print(f'Could not identify element: {self.el}')
+                raise
+            else:
+                self.Z = i.Z
+                assert isinstance(self.Z, int)
+                assert self.Z >= 0
+                self.N = self.A - self.Z
+                assert isinstance(self.N, int)
+                assert self.N >= 0
+
+                # long name
+                self.spec_name = f'{i.name}-{self.A}'
+
+                # latex formatted style
+                self.pretty = fr"{{}}^{{{self.A}}}\mathrm{{{self.el.capitalize()}}}"
+
+        try:
+            self.nucbind = _binding_table.get_nuclide(n=self.N, z=self.Z).nucbind
+        except NotImplementedError:
+            # the binding energy table doesn't know about this nucleus
+            self.nucbind = None
+
+    @classmethod
+    def from_cache(cls, name, dummy=False):
+        key = (name.lower(), dummy)
+        if key not in cls._cache:
+            cls._cache[key] = Nucleus(name, dummy)
+        return cls._cache[key]
+
+    def __repr__(self):
+        return self.raw
+
+    def __hash__(self):
+        return hash((self.Z, self.A))
+
+    def c(self):
+        """return the name capitalized"""
+        return self.caps_name
+
+    def __eq__(self, other):
+        if isinstance(other, Nucleus):
+            return self.el == other.el and \
+                self.Z == other.Z and self.A == other.A
+        if isinstance(other, tuple):
+            return (self.Z, self.A) == other
+        return NotImplemented
+
+    def __lt__(self, other):
+        if not self.Z == other.Z:
+            return self.Z < other.Z
+        return self.A < other.A
diff --git a/pynucastro/rates/__init__.py b/pynucastro/rates/__init__.py
index cca47890..d2f31cb4 100644
--- a/pynucastro/rates/__init__.py
+++ b/pynucastro/rates/__init__.py
@@ -2,5 +2,5 @@
 
 __all__ = ["rate", "library"]
 
-from .rate import Tfactors, Rate, RatePair, UnsupportedNucleus, Nucleus, _find_rate_file
+from .rate import Tfactors, Rate, RatePair, _find_rate_file
 from .library import Library, ReacLibLibrary, RateFilter, list_known_rates
diff --git a/pynucastro/rates/library.py b/pynucastro/rates/library.py
index b3c6283d..50d703d5 100644
--- a/pynucastro/rates/library.py
+++ b/pynucastro/rates/library.py
@@ -2,7 +2,8 @@ import os
 import io
 import collections
 
-from pynucastro.rates import Rate, Nucleus, UnsupportedNucleus, _find_rate_file
+from pynucastro.nucleus import Nucleus, UnsupportedNucleus
+from pynucastro.rates import Rate, _find_rate_file
 
 
 def list_known_rates():
@@ -47,7 +48,7 @@ class Library:
             elif isinstance(rates, (list, set)):
                 self._add_from_rate_list(rates)
         else:
-            self._rates = collections.OrderedDict()
+            self._rates = {}
         self._library_source_lines = collections.deque()
 
         if self._library_file and read_library:
@@ -81,7 +82,7 @@ class Library:
     def _add_from_rate_list(self, ratelist):
         """ Add to the rate dictionary from the supplied list of Rate objects. """
         if not self._rates:
-            self._rates = collections.OrderedDict()
+            self._rates = {}
         for r in ratelist:
             rid = r.get_rate_id()
             assert rid not in self._rates, "ERROR: supplied a Rate object already in the Library."
@@ -277,7 +278,7 @@ class Library:
                 raise
             else:
                 filter_specifications = filter_spec
-        matching_rates = collections.OrderedDict()
+        matching_rates = {}
         for rid, r in self._rates.items():
             for f in filter_specifications:
                 if f.matches(r):
diff --git a/pynucastro/rates/rate.py b/pynucastro/rates/rate.py
index 50035773..c348a35c 100644
--- a/pynucastro/rates/rate.py
+++ b/pynucastro/rates/rate.py
@@ -9,28 +9,19 @@ import numpy as np
 import matplotlib.pyplot as plt
 import numba
 
+from pynucastro.nucleus import Nucleus
+
 try:
     from numba.experimental import jitclass
 except ImportError:
     from numba import jitclass
 
-from pynucastro.nucdata import UnidentifiedElement, PeriodicTable, PartitionFunctionCollection, BindingTable, SpinTable
 
 _pynucastro_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
 _pynucastro_rates_dir = os.path.join(_pynucastro_dir, 'library')
 _pynucastro_tabular_dir = os.path.join(_pynucastro_rates_dir, 'tabular')
 
 
-#read the spin table once and store it at the module-level
-_spin_table = SpinTable(set_double_gs=False)
-
-# read the binding energy table once and store it at the module-level
-_binding_table = BindingTable()
-
-
-_pcollection = PartitionFunctionCollection(use_high_temperatures=True, use_set='frdm')
-
-
 def _find_rate_file(ratename):
     """locate the Reaclib or tabular rate or library file given its name.  Return
     None if the file cannot be located, otherwise return its path."""
@@ -178,156 +169,6 @@ class SingleSet:
         return string
 
 
-class UnsupportedNucleus(BaseException):
-    def __init__(self):
-        return
-
-
-class Nucleus:
-    """
-    a nucleus that participates in a reaction -- we store it in a
-    class to hold its properties, define a sorting, and give it a
-    pretty printing string.
-
-    :var Z:               atomic number
-    :var N:               neutron number
-    :var A:               atomic mass
-    :var nucbind:         nuclear binding energy (MeV / nucleon)
-    :var short_spec_name: nucleus abbrevation (e.g. "he4")
-    :var caps_name:       capitalized short species name (e.g. "He4")
-    :var el:              element name (e.g. "he")
-    :var pretty:          LaTeX formatted version of the nucleus name
-
-    """
-    _cache = {}
-
-    def __init__(self, name, dummy=False):
-        name = name.lower()
-        self.raw = name
-
-        # a dummy nucleus is one that we can use where a nucleus is needed
-        # but it is not considered to be part of the network
-        self.dummy = dummy
-
-        # element symbol and atomic weight
-        if name == "p":
-            self.el = "h"
-            self.A = 1
-            self.short_spec_name = "h1"
-            self.caps_name = "H1"
-        elif name == "d":
-            self.el = "h"
-            self.A = 2
-            self.short_spec_name = "h2"
-            self.caps_name = "H2"
-        elif name == "t":
-            self.el = "h"
-            self.A = 3
-            self.short_spec_name = "h3"
-            self.caps_name = "H3"
-        elif name == "a":
-            #this is a convenience, enabling the use of a commonly-used alias:
-            #    He4 --> \alpha --> "a" , e.g. c12(a,g)o16
-            self.el = "he"
-            self.A = 4
-            self.short_spec_name = "he4"
-            self.raw = "he4"
-            self.caps_name = "He4"
-        elif name == "n":
-            self.el = "n"
-            self.A = 1
-            self.Z = 0
-            self.N = 1
-            self.short_spec_name = "n"
-            self.spec_name = "neutron"
-            self.pretty = fr"\mathrm{{{self.el}}}"
-            self.caps_name = "N"
-        else:
-            e = re.match(r"([a-zA-Z]*)(\d*)", name)
-            self.el = e.group(1).title()  # chemical symbol
-            assert self.el
-            try:
-                self.A = int(e.group(2))
-            except (TypeError, ValueError):
-                if (name.strip() == 'al-6' or
-                    name.strip() == 'al*6'):
-                    raise UnsupportedNucleus()
-                else:
-                    raise
-            assert self.A >= 0
-            self.short_spec_name = name
-            self.caps_name = name.capitalize()
-
-        # set the number of spin states
-        try:
-            self.spin_states = _spin_table.get_spin_nuclide(self.short_spec_name).spin_states
-        except NotImplementedError:
-            self.spin_states = None
-
-        # use lowercase element abbreviation regardless the case of the input
-        self.el = self.el.lower()
-
-        # set a partition function object to every nucleus
-        self.partition_function = _pcollection.get_partition_function(self.short_spec_name)
-
-        # atomic number comes from periodic table
-        if name != "n":
-            try:
-                i = PeriodicTable.lookup_abbreviation(self.el)
-            except UnidentifiedElement:
-                print(f'Could not identify element: {self.el}')
-                raise
-            else:
-                self.Z = i.Z
-                assert isinstance(self.Z, int)
-                assert self.Z >= 0
-                self.N = self.A - self.Z
-                assert isinstance(self.N, int)
-                assert self.N >= 0
-
-                # long name
-                self.spec_name = f'{i.name}-{self.A}'
-
-                # latex formatted style
-                self.pretty = fr"{{}}^{{{self.A}}}\mathrm{{{self.el.capitalize()}}}"
-
-        try:
-            self.nucbind = _binding_table.get_nuclide(n=self.N, z=self.Z).nucbind
-        except NotImplementedError:
-            # the binding energy table doesn't know about this nucleus
-            self.nucbind = None
-
-    @classmethod
-    def from_cache(cls, name, dummy=False):
-        key = (name.lower(), dummy)
-        if key not in cls._cache:
-            cls._cache[key] = Nucleus(name, dummy)
-        return cls._cache[key]
-
-    def __repr__(self):
-        return self.raw
-
-    def __hash__(self):
-        return hash((self.Z, self.A))
-
-    def c(self):
-        """return the name capitalized"""
-        return self.caps_name
-
-    def __eq__(self, other):
-        if isinstance(other, Nucleus):
-            return self.el == other.el and \
-                self.Z == other.Z and self.A == other.A
-        if isinstance(other, tuple):
-            return (self.Z, self.A) == other
-        return NotImplemented
-
-    def __lt__(self, other):
-        if not self.Z == other.Z:
-            return self.Z < other.Z
-        return self.A < other.A
-
-
 class Rate:
     """A single reaction rate.  Currently, this can be a
     Reaclib rate, which can be composed of multiple sets, or a tabulated


Test patch:
diff --git a/pynucastro/networks/tests/test_networks.py b/pynucastro/networks/tests/test_networks.py
index ef1b09f5..a66bf0b6 100644
--- a/pynucastro/networks/tests/test_networks.py
+++ b/pynucastro/networks/tests/test_networks.py
@@ -1,6 +1,6 @@
 # unit tests for rates
 import pynucastro.networks as networks
-import pynucastro.rates as rates
+from pynucastro.nucleus import Nucleus
 
 from pytest import approx
 
@@ -18,12 +18,12 @@ class TestComposition:
 
     def setup_method(self):
         """ this is run before each test """
-        self.nuclei = [rates.Nucleus("h1"),
-                       rates.Nucleus("he4"),
-                       rates.Nucleus("c12"),
-                       rates.Nucleus("o16"),
-                       rates.Nucleus("n14"),
-                       rates.Nucleus("ca40")]
+        self.nuclei = [Nucleus("h1"),
+                       Nucleus("he4"),
+                       Nucleus("c12"),
+                       Nucleus("o16"),
+                       Nucleus("n14"),
+                       Nucleus("ca40")]
 
         self.comp = networks.Composition(self.nuclei)
 
@@ -39,7 +39,7 @@ class TestComposition:
             sum += self.comp.X[k]
 
         assert sum == approx(1.0)
-        assert self.comp.X[rates.Nucleus("h1")] == approx(0.7)
+        assert self.comp.X[Nucleus("h1")] == approx(0.7)
 
     def test_set_all(self):
         val = 1.0/len(self.nuclei)
@@ -55,7 +55,7 @@ class TestComposition:
     def test_get_molar(self):
         self.comp.set_solar_like(Z=0.02)
         molar = self.comp.get_molar()
-        assert molar[rates.Nucleus("he4")] == approx((0.3-0.02)/4.0)
+        assert molar[Nucleus("he4")] == approx((0.3-0.02)/4.0)
 
 
 class TestRateCollection:
@@ -81,15 +81,15 @@ class TestRateCollection:
                  "o15--n15-wc12"]
         self.rc = networks.RateCollection(files)
 
-        self.p = rates.Nucleus("p")
-        self.he4 = rates.Nucleus("he4")
-        self.c12 = rates.Nucleus("c12")
-        self.c13 = rates.Nucleus("c13")
-        self.n13 = rates.Nucleus("n13")
-        self.n14 = rates.Nucleus("n14")
-        self.n15 = rates.Nucleus("n15")
-        self.o14 = rates.Nucleus("o14")
-        self.o15 = rates.Nucleus("o15")
+        self.p = Nucleus("p")
+        self.he4 = Nucleus("he4")
+        self.c12 = Nucleus("c12")
+        self.c13 = Nucleus("c13")
+        self.n13 = Nucleus("n13")
+        self.n14 = Nucleus("n14")
+        self.n15 = Nucleus("n15")
+        self.o14 = Nucleus("o14")
+        self.o15 = Nucleus("o15")
 
     def teardown_method(self):
         """ this is run after each test """
diff --git a/pynucastro/networks/tests/test_starkiller_net.py b/pynucastro/networks/tests/test_starkiller_net.py
index b5937705..b9f5082f 100644
--- a/pynucastro/networks/tests/test_starkiller_net.py
+++ b/pynucastro/networks/tests/test_starkiller_net.py
@@ -1,6 +1,6 @@
 # unit tests for rates
 import pynucastro.networks as networks
-import pynucastro.rates as rates
+from pynucastro.nucleus import Nucleus
 import os
 import filecmp
 
@@ -241,17 +241,17 @@ class TestReaclibChapterNetwork:
 
         self.fn = networks.StarKillerNetwork(files)
 
-        self.n = rates.Nucleus("n")
-        self.p = rates.Nucleus("p")
-        self.d = rates.Nucleus("d")
-        self.t = rates.Nucleus("t")
-        self.he3 = rates.Nucleus("he3")
-        self.he4 = rates.Nucleus("he4")
-        self.he6 = rates.Nucleus("he6")
-        self.li7 = rates.Nucleus("li7")
-        self.b17 = rates.Nucleus("b17")
-        self.c12 = rates.Nucleus("c12")
-        self.c14 = rates.Nucleus("c14")
+        self.n = Nucleus("n")
+        self.p = Nucleus("p")
+        self.d = Nucleus("d")
+        self.t = Nucleus("t")
+        self.he3 = Nucleus("he3")
+        self.he4 = Nucleus("he4")
+        self.he6 = Nucleus("he6")
+        self.li7 = Nucleus("li7")
+        self.b17 = Nucleus("b17")
+        self.c12 = Nucleus("c12")
+        self.c14 = Nucleus("c14")
 
     def teardown_method(self):
         """ this is run after each test """
diff --git a/pynucastro/nucdata/tests/test_partition.py b/pynucastro/nucdata/tests/test_partition.py
new file mode 100644
index 00000000..a02d154a
--- /dev/null
+++ b/pynucastro/nucdata/tests/test_partition.py
@@ -0,0 +1,113 @@
+from numpy import array, all, ones_like
+from pynucastro.nucdata import PartitionFunctionTable, PartitionFunctionCollection
+import os
+
+nucdata_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
+pf_dir = os.path.join(nucdata_dir, 'PartitionFunction')
+
+dir_etfsiq_low = os.path.join(pf_dir, 'etfsiq_low.txt')
+dir_frdm_low = os.path.join(pf_dir, 'frdm_low.txt')
+dir_etfsiq_high = os.path.join(pf_dir, 'etfsiq_high.txt')
+dir_frdm_high = os.path.join(pf_dir, 'frdm_high.txt')
+
+ANSWER_ETFSIQ_LOW = array([1.000271, 1.002656, 1.009124, 1.035543, 1.076750, 1.128518, 1.187847, 1.252797,
+                           1.322103, 1.394926, 1.470693, 1.883077, 2.339548, 2.835353, 3.371056, 3.949365,
+                           4.574281, 5.250894, 5.985411, 7.659520, 9.675912, 12.147961, 15.237089, 19.172457])
+
+ANSWER_FRDM_LOW = array([1.000157, 1.001534, 1.005265, 1.020486, 1.044185, 1.073899, 1.107886, 1.145013,
+                         1.184544, 1.225988, 1.269010, 1.501456, 1.755494, 2.027655, 2.317420, 2.625339,
+                         2.952505, 3.300375, 3.670713, 4.487378, 5.423159, 6.505528, 7.771334, 9.270601])
+
+ANSWER_ETFSIQ_HIGH = array([5.79E+000, 1.07E+001, 2.13E+001, 4.38E+001, 9.23E+001, 1.97E+002, 4.23E+002,
+                             9.12E+002, 1.97E+003, 4.25E+003, 2.92E+004, 2.00E+005, 1.36E+006, 9.31E+006,
+                             6.34E+007, 4.31E+008, 2.92E+009, 1.97E+010, 1.33E+011, 8.93E+011, 5.98E+012,
+                             3.99E+013, 2.65E+014, 1.76E+015, 1.16E+016, 7.66E+016, 5.03E+017, 3.30E+018,
+                             2.16E+019, 1.41E+020, 9.21E+020, 6.00E+021, 3.91E+022, 2.54E+023, 1.65E+024,
+                             1.07E+025, 6.97E+025, 4.52E+026, 2.94E+027, 1.91E+028, 8.07E+029, 3.42E+031,
+                             1.46E+033, 6.23E+034, 2.68E+036, 1.16E+038, 5.03E+039, 6.45E+043])
+
+ANSWER_FRDM_HIGH = array([9.40e+007, 2.81e+009, 4.93e010, 1.95e+012, 8.84e+013, 3.66e+015, 1.44e+017,
+                          5.48e+018, 2.04e+020, 7.48e+021, 5.72e+025, 4.07e+029, 2.69e+033, 1.66e+037,
+                          9.60e+040, 5.20e+044, 2.65e+048, 1.28e+052, 5.85e+055, 2.55e+059, 1.06e+063,
+                          4.27e+066, 1.65e+070, 6.16e+073, 2.23e+077, 7.87e+080, 2.71e+084, 9.15e+087,
+                          3.03e+091, 9.86e+094, 3.17e+098, 1.00e+102, 3.14e+105, 9.77e+108, 3.01e+112,
+                          9.23e+115, 2.82e+119, 8.56e+122, 2.59e+126, 7.85e+129, 7.18e+136, 6.59e+143,
+                          6.11e+150, 5.74e+157, 5.48e+164, 5.35e+171, 5.34e+178, 1.88e+196])
+
+TEMPERATURES_LOW = array([0.01E+9, 0.15E+9, 0.2E+9, 0.3E+9, 0.4E+9, 0.5E+9, 0.6E+9,
+                          0.7E+9, 0.8E+9, 0.9E+9, 1.0E+9, 1.5E+9, 2.0E+9, 2.5E+9,
+                          3.0E+9, 3.5E+9, 4.0E+9, 4.5E+9, 5.0E+9, 6.0E+9, 7.0E+9,
+                          8.0E+9, 9.0E+9, 10.0E+9])
+
+TEMPERATURES_HIGH = array([12.0E+9, 14.0E+9, 16.0E+9, 18.0E+9, 20.0E+9, 22.0E+9, 24.0E+9,
+                           26.0E+9, 28.0E+9, 30.0E+9, 35.0E+9, 40.0E+9, 45.0E+9, 50.0E+9,
+                           55.0E+9, 60.0E+9, 65.0E+9, 70.0E+9, 75.0E+9, 80.0E+9, 85.0E+9,
+                           90.0E+9, 95.0E+9, 100.0E+9, 105.0E+9, 110.0E+9, 115.0E+9, 120.0E+9,
+                           125.0E+9, 130.0E+9, 135.0E+9, 140.0E+9, 145.0E+9, 150.0E+9, 155.0E+9,
+                           160.0E+9, 165.0E+9, 170.0E+9, 175.0E+9, 180.0E+9, 190.0E+9, 200.0E+9,
+                           210.0E+9, 220.0E+9, 230.0E+9, 240.0E+9, 250.0E+9, 275.0E+9])
+
+DEFAULT = ones_like(TEMPERATURES_LOW)
+
+
+class TestPartition:
+
+    @classmethod
+    def setup_class(cls):
+        """ this is run once for each class before any tests """
+        pass
+
+    @classmethod
+    def teardown_class(cls):
+        """ this is run once for each class before any tests """
+        pass
+
+    def setup_method(self):
+        """ this is run once for each class before any tests """
+
+        self.pf_table_etfsiq_low = PartitionFunctionTable(dir_etfsiq_low)
+        self.pf_table_frdm_low = PartitionFunctionTable(dir_frdm_low)
+        self.pf_table_etfsiq_high = PartitionFunctionTable(dir_etfsiq_high)
+        self.pf_table_frdm_high = PartitionFunctionTable(dir_frdm_high)
+
+        self.co46_pf_etfsiq_low = self.pf_table_etfsiq_low.get_partition_function('co46')
+        self.ne37_pf_frdm_low = self.pf_table_frdm_low.get_partition_function('ne37')
+        self.fe47_pf_etfsiq_high = self.pf_table_etfsiq_high.get_partition_function('fe47')
+        self.po188_pf_frdm_high = self.pf_table_frdm_high.get_partition_function('po188')
+
+        self.ne19_pf_frdm_low = self.pf_table_frdm_low.get_partition_function('ne19')
+        self.ne19_pf_frdm_high = self.pf_table_frdm_high.get_partition_function('ne19')
+
+        self.co60_pf_etfsiq_low = self.pf_table_etfsiq_low.get_partition_function('co60')
+        self.co60_pf_etfsiq_high = self.pf_table_etfsiq_high.get_partition_function('co60')
+
+        self.pf_collection_frdm = PartitionFunctionCollection(use_set='frdm')
+        self.pf_collection_etfsiq = PartitionFunctionCollection(use_set='etfsiq')
+
+    def teardown_method(self):
+        """ this is run once for each class before any tests """
+        pass
+
+    def test_pf(self):
+
+        assert all(self.pf_collection_frdm.get_partition_function('p').partition_function == DEFAULT)
+        assert all(self.pf_collection_etfsiq.get_partition_function('n').partition_function == DEFAULT)
+
+    def test_pf_table(self):
+
+        assert all(self.co46_pf_etfsiq_low.partition_function == ANSWER_ETFSIQ_LOW)
+        assert all(self.co46_pf_etfsiq_low.temperature == TEMPERATURES_LOW)
+
+        assert all(self.ne37_pf_frdm_low.partition_function == ANSWER_FRDM_LOW)
+        assert all(self.ne37_pf_frdm_low.temperature == TEMPERATURES_LOW)
+
+        assert all(self.fe47_pf_etfsiq_high.partition_function == ANSWER_ETFSIQ_HIGH)
+        assert all(self.fe47_pf_etfsiq_high.temperature == TEMPERATURES_HIGH)
+
+        assert all(self.po188_pf_frdm_high.partition_function == ANSWER_FRDM_HIGH)
+        assert all(self.po188_pf_frdm_high.temperature == TEMPERATURES_HIGH)
+
+    def test_pfsum(self):
+
+        assert self.pf_collection_etfsiq.get_partition_function('co60') == self.co60_pf_etfsiq_low + self.co60_pf_etfsiq_high
+        assert self.pf_collection_frdm.get_partition_function('ne19') == self.ne19_pf_frdm_high + self.ne19_pf_frdm_low
diff --git a/pynucastro/nucdata/tests/test_spin.py b/pynucastro/nucdata/tests/test_spin.py
new file mode 100644
index 00000000..ac39c1e4
--- /dev/null
+++ b/pynucastro/nucdata/tests/test_spin.py
@@ -0,0 +1,35 @@
+from pynucastro.nucdata import SpinNuclide, SpinTable
+from numpy import array
+
+
+class TestSpin:
+
+    @classmethod
+    def setup_class(cls):
+        """ this is run once for each class before any tests """
+        pass
+
+    @classmethod
+    def teardown_class(cls):
+        """ this is run once for each class before any tests """
+        pass
+
+    def setup_method(self):
+        """ this is run once for each class before any tests """
+        #Is import to remark that the spin state gs is unique.
+        #However, due to some experimental uncertainties two values
+        #are proposed.
+
+        self.spintable_gs = SpinTable(set_double_gs=False)
+        self.spintable_double_gs = SpinTable(set_double_gs=True)
+
+    def teardown_method(self):
+        """ this is run once for each class before any tests """
+        pass
+
+    def test_spin_table(self):
+
+        #assert self.spintable_gs.get_spin_nuclide('n') == SpinNuclide(a=1, z=0, spin_states=2)
+        assert self.spintable_gs.get_spin_nuclide('pd91') == SpinNuclide(a=91, z=46, spin_states=8)
+        assert self.spintable_double_gs.get_spin_nuclide('br89') == SpinNuclide(a=89, z=35, spin_states=array([4, 6]))
+        assert self.spintable_double_gs.get_spin_nuclide('pr130') == SpinNuclide(a=130, z=59, spin_states=array([13, 15]))
diff --git a/pynucastro/rates/tests/test_nucleus.py b/pynucastro/rates/tests/test_nucleus.py
index ff84949e..0af003bf 100644
--- a/pynucastro/rates/tests/test_nucleus.py
+++ b/pynucastro/rates/tests/test_nucleus.py
@@ -1,6 +1,6 @@
 # unit tests for rates
 
-import pynucastro.rates as rates
+from pynucastro.nucleus import Nucleus
 from pytest import approx
 
 
@@ -18,15 +18,15 @@ class TestNucleus:
     def setup_method(self):
         """ this is run before each test """
 
-        self.p = rates.Nucleus("p")
-        self.h1 = rates.Nucleus("H1")
-        self.d = rates.Nucleus("d")
-        self.he4 = rates.Nucleus("He4")
-        self.c12 = rates.Nucleus("C12")
-        self.o16 = rates.Nucleus("O16")
-        self.ni56 = rates.Nucleus("Ni56")
-        self.u238 = rates.Nucleus("U238")
-        self.he4_also = rates.Nucleus("he4")
+        self.p = Nucleus("p")
+        self.h1 = Nucleus("H1")
+        self.d = Nucleus("d")
+        self.he4 = Nucleus("He4")
+        self.c12 = Nucleus("C12")
+        self.o16 = Nucleus("O16")
+        self.ni56 = Nucleus("Ni56")
+        self.u238 = Nucleus("U238")
+        self.he4_also = Nucleus("he4")
 
     def teardown_method(self):
         """ this is run after each test """
diff --git a/pynucastro/rates/tests/test_ratefilter.py b/pynucastro/rates/tests/test_ratefilter.py
new file mode 100644
index 00000000..246b5159
--- /dev/null
+++ b/pynucastro/rates/tests/test_ratefilter.py
@@ -0,0 +1,74 @@
+# unit tests for rates
+
+import pynucastro as pyna
+
+
+class TestRateFilter:
+    @classmethod
+    def setup_class(cls):
+        """ this is run once for each class before any tests """
+        pass
+
+    @classmethod
+    def teardown_class(cls):
+        """ this is run once for each class after all tests """
+        pass
+
+    def setup_method(self):
+        """ this is run before each test """
+
+        files = ["c12-pg-n13-ls09",
+                 "c13-pg-n14-nacr",
+                 "n13-pg-o14-lg06",
+                 "n14-pg-o15-im05",
+                 "n15-pa-c12-nacr",
+                 "o14--n14-wc12",
+                 "o15--n15-wc12",
+                 "o14-ap-f17-Ha96c",
+                 "f17--o17-wc12",
+                 "f17-pg-ne18-cb09",
+                 "ne18--f18-wc12",
+                 "f18--o18-wc12",
+                 "n15-pg-o16-li10",
+                 "o16-pg-f17-ia08",
+                 "o17-pg-f18-il10",
+                 "f18-pa-o15-il10"]
+
+        rates = []
+        for f in files:
+            rates.append(pyna.Rate(f))
+
+        self.library = pyna.Library(rates=rates)
+
+    def teardown_method(self):
+        """ this is run after each test """
+        self.library = None
+
+    def test_inexact_filter(self):
+        filter = pyna.RateFilter(reactants=['c12'], exact=False)
+        newlib = self.library.filter(filter)
+
+        rates = newlib.get_rates()
+
+        assert len(rates) == 1
+        assert str(rates[0]) == "c12 + p --> n13"
+
+    def test_custom(self):
+
+        # filter out all the rates with fluorine
+
+        filter = pyna.RateFilter(filter_function=lambda r: len([q for q in r.reactants + r.products if q.Z == 9]))
+        newlib = self.library.filter(filter)
+
+        assert len(newlib.get_rates()) == 8
+
+    def test_exact(self):
+
+        filter = pyna.RateFilter(reactants=["n15", "p"])
+        newlib = self.library.filter(filter)
+
+        rates = newlib.get_rates()
+
+        assert len(rates) == 2
+        assert str(rates[0]) == "n15 + p --> he4 + c12"
+        assert str(rates[1]) == "n15 + p --> o16"
diff --git a/pynucastro/rates/tests/test_rates.py b/pynucastro/rates/tests/test_rates.py
index c80b7638..0c164b57 100644
--- a/pynucastro/rates/tests/test_rates.py
+++ b/pynucastro/rates/tests/test_rates.py
@@ -1,6 +1,7 @@
 # unit tests for rates
 import math
 
+from pynucastro.nucleus import Nucleus
 import pynucastro.rates as rates
 from pytest import approx
 
@@ -81,32 +82,32 @@ class TestRate:
         # chapter-11
         self.rate11 = rates.Rate("b17-nnn-c14-wc12")
 
-        self.n = rates.Nucleus("n")
+        self.n = Nucleus("n")
 
-        self.p = rates.Nucleus("p")
-        self.h1 = rates.Nucleus("H1")
-        self.d = rates.Nucleus("d")
-        self.h3 = rates.Nucleus("H3")
+        self.p = Nucleus("p")
+        self.h1 = Nucleus("H1")
+        self.d = Nucleus("d")
+        self.h3 = Nucleus("H3")
 
-        self.he3 = rates.Nucleus("He3")
-        self.he4 = rates.Nucleus("He4")
-        self.he6 = rates.Nucleus("He6")
+        self.he3 = Nucleus("He3")
+        self.he4 = Nucleus("He4")
+        self.he6 = Nucleus("He6")
 
-        self.li7 = rates.Nucleus("Li7")
+        self.li7 = Nucleus("Li7")
 
-        self.b17 = rates.Nucleus("B17")
+        self.b17 = Nucleus("B17")
 
-        self.c12 = rates.Nucleus("C12")
-        self.c14 = rates.Nucleus("C14")
+        self.c12 = Nucleus("C12")
+        self.c14 = Nucleus("C14")
 
-        self.n15 = rates.Nucleus("N15")
+        self.n15 = Nucleus("N15")
 
-        self.o15 = rates.Nucleus("O15")
-        self.o16 = rates.Nucleus("O16")
+        self.o15 = Nucleus("O15")
+        self.o16 = Nucleus("O16")
 
-        self.ni56 = rates.Nucleus("Ni56")
-        self.u238 = rates.Nucleus("U238")
-        self.he4_also = rates.Nucleus("he4")
+        self.ni56 = Nucleus("Ni56")
+        self.u238 = Nucleus("U238")
+        self.he4_also = Nucleus("he4")
 
     def teardown_method(self):
         """ this is run after each test """
@@ -194,14 +195,14 @@ class TestRate:
 
     def test_screen(self):
         assert not self.rate1.ion_screen
-        assert self.rate4.ion_screen == [rates.Nucleus("he4"), rates.Nucleus("c12")]
-        assert self.rate8.ion_screen == 3*[rates.Nucleus("he4")]
+        assert self.rate4.ion_screen == [Nucleus("he4"), Nucleus("c12")]
+        assert self.rate8.ion_screen == 3*[Nucleus("he4")]
 
     def test_heaviest_lightest(self):
-        assert self.rate4.heaviest() == rates.Nucleus("o16")
-        assert self.rate4.lightest() == rates.Nucleus("he4")
-        assert self.rate2.lightest() == rates.Nucleus("n")
-        assert self.rate2.heaviest() == rates.Nucleus("t")
+        assert self.rate4.heaviest() == Nucleus("o16")
+        assert self.rate4.lightest() == Nucleus("he4")
+        assert self.rate2.lightest() == Nucleus("n")
+        assert self.rate2.heaviest() == Nucleus("t")
 
 
 class TestModify:
@@ -230,5 +231,5 @@ class TestModify:
         self.rate.modify_products("mg24")
 
         assert self.rate.Q == approx(13.93356)
-        assert self.rate.products == [rates.Nucleus("mg24")]
+        assert self.rate.products == [Nucleus("mg24")]
         assert self.rate.modified