AeroViz 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of AeroViz might be problematic. Click here for more details.

Files changed (102) hide show
  1. AeroViz/__init__.py +15 -0
  2. AeroViz/dataProcess/Chemistry/__init__.py +63 -0
  3. AeroViz/dataProcess/Chemistry/_calculate.py +27 -0
  4. AeroViz/dataProcess/Chemistry/_isoropia.py +99 -0
  5. AeroViz/dataProcess/Chemistry/_mass_volume.py +175 -0
  6. AeroViz/dataProcess/Chemistry/_ocec.py +184 -0
  7. AeroViz/dataProcess/Chemistry/_partition.py +29 -0
  8. AeroViz/dataProcess/Chemistry/_teom.py +16 -0
  9. AeroViz/dataProcess/Optical/_IMPROVE.py +61 -0
  10. AeroViz/dataProcess/Optical/__init__.py +62 -0
  11. AeroViz/dataProcess/Optical/_absorption.py +54 -0
  12. AeroViz/dataProcess/Optical/_extinction.py +36 -0
  13. AeroViz/dataProcess/Optical/_mie.py +16 -0
  14. AeroViz/dataProcess/Optical/_mie_sd.py +143 -0
  15. AeroViz/dataProcess/Optical/_scattering.py +30 -0
  16. AeroViz/dataProcess/SizeDistr/__init__.py +61 -0
  17. AeroViz/dataProcess/SizeDistr/__merge.py +250 -0
  18. AeroViz/dataProcess/SizeDistr/_merge.py +245 -0
  19. AeroViz/dataProcess/SizeDistr/_merge_v1.py +254 -0
  20. AeroViz/dataProcess/SizeDistr/_merge_v2.py +243 -0
  21. AeroViz/dataProcess/SizeDistr/_merge_v3.py +518 -0
  22. AeroViz/dataProcess/SizeDistr/_merge_v4.py +424 -0
  23. AeroViz/dataProcess/SizeDistr/_size_distr.py +93 -0
  24. AeroViz/dataProcess/VOC/__init__.py +19 -0
  25. AeroViz/dataProcess/VOC/_potential_par.py +76 -0
  26. AeroViz/dataProcess/__init__.py +11 -0
  27. AeroViz/dataProcess/core/__init__.py +92 -0
  28. AeroViz/plot/__init__.py +7 -0
  29. AeroViz/plot/distribution/__init__.py +1 -0
  30. AeroViz/plot/distribution/distribution.py +582 -0
  31. AeroViz/plot/improve/__init__.py +1 -0
  32. AeroViz/plot/improve/improve.py +240 -0
  33. AeroViz/plot/meteorology/__init__.py +1 -0
  34. AeroViz/plot/meteorology/meteorology.py +317 -0
  35. AeroViz/plot/optical/__init__.py +2 -0
  36. AeroViz/plot/optical/aethalometer.py +77 -0
  37. AeroViz/plot/optical/optical.py +388 -0
  38. AeroViz/plot/templates/__init__.py +8 -0
  39. AeroViz/plot/templates/contour.py +47 -0
  40. AeroViz/plot/templates/corr_matrix.py +108 -0
  41. AeroViz/plot/templates/diurnal_pattern.py +42 -0
  42. AeroViz/plot/templates/event_evolution.py +65 -0
  43. AeroViz/plot/templates/koschmieder.py +156 -0
  44. AeroViz/plot/templates/metal_heatmap.py +57 -0
  45. AeroViz/plot/templates/regression.py +256 -0
  46. AeroViz/plot/templates/scatter.py +130 -0
  47. AeroViz/plot/templates/templates.py +398 -0
  48. AeroViz/plot/timeseries/__init__.py +1 -0
  49. AeroViz/plot/timeseries/timeseries.py +317 -0
  50. AeroViz/plot/utils/__init__.py +3 -0
  51. AeroViz/plot/utils/_color.py +71 -0
  52. AeroViz/plot/utils/_decorator.py +74 -0
  53. AeroViz/plot/utils/_unit.py +55 -0
  54. AeroViz/process/__init__.py +31 -0
  55. AeroViz/process/core/DataProc.py +19 -0
  56. AeroViz/process/core/SizeDist.py +90 -0
  57. AeroViz/process/core/__init__.py +4 -0
  58. AeroViz/process/method/PyMieScatt_update.py +567 -0
  59. AeroViz/process/method/__init__.py +2 -0
  60. AeroViz/process/method/mie_theory.py +258 -0
  61. AeroViz/process/method/prop.py +62 -0
  62. AeroViz/process/script/AbstractDistCalc.py +143 -0
  63. AeroViz/process/script/Chemical.py +176 -0
  64. AeroViz/process/script/IMPACT.py +49 -0
  65. AeroViz/process/script/IMPROVE.py +161 -0
  66. AeroViz/process/script/Others.py +65 -0
  67. AeroViz/process/script/PSD.py +103 -0
  68. AeroViz/process/script/PSD_dry.py +94 -0
  69. AeroViz/process/script/__init__.py +5 -0
  70. AeroViz/process/script/retrieve_RI.py +70 -0
  71. AeroViz/rawDataReader/__init__.py +68 -0
  72. AeroViz/rawDataReader/core/__init__.py +397 -0
  73. AeroViz/rawDataReader/script/AE33.py +31 -0
  74. AeroViz/rawDataReader/script/AE43.py +34 -0
  75. AeroViz/rawDataReader/script/APS_3321.py +47 -0
  76. AeroViz/rawDataReader/script/Aurora.py +38 -0
  77. AeroViz/rawDataReader/script/BC1054.py +46 -0
  78. AeroViz/rawDataReader/script/EPA_vertical.py +18 -0
  79. AeroViz/rawDataReader/script/GRIMM.py +35 -0
  80. AeroViz/rawDataReader/script/IGAC_TH.py +104 -0
  81. AeroViz/rawDataReader/script/IGAC_ZM.py +90 -0
  82. AeroViz/rawDataReader/script/MA350.py +45 -0
  83. AeroViz/rawDataReader/script/NEPH.py +57 -0
  84. AeroViz/rawDataReader/script/OCEC_LCRES.py +34 -0
  85. AeroViz/rawDataReader/script/OCEC_RES.py +28 -0
  86. AeroViz/rawDataReader/script/SMPS_TH.py +41 -0
  87. AeroViz/rawDataReader/script/SMPS_aim11.py +51 -0
  88. AeroViz/rawDataReader/script/SMPS_genr.py +51 -0
  89. AeroViz/rawDataReader/script/TEOM.py +46 -0
  90. AeroViz/rawDataReader/script/Table.py +28 -0
  91. AeroViz/rawDataReader/script/VOC_TH.py +30 -0
  92. AeroViz/rawDataReader/script/VOC_ZM.py +37 -0
  93. AeroViz/rawDataReader/script/__init__.py +22 -0
  94. AeroViz/tools/__init__.py +3 -0
  95. AeroViz/tools/database.py +94 -0
  96. AeroViz/tools/dataclassifier.py +117 -0
  97. AeroViz/tools/datareader.py +66 -0
  98. AeroViz-0.1.0.dist-info/LICENSE +21 -0
  99. AeroViz-0.1.0.dist-info/METADATA +117 -0
  100. AeroViz-0.1.0.dist-info/RECORD +102 -0
  101. AeroViz-0.1.0.dist-info/WHEEL +5 -0
  102. AeroViz-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,258 @@
