Repository: uncscode/particula

Issue:
allow multi mode dists
allow for kwargs like the following

multi_mode = {
    "mode": [5000e-9, 50e-9, 300e-9], # coarse, nano, fine mode
    "nbins": 1000, # 1000 bins
    "nparticles": [1e6,1e9,1e7], # 1e4 #
    "volume": 1e-6, # per 1e-6 m^3 (or 1 cc)
    "gsigma": [1.5,1.2,1.5], # 
    "spacing": "logspace", # bin spacing, 
}

Comments:

Patch:
diff --git a/particula/particle.py b/particula/particle.py
index 3b9e2f7..8e85fd2 100644
--- a/particula/particle.py
+++ b/particula/particle.py
@@ -35,7 +35,7 @@ class ParticleDistribution(Vapor):
 
         self.spacing = kwargs.get("spacing", "linspace")
         self.nbins = in_scalar(kwargs.get("nbins", 1000)).m
-        self.nparticles = in_scalar(kwargs.get("nparticles", 1e5))
+        self.nparticles = in_scalar(kwargs.get("nparticles", 1e5)).m
         self.volume = in_volume(kwargs.get("volume", 1e-6))
         self.cutoff = in_scalar(kwargs.get("cutoff", 0.9999)).m
         self.gsigma = in_scalar(kwargs.get("gsigma", 1.25)).m
@@ -89,7 +89,8 @@ class ParticleDistribution(Vapor):
             interval=self.pre_radius(),
             disttype="lognormal",
             gsigma=self.gsigma,
-            mode=self.mode
+            mode=self.mode,
+            nparticles=self.nparticles
         )
 
     def pre_distribution(self):
@@ -102,7 +103,9 @@ class ParticleDistribution(Vapor):
                 mode    : geometric mean radius of the particles
         """
 
-        return self.nparticles*self.pre_discretize()/self.volume
+        return np.array(
+            [self.nparticles]
+        ).sum()*self.pre_discretize()/self.volume
 
 
 class ParticleInstances(ParticleDistribution):
diff --git a/particula/util/distribution_discretization.py b/particula/util/distribution_discretization.py
index 76facea..93a8e43 100644
--- a/particula/util/distribution_discretization.py
+++ b/particula/util/distribution_discretization.py
@@ -11,6 +11,7 @@ def discretize(
     disttype="lognormal",
     gsigma=in_scalar(1.25).m,
     mode=in_radius(100e-9).m,
+    nparticles=in_scalar(1e5).m,
     **kwargs
 ):
     """ discretize the distribution of the particles
