AeroViz 0.1.3__py3-none-any.whl → 0.1.4__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 (121) hide show
  1. AeroViz/__init__.py +7 -5
  2. AeroViz/{config → data}/DEFAULT_DATA.csv +1 -1
  3. AeroViz/dataProcess/Chemistry/__init__.py +40 -40
  4. AeroViz/dataProcess/Chemistry/_calculate.py +15 -15
  5. AeroViz/dataProcess/Chemistry/_isoropia.py +72 -68
  6. AeroViz/dataProcess/Chemistry/_mass_volume.py +158 -161
  7. AeroViz/dataProcess/Chemistry/_ocec.py +109 -109
  8. AeroViz/dataProcess/Chemistry/_partition.py +19 -18
  9. AeroViz/dataProcess/Chemistry/_teom.py +9 -11
  10. AeroViz/dataProcess/Chemistry/isrpia.cnf +21 -0
  11. AeroViz/dataProcess/Optical/Angstrom_exponent.py +20 -0
  12. AeroViz/dataProcess/Optical/_IMPROVE.py +40 -41
  13. AeroViz/dataProcess/Optical/__init__.py +29 -44
  14. AeroViz/dataProcess/Optical/_absorption.py +21 -47
  15. AeroViz/dataProcess/Optical/_extinction.py +31 -25
  16. AeroViz/dataProcess/Optical/_mie.py +5 -7
  17. AeroViz/dataProcess/Optical/_mie_sd.py +89 -90
  18. AeroViz/dataProcess/Optical/_scattering.py +19 -20
  19. AeroViz/dataProcess/SizeDistr/__init__.py +39 -39
  20. AeroViz/dataProcess/SizeDistr/__merge.py +159 -158
  21. AeroViz/dataProcess/SizeDistr/_merge.py +155 -154
  22. AeroViz/dataProcess/SizeDistr/_merge_v1.py +162 -161
  23. AeroViz/dataProcess/SizeDistr/_merge_v2.py +153 -152
  24. AeroViz/dataProcess/SizeDistr/_merge_v3.py +327 -327
  25. AeroViz/dataProcess/SizeDistr/_merge_v4.py +273 -275
  26. AeroViz/dataProcess/SizeDistr/_size_distr.py +51 -51
  27. AeroViz/dataProcess/VOC/__init__.py +9 -9
  28. AeroViz/dataProcess/VOC/_potential_par.py +53 -55
  29. AeroViz/dataProcess/__init__.py +28 -6
  30. AeroViz/dataProcess/core/__init__.py +59 -65
  31. AeroViz/plot/__init__.py +7 -2
  32. AeroViz/plot/bar.py +126 -0
  33. AeroViz/plot/box.py +69 -0
  34. AeroViz/plot/distribution/distribution.py +421 -427
  35. AeroViz/plot/meteorology/meteorology.py +240 -292
  36. AeroViz/plot/optical/__init__.py +0 -1
  37. AeroViz/plot/optical/optical.py +230 -230
  38. AeroViz/plot/pie.py +198 -0
  39. AeroViz/plot/regression.py +196 -0
  40. AeroViz/plot/scatter.py +165 -0
  41. AeroViz/plot/templates/__init__.py +2 -4
  42. AeroViz/plot/templates/ammonium_rich.py +34 -0
  43. AeroViz/plot/templates/contour.py +25 -25
  44. AeroViz/plot/templates/corr_matrix.py +86 -93
  45. AeroViz/plot/templates/diurnal_pattern.py +28 -26
  46. AeroViz/plot/templates/koschmieder.py +59 -123
  47. AeroViz/plot/templates/metal_heatmap.py +135 -37
  48. AeroViz/plot/timeseries/__init__.py +1 -0
  49. AeroViz/plot/timeseries/template.py +47 -0
  50. AeroViz/plot/timeseries/timeseries.py +324 -264
  51. AeroViz/plot/utils/__init__.py +2 -1
  52. AeroViz/plot/utils/_color.py +57 -57
  53. AeroViz/plot/utils/_unit.py +48 -48
  54. AeroViz/plot/utils/plt_utils.py +92 -0
  55. AeroViz/plot/utils/sklearn_utils.py +49 -0
  56. AeroViz/plot/utils/units.json +5 -0
  57. AeroViz/plot/violin.py +80 -0
  58. AeroViz/process/__init__.py +17 -17
  59. AeroViz/process/core/DataProc.py +9 -9
  60. AeroViz/process/core/SizeDist.py +81 -81
  61. AeroViz/process/method/PyMieScatt_update.py +488 -488
  62. AeroViz/process/method/mie_theory.py +231 -229
  63. AeroViz/process/method/prop.py +40 -40
  64. AeroViz/process/script/AbstractDistCalc.py +103 -103
  65. AeroViz/process/script/Chemical.py +168 -167
  66. AeroViz/process/script/IMPACT.py +40 -40
  67. AeroViz/process/script/IMPROVE.py +152 -152
  68. AeroViz/process/script/Others.py +45 -45
  69. AeroViz/process/script/PSD.py +26 -26
  70. AeroViz/process/script/PSD_dry.py +69 -70
  71. AeroViz/process/script/retrieve_RI.py +50 -51
  72. AeroViz/rawDataReader/__init__.py +53 -58
  73. AeroViz/rawDataReader/config/supported_instruments.py +155 -0
  74. AeroViz/rawDataReader/core/__init__.py +233 -356
  75. AeroViz/rawDataReader/script/AE33.py +17 -18
  76. AeroViz/rawDataReader/script/AE43.py +18 -21
  77. AeroViz/rawDataReader/script/APS_3321.py +30 -30
  78. AeroViz/rawDataReader/script/Aurora.py +23 -24
  79. AeroViz/rawDataReader/script/BC1054.py +36 -40
  80. AeroViz/rawDataReader/script/EPA_vertical.py +37 -9
  81. AeroViz/rawDataReader/script/GRIMM.py +16 -23
  82. AeroViz/rawDataReader/script/IGAC.py +90 -0
  83. AeroViz/rawDataReader/script/MA350.py +32 -39
  84. AeroViz/rawDataReader/script/Minion.py +103 -0
  85. AeroViz/rawDataReader/script/NEPH.py +69 -74
  86. AeroViz/rawDataReader/script/SMPS_TH.py +25 -25
  87. AeroViz/rawDataReader/script/SMPS_aim11.py +32 -32
  88. AeroViz/rawDataReader/script/SMPS_genr.py +31 -31
  89. AeroViz/rawDataReader/script/Sunset_OCEC.py +60 -0
  90. AeroViz/rawDataReader/script/TEOM.py +30 -28
  91. AeroViz/rawDataReader/script/Table.py +13 -14
  92. AeroViz/rawDataReader/script/VOC.py +26 -0
  93. AeroViz/rawDataReader/script/__init__.py +18 -20
  94. AeroViz/tools/database.py +64 -66
  95. AeroViz/tools/dataclassifier.py +106 -106
  96. AeroViz/tools/dataprinter.py +51 -51
  97. AeroViz/tools/datareader.py +38 -38
  98. {AeroViz-0.1.3.dist-info → AeroViz-0.1.4.dist-info}/METADATA +5 -4
  99. AeroViz-0.1.4.dist-info/RECORD +112 -0
  100. AeroViz/plot/improve/__init__.py +0 -1
  101. AeroViz/plot/improve/improve.py +0 -240
  102. AeroViz/plot/optical/aethalometer.py +0 -77
  103. AeroViz/plot/templates/event_evolution.py +0 -65
  104. AeroViz/plot/templates/regression.py +0 -256
  105. AeroViz/plot/templates/scatter.py +0 -130
  106. AeroViz/plot/templates/templates.py +0 -398
  107. AeroViz/plot/utils/_decorator.py +0 -74
  108. AeroViz/rawDataReader/script/IGAC_TH.py +0 -104
  109. AeroViz/rawDataReader/script/IGAC_ZM.py +0 -90
  110. AeroViz/rawDataReader/script/OCEC_LCRES.py +0 -34
  111. AeroViz/rawDataReader/script/OCEC_RES.py +0 -28
  112. AeroViz/rawDataReader/script/VOC_TH.py +0 -30
  113. AeroViz/rawDataReader/script/VOC_ZM.py +0 -37
  114. AeroViz/rawDataReader/utils/__init__.py +0 -0
  115. AeroViz/rawDataReader/utils/config.py +0 -169
  116. AeroViz-0.1.3.dist-info/RECORD +0 -111
  117. /AeroViz/{config → data}/DEFAULT_PNSD_DATA.csv +0 -0
  118. /AeroViz/{config → rawDataReader/config}/__init__.py +0 -0
  119. {AeroViz-0.1.3.dist-info → AeroViz-0.1.4.dist-info}/LICENSE +0 -0
  120. {AeroViz-0.1.3.dist-info → AeroViz-0.1.4.dist-info}/WHEEL +0 -0
  121. {AeroViz-0.1.3.dist-info → AeroViz-0.1.4.dist-info}/top_level.txt +0 -0
