""" Contains all the functions to generate the model objects: model box, model compartments and the model particles (MP ans SPM)"""
import math
from utopia.globalConstants import *
[docs]
class Box:
"""Class box generates one object box representing the unit world in the case of the UTOPIA parameterization that can contain 17 compartments and that can have conexions to other boxes (for example if conecting several UTOPIA boxes to give spatial resolution)"""
# class attributes
description = "Generic Box class"
def __init__(
self,
Bname,
Bdepth_m=None,
Blength_m=None,
Bwidth_m=None,
Bvolume_m3=None,
Bconexions=None,
):
# Assign attributes to self (instance attributes). Those set up as None are optional attributes
self.Bname = Bname # name of the box
self.Bdepth_m = Bdepth_m # depth of the box
self.Blength_m = Blength_m # length of the box
self.Bwidth_m = Bwidth_m # width of the box
self.Bvolume_m3 = Bvolume_m3 # volume of the box
self.compartments = [] # list of compartments in the box
self.Bconexions = Bconexions # conexions to other model boxes
[docs]
def __repr__(self):
return (
"{"
+ self.Bname
+ ", "
+ str(self.Bdepth_m)
+ ", "
+ str(self.Blength_m)
+ ", "
+ str(self.Bwidth_m)
+ "}"
)
[docs]
def add_compartment(self, comp):
self.compartments.append(comp)
comp.assign_box(self)
[docs]
def calc_Bvolume_m3(self):
if self.Bvolume_m3 is None:
if any(
attr is None for attr in [self.Bdepth_m, self.Blength_m, self.Bwidth_m]
):
print(
"Missing parameters needded to calculate Box volume --> calculating based on compartments volume"
)
if len(self.compartments) == 0:
print(
"No compartments assigned to this model box --> use add_compartment(comp)"
)
else:
vol = []
for c in range(len(self.compartments)):
if self.compartments[c].Cvolume_m3 is None:
print(
"Volume of compartment "
+ self.compartments[c].Cname
+ " is missing"
)
continue
else:
vol.append(self.compartments[c].Cvolume_m3)
self.Bvolume_m3 = sum(vol)
else:
self.Bvolume_m3 = self.Bdepth_m * self.Blength_m * self.Bwidth_m
# print("Box volume: " + str(self.Bvolume_m3)+" m3")
else:
print("Box volume already assigned: " + str(self.Bvolume_m3) + " m3")
[docs]
class Compartment:
"""Class Compartment (parent class) generates compartment objects that belong by default to an assigned model box (Cbox). Each compartment contains four different particle objects corresponding to the 4 described aggregation states of UTOPIA (freeMP, heterMP, biofMP, heterBiofMP) and the processes that can occur in the compartment are listed under the processess attribute. Each compartment has a set of connexions withing the UTOPIA box listed in the conexions attribute wich will be asigned by reading on the conexions input file of the model."""
def __init__(
self,
Cname,
Cdepth_m=None,
Clength_m=None,
Cwidth_m=None,
Cvolume_m3=None,
CsurfaceArea_m2=None,
):
self.Cname = Cname
self.Cdepth_m = Cdepth_m
self.Clength_m = Clength_m
self.Cwidth_m = Cwidth_m
self.Cvolume_m3 = Cvolume_m3
self.CsurfaceArea_m2 = CsurfaceArea_m2
self.particles = {
"freeMP": [],
"heterMP": [],
"biofMP": [],
"heterBiofMP": [],
} # dictionary of particles in the compartment
self.processess = [
"degradation",
"fragmentation",
"heteroaggregation",
"heteroaggregate_breackup",
"biofouling",
"defouling",
"advective_transport",
"settling",
"rising",
]
self.connexions = []
[docs]
def assign_box(self, Box):
self.CBox = Box
[docs]
def add_particles(self, particle):
self.particles[particle.Pform].append(particle)
particle.assign_compartment(self)
[docs]
def calc_volume(self):
if self.Cvolume_m3 is None:
if any(
attr is None for attr in [self.Cdepth_m, self.Clength_m, self.Cwidth_m]
):
print(
"Missing parameters needded to calculate compartment volume --> Try calc_vol_fromBox or add missing values to compartment dimensions"
)
else:
self.Cvolume_m3 = self.Cdepth_m * self.Clength_m * self.Cwidth_m
# print(
# "Calculated "
# + self.Cname
# + " volume: "
# + str(self.Cvolume_m3)
# + " m3"
# )
else:
pass
# print("Assigned " + self.Cname + " volume: " + str(self.Cvolume_m3) + " m3")
[docs]
def calc_vol_fromBox(self):
self.Cvolume_m3 = (
self.CBox.Bvolume_m3 * self.CBox.CvolFractionBox[self.Cname.lower()]
)
[docs]
def calc_particleConcentration_Nm3_initial(self):
for p in self.particles:
for s in self.particles[p]:
self.particles[p][s].initial_conc_Nm3 = (
self.particles[p][s].Pnumber / self.Cvolume_m3
)
"""Compartment Subclasses (inheritances) of the class compartment add extra attributes to the compatment that define the type of compartment """
[docs]
class compartment_water(Compartment):
def __init__(
self,
Cname,
SPM_mgL,
waterFlow_m3_s,
T_K,
G,
Cdepth_m=None,
Clength_m=None,
Cwidth_m=None,
Cvolume_m3=None,
CsurfaceArea_m2=None,
flowVelocity_m_s=None,
):
super().__init__(
Cname, Cdepth_m, Clength_m, Cwidth_m, Cvolume_m3, CsurfaceArea_m2
)
self.SPM_mgL = SPM_mgL
self.flowVelocity_m_s = flowVelocity_m_s
self.waterFlow_m3_s = waterFlow_m3_s
self.T_K = T_K
self.G = G # Shear rate (G, in s−1)
self.processess = [
"discorporation",
"fragmentation",
"heteroaggregation",
"heteroaggregate_breackup",
"biofouling",
"defouling",
"advective_transport",
"settling",
"rising",
"mixing",
]
[docs]
class compartment_surfaceSea_water(Compartment):
# added new processess to the list of processess. new attributes that migth be needed to this processess should be added here
def __init__(
self,
Cname,
SPM_mgL,
waterFlow_m3_s,
T_K,
G,
Cdepth_m=None,
Clength_m=None,
Cwidth_m=None,
Cvolume_m3=None,
CsurfaceArea_m2=None,
flowVelocity_m_s=None,
):
super().__init__(
Cname, Cdepth_m, Clength_m, Cwidth_m, Cvolume_m3, CsurfaceArea_m2
)
self.SPM_mgL = SPM_mgL
self.flowVelocity_m_s = flowVelocity_m_s
self.waterFlow_m3_s = waterFlow_m3_s
self.T_K = T_K
self.G = G # Shear rate (G, in s−1)
self.processess = [
"discorporation",
"fragmentation",
"heteroaggregation",
"heteroaggregate_breackup",
"biofouling",
"defouling",
"advective_transport",
"settling",
"rising",
"mixing",
"sea_spray_aerosol",
"beaching",
]
[docs]
class compartment_sediment(Compartment):
def __init__(
self,
Cname,
Cdepth_m=None,
Clength_m=None,
Cwidth_m=None,
Cvolume_m3=None,
CsurfaceArea_m2=None,
):
super().__init__(
Cname, Cdepth_m, Clength_m, Cwidth_m, Cvolume_m3, CsurfaceArea_m2
)
self.processess = [
"discorporation",
"fragmentation",
"sediment_resuspension",
"burial",
]
[docs]
class compartment_soil_surface(Compartment):
def __init__(
self,
Cname,
Cdepth_m=None,
Clength_m=None,
Cwidth_m=None,
Cvolume_m3=None,
CsurfaceArea_m2=None,
):
super().__init__(
Cname, Cdepth_m, Clength_m, Cwidth_m, Cvolume_m3, CsurfaceArea_m2
)
self.processess = [
"discorporation",
"fragmentation",
"runoff_transport",
"percolation",
"soil_air_resuspension",
"soil_convection",
]
# self.earthworm_density_in_m3 = earthworm_density_in_m3
# self.Qrunoff_m3 = Qrunoff_m3
[docs]
class compartment_deep_soil(Compartment):
def __init__(
self,
Cname,
Cdepth_m=None,
Clength_m=None,
Cwidth_m=None,
Cvolume_m3=None,
CsurfaceArea_m2=None,
):
super().__init__(
Cname, Cdepth_m, Clength_m, Cwidth_m, Cvolume_m3, CsurfaceArea_m2
)
self.processess = [
"discorporation",
"fragmentation",
"sequestration_deep_soils",
"soil_convection",
]
# retention_in_soil (straining?) of the particles in soil following heteroaggregation with geocolloids?
# shall we also include heteroaggregation/heteroaggegrate break-up processess in the soil compartment? In SimpleBox for Nano they do account for aggregation and attachment
# Difference between retention in soil and sequestration deep soil: sequestrations deep soil is like burial in deep sediments (elemination process-->out of the system)
[docs]
class compartment_air(Compartment):
def __init__(
self,
Cname,
T_K=None,
wind_speed_m_s=None,
I_rainfall_mm=None,
Cdepth_m=None,
Clength_m=None,
Cwidth_m=None,
Cvolume_m3=None,
CsurfaceArea_m2=None,
flowVelocity_m_s=None,
):
super().__init__(
Cname, Cdepth_m, Clength_m, Cwidth_m, Cvolume_m3, CsurfaceArea_m2
)
self.T_K = T_K
self.wind_speed_m_s = wind_speed_m_s
self.I_rainfall_mm = I_rainfall_mm
self.flowVelocity_m_s = flowVelocity_m_s
self.processess = [
"discorporation",
"fragmentation",
"wind_trasport",
"dry_deposition",
"wet_deposition",
]
# shall we also include heteroaggregation/heteroaggegrate break-up processess in the air compartment?
[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
self.radius_m = (
self.PdimensionX_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.Pemiss_t_y = 0 # set as 0 until the inputs_emissions.csv file is used
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.radius_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.radius_m) / math.sqrt(self.PdimensionY_m * self.radius_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)
# 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")
[docs]
def generate_objects():
"""Function for generating the UTOPIA model objects: model box, model compartments and the model particles"""
###CONTINUE HERE###