1
+ from typing import Sequence, Literal
2
+
3
+ import numpy as np
4
+ import pandas as pd
5
+ from .PyMieScatt_update import AutoMieQ
6
+ from numpy import exp, log, log10, sqrt, pi
7
+
8
+
9
+ def Mie_Q(m: complex,
10
+ wavelength: float,
11
+ dp: float | Sequence[float]
12
+ ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
13
+ """
14
+ Calculate Mie scattering efficiency (Q) for given spherical particle diameter(s).
15
+
16
+ Parameters
17
+ ----------
18
+ m : complex
19
+ The complex refractive index of the particles.
20
+ wavelength : float
21
+ The wavelength of the incident light (in nm).
22
+ dp : float | Sequence[float]
23
+ Particle diameters (in nm), can be a single value or Sequence object.
24
+
25
+ Returns
26
+ -------
27
+ Q_ext : ndarray
28
+ The Mie extinction efficiency for each particle diameter.
29
+ Q_sca : ndarray
30
+ The Mie scattering efficiency for each particle diameter.
31
+ Q_abs : ndarray
32
+ The Mie absorption efficiency for each particle diameter.
33
+
34
+ Examples
35
+ --------
36
+ >>> Q_ext, Q_sca, Q_abs = Mie_Q(m=complex(1.5, 0.02), wavelength=550, dp=[100, 200, 300, 400])
37
+ """
38
+ # Ensure dp is a numpy array
39
+ dp = np.atleast_1d(dp)
40
+
41
+ # Transpose for proper unpacking
42
+ Q_ext, Q_sca, Q_abs, g, Q_pr, Q_back, Q_ratio = np.array([AutoMieQ(m, wavelength, _dp) for _dp in dp]).T
43
+
44
+ return Q_ext, Q_sca, Q_abs
45
+
46
+
47
+ def Mie_MEE(m: complex,
48
+ wavelength: float,
49
+ dp: float | Sequence[float],
50
+ density: float
51
+ ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
52
+ """
53
+ Calculate mass extinction efficiency and other parameters.
54
+
55
+ Parameters
56
+ ----------
57
+ m : complex
58
+ The complex refractive index of the particles.
59
+ wavelength : float
60
+ The wavelength of the incident light.
61
+ dp : float | Sequence[float]
62
+ List of particle sizes or a single value.
63
+ density : float
64
+ The density of particles.
65
+
66
+ Returns
67
+ -------
68
+ MEE : ndarray
69
+ The mass extinction efficiency for each particle diameter.
70
+ MSE : ndarray
71
+ The mass scattering efficiency for each particle diameter.
72
+ MAE : ndarray
73
+ The mass absorption efficiency for each particle diameter.
74
+
75
+ Examples
76
+ --------
77
+ >>> MEE, MSE, MAE = Mie_MEE(m=complex(1.5, 0.02), wavelength=550, dp=[100, 200, 300, 400], density=1.2)
78
+ """
79
+ Q_ext, Q_sca, Q_abs = Mie_Q(m, wavelength, dp)
80
+
81
+ MEE = (3 * Q_ext) / (2 * density * dp) * 1000
82
+ MSE = (3 * Q_sca) / (2 * density * dp) * 1000
83
+ MAE = (3 * Q_abs) / (2 * density * dp) * 1000
84
+
85
+ return MEE, MSE, MAE
86
+
87
+
88
+ def Mie_PESD(m: complex,
89
+ wavelength: float = 550,
90
+ dp: float | Sequence[float] = None,
91
+ ndp: float | Sequence[float] = None,
92
+ lognormal: bool = False,
93
+ dp_range: tuple = (1, 2500),
94
+ geoMean: float = 200,
95
+ geoStdDev: float = 2,
96
+ numberOfParticles: float = 1e6,
97
+ numberOfBins: int = 167,
98
+ ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
99
+ """
100
+ Simultaneously calculate "extinction distribution" and "integrated results" using the Mie_Q method.
101
+
102
+ Parameters
103
+ ----------
104
+ m : complex
105
+ The complex refractive index of the particles.
106
+ wavelength : float
107
+ The wavelength of the incident light.
108
+ dp : float | Sequence[float]
109
+ Particle sizes.
110
+ ndp : float | Sequence[float]
111
+ Number concentration from SMPS or APS in the units of dN/dlogdp.
112
+ lognormal : bool, optional
113
+ Whether to use lognormal distribution for ndp. Default is False.
114
+ dp_range : tuple, optional
115
+ Range of particle sizes. Default is (1, 2500) nm.
116
+ geoMean : float, optional
117
+ Geometric mean of the particle size distribution. Default is 200 nm.
118
+ geoStdDev : float, optional
119
+ Geometric standard deviation of the particle size distribution. Default is 2.
120
+ numberOfParticles : float, optional
121
+ Number of particles. Default is 1e6.
122
+ numberOfBins : int, optional
123
+ Number of bins for the lognormal distribution. Default is 167.
124
+
125
+ Returns
126
+ -------
127
+ ext_dist : ndarray
128
+ The extinction distribution for the given data.
129
+ sca_dist : ndarray
130
+ The scattering distribution for the given data.
131
+ abs_dist : ndarray
132
+ The absorption distribution for the given data.
133
+
134
+ Notes
135
+ -----
136
+ return in "dext/dlogdp", please make sure input the dNdlogdp data.
137
+
138
+ Examples
139
+ --------
140
+ >>> Ext, Sca, Abs = Mie_PESD(m=complex(1.5, 0.02), wavelength=550, dp=[100, 200, 500, 1000], ndp=[100, 50, 30, 20])
141
+ """
142
+ if lognormal:
143
+ dp = np.logspace(log10(dp_range[0]), log10(dp_range[1]), numberOfBins)
144
+
145
+ ndp = numberOfParticles * (1 / (log(geoStdDev) * sqrt(2 * pi)) *
146
+ exp(-(log(dp) - log(geoMean)) ** 2 / (2 * log(geoStdDev) ** 2)))
147
+
148
+ # dN / dlogdp
149
+ ndp = np.atleast_1d(ndp)
150
+
151
+ Q_ext, Q_sca, Q_abs = Mie_Q(m, wavelength, dp)
152
+
153
+ # The 1e-6 here is so that the final value is the same as the unit 1/10^6m.
154
+ Ext = Q_ext * (pi / 4 * dp ** 2) * ndp * 1e-6
155
+ Sca = Q_sca * (pi / 4 * dp ** 2) * ndp * 1e-6
156
+ Abs = Q_abs * (pi / 4 * dp ** 2) * ndp * 1e-6
157
+
158
+ return Ext, Sca, Abs
159
+
160
+
161
+ def internal(dist: pd.Series,
162
+ dp: float | Sequence[float],
163
+ wavelength: float = 550,
164
+ result_type: Literal['extinction', 'scattering', 'absorption'] = 'extinction'
165
+ ) -> np.ndarray:
166
+ """
167
+ Calculate the extinction distribution by internal mixing model.
168
+
169
+ Parameters
170
+ ----------
171
+ dist : pd.Series
172
+ Particle size distribution data.
173
+ dp : float | Sequence[float]
174
+ Diameter(s) of the particles, either a single value or a sequence.
175
+ wavelength : float, optional
176
+ Wavelength of the incident light, default is 550 nm.
177
+ result_type : {'extinction', 'scattering', 'absorption'}, optional
178
+ Type of result to calculate, defaults to 'extinction'.
179
+
180
+ Returns
181
+ -------
182
+ np.ndarray
183
+ Extinction distribution calculated based on the internal mixing model.
184
+ """
185
+ ext_dist, sca_dist, abs_dist = Mie_PESD(m=complex(dist['n_amb'], dist['k_amb']),
186
+ wavelength=wavelength,
187
+ dp=dp,
188
+ ndp=np.array(dist[:np.size(dp)]))
189
+
190
+ if result_type == 'extinction':
191
+ return ext_dist
192
+ elif result_type == 'scattering':
193
+ return sca_dist
194
+ else:
195
+ return abs_dist
196
+
197
+ # return dict(ext=ext_dist, sca=sca_dist, abs=abs_dist)
198
+
199
+
200
+ def external(dist: pd.Series,
201
+ dp: float | Sequence[float],
202
+ wavelength: float = 550,
203
+ result_type: Literal['extinction', 'scattering', 'absorption'] = 'extinction'
204
+ ) -> np.ndarray:
205
+ """
206
+ Calculate the extinction distribution by external mixing model.
207
+
208
+ Parameters
209
+ ----------
210
+ dist : pd.Series
211
+ Particle size distribution data.
212
+ dp : float | Sequence[float]
213
+ Diameter(s) of the particles, either a single value or a sequence.
214
+ wavelength : float, optional
215
+ Wavelength of the incident light, default is 550 nm.
216
+ result_type : {'extinction', 'scattering', 'absorption'}, optional
217
+ Type of result to calculate, defaults to 'extinction'.
218
+
219
+ Returns
220
+ -------
221
+ np.ndarray
222
+ Extinction distribution calculated based on the external mixing model.
223
+ """
224
+ refractive_dic = {'AS_volume_ratio': complex(1.53, 0.00),
225
+ 'AN_volume_ratio': complex(1.55, 0.00),
226
+ 'OM_volume_ratio': complex(1.54, 0.00),
227
+ 'Soil_volume_ratio': complex(1.56, 0.01),
228
+ 'SS_volume_ratio': complex(1.54, 0.00),
229
+ 'EC_volume_ratio': complex(1.80, 0.54),
230
+ 'ALWC_volume_ratio': complex(1.33, 0.00)}
231
+
232
+ ndp = np.array(dist[:np.size(dp)])
233
+ mie_results = (
234
+ Mie_PESD(refractive_dic[_specie], wavelength, dp, dist[_specie] / (1 + dist['ALWC_volume_ratio']) * ndp) for
235
+ _specie in refractive_dic)
236
+
237
+ ext_dist, sca_dist, abs_dist = (np.sum([res[0] for res in mie_results], axis=0),
238
+ np.sum([res[1] for res in mie_results], axis=0),
239
+ np.sum([res[2] for res in mie_results], axis=0))
240
+
241
+ if result_type == 'extinction':
242
+ return ext_dist
243
+ elif result_type == 'scattering':
244
+ return sca_dist
245
+ else:
246
+ return abs_dist
247
+
248
+
249
+ def core_shell():
250
+ pass
251
+
252
+
253
+ def sensitivity():
254
+ pass
255
+
256
+
257
+ if __name__ == '__main__':
258
+ result = Mie_Q(m=complex(1.5, 0.02), wavelength=550, dp=[100., 200.])
@@ -0,0 +1,62 @@
1
+ import numpy as np
2
+ from numpy import exp, log
3
+ from scipy.signal import find_peaks
4
+
5
+
6
+ def geometric(dp: np.ndarray,
7
+ dist: np.ndarray
8
+ ) -> tuple[float, float]:
9
+ """ Calculate the geometric mean and standard deviation. """
10
+
11
+ _gmd = (((dist * log(dp)).sum()) / dist.sum())
12
+
13
+ logdp_mesh, gmd_mesh = np.meshgrid(log(dp), _gmd)
14
+ _gsd = ((((logdp_mesh - gmd_mesh) ** 2) * dist).sum() / dist.sum()) ** .5
15
+
16
+ return exp(_gmd), exp(_gsd)
17
+
18
+
19
+ def contribution(dp: np.ndarray,
20
+ dist: np.ndarray
21
+ ) -> tuple[float, float, float]:
22
+ """ Calculate the relative contribution of each mode. """
23
+
24
+ ultra = dist[(dp >= 11.8) & (dp < 100)].sum() / dist.sum()
25
+ accum = dist[(dp >= 100) & (dp < 1000)].sum() / dist.sum()
26
+ coars = dist[(dp >= 1000) & (dp < 2500)].sum() / dist.sum()
27
+
28
+ return ultra, accum, coars
29
+
30
+
31
+ def mode(dp: np.ndarray,
32
+ dist: np.ndarray
33
+ ) -> np.ndarray:
34
+ """ Find three peak mode in distribution. """
35
+
36
+ min_value = np.array([dist.min()])
37
+ mode, _ = find_peaks(np.concatenate([min_value, dist, min_value]), distance=len(dist) - 1)
38
+
39
+ return dp[mode - 1]
40
+
41
+
42
+ def properties(dist,
43
+ dp: np.ndarray,
44
+ dlogdp: np.ndarray,
45
+ weighting: str
46
+ ) -> dict:
47
+ """ for apply """
48
+ dist = np.array(dist)
49
+
50
+ gmd, gsd = geometric(dp, dist)
51
+ ultra, accum, coarse = contribution(dp, dist)
52
+ peak = mode(dp, dist)
53
+
54
+ return {key: round(value, 3) for key, value in
55
+ {f'total_{weighting}': (dist * dlogdp).sum(),
56
+ f'GMD_{weighting}': gmd,
57
+ f'GSD_{weighting}': gsd,
58
+ f'mode_{weighting}': peak[0],
59
+ f'ultra_{weighting}': ultra,
60
+ f'accum_{weighting}': accum,
61
+ f'coarse_{weighting}': coarse}
62
+ .items()}
@@ -0,0 +1,143 @@
1
+ from abc import ABC, abstractmethod
2
+ from functools import partial
3
+ from typing import Literal
4
+
5
+ import numpy as np
6
+ from pandas import DataFrame, concat
7
+
8
+ from AeroViz.process.core.SizeDist import SizeDist
9
+ from AeroViz.process.method import properties, internal, external, core_shell, sensitivity
10
+
11
+
12
+ class AbstractDistCalc(ABC):
13
+ @abstractmethod
14
+ def useApply(self) -> DataFrame:
15
+ pass
16
+
17
+
18
+ class NumberDistCalc(AbstractDistCalc):
19
+ def __init__(self, psd: SizeDist):
20
+ self.psd = psd
21
+
22
+ def useApply(self) -> DataFrame:
23
+ """ Calculate number distribution """
24
+ return self.psd.data
25
+
26
+
27
+ class SurfaceDistCalc(AbstractDistCalc):
28
+ def __init__(self, psd: SizeDist):
29
+ self.psd = psd
30
+
31
+ def useApply(self) -> DataFrame:
32
+ """ Calculate surface distribution """
33
+ return self.psd.data.dropna().apply(lambda col: np.pi * self.psd.dp ** 2 * np.array(col),
34
+ axis=1, result_type='broadcast').reindex(self.psd.index)
35
+
36
+
37
+ class VolumeDistCalc(AbstractDistCalc):
38
+ def __init__(self, psd: SizeDist):
39
+ self.psd = psd
40
+
41
+ def useApply(self) -> DataFrame:
42
+ """ Calculate volume distribution """
43
+ return self.psd.data.dropna().apply(lambda col: np.pi / 6 * self.psd.dp ** 3 * np.array(col),
44
+ axis=1, result_type='broadcast').reindex(self.psd.index)
45
+
46
+
47
+ class PropertiesDistCalc(AbstractDistCalc):
48
+ def __init__(self, psd: SizeDist):
49
+ self.psd = psd
50
+
51
+ def useApply(self) -> DataFrame:
52
+ """ Calculate properties of distribution """
53
+ return self.psd.data.dropna().apply(partial(properties, dp=self.psd.dp, dlogdp=self.psd.dlogdp,
54
+ weighting=self.psd.weighting),
55
+ axis=1, result_type='expand').reindex(self.psd.index)
56
+
57
+
58
+ class ExtinctionDistCalc(AbstractDistCalc):
59
+ mapping = {'internal': internal,
60
+ 'external': external,
61
+ 'core_shell': core_shell,
62
+ 'sensitivity': sensitivity}
63
+
64
+ def __init__(self,
65
+ psd: SizeDist,
66
+ RI: DataFrame,
67
+ method: Literal['internal', 'external', 'utils-shell', 'sensitivity'],
68
+ result_type: Literal['extinction', 'scattering', 'absorption'] = 'extinction'
69
+ ):
70
+ self.psd = psd
71
+ self.RI = RI
72
+ if method not in ExtinctionDistCalc.mapping:
73
+ raise ValueError(f"Invalid method: {method}. Valid methods are: {list(ExtinctionDistCalc.mapping.keys())}")
74
+ self.method = ExtinctionDistCalc.mapping[method]
75
+ self.result_type = result_type
76
+
77
+ def useApply(self) -> DataFrame:
78
+ """ Calculate volume distribution """
79
+ combined_data = concat([self.psd.data, self.RI], axis=1).dropna()
80
+ return combined_data.apply(partial(self.method, dp=self.psd.dp, result_type=self.result_type),
81
+ axis=1, result_type='expand').reindex(self.psd.index).set_axis(self.psd.dp, axis=1)
82
+
83
+
84
+ # TODO:
85
+ class LungDepositsDistCalc(AbstractDistCalc):
86
+
87
+ def __init__(self, psd: SizeDist, lung_curve):
88
+ self.psd = psd
89
+ self.lung_curve = lung_curve
90
+
91
+ def useApply(self) -> DataFrame:
92
+ pass
93
+
94
+
95
+ class DistributionCalculator: # 策略模式 (Strategy Pattern)
96
+ """ Interface for distribution calculator """
97
+
98
+ mapping = {'number': NumberDistCalc,
99
+ 'surface': SurfaceDistCalc,
100
+ 'volume': VolumeDistCalc,
101
+ 'property': PropertiesDistCalc,
102
+ 'extinction': ExtinctionDistCalc,
103
+ 'lung_deposit': LungDepositsDistCalc}
104
+
105
+ def __init__(self,
106
+ calculator: Literal['number', 'surface', 'volume', 'property', 'extinction'],
107
+ psd: SizeDist,
108
+ RI: DataFrame = None,
109
+ method: str = None,
110
+ result_type: str = None
111
+ ):
112
+ """
113
+ Initialize the DistributionCalculator.
114
+
115
+ Parameters:
116
+ calculator (CalculatorType): The type of calculator.
117
+ psd (SizeDist): The particle size distribution data.
118
+ RI (Optional[DataFrame]): The refractive index data. Default is None.
119
+ method (Optional[str]): The method to use. Default is None.
120
+ result_type (Optional[str]): The result type. Default is None.
121
+ """
122
+ if calculator not in DistributionCalculator.mapping.keys():
123
+ raise ValueError(
124
+ f"Invalid calculator: {calculator}. Valid calculators are: {list(DistributionCalculator.mapping.keys())}")
125
+ self.calculator = DistributionCalculator.mapping[calculator]
126
+ self.psd = psd
127
+ self.RI = RI
128
+ self.method = method
129
+ self.result_type = result_type
130
+
131
+ def useApply(self) -> DataFrame:
132
+ """
133
+ Apply the calculator to the data.
134
+
135
+ Returns:
136
+ DataFrame: The calculated data.
137
+ """
138
+ if self.RI is not None:
139
+ return self.calculator(self.psd, self.RI, self.method, self.result_type).useApply()
140
+ elif issubclass(self.calculator, (NumberDistCalc, SurfaceDistCalc, VolumeDistCalc, PropertiesDistCalc)):
141
+ return self.calculator(self.psd).useApply()
142
+ else:
143
+ raise ValueError("RI parameter is required for this calculator type")
@@ -0,0 +1,176 @@
1
+ from pathlib import Path
2
+
3
+ import numpy as np
4
+ from pandas import read_csv, concat, notna, DataFrame
5
+
6
+ from AeroViz.process.core import DataProc
7
+ from AeroViz.tools.datareader import DataReader
8
+
9
+
10
+ class ChemicalProc(DataProc):
11
+ """
12
+ A class for process chemical data.
13
+
14
+ Parameters:
15
+ -----------
16
+ reset : bool, optional
17
+ If True, resets the process. Default is False.
18
+ filename : str, optional
19
+ The name of the file to process. Default is None.
20
+
21
+ Methods:
22
+ --------
23
+ mass(_df):
24
+ Calculate mass-related parameters.
25
+
26
+ volume(_df):
27
+ Calculate volume-related parameters.
28
+
29
+ volume_average_mixing(_df):
30
+ Calculate volume average mixing parameters.
31
+
32
+ process_data():
33
+ Process data and save the result.
34
+
35
+ Attributes:
36
+ -----------
37
+ DEFAULT_PATH : Path
38
+ The default path for data files.
39
+
40
+ Examples:
41
+ ---------
42
+
43
+ """
44
+
45
+ def __init__(self, file_paths: list[Path | str] = None):
46
+ super().__init__()
47
+ self.file_paths = [Path(fp) for fp in file_paths]
48
+
49
+ @staticmethod
50
+ def mass(_df): # Series like
51
+ Ammonium, Sulfate, Nitrate, OC, Soil, SS, EC, PM25 = _df
52
+ status = (Ammonium / 18) / (2 * (Sulfate / 96) + (Nitrate / 62))
53
+
54
+ if status >= 1:
55
+ _df['NH4_status'] = 'Enough'
56
+ _df['AS'] = 1.375 * Sulfate
57
+ _df['AN'] = 1.29 * Nitrate
58
+
59
+ if status < 1:
60
+ _df['NH4_status'] = 'Deficiency'
61
+ mol_A = Ammonium / 18
62
+ mol_S = Sulfate / 96
63
+ mol_N = Nitrate / 62
64
+ residual = mol_A - 2 * mol_S
65
+
66
+ if residual > 0:
67
+ _df['AS'] = 1.375 * Sulfate
68
+ _df['AN'] = residual * 80 if residual <= mol_N else mol_N * 80
69
+
70
+ else:
71
+ _df['AS'] = mol_A / 2 * 132 if mol_A <= 2 * mol_S else mol_S * 132
72
+ _df['AN'] = 0
73
+
74
+ _df['OM'] = 1.8 * OC
75
+ _df['Soil'] = 28.57 * Soil
76
+ _df['SS'] = 2.54 * SS
77
+ _df['EC'] = EC
78
+ _df['SIA'] = _df['AS'] + _df['AN']
79
+ _df['total_mass'] = _df[['AS', 'AN', 'OM', 'Soil', 'SS', 'EC']].sum()
80
+ species_lst = ['AS', 'AN', 'OM', 'Soil', 'SS', 'EC', 'SIA', 'unknown_mass']
81
+
82
+ _df['unknown_mass'] = PM25 - _df['total_mass'] if PM25 >= _df['total_mass'] else 0
83
+ for _species, _val in _df[species_lst].items():
84
+ _df[f'{_species}_ratio'] = _val / PM25 if PM25 >= _df['total_mass'] else _val / _df['total_mass']
85
+
86
+ return _df['NH4_status':]
87
+
88
+ @staticmethod
89
+ def volume(_df):
90
+ _df['AS_volume'] = (_df['AS'] / 1.76)
91
+ _df['AN_volume'] = (_df['AN'] / 1.73)
92
+ _df['OM_volume'] = (_df['OM'] / 1.4)
93
+ _df['Soil_volume'] = (_df['Soil'] / 2.6)
94
+ _df['SS_volume'] = (_df['SS'] / 2.16)
95
+ _df['EC_volume'] = (_df['EC'] / 1.5)
96
+ _df['ALWC_volume'] = _df['ALWC']
97
+ _df['total_volume'] = sum(_df['AS_volume':'EC_volume'])
98
+
99
+ for _species, _val in _df['AS_volume':'ALWC_volume'].items():
100
+ _df[f'{_species}_ratio'] = _val / _df['total_volume']
101
+
102
+ _df['density'] = _df['total_mass'] / _df['total_volume']
103
+ return _df['AS_volume':]
104
+
105
+ @staticmethod
106
+ def volume_average_mixing(_df):
107
+ _df['n_dry'] = (1.53 * _df['AS_volume_ratio'] +
108
+ 1.55 * _df['AN_volume_ratio'] +
109
+ 1.55 * _df['OM_volume_ratio'] +
110
+ 1.56 * _df['Soil_volume_ratio'] +
111
+ 1.54 * _df['SS_volume_ratio'] +
112
+ 1.80 * _df['EC_volume_ratio'])
113
+
114
+ _df['k_dry'] = (0.00 * _df['OM_volume_ratio'] +
115
+ 0.01 * _df['Soil_volume_ratio'] +
116
+ 0.54 * _df["EC_volume_ratio"])
117
+
118
+ # 檢查_df['ALWC']是否缺失 -> 有值才計算ambient的折射率
119
+ if notna(_df['ALWC']):
120
+ v_dry = _df['total_volume']
121
+ v_wet = _df['total_volume'] + _df['ALWC']
122
+
123
+ multiplier = v_dry / v_wet
124
+ _df['ALWC_volume_ratio'] = (1 - multiplier)
125
+
126
+ _df['n_amb'] = (1.53 * _df['AS_volume_ratio'] +
127
+ 1.55 * _df['AN_volume_ratio'] +
128
+ 1.55 * _df['OM_volume_ratio'] +
129
+ 1.56 * _df['Soil_volume_ratio'] +
130
+ 1.54 * _df['SS_volume_ratio'] +
131
+ 1.80 * _df['EC_volume_ratio']) * multiplier + \
132
+ (1.33 * _df['ALWC_volume_ratio'])
133
+
134
+ _df['k_amb'] = (0.00 * _df['OM_volume_ratio'] +
135
+ 0.01 * _df['Soil_volume_ratio'] +
136
+ 0.54 * _df['EC_volume_ratio']) * multiplier
137
+
138
+ _df['gRH'] = (v_wet / v_dry) ** (1 / 3)
139
+
140
+ return _df[['n_dry', 'k_dry', 'n_amb', 'k_amb', 'gRH']]
141
+
142
+ @staticmethod
143
+ def kappa(_df, diameter=0.5):
144
+ surface_tension, Mw, density, universal_gas_constant = 0.072, 18, 1, 8.314 # J/mole*K
145
+
146
+ A = 4 * (surface_tension * Mw) / (density * universal_gas_constant * (_df['AT'] + 273))
147
+ power = A / diameter
148
+ a_w = (_df['RH'] / 100) * (np.exp(-power))
149
+
150
+ _df['kappa_chem'] = (_df['gRH'] ** 3 - 1) * (1 - a_w) / a_w
151
+ _df['kappa_vam'] = np.nan
152
+
153
+ @staticmethod
154
+ def ISORROPIA():
155
+ pass
156
+
157
+ def process_data(self, reset: bool = False, save_file: Path | str = None) -> DataFrame:
158
+ save_file = Path(save_file)
159
+ if save_file.exists() and not reset:
160
+ return read_csv(save_file, parse_dates=['Time'], index_col='Time')
161
+ else:
162
+ df = concat([DataReader(file) for file in self.file_paths], axis=1)
163
+
164
+ df_mass = df[['NH4+', 'SO42-', 'NO3-', 'O_OC', 'Fe', 'Na+', 'O_EC', 'PM25']].dropna().apply(self.mass,
165
+ axis=1)
166
+ df_mass['ALWC'] = df['ALWC']
167
+ df_volume = df_mass[['AS', 'AN', 'OM', 'Soil', 'SS', 'EC', 'total_mass', 'ALWC']].dropna().apply(
168
+ self.volume,
169
+ axis=1)
170
+ df_volume['ALWC'] = df['ALWC']
171
+ df_vam = df_volume.dropna().apply(self.volume_average_mixing, axis=1)
172
+
173
+ _df = concat([df_mass, df_volume.drop(['ALWC'], axis=1), df_vam], axis=1).reindex(df.index.copy())
174
+ _df.to_csv(save_file)
175
+
176
+ return _df
@@ -0,0 +1,49 @@
1
+ from pathlib import Path
2
+
3
+ from pandas import DataFrame, read_csv, concat
4
+
5
+ from AeroViz.process.core import DataProc
6
+ from AeroViz.tools.datareader import DataReader
7
+
8
+
9
+ class ImpactProc(DataProc):
10
+ """
11
+ A class for processing impact data.
12
+
13
+ Parameters:
14
+ -----------
15
+ reset : bool, optional
16
+ If True, resets the processing. Default is False.
17
+ save_filename : str or Path, optional
18
+ The name or path to save the processed data. Default is 'IMPACT.csv'.
19
+
20
+ Methods:
21
+ --------
22
+ process_data(reset: bool = False, save_filename: str | Path = 'IMPACT.csv') -> DataFrame:
23
+ Process data and save the result.
24
+
25
+ save_data(data: DataFrame, save_filename: str | Path):
26
+ Save processed data to a file.
27
+
28
+ Attributes:
29
+ -----------
30
+ DEFAULT_PATH : Path
31
+ The default path for data files.
32
+
33
+ Examples:
34
+ ---------
35
+ >>> df_custom = ImpactProc().process_data(reset=True, save_filename='custom_file.csv')
36
+ """
37
+
38
+ def __init__(self, file_paths: list[Path | str] = None):
39
+ super().__init__()
40
+ self.file_paths = [Path(fp) for fp in file_paths]
41
+
42
+ def process_data(self, reset: bool = False, save_file: Path | str = None) -> DataFrame:
43
+ save_file = Path(save_file)
44
+ if save_file.exists() and not reset:
45
+ return read_csv(save_file, parse_dates=['Time'], index_col='Time')
46
+ else:
47
+ _df = concat([DataReader(file) for file in self.file_paths], axis=1)
48
+ _df.to_csv(save_file)
49
+ return _df