@@ -10,134 +10,134 @@ from AeroViz.process.method import properties, internal, external, core_shell, s
10
10
 
11
11
 
12
12
  class AbstractDistCalc(ABC):
13
- @abstractmethod
14
- def useApply(self) -> DataFrame:
15
- pass
13
+ @abstractmethod
14
+ def useApply(self) -> DataFrame:
15
+ pass
16
16
 
17
17
 
18
18
  class NumberDistCalc(AbstractDistCalc):
19
- def __init__(self, psd: SizeDist):
20
- self.psd = psd
19
+ def __init__(self, psd: SizeDist):
20
+ self.psd = psd
21
21
 
22
- def useApply(self) -> DataFrame:
23
- """ Calculate number distribution """
24
- return self.psd.data
22
+ def useApply(self) -> DataFrame:
23
+ """ Calculate number distribution """
24
+ return self.psd.data
25
25
 
26
26
 
27
27
  class SurfaceDistCalc(AbstractDistCalc):
28
- def __init__(self, psd: SizeDist):
29
- self.psd = psd
28
+ def __init__(self, psd: SizeDist):
29
+ self.psd = psd
30
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)
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
35
 
36
36
 
37
37
  class VolumeDistCalc(AbstractDistCalc):
38
- def __init__(self, psd: SizeDist):
39
- self.psd = psd
38
+ def __init__(self, psd: SizeDist):
39
+ self.psd = psd
40
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)
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
45
 
