AeroViz 0.1.6__py3-none-any.whl → 0.1.7__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 (51) hide show
  1. AeroViz/dataProcess/Chemistry/_ocec.py +20 -7
  2. AeroViz/plot/__init__.py +1 -0
  3. AeroViz/plot/meteorology/meteorology.py +2 -0
  4. AeroViz/plot/optical/optical.py +1 -1
  5. AeroViz/plot/pie.py +14 -2
  6. AeroViz/plot/radar.py +184 -0
  7. AeroViz/plot/scatter.py +16 -7
  8. AeroViz/plot/templates/koschmieder.py +11 -8
  9. AeroViz/plot/timeseries/timeseries.py +0 -1
  10. AeroViz/rawDataReader/__init__.py +74 -67
  11. AeroViz/rawDataReader/config/supported_instruments.py +52 -19
  12. AeroViz/rawDataReader/core/__init__.py +129 -104
  13. AeroViz/rawDataReader/script/AE33.py +1 -1
  14. AeroViz/rawDataReader/script/AE43.py +1 -1
  15. AeroViz/rawDataReader/script/Aurora.py +1 -1
  16. AeroViz/rawDataReader/script/BC1054.py +1 -1
  17. AeroViz/rawDataReader/script/EPA.py +39 -0
  18. AeroViz/rawDataReader/script/GRIMM.py +1 -1
  19. AeroViz/rawDataReader/script/IGAC.py +6 -23
  20. AeroViz/rawDataReader/script/MA350.py +1 -1
  21. AeroViz/rawDataReader/script/Minion.py +102 -30
  22. AeroViz/rawDataReader/script/NEPH.py +1 -1
  23. AeroViz/rawDataReader/script/OCEC.py +1 -1
  24. AeroViz/rawDataReader/script/SMPS.py +1 -0
  25. AeroViz/rawDataReader/script/TEOM.py +2 -2
  26. AeroViz/rawDataReader/script/XRF.py +11 -0
  27. AeroViz/rawDataReader/script/__init__.py +2 -2
  28. {AeroViz-0.1.6.dist-info → AeroViz-0.1.7.dist-info}/METADATA +46 -24
  29. {AeroViz-0.1.6.dist-info → AeroViz-0.1.7.dist-info}/RECORD +32 -48
  30. AeroViz/process/__init__.py +0 -31
  31. AeroViz/process/core/DataProc.py +0 -19
  32. AeroViz/process/core/SizeDist.py +0 -90
  33. AeroViz/process/core/__init__.py +0 -4
  34. AeroViz/process/method/PyMieScatt_update.py +0 -567
  35. AeroViz/process/method/__init__.py +0 -2
  36. AeroViz/process/method/mie_theory.py +0 -260
  37. AeroViz/process/method/prop.py +0 -62
  38. AeroViz/process/script/AbstractDistCalc.py +0 -143
  39. AeroViz/process/script/Chemical.py +0 -177
  40. AeroViz/process/script/IMPACT.py +0 -49
  41. AeroViz/process/script/IMPROVE.py +0 -161
  42. AeroViz/process/script/Others.py +0 -65
  43. AeroViz/process/script/PSD.py +0 -103
  44. AeroViz/process/script/PSD_dry.py +0 -93
  45. AeroViz/process/script/__init__.py +0 -5
  46. AeroViz/process/script/retrieve_RI.py +0 -69
  47. AeroViz/rawDataReader/script/EPA_vertical.py +0 -46
  48. AeroViz/rawDataReader/script/Table.py +0 -27
  49. {AeroViz-0.1.6.dist-info → AeroViz-0.1.7.dist-info}/LICENSE +0 -0
  50. {AeroViz-0.1.6.dist-info → AeroViz-0.1.7.dist-info}/WHEEL +0 -0
  51. {AeroViz-0.1.6.dist-info → AeroViz-0.1.7.dist-info}/top_level.txt +0 -0