@@ -30,13 +31,17 @@ def discretize(
     if not isinstance(interval, u.Quantity):
         interval = u.Quantity(interval, " ")
 
-    if disttype == "lognormal":
-        dist = lognorm.pdf(
-            x=interval.m,
-            s=np.log(gsigma),
-            scale=mode,
-        )/interval.u
-    else:
+    if disttype != "lognormal":
         raise ValueError("the 'disttype' must be 'lognormal' for now!")
 
-    return dist
+    return ((
+        lognorm.pdf(
+            x=interval.m,
+            s=np.reshape(np.log(gsigma), (np.array([gsigma]).size, 1)),
+            scale=np.reshape([mode], (np.array([mode]).size, 1)),
+        ) / interval.u
+        * np.reshape([nparticles], (np.array([nparticles]).size, 1))
+        ).sum(axis=0) /
+        np.array([nparticles]).sum() /
+        np.max([np.array([mode]).size, np.array([gsigma]).size])
+    )
diff --git a/particula/util/radius_cutoff.py b/particula/util/radius_cutoff.py
index 2212046..346ce66 100644
--- a/particula/util/radius_cutoff.py
+++ b/particula/util/radius_cutoff.py
@@ -26,10 +26,19 @@ def cut_rad(
 
     _ = kwargs.get("something", None)
 
-    (rad_start, rad_end) = lognorm.interval(
-        alpha=cutoff,
-        s=np.log(gsigma),
-        scale=mode,
-    )
+    if np.array([mode]).size == 1:
+        (rad_start, rad_end) = lognorm.interval(
+            alpha=cutoff,
+            s=np.log(gsigma),
+            scale=mode,
+        )
+    else:
+        (rad_start_pre, rad_end_pre) = lognorm.interval(
+            alpha=cutoff,
+            s=np.log(gsigma),
+            scale=mode,
+        )
+        rad_start = rad_start_pre.min()
+        rad_end = rad_end_pre.max()
 
     return (rad_start, rad_end)


Test patch:
diff --git a/particula/tests/particle_test.py b/particula/tests/particle_test.py
index 81fb0ce..194b441 100644
--- a/particula/tests/particle_test.py
+++ b/particula/tests/particle_test.py
@@ -49,6 +49,52 @@ standard_environment_ip = environment.Environment(
     pressure=101325 * u.Pa,
 )
 
+single_mode = {
+    "mode": 500e-9,  # 500 nm median
+    "nbins": 1000,  # 1000 bins
+    "nparticles": 1e6,  # 1e4 #
+    "volume": 1e-6,  # per 1e-6 m^3 (or 1 cc)
+    "gsigma": 1.5,  # relatively narrow
+    "spacing": "linspace",  # bin spacing,
+}
+
+multi_mode = {
+    "mode": [500e-9, 500e-9],  # 500 nm median
+    "nbins": 1000,  # 1000 bins
+    "nparticles": [1e6, 1e6],  # 1e4 #
+    "volume": 1e-6,  # per 1e-6 m^3 (or 1 cc)
+    "gsigma": [1.2, 1.2],  # relatively narrow
+    "spacing": "linspace",  # bin spacing,
+}
+
+
+def test_particle_distribution():
+    """"Test that the particle distribution is calculated for single mode and
+    multi mode.
+    """
+    def pdf_total(radius, pdf_distribution):
+        return np.trapz(y=pdf_distribution, x=radius)
+
+    # single mode
+    particle_distribution_1 = particle.ParticleDistribution(
+        **single_mode
+    )
+    total_number = pdf_total(
+        particle_distribution_1.pre_radius().m,
+        particle_distribution_1.pre_distribution().m,
+    )
+    assert total_number == pytest.approx(1e12, rel=1e10)
+
+    # multi mode
+    particle_distribution_2 = particle.ParticleDistribution(
+        **multi_mode
+    )
+    total_number2 = pdf_total(
+        particle_distribution_2.pre_radius().m,
+        particle_distribution_2.pre_distribution().m
+    )
+    assert total_number2 == pytest.approx(2e12, rel=1e10)
+
 
 def test_getters():
     """Test that the getters work.
diff --git a/particula/util/tests/distribution_discretization_test.py b/particula/util/tests/distribution_discretization_test.py
index 7c88d68..c99c5be 100644
--- a/particula/util/tests/distribution_discretization_test.py
+++ b/particula/util/tests/distribution_discretization_test.py
@@ -26,3 +26,25 @@ def test_discretize():
         discretize(
             interval=spans, disttype="linear", gsigma=sigma, mode=modes,
         )
+
+
+def test_multi_discretize():
+    """ testing different modes
+    """
+
+    spans = np.linspace(1, 1000, 1000)
+    sigma = 1.25
+    modes = [100, 200]
+
+    assert discretize(
+        interval=spans, disttype="lognormal", gsigma=sigma, mode=modes
+    ).size == spans.size
+
+    assert np.trapz(discretize(
+        interval=spans, disttype="lognormal", gsigma=sigma, mode=modes
+    ), spans) == pytest.approx(1, rel=1e-5)
+
+    with pytest.raises(ValueError):
+        discretize(
+            interval=spans, disttype="linear", gsigma=sigma, mode=modes,
+        )
diff --git a/particula/util/tests/radius_cutoff_test.py b/particula/util/tests/radius_cutoff_test.py
index ae51714..72773d8 100644
--- a/particula/util/tests/radius_cutoff_test.py
+++ b/particula/util/tests/radius_cutoff_test.py
@@ -41,3 +41,56 @@ def test_cuts():
         <=
         cut_rad(cutoff=.9999, gsigma=1.35, mode=1e-7)[1]
     )
+
+
+def test_multi_cuts():
+    """ test case for different modes
+    """
+
+    assert (
+        cut_rad(cutoff=.9999, gsigma=1.25, mode=[1e-7, 1e-8])[0]
+        <=
+        cut_rad(cutoff=.9999, gsigma=1.25, mode=[1e-7, 1e-8])[1]
+    )
+
+    assert (
+        cut_rad(cutoff=.9999, gsigma=1.25, mode=[1e-7, 1e-8])[0]
+        <=
+        cut_rad(cutoff=.9990, gsigma=1.25, mode=[1e-7, 1e-8])[0]
+    )
+
+    assert (
+        cut_rad(cutoff=.9999, gsigma=1.25, mode=[1e-7, 1e-8])[1]
+        >=
+        cut_rad(cutoff=.9990, gsigma=1.25, mode=[1e-7, 1e-8])[1]
+    )
+
+    assert (
+        cut_rad(cutoff=.9999, gsigma=1.25, mode=[1e-7, 1e-8])[1]
+        <=
+        cut_rad(cutoff=.9999, gsigma=1.25, mode=[1e-7, 1e-8])[1]
+    )
+
+    assert (
+        cut_rad(cutoff=.9999, gsigma=1.25, mode=[1e-7, 1e-8])[1]
+        ==
+        cut_rad(cutoff=.9999, gsigma=1.25, mode=[1e-7])[1]
+    )
+
+    assert (
+        cut_rad(cutoff=.9999, gsigma=1.25, mode=[1e-7, 1e-8])[1]
+        >=
+        cut_rad(cutoff=.9999, gsigma=1.25, mode=[1e-8])[1]
+    )
+
+    assert (
+        cut_rad(cutoff=.9999, gsigma=1.25, mode=[1e-7, 1e-8])[0]
+        ==
+        cut_rad(cutoff=.9999, gsigma=1.25, mode=[1e-8])[0]
+    )
+
+    assert (
+        cut_rad(cutoff=.9999, gsigma=1.25, mode=[1e-7, 1e-8])[0]
+        <=
+        cut_rad(cutoff=.9999, gsigma=1.25, mode=[1e-7])[0]
+    )