46
46
 
47
47
  class PropertiesDistCalc(AbstractDistCalc):
48
- def __init__(self, psd: SizeDist):
49
- self.psd = psd
48
+ def __init__(self, psd: SizeDist):
49
+ self.psd = psd
50
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)
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
56
 
57
57
 
58
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)
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
82
 
83
83
 
84
84
  # TODO:
85
85
  class LungDepositsDistCalc(AbstractDistCalc):
86
86
 
87
- def __init__(self, psd: SizeDist, lung_curve):
88
- self.psd = psd
89
- self.lung_curve = lung_curve
87
+ def __init__(self, psd: SizeDist, lung_curve):
88
+ self.psd = psd
89
+ self.lung_curve = lung_curve
90
90
 
91
- def useApply(self) -> DataFrame:
92
- pass
91
+ def useApply(self) -> DataFrame:
92
+ pass
93
93
 
94
94
 
95
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")
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,176 +1,177 @@
1
1
  from pathlib import Path
2
2
 
3
3
  import numpy as np
4
- from pandas import read_csv, concat, notna, DataFrame
4
+ from pandas import read_csv, concat, notna, DataFrame, to_numeric
5
5
 
6
6
  from AeroViz.process.core import DataProc
7
7
  from AeroViz.tools.datareader import DataReader
8
8
 
9
9
 
10
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
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