AeroViz 0.1.21__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.
Files changed (180) hide show
  1. AeroViz/__init__.py +13 -0
  2. AeroViz/__pycache__/__init__.cpython-312.pyc +0 -0
  3. AeroViz/data/DEFAULT_DATA.csv +1417 -0
  4. AeroViz/data/DEFAULT_PNSD_DATA.csv +1417 -0
  5. AeroViz/data/hysplit_example_data.txt +101 -0
  6. AeroViz/dataProcess/Chemistry/__init__.py +149 -0
  7. AeroViz/dataProcess/Chemistry/__pycache__/__init__.cpython-312.pyc +0 -0
  8. AeroViz/dataProcess/Chemistry/_calculate.py +557 -0
  9. AeroViz/dataProcess/Chemistry/_isoropia.py +150 -0
  10. AeroViz/dataProcess/Chemistry/_mass_volume.py +487 -0
  11. AeroViz/dataProcess/Chemistry/_ocec.py +172 -0
  12. AeroViz/dataProcess/Chemistry/isrpia.cnf +21 -0
  13. AeroViz/dataProcess/Chemistry/isrpia2.exe +0 -0
  14. AeroViz/dataProcess/Optical/PyMieScatt_update.py +577 -0
  15. AeroViz/dataProcess/Optical/_IMPROVE.py +452 -0
  16. AeroViz/dataProcess/Optical/__init__.py +281 -0
  17. AeroViz/dataProcess/Optical/__pycache__/PyMieScatt_update.cpython-312.pyc +0 -0
  18. AeroViz/dataProcess/Optical/__pycache__/__init__.cpython-312.pyc +0 -0
  19. AeroViz/dataProcess/Optical/__pycache__/mie_theory.cpython-312.pyc +0 -0
  20. AeroViz/dataProcess/Optical/_derived.py +518 -0
  21. AeroViz/dataProcess/Optical/_extinction.py +123 -0
  22. AeroViz/dataProcess/Optical/_mie_sd.py +912 -0
  23. AeroViz/dataProcess/Optical/_retrieve_RI.py +243 -0
  24. AeroViz/dataProcess/Optical/coefficient.py +72 -0
  25. AeroViz/dataProcess/Optical/fRH.pkl +0 -0
  26. AeroViz/dataProcess/Optical/mie_theory.py +260 -0
  27. AeroViz/dataProcess/README.md +271 -0
  28. AeroViz/dataProcess/SizeDistr/__init__.py +245 -0
  29. AeroViz/dataProcess/SizeDistr/__pycache__/__init__.cpython-312.pyc +0 -0
  30. AeroViz/dataProcess/SizeDistr/__pycache__/_size_dist.cpython-312.pyc +0 -0
  31. AeroViz/dataProcess/SizeDistr/_size_dist.py +810 -0
  32. AeroViz/dataProcess/SizeDistr/merge/README.md +93 -0
  33. AeroViz/dataProcess/SizeDistr/merge/__init__.py +20 -0
  34. AeroViz/dataProcess/SizeDistr/merge/_merge_v0.py +251 -0
  35. AeroViz/dataProcess/SizeDistr/merge/_merge_v0_1.py +246 -0
  36. AeroViz/dataProcess/SizeDistr/merge/_merge_v1.py +255 -0
  37. AeroViz/dataProcess/SizeDistr/merge/_merge_v2.py +244 -0
  38. AeroViz/dataProcess/SizeDistr/merge/_merge_v3.py +518 -0
  39. AeroViz/dataProcess/SizeDistr/merge/_merge_v4.py +422 -0
  40. AeroViz/dataProcess/SizeDistr/prop.py +62 -0
  41. AeroViz/dataProcess/VOC/__init__.py +14 -0
  42. AeroViz/dataProcess/VOC/__pycache__/__init__.cpython-312.pyc +0 -0
  43. AeroViz/dataProcess/VOC/_potential_par.py +108 -0
  44. AeroViz/dataProcess/VOC/support_voc.json +446 -0
  45. AeroViz/dataProcess/__init__.py +66 -0
  46. AeroViz/dataProcess/__pycache__/__init__.cpython-312.pyc +0 -0
  47. AeroViz/dataProcess/core/__init__.py +272 -0
  48. AeroViz/dataProcess/core/__pycache__/__init__.cpython-312.pyc +0 -0
  49. AeroViz/mcp_server.py +352 -0
  50. AeroViz/plot/__init__.py +13 -0
  51. AeroViz/plot/__pycache__/__init__.cpython-312.pyc +0 -0
  52. AeroViz/plot/__pycache__/bar.cpython-312.pyc +0 -0
  53. AeroViz/plot/__pycache__/box.cpython-312.pyc +0 -0
  54. AeroViz/plot/__pycache__/pie.cpython-312.pyc +0 -0
  55. AeroViz/plot/__pycache__/radar.cpython-312.pyc +0 -0
  56. AeroViz/plot/__pycache__/regression.cpython-312.pyc +0 -0
  57. AeroViz/plot/__pycache__/scatter.cpython-312.pyc +0 -0
  58. AeroViz/plot/__pycache__/violin.cpython-312.pyc +0 -0
  59. AeroViz/plot/bar.py +126 -0
  60. AeroViz/plot/box.py +69 -0
  61. AeroViz/plot/distribution/__init__.py +1 -0
  62. AeroViz/plot/distribution/__pycache__/__init__.cpython-312.pyc +0 -0
  63. AeroViz/plot/distribution/__pycache__/distribution.cpython-312.pyc +0 -0
  64. AeroViz/plot/distribution/distribution.py +576 -0
  65. AeroViz/plot/meteorology/CBPF.py +295 -0
  66. AeroViz/plot/meteorology/__init__.py +3 -0
  67. AeroViz/plot/meteorology/__pycache__/CBPF.cpython-312.pyc +0 -0
  68. AeroViz/plot/meteorology/__pycache__/__init__.cpython-312.pyc +0 -0
  69. AeroViz/plot/meteorology/__pycache__/hysplit.cpython-312.pyc +0 -0
  70. AeroViz/plot/meteorology/__pycache__/wind_rose.cpython-312.pyc +0 -0
  71. AeroViz/plot/meteorology/hysplit.py +93 -0
  72. AeroViz/plot/meteorology/wind_rose.py +77 -0
  73. AeroViz/plot/optical/__init__.py +1 -0
  74. AeroViz/plot/optical/__pycache__/__init__.cpython-312.pyc +0 -0
  75. AeroViz/plot/optical/__pycache__/optical.cpython-312.pyc +0 -0
  76. AeroViz/plot/optical/optical.py +388 -0
  77. AeroViz/plot/pie.py +210 -0
  78. AeroViz/plot/radar.py +184 -0
  79. AeroViz/plot/regression.py +200 -0
  80. AeroViz/plot/scatter.py +174 -0
  81. AeroViz/plot/templates/__init__.py +6 -0
  82. AeroViz/plot/templates/__pycache__/__init__.cpython-312.pyc +0 -0
  83. AeroViz/plot/templates/__pycache__/ammonium_rich.cpython-312.pyc +0 -0
  84. AeroViz/plot/templates/__pycache__/contour.cpython-312.pyc +0 -0
  85. AeroViz/plot/templates/__pycache__/corr_matrix.cpython-312.pyc +0 -0
  86. AeroViz/plot/templates/__pycache__/diurnal_pattern.cpython-312.pyc +0 -0
  87. AeroViz/plot/templates/__pycache__/koschmieder.cpython-312.pyc +0 -0
  88. AeroViz/plot/templates/__pycache__/metal_heatmap.cpython-312.pyc +0 -0
  89. AeroViz/plot/templates/ammonium_rich.py +34 -0
  90. AeroViz/plot/templates/contour.py +47 -0
  91. AeroViz/plot/templates/corr_matrix.py +267 -0
  92. AeroViz/plot/templates/diurnal_pattern.py +61 -0
  93. AeroViz/plot/templates/koschmieder.py +95 -0
  94. AeroViz/plot/templates/metal_heatmap.py +164 -0
  95. AeroViz/plot/timeseries/__init__.py +2 -0
  96. AeroViz/plot/timeseries/__pycache__/__init__.cpython-312.pyc +0 -0
  97. AeroViz/plot/timeseries/__pycache__/template.cpython-312.pyc +0 -0
  98. AeroViz/plot/timeseries/__pycache__/timeseries.cpython-312.pyc +0 -0
  99. AeroViz/plot/timeseries/template.py +47 -0
  100. AeroViz/plot/timeseries/timeseries.py +446 -0
  101. AeroViz/plot/utils/__init__.py +4 -0
  102. AeroViz/plot/utils/__pycache__/__init__.cpython-312.pyc +0 -0
  103. AeroViz/plot/utils/__pycache__/_color.cpython-312.pyc +0 -0
  104. AeroViz/plot/utils/__pycache__/_unit.cpython-312.pyc +0 -0
  105. AeroViz/plot/utils/__pycache__/plt_utils.cpython-312.pyc +0 -0
  106. AeroViz/plot/utils/__pycache__/sklearn_utils.cpython-312.pyc +0 -0
  107. AeroViz/plot/utils/_color.py +71 -0
  108. AeroViz/plot/utils/_unit.py +55 -0
  109. AeroViz/plot/utils/fRH.json +390 -0
  110. AeroViz/plot/utils/plt_utils.py +92 -0
  111. AeroViz/plot/utils/sklearn_utils.py +49 -0
  112. AeroViz/plot/utils/units.json +89 -0
  113. AeroViz/plot/violin.py +80 -0
  114. AeroViz/rawDataReader/FLOW.md +138 -0
  115. AeroViz/rawDataReader/__init__.py +220 -0
  116. AeroViz/rawDataReader/__pycache__/__init__.cpython-312.pyc +0 -0
  117. AeroViz/rawDataReader/config/__init__.py +0 -0
  118. AeroViz/rawDataReader/config/__pycache__/__init__.cpython-312.pyc +0 -0
  119. AeroViz/rawDataReader/config/__pycache__/supported_instruments.cpython-312.pyc +0 -0
  120. AeroViz/rawDataReader/config/supported_instruments.py +135 -0
  121. AeroViz/rawDataReader/core/__init__.py +658 -0
  122. AeroViz/rawDataReader/core/__pycache__/__init__.cpython-312.pyc +0 -0
  123. AeroViz/rawDataReader/core/__pycache__/logger.cpython-312.pyc +0 -0
  124. AeroViz/rawDataReader/core/__pycache__/pre_process.cpython-312.pyc +0 -0
  125. AeroViz/rawDataReader/core/__pycache__/qc.cpython-312.pyc +0 -0
  126. AeroViz/rawDataReader/core/__pycache__/report.cpython-312.pyc +0 -0
  127. AeroViz/rawDataReader/core/logger.py +171 -0
  128. AeroViz/rawDataReader/core/pre_process.py +308 -0
  129. AeroViz/rawDataReader/core/qc.py +961 -0
  130. AeroViz/rawDataReader/core/report.py +579 -0
  131. AeroViz/rawDataReader/script/AE33.py +173 -0
  132. AeroViz/rawDataReader/script/AE43.py +151 -0
  133. AeroViz/rawDataReader/script/APS.py +339 -0
  134. AeroViz/rawDataReader/script/Aurora.py +191 -0
  135. AeroViz/rawDataReader/script/BAM1020.py +90 -0
  136. AeroViz/rawDataReader/script/BC1054.py +161 -0
  137. AeroViz/rawDataReader/script/EPA.py +79 -0
  138. AeroViz/rawDataReader/script/GRIMM.py +68 -0
  139. AeroViz/rawDataReader/script/IGAC.py +140 -0
  140. AeroViz/rawDataReader/script/MA350.py +179 -0
  141. AeroViz/rawDataReader/script/Minion.py +218 -0
  142. AeroViz/rawDataReader/script/NEPH.py +199 -0
  143. AeroViz/rawDataReader/script/OCEC.py +173 -0
  144. AeroViz/rawDataReader/script/Q-ACSM.py +12 -0
  145. AeroViz/rawDataReader/script/SMPS.py +389 -0
  146. AeroViz/rawDataReader/script/TEOM.py +181 -0
  147. AeroViz/rawDataReader/script/VOC.py +106 -0
  148. AeroViz/rawDataReader/script/Xact.py +244 -0
  149. AeroViz/rawDataReader/script/__init__.py +28 -0
  150. AeroViz/rawDataReader/script/__pycache__/AE33.cpython-312.pyc +0 -0
  151. AeroViz/rawDataReader/script/__pycache__/AE43.cpython-312.pyc +0 -0
  152. AeroViz/rawDataReader/script/__pycache__/APS.cpython-312.pyc +0 -0
  153. AeroViz/rawDataReader/script/__pycache__/Aurora.cpython-312.pyc +0 -0
  154. AeroViz/rawDataReader/script/__pycache__/BAM1020.cpython-312.pyc +0 -0
  155. AeroViz/rawDataReader/script/__pycache__/BC1054.cpython-312.pyc +0 -0
  156. AeroViz/rawDataReader/script/__pycache__/EPA.cpython-312.pyc +0 -0
  157. AeroViz/rawDataReader/script/__pycache__/GRIMM.cpython-312.pyc +0 -0
  158. AeroViz/rawDataReader/script/__pycache__/IGAC.cpython-312.pyc +0 -0
  159. AeroViz/rawDataReader/script/__pycache__/MA350.cpython-312.pyc +0 -0
  160. AeroViz/rawDataReader/script/__pycache__/Minion.cpython-312.pyc +0 -0
  161. AeroViz/rawDataReader/script/__pycache__/NEPH.cpython-312.pyc +0 -0
  162. AeroViz/rawDataReader/script/__pycache__/OCEC.cpython-312.pyc +0 -0
  163. AeroViz/rawDataReader/script/__pycache__/Q-ACSM.cpython-312.pyc +0 -0
  164. AeroViz/rawDataReader/script/__pycache__/SMPS.cpython-312.pyc +0 -0
  165. AeroViz/rawDataReader/script/__pycache__/TEOM.cpython-312.pyc +0 -0
  166. AeroViz/rawDataReader/script/__pycache__/VOC.cpython-312.pyc +0 -0
  167. AeroViz/rawDataReader/script/__pycache__/Xact.cpython-312.pyc +0 -0
  168. AeroViz/rawDataReader/script/__pycache__/__init__.cpython-312.pyc +0 -0
  169. AeroViz/tools/__init__.py +2 -0
  170. AeroViz/tools/__pycache__/__init__.cpython-312.pyc +0 -0
  171. AeroViz/tools/__pycache__/database.cpython-312.pyc +0 -0
  172. AeroViz/tools/__pycache__/dataclassifier.cpython-312.pyc +0 -0
  173. AeroViz/tools/database.py +95 -0
  174. AeroViz/tools/dataclassifier.py +117 -0
  175. AeroViz/tools/dataprinter.py +58 -0
  176. aeroviz-0.1.21.dist-info/METADATA +294 -0
  177. aeroviz-0.1.21.dist-info/RECORD +180 -0
  178. aeroviz-0.1.21.dist-info/WHEEL +5 -0
  179. aeroviz-0.1.21.dist-info/licenses/LICENSE +21 -0
  180. aeroviz-0.1.21.dist-info/top_level.txt +1 -0
