import math
import numpy as np
[docs]
class Particulates:
"""Class Particulates generates particulate objects, especifically microplastic particle objects. The class defines a particle object by its composition, shape and dimensions"""
# constructor
def __init__(
self,
Pname,
Pform,
Pcomposition,
Pdensity_kg_m3,
Pshape,
PdimensionX_um,
PdimensionY_um,
PdimensionZ_um,
t_half_d=5000,
Pnumber_t0=None,
):
self.Pname = Pname
self.Pform = Pform # Pform has to be in the particles type list: ["freeMP",""heterMP","biofMP","heterBiofMP"]
self.Pcomposition = Pcomposition
self.Pdensity_kg_m3 = Pdensity_kg_m3
self.Pshape = Pshape
self.PdimensionX_um = PdimensionX_um # shortest size
self.PdimensionY_um = PdimensionY_um # longest size
self.PdimensionZ_um = PdimensionZ_um # intermediate size
self.PdimensionX_m = PdimensionX_um / 1000000 # shortest size
self.PdimensionY_m = PdimensionY_um / 1000000 # longest size
self.PdimensionZ_m = PdimensionZ_um / 1000000 # intermediate size
self.Pnumber_t0 = Pnumber_t0 # number of particles at time 0. to be objetained from emissions and background concentration of the compartment
# extract radius (for spherical particles) or calculate equivalent radius (for fibers)
if self.Pshape == "sphere":
self.radius_m = (
self.PdimensionX_m / 2
) # In spherical particles from MP radius (x dimension)
elif self.Pshape in {"fiber", "fibre", "cylinder"}:
self.radius_m = (
(3 / 2) * self.PdimensionX_m * self.PdimensionY_m * self.PdimensionZ_m
) ** (1 / 3) / 2
else:
print("Error: shape not supported yet")
# print error message for shapes other than spheres
self.diameter_m = self.radius_m * 2
self.diameter_um = self.diameter_m * 1e6
self.Pemiss_t_y = 0 # set as 0
self.t_half_d = t_half_d
[docs]
def __repr__(self):
return (
"{"
+ self.Pname
+ ", "
+ self.Pform
+ ", "
+ self.Pcomposition
+ ", "
+ self.Pshape
+ ", "
+ str(self.Pdensity_kg_m3)
+ ", "
+ str(self.radius_m)
+ "}"
)
# methods
[docs]
def calc_volume(self):
"""Particle volume calculation. Different formulas for different particle shapes, currently defined for spheres, fibres, cylinders, pellets and irregular fragments"""
if self.Pshape == "sphere":
self.Pvolume_m3 = 4 / 3 * math.pi * (self.radius_m) ** 3
# calculates volume (in m3) of spherical particles from MP radius (x dimension)
self.CSF = 1
# calculate corey shape factor (CSF)
# (Waldschlaeger 2019, doi:10.1021/acs.est.8b06794)
# print(
# "Calculated " + self.Pname + " volume: " + str(self.Pvolume_m3) + " m3"
# )
# print("Calculated Corey Shape Factor: " + str(self.CSF))
elif (
self.Pshape == "fibre"
or self.Pshape == "fiber"
or self.Pshape == "cylinder"
):
self.Pvolume_m3 = math.pi * (self.PdimensionX_m) ** 2 * (self.PdimensionY_m)
# calculates volume (in m3) of fibres or cylinders from diameter and
# length assuming cylindrical shape where X is the shorterst size (radius) ans Y the longest (heigth)
self.CSF = (self.PdimensionX_m) / math.sqrt(self.PdimensionY_m * self.PdimensionZ_m)
# calculate corey shape factor (CSF)
# (Waldschlaeger 2019, doi:10.1021/acs.est.8b06794)
# print(
# "Calculated " + self.Pname + " volume: " + str(self.Pvolume_m3) + " m3"
# )
# print("Calculated Corey Shape Factor: " + str(self.CSF))
elif self.Pshape == "pellet" or self.Pshape == "fragment":
self.Pvolume_m3 = (
self.PdimensionX_m * self.PdimensionY_m * self.PdimensionZ_m
)
# approximate volume calculation for irregular fragments
# approximated as a cuboid using longest, intermediate and shortest length
#!! Note: not sure if pellets fits best here or rather as sphere/cylinder
# might adjust later!!
self.CSF = self.PdimensionX_m / math.sqrt(
self.PdimensionY_m * self.PdimensionZ_m
)
# calculate corey shape factor (CSF)
# (Waldschlaeger 2019, doi:10.1021/acs.est.8b06794)
# print(
# "Calculated " + self.Pname + " volume: " + str(self.Pvolume_m3) + " m3"
# )
# print("Calculated Corey Shape Factor: " + str(self.CSF))
else:
print("Error: unknown shape")
# print error message for shapes other than spheres
# (to be removed when other volume calculations are implemented)
[docs]
def calc_numConc(self, concMass_mg_L, concNum_part_L):
if concNum_part_L == 0:
self.concNum_part_m3 = (
concMass_mg_L / 1000 / self.Pdensity_kg_m3 / self.Pvolume_m3
)
# if mass concentration is given, it is converted to number concentration
else:
self.concNum_part_m3 = concNum_part_L * 1000
# if number concentration is given, it is converted from part/L to part/m3
[docs]
def assign_compartment(self, comp):
self.Pcompartment = comp
[docs]
class ParticulatesBF(Particulates):
"This is a class to create ParticulatesBIOFILM objects"
# class attribute
species = "particulate"
# constructor
def __init__(self, parentMP, spm):
self.Pname = parentMP.Pname + "_BF"
self.Pcomposition = parentMP.Pcomposition
self.Pform = "biofMP"
self.parentMP = parentMP
self.BF_density_kg_m3 = spm.Pdensity_kg_m3
self.BF_thickness_um = spm.PdimensionX_um
self.radius_m = parentMP.radius_m + (
self.BF_thickness_um / 1e6
) # In spherical particles from MP radius (x dimension)
self.diameter_m = self.radius_m * 2
self.diameter_um = self.diameter_m * 1e6
self.t_half_d = 25000 # As per The Full Multi parameterization
if parentMP.PdimensionY_um == 0:
self.PdimensionY_um = 0
else:
self.PdimensionY_um = parentMP.PdimensionY_um + self.BF_thickness_um * 2
if parentMP.PdimensionZ_um == 0:
self.PdimensionZ_um = 0
else:
self.PdimensionZ_um = parentMP.PdimensionZ_um + self.BF_thickness_um * 2
if parentMP.PdimensionX_um == 0:
self.PdimensionX_um = 0
else:
self.PdimensionX_um = parentMP.PdimensionX_um + self.BF_thickness_um * 2
self.Pshape = (
parentMP.Pshape
) # to be updated for biofilm, could argue that shape is retained (unlike for SPM-bound)
self.Pdensity_kg_m3 = (
self.parentMP.radius_m**3 * self.parentMP.Pdensity_kg_m3
+ (
(self.parentMP.radius_m + (self.BF_thickness_um / 1e6)) ** 3
- self.parentMP.radius_m**3
)
* self.BF_density_kg_m3
) / ((self.parentMP.radius_m + (self.BF_thickness_um / 1e6)) ** 3)
# equation from Kooi et al for density
self.PdimensionX_m = self.PdimensionX_um / 1000000 # shortest size
self.PdimensionY_m = self.PdimensionY_um / 1000000 # longest size
self.PdimensionZ_m = self.PdimensionZ_um / 1000000 # intermediate size
[docs]
class ParticulatesSPM(Particulates):
"This is a class to create ParticulatesSPM objects"
# class attribute
species = "particulate"
# constructor
def __init__(self, parentSPM, parentMP):
self.Pname = parentMP.Pname + "_SPM"
self.Pcomposition = parentMP.Pcomposition
if parentMP.Pform == "biofMP":
self.Pform = "heterBiofMP"
self.t_half_d = 50000 # As per The Full multi parameterization
else:
self.Pform = "heterMP"
self.t_half_d = 100000 # As per The Full multi parameterizatio
self.parentMP = parentMP
self.parentSPM = parentSPM
self.Pdensity_kg_m3 = parentMP.Pdensity_kg_m3 * (
parentMP.Pvolume_m3 / (parentMP.Pvolume_m3 + parentSPM.Pvolume_m3)
) + parentSPM.Pdensity_kg_m3 * (
parentSPM.Pvolume_m3 / (parentMP.Pvolume_m3 + parentSPM.Pvolume_m3)
)
self.radius_m = (
3 * (parentMP.Pvolume_m3 + parentSPM.Pvolume_m3) / (4 * math.pi)
) ** (
1 / 3
) # Note: this is an equivalent radius. MP-SPM most likely not truly spherical
self.diameter_m = self.radius_m * 2
self.diameter_um = self.diameter_m * 1e6
self.Pshape = (
parentMP.Pshape
) # to be updated for biofilm, could argue that shape is retained (unlike for SPM-bound)
# add dimensions
if parentMP.PdimensionY_um == 0:
self.PdimensionY_um = 0
else:
self.PdimensionY_um = parentMP.PdimensionY_um + parentSPM.diameter_um
if parentMP.PdimensionZ_um == 0:
self.PdimensionZ_um = 0
else:
self.PdimensionZ_um = parentMP.PdimensionZ_um + parentSPM.diameter_um
if parentMP.PdimensionX_um == 0:
self.PdimensionX_um = 0
else:
self.PdimensionX_um = parentMP.PdimensionX_um + parentSPM.diameter_um
self.PdimensionX_m = self.PdimensionX_um / 1000000 # shortest size
self.PdimensionY_m = self.PdimensionY_um / 1000000 # longest size
self.PdimensionZ_m = self.PdimensionZ_um / 1000000 # intermediate size
# methods
# volume calculation - currently simple version.
# more complexity to be added later:
# different formulas for different particle shapes.
# currently defined for spheres, fibres, cylinders, pellets and irregular fragments
[docs]
def calc_volume_heter(self, parentMP, parentSPM):
if self.Pshape == "sphere":
self.Pvolume_m3 = parentMP.Pvolume_m3 + parentSPM.Pvolume_m3
# calculates volume (in m3) of spherical particles from MP radius (x dimension)
self.CSF = 1
# calculate corey shape factor (CSF)
# (Waldschlaeger 2019, doi:10.1021/acs.est.8b06794)
elif (
self.Pshape == "fibre"
or self.Pshape == "fiber"
or self.Pshape == "cylinder"
):
self.Pvolume_m3 = parentMP.Pvolume_m3 + parentSPM.Pvolume_m3
# calculates volume (in m3) of fibres or cylinders from diameter and
# length assuming cylindrical shape where X is the shorterst size (radius) ans Y the longest (heigth)
self.CSF = (self.radius_m) / math.sqrt(self.PdimensionY_m * self.radius_m)
# calculate corey shape factor (CSF)
# (Waldschlaeger 2019, doi:10.1021/acs.est.8b06794)
elif self.Pshape == "pellet" or self.Pshape == "fragment":
self.Pvolume_m3 = parentMP.Pvolume_m3 + parentSPM.Pvolume_m3
# approximate volume calculation for irregular fragments
# approximated as a cuboid using longest, intermediate and shortest length
#!! Note: not sure if pellets fits best here or rather as sphere/cylinder
# might adjust later!!
self.CSF = self.PdimensionX_m / math.sqrt(
self.PdimensionY_m * self.PdimensionZ_m
)
# calculate corey shape factor (CSF)
# (Waldschlaeger 2019, doi:10.1021/acs.est.8b06794)
else:
print("Error: unknown shape")
# print("Calculated " + self.Pname + " volume: " + str(self.Pvolume_m3) + " m3")