@@ -1,260 +0,0 @@
1
- from typing import Sequence, Literal
2
-
3
- import numpy as np
4
- import pandas as pd
5
- from numpy import exp, log, log10, sqrt, pi
6
-
7
- from .PyMieScatt_update import AutoMieQ
8
-
9
-
10
- def Mie_Q(m: complex,
11
- wavelength: float,
12
- dp: float | Sequence[float]
13
- ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
14
- """
15
- Calculate Mie scattering efficiency (Q) for given spherical particle diameter(s).
16
-
17
- Parameters
18
- ----------
19
- m : complex
20
- The complex refractive index of the particles.
21
- wavelength : float
22
- The wavelength of the incident light (in nm).
23
- dp : float | Sequence[float]
24
- Particle diameters (in nm), can be a single value or Sequence object.
25
-
26
- Returns
27
- -------
28
- Q_ext : ndarray
29
- The Mie extinction efficiency for each particle diameter.
30
- Q_sca : ndarray
31
- The Mie scattering efficiency for each particle diameter.
32
- Q_abs : ndarray
33
- The Mie absorption efficiency for each particle diameter.
34
-
35
- Examples
36
- --------
37
- >>> Q_ext, Q_sca, Q_abs = Mie_Q(m=complex(1.5, 0.02), wavelength=550, dp=[100, 200, 300, 400])
38
- """
39
- # Ensure dp is a numpy array
40
- dp = np.atleast_1d(dp)
41
-
42
- # Transpose for proper unpacking
43
- Q_ext, Q_sca, Q_abs, g, Q_pr, Q_back, Q_ratio = np.array([AutoMieQ(m, wavelength, _dp) for _dp in dp]).T
44
-
45
- return Q_ext, Q_sca, Q_abs
46
-
47
-
48
- def Mie_MEE(m: complex,
49
- wavelength: float,
50
- dp: float | Sequence[float],
51
- density: float
52
- ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
53
- """
54
- Calculate mass extinction efficiency and other parameters.
55
-
56
- Parameters
57
- ----------
58
- m : complex
59
- The complex refractive index of the particles.
60
- wavelength : float
61
- The wavelength of the incident light.
62
- dp : float | Sequence[float]
63
- List of particle sizes or a single value.
64
- density : float
65
- The density of particles.
66
-
67
- Returns
68
- -------
69
- MEE : ndarray
70
- The mass extinction efficiency for each particle diameter.
71
- MSE : ndarray
72
- The mass scattering efficiency for each particle diameter.
73
- MAE : ndarray
74
- The mass absorption efficiency for each particle diameter.
75
-
76
- Examples
77
- --------
78
- >>> MEE, MSE, MAE = Mie_MEE(m=complex(1.5, 0.02), wavelength=550, dp=[100, 200, 300, 400], density=1.2)
79
- """
80
- Q_ext, Q_sca, Q_abs = Mie_Q(m, wavelength, dp)
81
-
82
- MEE = (3 * Q_ext) / (2 * density * dp) * 1000
83
- MSE = (3 * Q_sca) / (2 * density * dp) * 1000
84
- MAE = (3 * Q_abs) / (2 * density * dp) * 1000
85
-
86
- return MEE, MSE, MAE
87
-
88
-
89
- def Mie_PESD(m: complex,
90
- wavelength: float = 550,
91
- dp: float | Sequence[float] = None,
92
- ndp: float | Sequence[float] = None,
93
- lognormal: bool = False,
94
- dp_range: tuple = (1, 2500),
95
- geoMean: float = 200,
96
- geoStdDev: float = 2,
97
- numberOfParticles: float = 1e6,
98
- numberOfBins: int = 167,
99
- ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
100
- """
101
- Simultaneously calculate "extinction distribution" and "integrated results" using the Mie_Q method.
102
-
103
- Parameters
104
- ----------
105
- m : complex
106
- The complex refractive index of the particles.
107
- wavelength : float
108
- The wavelength of the incident light.
109
- dp : float | Sequence[float]
110
- Particle sizes.
111
- ndp : float | Sequence[float]
112
- Number concentration from SMPS or APS in the units of dN/dlogdp.
113
- lognormal : bool, optional
114
- Whether to use lognormal distribution for ndp. Default is False.
115
- dp_range : tuple, optional
116
- Range of particle sizes. Default is (1, 2500) nm.
117
- geoMean : float, optional
118
- Geometric mean of the particle size distribution. Default is 200 nm.
119
- geoStdDev : float, optional
120
- Geometric standard deviation of the particle size distribution. Default is 2.
121
- numberOfParticles : float, optional
122
- Number of particles. Default is 1e6.
123
- numberOfBins : int, optional
124
- Number of bins for the lognormal distribution. Default is 167.
125
-
126
- Returns
127
- -------
128
- ext_dist : ndarray
129
- The extinction distribution for the given data.
130
- sca_dist : ndarray
131
- The scattering distribution for the given data.
132
- abs_dist : ndarray
133
- The absorption distribution for the given data.
134
-
135
- Notes
136
- -----
137
- return in "dext/dlogdp", please make sure input the dNdlogdp data.
138
-
139
- Examples
140
- --------
141
- >>> Ext, Sca, Abs = Mie_PESD(m=complex(1.5, 0.02), wavelength=550, dp=[100, 200, 500, 1000], ndp=[100, 50, 30, 20])
142
- """
143
- if lognormal:
144
- dp = np.logspace(log10(dp_range[0]), log10(dp_range[1]), numberOfBins)
145
-
146
- ndp = numberOfParticles * (1 / (log(geoStdDev) * sqrt(2 * pi)) *
147
- exp(-(log(dp) - log(geoMean)) ** 2 / (2 * log(geoStdDev) ** 2)))
148
-
149
- # dN / dlogdp
150
- ndp = np.atleast_1d(ndp)
151
-
152
- Q_ext, Q_sca, Q_abs = Mie_Q(m, wavelength, dp)
153
-
154
- # The 1e-6 here is so that the final value is the same as the unit 1/10^6m.
155
- Ext = Q_ext * (pi / 4 * dp ** 2) * ndp * 1e-6
156
- Sca = Q_sca * (pi / 4 * dp ** 2) * ndp * 1e-6
157
- Abs = Q_abs * (pi / 4 * dp ** 2) * ndp * 1e-6
158
-
159
- return Ext, Sca, Abs
160
-
161
-
162
- def internal(dist: pd.Series,
163
- dp: float | Sequence[float],
164
- wavelength: float = 550,
165
- result_type: Literal['extinction', 'scattering', 'absorption'] = 'extinction'
166
- ) -> np.ndarray:
167
- """
168
- Calculate the extinction distribution by internal mixing model.
169
-
170
- Parameters
171
- ----------
172
- dist : pd.Series
173
- Particle size distribution data.
174
- dp : float | Sequence[float]
175
- Diameter(s) of the particles, either a single value or a sequence.
176
- wavelength : float, optional
177
- Wavelength of the incident light, default is 550 nm.
178
- result_type : {'extinction', 'scattering', 'absorption'}, optional
179
- Type of result to calculate, defaults to 'extinction'.
180
-
181
- Returns
182
- -------
183
- np.ndarray
184
- Extinction distribution calculated based on the internal mixing model.
185
- """
186
- ext_dist, sca_dist, abs_dist = Mie_PESD(m=complex(dist['n_amb'], dist['k_amb']),
187
- wavelength=wavelength,
188
- dp=dp,
189
- ndp=np.array(dist[:np.size(dp)]))
190
-
191
- if result_type == 'extinction':
192
- return ext_dist
193
- elif result_type == 'scattering':
194
- return sca_dist
195
- else:
196
- return abs_dist
197
-
198
-
199
- # return dict(ext=ext_dist, sca=sca_dist, abs=abs_dist)
200
-
201
-
202
- def external(dist: pd.Series,
203
- dp: float | Sequence[float],
204
- wavelength: float = 550,
205
- result_type: Literal['extinction', 'scattering', 'absorption'] = 'extinction'
206
- ) -> np.ndarray:
207
- """
208
- Calculate the extinction distribution by external mixing model.
209
-
210
- Parameters
211
- ----------
212
- dist : pd.Series
213
- Particle size distribution data.
214
- dp : float | Sequence[float]
215
- Diameter(s) of the particles, either a single value or a sequence.
216
- wavelength : float, optional
217
- Wavelength of the incident light, default is 550 nm.
218
- result_type : {'extinction', 'scattering', 'absorption'}, optional
219
- Type of result to calculate, defaults to 'extinction'.
220
-
221
- Returns
222
- -------
223
- np.ndarray
224
- Extinction distribution calculated based on the external mixing model.
225
- """
226
- refractive_dic = {'AS_volume_ratio': complex(1.53, 0.00),
227
- 'AN_volume_ratio': complex(1.55, 0.00),
228
- 'OM_volume_ratio': complex(1.54, 0.00),
229
- 'Soil_volume_ratio': complex(1.56, 0.01),
230
- 'SS_volume_ratio': complex(1.54, 0.00),
231
- 'EC_volume_ratio': complex(1.80, 0.54),
232
- 'ALWC_volume_ratio': complex(1.33, 0.00)}
233
-
234
- ndp = np.array(dist[:np.size(dp)])
235
- mie_results = (
236
- Mie_PESD(refractive_dic[_specie], wavelength, dp, dist[_specie] / (1 + dist['ALWC_volume_ratio']) * ndp) for
237
- _specie in refractive_dic)
238
-
239
- ext_dist, sca_dist, abs_dist = (np.sum([res[0] for res in mie_results], axis=0),
240
- np.sum([res[1] for res in mie_results], axis=0),
241
- np.sum([res[2] for res in mie_results], axis=0))
242
-
243
- if result_type == 'extinction':
244
- return ext_dist
245
- elif result_type == 'scattering':
246
- return sca_dist
247
- else:
248
- return abs_dist
249
-
250
-
251
- def core_shell():
252
- pass
253
-
254
-
255
- def sensitivity():
256
- pass
257
-
258
-
259
- if __name__ == '__main__':
260
- result = Mie_Q(m=complex(1.5, 0.02), wavelength=550, dp=[100., 200.])
@@ -1,62 +0,0 @@
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()}
@@ -1,143 +0,0 @@
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', 'config-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")
@@ -1,177 +0,0 @@
1
- from pathlib import Path
2
-
3
- import numpy as np
4
- from pandas import read_csv, concat, notna, DataFrame, to_numeric
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 / 1000
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).apply(to_numeric, errors='coerce')
163
-
164
- df_mass = df[['NH4+', 'SO42-', 'NO3-', 'Optical_OC', 'Fe', 'Na+', 'Optical_EC', 'PM2.5']].dropna().apply(
165
- self.mass,
166
- axis=1)
167
- df_mass['ALWC'] = df['ALWC']
168
- df_volume = df_mass[['AS', 'AN', 'OM', 'Soil', 'SS', 'EC', 'total_mass', 'ALWC']].dropna().apply(
169
- self.volume,
170
- axis=1)
171
- df_volume['ALWC'] = df['ALWC']
172
- df_vam = df_volume.dropna().apply(self.volume_average_mixing, axis=1)
173
-
174
- _df = concat([df_mass, df_volume.drop(['ALWC'], axis=1), df_vam], axis=1).reindex(df.index.copy())
175
- _df.to_csv(save_file)
176
-
177
- return _df
@@ -1,49 +0,0 @@
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