@@ -0,0 +1,518 @@
1
+ """
2
+ Derived optical and atmospheric parameters.
3
+
4
+ This module provides functions for calculating various derived parameters
5
+ from optical and chemical measurements.
6
+
7
+ Available Functions
8
+ -------------------
9
+ - derived_parameters: Calculate multiple derived parameters at once
10
+ - calculate_visibility: Calculate visibility from extinction
11
+ - calculate_MAC: Mass Absorption Cross-section
12
+ - calculate_Ox: Total oxidant (NO2 + O3)
13
+ - calculate_fRH: Hygroscopic growth factor for extinction
14
+ """
15
+
16
+ import numpy as np
17
+ from pandas import DataFrame, concat
18
+
19
+ from AeroViz.dataProcess.core import validate_inputs
20
+
21
+ __all__ = ['derived_parameters', 'calculate_visibility', 'calculate_MAC',
22
+ 'calculate_Ox', 'calculate_fRH', 'get_required_columns',
23
+ 'calculate_BrC_absorption']
24
+
25
+ # Column descriptions for validation
26
+ COLUMN_DESCRIPTIONS = {
27
+ 'Scattering': 'Scattering coefficient 散射係數 (Mm-1)',
28
+ 'Absorption': 'Absorption coefficient 吸收係數 (Mm-1)',
29
+ 'Extinction': 'Extinction coefficient 消光係數 (Mm-1)',
30
+ 'NO2': 'Nitrogen dioxide 二氧化氮 (ppb)',
31
+ 'O3': 'Ozone 臭氧 (ppb)',
32
+ 'EC': 'Elemental carbon 元素碳 (ug/m3)',
33
+ 'OC': 'Organic carbon 有機碳 (ug/m3)',
34
+ 'PM1': 'PM1 (ug/m3)',
35
+ 'PM2.5': 'PM2.5 (ug/m3)',
36
+ }
37
+
38
+
39
+ def derived_parameters(df_sca=None,
40
+ df_abs=None,
41
+ df_ext=None,
42
+ df_no2=None,
43
+ df_o3=None,
44
+ df_ec=None,
45
+ df_oc=None,
46
+ df_pm1=None,
47
+ df_pm25=None,
48
+ df_improve=None
49
+ ) -> DataFrame:
50
+ """
51
+ Calculate various derived atmospheric parameters.
52
+
53
+ Parameters
54
+ ----------
55
+ df_sca : DataFrame, optional
56
+ Scattering coefficient data (Mm-1).
57
+ df_abs : DataFrame, optional
58
+ Absorption coefficient data (Mm-1).
59
+ df_ext : DataFrame, optional
60
+ Extinction coefficient data (Mm-1).
61
+ df_no2 : DataFrame, optional
62
+ NO2 concentration (ppb).
63
+ df_o3 : DataFrame, optional
64
+ O3 concentration (ppb).
65
+ df_ec : DataFrame, optional
66
+ Elemental carbon concentration (ug/m3).
67
+ df_oc : DataFrame, optional
68
+ Organic carbon concentration (ug/m3).
69
+ df_pm1 : DataFrame, optional
70
+ PM1 concentration (ug/m3).
71
+ df_pm25 : DataFrame, optional
72
+ PM2.5 concentration (ug/m3).
73
+ df_improve : DataFrame, optional
74
+ IMPROVE extinction data with 'total_ext' and 'total_ext_dry' columns.
75
+
76
+ Returns
77
+ -------
78
+ DataFrame
79
+ Derived parameters including:
80
+ - PG: Total extinction (particle + gas)
81
+ - MAC: Mass Absorption Cross-section
82
+ - Ox: Oxidant (NO2 + O3)
83
+ - N2O5_tracer: NO2 * O3 indicator
84
+ - Vis_cal: Calculated visibility (km)
85
+ - fRH_IMPR: Hygroscopic growth factor from IMPROVE
86
+ - OCEC_ratio: OC/EC ratio
87
+ - PM1_PM25_ratio: PM1/PM2.5 ratio
88
+ """
89
+ # Combine all inputs to get common index
90
+ all_dfs = [df for df in [df_sca, df_abs, df_ext, df_no2, df_o3, df_ec,
91
+ df_oc, df_pm1, df_pm25, df_improve] if df is not None]
92
+
93
+ if not all_dfs:
94
+ return DataFrame()
95
+
96
+ common_index = concat(all_dfs, axis=1).index
97
+ result = DataFrame(index=common_index)
98
+
99
+ # Total extinction (particle + gas)
100
+ if df_sca is not None and df_abs is not None:
101
+ result['Bsp'] = df_sca.iloc[:, 0] if isinstance(df_sca, DataFrame) else df_sca
102
+ result['Bap'] = df_abs.iloc[:, 0] if isinstance(df_abs, DataFrame) else df_abs
103
+ result['PG'] = result['Bsp'] + result['Bap']
104
+
105
+ # Mass Absorption Cross-section (MAC)
106
+ if df_abs is not None and df_ec is not None:
107
+ abs_val = df_abs.iloc[:, 0] if isinstance(df_abs, DataFrame) else df_abs
108
+ ec_val = df_ec.iloc[:, 0] if isinstance(df_ec, DataFrame) else df_ec
109
+ result['MAC'] = abs_val / ec_val
110
+
111
+ # Oxidant (Ox = NO2 + O3)
112
+ if df_no2 is not None and df_o3 is not None:
113
+ no2_val = df_no2.iloc[:, 0] if isinstance(df_no2, DataFrame) else df_no2
114
+ o3_val = df_o3.iloc[:, 0] if isinstance(df_o3, DataFrame) else df_o3
115
+ result['Ox'] = no2_val + o3_val
116
+
117
+ # N2O5 tracer (NO2 * O3)
118
+ result['N2O5_tracer'] = no2_val * o3_val
119
+
120
+ # Visibility calculation
121
+ if df_ext is not None:
122
+ ext_val = df_ext.iloc[:, 0] if isinstance(df_ext, DataFrame) else df_ext
123
+ result['Vis_cal'] = 1096 / ext_val # Koschmieder equation, visibility in km
124
+
125
+ # fRH from IMPROVE
126
+ if df_improve is not None and 'total_ext' in df_improve.columns and 'total_ext_dry' in df_improve.columns:
127
+ result['fRH_IMPR'] = df_improve['total_ext'] / df_improve['total_ext_dry']
128
+
129
+ # OC/EC ratio
130
+ if df_oc is not None and df_ec is not None:
131
+ oc_val = df_oc.iloc[:, 0] if isinstance(df_oc, DataFrame) else df_oc
132
+ ec_val = df_ec.iloc[:, 0] if isinstance(df_ec, DataFrame) else df_ec
133
+ result['OCEC_ratio'] = oc_val / ec_val
134
+
135
+ # PM1/PM2.5 ratio
136
+ if df_pm1 is not None and df_pm25 is not None:
137
+ pm1_val = df_pm1.iloc[:, 0] if isinstance(df_pm1, DataFrame) else df_pm1
138
+ pm25_val = df_pm25.iloc[:, 0] if isinstance(df_pm25, DataFrame) else df_pm25
139
+ ratio = pm1_val / pm25_val
140
+ result['PM1_PM25_ratio'] = np.where(ratio < 1, ratio, np.nan)
141
+
142
+ return result
143
+
144
+
145
+ def calculate_visibility(df_ext: DataFrame) -> DataFrame:
146
+ """
147
+ Calculate visibility from extinction coefficient.
148
+
149
+ Uses the Koschmieder equation: Visibility = 3.912 / Bext
150
+ For Bext in Mm-1, Visibility in km: Visibility = 1096 / Bext
151
+
152
+ Parameters
153
+ ----------
154
+ df_ext : DataFrame
155
+ Extinction coefficient data (Mm-1). Any column name accepted.
156
+
157
+ Returns
158
+ -------
159
+ DataFrame
160
+ Visibility in kilometers.
161
+
162
+ Raises
163
+ ------
164
+ ValueError
165
+ If df_ext is None or empty.
166
+ """
167
+ if df_ext is None or df_ext.empty:
168
+ raise ValueError(
169
+ "\ncalculate_visibility() 需要消光係數資料!\n"
170
+ " 必要輸入: df_ext (Extinction coefficient, Mm-1)"
171
+ )
172
+
173
+ result = DataFrame(index=df_ext.index)
174
+ ext_val = df_ext.iloc[:, 0] if isinstance(df_ext, DataFrame) else df_ext
175
+ result['Visibility'] = 1096 / ext_val
176
+ return result
177
+
178
+
179
+ def calculate_MAC(df_abs: DataFrame, df_ec: DataFrame) -> DataFrame:
180
+ """
181
+ Calculate Mass Absorption Cross-section (MAC).
182
+
183
+ MAC = Babs / EC_mass
184
+
185
+ Parameters
186
+ ----------
187
+ df_abs : DataFrame
188
+ Absorption coefficient data (Mm-1). Any column name accepted.
189
+ df_ec : DataFrame
190
+ Elemental carbon concentration (ug/m3). Any column name accepted.
191
+
192
+ Returns
193
+ -------
194
+ DataFrame
195
+ MAC values (m2/g).
196
+
197
+ Raises
198
+ ------
199
+ ValueError
200
+ If df_abs or df_ec is None or empty.
201
+ """
202
+ if df_abs is None or (hasattr(df_abs, 'empty') and df_abs.empty):
203
+ raise ValueError(
204
+ "\ncalculate_MAC() 需要吸收係數資料!\n"
205
+ " 必要輸入: df_abs (Absorption coefficient, Mm-1)"
206
+ )
207
+ if df_ec is None or (hasattr(df_ec, 'empty') and df_ec.empty):
208
+ raise ValueError(
209
+ "\ncalculate_MAC() 需要元素碳資料!\n"
210
+ " 必要輸入: df_ec (Elemental carbon, ug/m3)"
211
+ )
212
+
213
+ result = DataFrame(index=df_abs.index)
214
+ abs_val = df_abs.iloc[:, 0] if isinstance(df_abs, DataFrame) else df_abs
215
+ ec_val = df_ec.iloc[:, 0] if isinstance(df_ec, DataFrame) else df_ec
216
+ result['MAC'] = abs_val / ec_val
217
+ return result
218
+
219
+
220
+ def calculate_Ox(df_no2: DataFrame, df_o3: DataFrame) -> DataFrame:
221
+ """
222
+ Calculate total oxidant (Ox = NO2 + O3).
223
+
224
+ Parameters
225
+ ----------
226
+ df_no2 : DataFrame
227
+ NO2 concentration (ppb). Any column name accepted.
228
+ df_o3 : DataFrame
229
+ O3 concentration (ppb). Any column name accepted.
230
+
231
+ Returns
232
+ -------
233
+ DataFrame
234
+ Ox values (ppb).
235
+
236
+ Raises
237
+ ------
238
+ ValueError
239
+ If df_no2 or df_o3 is None or empty.
240
+ """
241
+ if df_no2 is None or (hasattr(df_no2, 'empty') and df_no2.empty):
242
+ raise ValueError(
243
+ "\ncalculate_Ox() 需要 NO2 資料!\n"
244
+ " 必要輸入: df_no2 (NO2 concentration, ppb)"
245
+ )
246
+ if df_o3 is None or (hasattr(df_o3, 'empty') and df_o3.empty):
247
+ raise ValueError(
248
+ "\ncalculate_Ox() 需要 O3 資料!\n"
249
+ " 必要輸入: df_o3 (O3 concentration, ppb)"
250
+ )
251
+
252
+ result = DataFrame(index=df_no2.index)
253
+ no2_val = df_no2.iloc[:, 0] if isinstance(df_no2, DataFrame) else df_no2
254
+ o3_val = df_o3.iloc[:, 0] if isinstance(df_o3, DataFrame) else df_o3
255
+ result['Ox'] = no2_val + o3_val
256
+ return result
257
+
258
+
259
+ def calculate_fRH(df_ext_wet: DataFrame, df_ext_dry: DataFrame) -> DataFrame:
260
+ """
261
+ Calculate the hygroscopic growth factor for extinction (fRH).
262
+
263
+ fRH = Bext(wet) / Bext(dry)
264
+
265
+ Parameters
266
+ ----------
267
+ df_ext_wet : DataFrame
268
+ Wet extinction coefficient. Any column name accepted.
269
+ df_ext_dry : DataFrame
270
+ Dry extinction coefficient. Any column name accepted.
271
+
272
+ Returns
273
+ -------
274
+ DataFrame
275
+ fRH values.
276
+
277
+ Raises
278
+ ------
279
+ ValueError
280
+ If df_ext_wet or df_ext_dry is None or empty.
281
+ """
282
+ if df_ext_wet is None or (hasattr(df_ext_wet, 'empty') and df_ext_wet.empty):
283
+ raise ValueError(
284
+ "\ncalculate_fRH() 需要濕消光係數資料!\n"
285
+ " 必要輸入: df_ext_wet (Wet extinction coefficient)"
286
+ )
287
+ if df_ext_dry is None or (hasattr(df_ext_dry, 'empty') and df_ext_dry.empty):
288
+ raise ValueError(
289
+ "\ncalculate_fRH() 需要乾消光係數資料!\n"
290
+ " 必要輸入: df_ext_dry (Dry extinction coefficient)"
291
+ )
292
+
293
+ result = DataFrame(index=df_ext_wet.index)
294
+ wet_val = df_ext_wet.iloc[:, 0] if isinstance(df_ext_wet, DataFrame) else df_ext_wet
295
+ dry_val = df_ext_dry.iloc[:, 0] if isinstance(df_ext_dry, DataFrame) else df_ext_dry
296
+ result['fRH'] = wet_val / dry_val
297
+ return result
298
+
299
+
300
+ def get_required_columns():
301
+ """
302
+ Get required inputs for derived parameter functions.
303
+
304
+ Returns
305
+ -------
306
+ dict
307
+ Dictionary with function names as keys and required inputs as values.
308
+
309
+ Examples
310
+ --------
311
+ >>> cols = get_required_columns()
312
+ >>> print(cols['calculate_MAC'])
313
+ """
314
+ return {
315
+ 'derived_parameters': {
316
+ 'description': '所有輸入皆為可選,根據提供的資料計算相應的衍生參數',
317
+ 'PG': 'df_sca + df_abs',
318
+ 'MAC': 'df_abs + df_ec',
319
+ 'Ox': 'df_no2 + df_o3',
320
+ 'Vis_cal': 'df_ext',
321
+ 'fRH_IMPR': "df_improve['total_ext', 'total_ext_dry']",
322
+ 'OCEC_ratio': 'df_oc + df_ec',
323
+ 'PM1_PM25_ratio': 'df_pm1 + df_pm25'
324
+ },
325
+ 'calculate_visibility': ['Extinction coefficient (any column)'],
326
+ 'calculate_MAC': ['Absorption coefficient (any column)', 'EC (any column)'],
327
+ 'calculate_Ox': ['NO2 (any column)', 'O3 (any column)'],
328
+ 'calculate_fRH': ['Wet extinction (any column)', 'Dry extinction (any column)'],
329
+ 'calculate_BrC_absorption': ['Absorption coefficients at multiple wavelengths (DataFrame with abs_370, abs_470, etc.)']
330
+ }
331
+
332
+
333
+ def calculate_BrC_absorption(df_abs: DataFrame,
334
+ wavelengths: list[int] = None,
335
+ ref_wavelength: int = 880,
336
+ aae_bc: float = 1.0) -> DataFrame:
337
+ """
338
+ Calculate Brown Carbon (BrC) absorption by separating BC and BrC contributions.
339
+
340
+ This method assumes:
341
+ 1. Black Carbon (BC) has a wavelength-independent AAE (default: 1.0)
342
+ 2. Absorption at the reference wavelength (880nm) is entirely from BC
343
+ 3. BrC absorption = Total absorption - BC absorption
344
+
345
+ The BC absorption at any wavelength λ is calculated as:
346
+ abs_BC(λ) = abs_ref * (ref_wavelength / λ)^AAE_BC
347
+
348
+ Parameters
349
+ ----------
350
+ df_abs : DataFrame
351
+ Absorption coefficient data with columns like 'abs_370', 'abs_470', etc.
352
+ Units should be Mm-1.
353
+ wavelengths : list[int], optional
354
+ List of wavelengths to calculate BrC absorption for.
355
+ Default: [370, 470, 520, 590, 660] (all wavelengths shorter than reference)
356
+ ref_wavelength : int, default=880
357
+ Reference wavelength (nm) where absorption is assumed to be purely BC.
358
+ aae_bc : float, default=1.0
359
+ Absorption Ångström Exponent for Black Carbon.
360
+ Literature values typically range from 0.8 to 1.1 for fresh BC.
361
+
362
+ Returns
363
+ -------
364
+ DataFrame
365
+ DataFrame with columns:
366
+ - abs_BC_{wl}: BC absorption at each wavelength (Mm-1)
367
+ - abs_BrC_{wl}: BrC absorption at each wavelength (Mm-1, NaN if invalid)
368
+ - BrC_fraction_{wl}: BrC contribution fraction (0-1, NaN if invalid)
369
+ - AAE_BrC: Absorption Ångström Exponent of BrC (NaN if invalid)
370
+
371
+ Notes
372
+ -----
373
+ This separation method is based on the assumption that BC has a constant AAE
374
+ of approximately 1.0 across all wavelengths, while BrC exhibits stronger
375
+ absorption at shorter wavelengths (higher AAE).
376
+
377
+ The AAE_BC = 1.0 assumption comes from Mie theory calculations for pure
378
+ graphitic carbon particles. However, this value can vary depending on
379
+ particle size and mixing state.
380
+
381
+ **Validity check**: If calculated BC absorption exceeds total absorption
382
+ at ANY wavelength, the entire row is marked as invalid (NaN for all BrC values).
383
+ This indicates the separation assumption is not valid for that data point.
384
+
385
+ References
386
+ ----------
387
+ - Lack, D.A. and Langridge, J.M. (2013). Atmos. Chem. Phys., 13, 8321-8341.
388
+ - Kirchstetter, T.W. et al. (2004). J. Geophys. Res., 109, D21208.
389
+
390
+ Examples
391
+ --------
392
+ >>> from AeroViz import DataProcess
393
+ >>> optical = DataProcess(method='Optical')
394
+ >>> brc_result = optical.BrC(df_abs, aae_bc=1.0)
395
+ """
396
+ if df_abs is None or df_abs.empty:
397
+ raise ValueError(
398
+ "\ncalculate_BrC_absorption() 需要多波長吸收係數資料!\n"
399
+ " 必要輸入: df_abs (含有 abs_370, abs_470, ... 等欄位的 DataFrame)"
400
+ )
401
+
402
+ # Default wavelengths for BrC calculation (shorter than reference)
403
+ if wavelengths is None:
404
+ wavelengths = [370, 470, 520, 590, 660]
405
+
406
+ # Find available absorption columns
407
+ abs_cols = [col for col in df_abs.columns if col.startswith('abs_')]
408
+ available_wl = []
409
+ for col in abs_cols:
410
+ try:
411
+ wl = int(col.split('_')[1])
412
+ available_wl.append(wl)
413
+ except (ValueError, IndexError):
414
+ continue
415
+
416
+ # Check if reference wavelength is available
417
+ ref_col = f'abs_{ref_wavelength}'
418
+ if ref_col not in df_abs.columns:
419
+ raise ValueError(
420
+ f"\n找不到參考波長 {ref_wavelength}nm 的吸收資料!\n"
421
+ f" 可用的波長: {sorted(available_wl)}\n"
422
+ f" 請確保 df_abs 包含 '{ref_col}' 欄位"
423
+ )
424
+
425
+ # Filter wavelengths to those available and shorter than reference
426
+ calc_wavelengths = [wl for wl in wavelengths
427
+ if wl in available_wl and wl < ref_wavelength]
428
+
429
+ if not calc_wavelengths:
430
+ raise ValueError(
431
+ f"\n沒有可用的短波長資料用於 BrC 計算!\n"
432
+ f" 請求的波長: {wavelengths}\n"
433
+ f" 可用的波長: {sorted(available_wl)}\n"
434
+ f" 參考波長: {ref_wavelength}nm"
435
+ )
436
+
437
+ result = DataFrame(index=df_abs.index)
438
+
439
+ # Reference absorption (assumed to be pure BC at 880nm)
440
+ abs_ref = df_abs[ref_col]
441
+
442
+ # Track if BC > total at any wavelength (invalid separation)
443
+ bc_exceeds_total = np.zeros(len(df_abs), dtype=bool)
444
+
445
+ # Calculate BC and BrC absorption at each wavelength
446
+ brc_abs_data = {}
447
+ bc_abs_data = {}
448
+
449
+ for wl in calc_wavelengths:
450
+ abs_col = f'abs_{wl}'
451
+ abs_total = df_abs[abs_col]
452
+
453
+ # BC absorption at this wavelength using power law
454
+ # abs_BC(λ) = abs_ref * (λ_ref / λ)^AAE_BC
455
+ abs_bc = abs_ref * (ref_wavelength / wl) ** aae_bc
456
+
457
+ # Check if BC exceeds total at this wavelength
458
+ bc_exceeds_total |= (abs_bc > abs_total).values
459
+
460
+ # BrC absorption = Total - BC
461
+ abs_brc = abs_total - abs_bc
462
+
463
+ # BrC fraction (before clipping)
464
+ brc_fraction = np.where(abs_total > 0, abs_brc / abs_total, np.nan)
465
+
466
+ # Store raw values
467
+ bc_abs_data[wl] = abs_bc
468
+ brc_abs_data[wl] = abs_brc.clip(lower=0) # Clip for AAE calculation
469
+
470
+ result[f'abs_BC_{wl}'] = abs_bc
471
+ result[f'abs_BrC_{wl}'] = abs_brc.clip(lower=0)
472
+ result[f'BrC_fraction_{wl}'] = np.where(brc_fraction >= 0, brc_fraction, np.nan)
473
+
474
+ # Calculate BrC AAE using linear regression on log-log scale
475
+ brc_wavelengths = np.array(sorted(brc_abs_data.keys()))
476
+
477
+ def calc_brc_aae(row_data):
478
+ """Calculate AAE for a single row of BrC absorption data."""
479
+ brc_values = np.array([row_data.get(wl, np.nan) for wl in brc_wavelengths])
480
+
481
+ # Need at least 2 valid points for AAE calculation
482
+ valid_mask = (brc_values > 0) & np.isfinite(brc_values)
483
+ if valid_mask.sum() < 2:
484
+ return np.nan
485
+
486
+ valid_wl = brc_wavelengths[valid_mask]
487
+ valid_brc = brc_values[valid_mask]
488
+
489
+ # Linear fit on log-log scale: log(abs) = -AAE * log(λ) + intercept
490
+ try:
491
+ log_wl = np.log(valid_wl)
492
+ log_brc = np.log(valid_brc)
493
+ slope, _ = np.polyfit(log_wl, log_brc, 1)
494
+ return -slope # AAE is negative of slope
495
+ except (np.linalg.LinAlgError, ValueError):
496
+ return np.nan
497
+
498
+ # Calculate AAE_BrC for each row
499
+ aae_brc_values = []
500
+ for idx in df_abs.index:
501
+ row_data = {wl: brc_abs_data[wl].loc[idx] for wl in brc_wavelengths}
502
+ aae_brc_values.append(calc_brc_aae(row_data))
503
+
504
+ aae_raw = np.array(aae_brc_values)
505
+
506
+ # Validity check: BC must not exceed total absorption at any wavelength
507
+ # If BC > total at any wavelength, the entire row is invalid
508
+ valid_separation = ~bc_exceeds_total
509
+
510
+ # Set all values to NaN for invalid rows (including BC)
511
+ for wl in calc_wavelengths:
512
+ result.loc[~valid_separation, f'abs_BC_{wl}'] = np.nan
513
+ result.loc[~valid_separation, f'abs_BrC_{wl}'] = np.nan
514
+ result.loc[~valid_separation, f'BrC_fraction_{wl}'] = np.nan
515
+
516
+ result['AAE_BrC'] = np.where(valid_separation, aae_raw, np.nan)
517
+
518
+ return result
@@ -0,0 +1,123 @@
1
+ """
2
+ Basic extinction and optical property calculations.
3
+
4
+ Required Columns
5
+ ----------------
6
+ df_sca:
7
+ - sca_550 : Scattering coefficient at 550nm (Mm-1)
8
+ - SAE : Scattering Angstrom Exponent
9
+ df_abs:
10
+ - abs_550 : Absorption coefficient at 550nm (Mm-1)
11
+ - AAE : Absorption Angstrom Exponent
12
+ - eBC : Equivalent Black Carbon (ng/m3)
13
+ """
14
+
15
+ from pandas import DataFrame
16
+
17
+ from AeroViz.dataProcess.core import union_index, validate_inputs
18
+
19
+ # Required columns
20
+ REQUIRED_SCA_COLUMNS = ['sca_550', 'SAE']
21
+ REQUIRED_ABS_COLUMNS = ['abs_550', 'AAE', 'eBC']
22
+
23
+ COLUMN_DESCRIPTIONS = {
24
+ 'sca_550': 'Scattering coefficient at 550nm 散射係數 (Mm-1)',
25
+ 'SAE': 'Scattering Angstrom Exponent 散射埃指數',
26
+ 'abs_550': 'Absorption coefficient at 550nm 吸收係數 (Mm-1)',
27
+ 'AAE': 'Absorption Angstrom Exponent 吸收埃指數',
28
+ 'eBC': 'Equivalent Black Carbon 等效黑碳 (ng/m3)',
29
+ }
30
+
31
+
32
+ def _basic(df_sca, df_abs, df_mass=None, df_no2=None, df_temp=None):
33
+ """
34
+ Calculate basic optical properties and extinction.
35
+
36
+ Parameters
37
+ ----------
38
+ df_sca : DataFrame
39
+ Scattering data with columns: sca_550, SAE
40
+ df_abs : DataFrame
41
+ Absorption data with columns: abs_550, AAE, eBC
42
+ df_mass : DataFrame, optional
43
+ PM mass concentration (ug/m3) for MAE/MSE/MEE calculation
44
+ df_no2 : DataFrame, optional
45
+ NO2 concentration (ppb) for gas absorption
46
+ df_temp : DataFrame, optional
47
+ Temperature (Celsius) for Rayleigh scattering
48
+
49
+ Returns
50
+ -------
51
+ DataFrame
52
+ Optical properties: abs, sca, ext, SSA, SAE, AAE, eBC,
53
+ and optionally MAE, MSE, MEE, abs_gas, sca_gas, ext_all
54
+
55
+ Raises
56
+ ------
57
+ ValueError
58
+ If required columns are missing from df_sca or df_abs
59
+ """
60
+ # Validate required columns
61
+ validate_inputs(df_sca, REQUIRED_SCA_COLUMNS, '_basic (df_sca)', COLUMN_DESCRIPTIONS)
62
+ validate_inputs(df_abs, REQUIRED_ABS_COLUMNS, '_basic (df_abs)', COLUMN_DESCRIPTIONS)
63
+
64
+ df_sca, df_abs, df_mass, df_no2, df_temp = union_index(df_sca, df_abs, df_mass, df_no2, df_temp)
65
+
66
+ df_out = DataFrame()
67
+
68
+ # abs and sca coe
69
+ df_out['abs'] = df_abs['abs_550'].copy()
70
+ df_out['sca'] = df_sca['sca_550'].copy()
71
+
72
+ # extinction coe.
73
+ df_out['ext'] = df_out['abs'] + df_out['sca']
74
+
75
+ # SSA
76
+ df_out['SSA'] = df_out['sca'] / df_out['ext']
77
+
78
+ # SAE, AAE, eBC
79
+ df_out['SAE'] = df_sca['SAE'].copy()
80
+ df_out['AAE'] = df_abs['AAE'].copy()
81
+ df_out['eBC'] = df_abs['eBC'].copy() / 1e3
82
+
83
+ # MAE, MSE, MEE
84
+ if df_mass is not None:
85
+ df_out['MAE'] = df_out['abs'] / df_mass
86
+ df_out['MSE'] = df_out['sca'] / df_mass
87
+ df_out['MEE'] = df_out['MSE'] + df_out['MAE']
88
+
89
+ # gas absorbtion
90
+ if df_no2 is not None:
91
+ df_out['abs_gas'] = df_no2 * .33
92
+
93
+ if df_temp is not None:
94
+ df_out['sca_gas'] = (11.4 * 293 / (273 + df_temp))
95
+
96
+ if df_no2 is not None and df_temp is not None:
97
+ df_out['ext_all'] = df_out['ext'] + df_out['abs_gas'] + df_out['sca_gas']
98
+
99
+ return df_out
100
+
101
+
102
+ def get_required_columns():
103
+ """
104
+ Get required column names for basic extinction calculation.
105
+
106
+ Returns
107
+ -------
108
+ dict
109
+ Dictionary with input names as keys and required columns as values.
110
+
111
+ Examples
112
+ --------
113
+ >>> cols = get_required_columns()
114
+ >>> print(cols['df_sca'])
115
+ ['sca_550', 'SAE']
116
+ """
117
+ return {
118
+ 'df_sca': REQUIRED_SCA_COLUMNS.copy(),
119
+ 'df_abs': REQUIRED_ABS_COLUMNS.copy(),
120
+ 'df_mass': 'PM mass (any column) - optional',
121
+ 'df_no2': 'NO2 concentration (any column) - optional',
122
+ 'df_temp': 'Temperature in Celsius (any column) - optional'
123
+ }