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,452 @@
1
+ """
2
+ IMPROVE extinction reconstruction algorithms.
3
+
4
+ This module implements the IMPROVE (Interagency Monitoring of Protected
5
+ Visual Environments) equation for reconstructing aerosol light extinction
6
+ from chemical composition data.
7
+
8
+ Required Columns
9
+ ----------------
10
+ For revised/modified functions:
11
+ - AS : Ammonium Sulfate (ug/m3)
12
+ - AN : Ammonium Nitrate (ug/m3)
13
+ - OM : Organic Matter (ug/m3)
14
+ - Soil: Soil/Crustal (ug/m3)
15
+ - SS : Sea Salt (ug/m3)
16
+ - EC : Elemental Carbon (ug/m3)
17
+
18
+ References
19
+ ----------
20
+ Pitchford, M., et al. (2007). Revised Algorithm for Estimating Light
21
+ Extinction from IMPROVE Particle Speciation Data. JAPCA J. Air Waste Ma.
22
+ """
23
+
24
+ from pathlib import Path
25
+
26
+ import numpy as np
27
+ from pandas import DataFrame, read_pickle
28
+
29
+ from AeroViz.dataProcess.core import union_index, validate_inputs
30
+
31
+ # Required columns and descriptions
32
+ REQUIRED_MASS_COLUMNS = ['AS', 'AN', 'OM', 'Soil', 'SS', 'EC']
33
+
34
+ COLUMN_DESCRIPTIONS = {
35
+ 'AS': 'Ammonium Sulfate 硫酸銨 (ug/m3)',
36
+ 'AN': 'Ammonium Nitrate 硝酸銨 (ug/m3)',
37
+ 'OM': 'Organic Matter 有機物 (ug/m3)',
38
+ 'Soil': 'Soil/Crustal 土壤/地殼 (ug/m3)',
39
+ 'SS': 'Sea Salt 海鹽 (ug/m3)',
40
+ 'EC': 'Elemental Carbon 元素碳 (ug/m3)',
41
+ }
42
+
43
+ # Mass extinction efficiencies (m2/g) for reference
44
+ EXTINCTION_COEFFICIENTS = {
45
+ 'revised': {
46
+ 'small_mode': {'AS': 2.2, 'AN': 2.4, 'OM': 2.8},
47
+ 'large_mode': {'AS': 4.8, 'AN': 5.1, 'OM': 6.1},
48
+ 'other': {'Soil': 1.0, 'SS': 1.7, 'EC': 10.0}
49
+ },
50
+ 'modified': {
51
+ 'AS': 3.0, 'AN': 3.0, 'OM': 4.0,
52
+ 'Soil': 1.0, 'SS': 1.7, 'EC': 10.0
53
+ }
54
+ }
55
+
56
+ # Cache for fRH lookup table
57
+ _FRH_CACHE = None
58
+
59
+
60
+ def load_fRH():
61
+ """
62
+ Load the f(RH) lookup table from pickle file.
63
+
64
+ Returns
65
+ -------
66
+ DataFrame
67
+ f(RH) values indexed by relative humidity (0-95%).
68
+ """
69
+ global _FRH_CACHE
70
+ if _FRH_CACHE is None:
71
+ with (Path(__file__).parent / 'fRH.pkl').open('rb') as f:
72
+ _FRH_CACHE = read_pickle(f)
73
+ _FRH_CACHE.loc[np.nan] = np.nan
74
+ return _FRH_CACHE
75
+
76
+
77
+ def get_fRH_factors(rh_data, fRH_table):
78
+ """
79
+ Get hygroscopic growth factors for given RH values.
80
+
81
+ Parameters
82
+ ----------
83
+ rh_data : Series or None
84
+ Relative humidity data (%).
85
+ fRH_table : DataFrame
86
+ f(RH) lookup table.
87
+
88
+ Returns
89
+ -------
90
+ tuple
91
+ (f_rh, f_rh_ss, f_rh_small, f_rh_large) growth factors.
92
+ """
93
+ if rh_data is None:
94
+ return 1, 1, 1, 1
95
+
96
+ rh_clipped = rh_data.mask(rh_data > 95, 95).round(0)
97
+ return fRH_table.loc[rh_clipped].values.T
98
+
99
+
100
+ def split_size_modes(mass_data):
101
+ """
102
+ Split mass into small and large size modes.
103
+
104
+ For mass < 20 ug/m3:
105
+ large = mass^2 / 20
106
+ small = mass - large
107
+ For mass >= 20 ug/m3:
108
+ large = mass
109
+ small = 0
110
+
111
+ Parameters
112
+ ----------
113
+ mass_data : DataFrame
114
+ Mass concentrations with columns AS, AN, OM.
115
+
116
+ Returns
117
+ -------
118
+ tuple
119
+ (large_mode, small_mode) DataFrames.
120
+ """
121
+ mode_data = mass_data[['AS', 'AN', 'OM']].copy()
122
+
123
+ large_mode = mode_data.mask(mode_data < 20, mode_data ** 2 / 20)
124
+ small_mode = mode_data.values - large_mode
125
+
126
+ large_mode.columns = ['L_AS', 'L_AN', 'L_OM']
127
+ small_mode.columns = ['S_AS', 'S_AN', 'S_OM']
128
+
129
+ return large_mode, small_mode
130
+
131
+
132
+ def revised(df_mass, df_rh=None, df_nh4_status=None):
133
+ """
134
+ Calculate extinction using the revised IMPROVE equation.
135
+
136
+ The revised IMPROVE algorithm uses size-dependent mass extinction
137
+ efficiencies with separate coefficients for small and large modes.
138
+
139
+ Parameters
140
+ ----------
141
+ df_mass : DataFrame
142
+ Mass concentrations (ug/m3) with required columns:
143
+ - AS : Ammonium Sulfate 硫酸銨
144
+ - AN : Ammonium Nitrate 硝酸銨
145
+ - OM : Organic Matter 有機物
146
+ - Soil : Soil/Crustal 土壤
147
+ - SS : Sea Salt 海鹽
148
+ - EC : Elemental Carbon 元素碳
149
+ df_rh : DataFrame or None, optional
150
+ Relative humidity data (%). If None, only dry extinction is calculated.
151
+ df_nh4_status : DataFrame or None, optional
152
+ NH4 status from reconstruction_basic(). If provided, rows with
153
+ 'Deficiency' status will be excluded from calculation.
154
+ Must have 'status' column with 'Enough' or 'Deficiency' values.
155
+
156
+ Returns
157
+ -------
158
+ dict
159
+ Dictionary with keys:
160
+ - 'dry': Dry extinction DataFrame (AS, AN, OM, Soil, SS, EC, total)
161
+ - 'wet': Wet extinction DataFrame (if df_rh provided)
162
+ - 'ALWC': Water contribution to extinction (wet - dry) for AS, AN, SS, total
163
+ - 'fRH': Hygroscopic growth factor (wet_total / dry_total)
164
+
165
+ Raises
166
+ ------
167
+ ValueError
168
+ If required columns are missing from df_mass.
169
+
170
+ Notes
171
+ -----
172
+ Mass extinction efficiencies (m2/g):
173
+ - Small mode: AS=2.2, AN=2.4, OM=2.8
174
+ - Large mode: AS=4.8, AN=5.1, OM=6.1
175
+ - Soil=1.0, SS=1.7, EC=10.0
176
+
177
+ Examples
178
+ --------
179
+ >>> # Basic usage
180
+ >>> result = revised(df_mass, df_rh)
181
+ >>>
182
+ >>> # With NH4 status filtering (exclude deficient samples)
183
+ >>> chem_result = reconstruction_basic(...)
184
+ >>> result = revised(df_mass, df_rh, df_nh4_status=chem_result['NH4_status'])
185
+ """
186
+ # Validate input columns
187
+ validate_inputs(df_mass, REQUIRED_MASS_COLUMNS, 'revised', COLUMN_DESCRIPTIONS)
188
+
189
+ # Store original index for reindexing at the end
190
+ original_index = df_mass.index.copy()
191
+
192
+ # Filter out NH4 deficient samples if status provided
193
+ if df_nh4_status is not None:
194
+ if 'status' not in df_nh4_status.columns:
195
+ raise ValueError(
196
+ "\ndf_nh4_status 需要 'status' 欄位!\n"
197
+ " 應使用 reconstruction_basic() 的 'NH4_status' 輸出"
198
+ )
199
+ enough_mask = df_nh4_status['status'] == 'Enough'
200
+ df_mass = df_mass.loc[enough_mask].copy()
201
+ if df_rh is not None:
202
+ df_rh = df_rh.loc[enough_mask].copy()
203
+
204
+ df_mass, df_rh = union_index(df_mass, df_rh)
205
+ fRH_table = load_fRH()
206
+
207
+ # Split into size modes
208
+ large_mode, small_mode = split_size_modes(df_mass)
209
+ df_mass = df_mass.join(large_mode).join(small_mode)
210
+
211
+ def calculate_extinction(rh_data=None):
212
+ f_rh, f_rh_ss, f_rh_small, f_rh_large = get_fRH_factors(rh_data, fRH_table)
213
+
214
+ ext = DataFrame(index=df_mass.index)
215
+
216
+ # Revised IMPROVE coefficients with size-dependent modes
217
+ ext['AS'] = 2.2 * f_rh_small * df_mass['S_AS'] + 4.8 * f_rh_large * df_mass['L_AS']
218
+ ext['AN'] = 2.4 * f_rh_small * df_mass['S_AN'] + 5.1 * f_rh_large * df_mass['L_AN']
219
+ ext['OM'] = 2.8 * df_mass['S_OM'] + 6.1 * df_mass['L_OM']
220
+ ext['Soil'] = 1.0 * df_mass['Soil']
221
+ ext['SS'] = 1.7 * f_rh_ss * df_mass['SS']
222
+ ext['EC'] = 10.0 * df_mass['EC']
223
+
224
+ ext['total'] = ext.sum(axis=1)
225
+
226
+ return ext.dropna()
227
+
228
+ result = {'dry': calculate_extinction()}
229
+
230
+ if df_rh is not None:
231
+ result['wet'] = calculate_extinction(df_rh)
232
+
233
+ # Calculate ALWC contribution (wet - dry)
234
+ alwc = DataFrame(index=result['dry'].index)
235
+ alwc['AS'] = result['wet']['AS'] - result['dry']['AS']
236
+ alwc['AN'] = result['wet']['AN'] - result['dry']['AN']
237
+ alwc['SS'] = result['wet']['SS'] - result['dry']['SS']
238
+ alwc['total'] = result['wet']['total'] - result['dry']['total']
239
+ result['ALWC'] = alwc
240
+
241
+ # Calculate fRH (hygroscopic growth factor)
242
+ result['fRH'] = result['wet']['total'] / result['dry']['total']
243
+
244
+ # Reindex to original index (NH4 deficient rows will be NaN)
245
+ for key in result:
246
+ if isinstance(result[key], DataFrame):
247
+ result[key] = result[key].reindex(original_index)
248
+ else:
249
+ result[key] = result[key].reindex(original_index)
250
+
251
+ return result
252
+
253
+
254
+ def modified(df_mass, df_rh=None, df_nh4_status=None):
255
+ """
256
+ Calculate extinction using the modified IMPROVE equation.
257
+
258
+ The modified version uses simpler coefficients without
259
+ size-dependent modes.
260
+
261
+ Parameters
262
+ ----------
263
+ df_mass : DataFrame
264
+ Mass concentrations (ug/m3) with required columns:
265
+ - AS : Ammonium Sulfate 硫酸銨
266
+ - AN : Ammonium Nitrate 硝酸銨
267
+ - OM : Organic Matter 有機物
268
+ - Soil : Soil/Crustal 土壤
269
+ - SS : Sea Salt 海鹽
270
+ - EC : Elemental Carbon 元素碳
271
+ df_rh : DataFrame or None, optional
272
+ Relative humidity data (%).
273
+ df_nh4_status : DataFrame or None, optional
274
+ NH4 status from reconstruction_basic(). If provided, rows with
275
+ 'Deficiency' status will be excluded from calculation.
276
+ Must have 'status' column with 'Enough' or 'Deficiency' values.
277
+
278
+ Returns
279
+ -------
280
+ dict
281
+ Dictionary with keys:
282
+ - 'dry': Dry extinction DataFrame (AS, AN, OM, Soil, SS, EC, total)
283
+ - 'wet': Wet extinction DataFrame (if df_rh provided)
284
+ - 'ALWC': Water contribution to extinction (wet - dry) for AS, AN, SS, total
285
+ - 'fRH': Hygroscopic growth factor (wet_total / dry_total)
286
+
287
+ Raises
288
+ ------
289
+ ValueError
290
+ If required columns are missing from df_mass.
291
+
292
+ Notes
293
+ -----
294
+ Mass extinction efficiencies (m2/g):
295
+ - AS=3.0, AN=3.0, OM=4.0
296
+ - Soil=1.0, SS=1.7, EC=10.0
297
+
298
+ Examples
299
+ --------
300
+ >>> # With NH4 status filtering (exclude deficient samples)
301
+ >>> chem_result = reconstruction_basic(...)
302
+ >>> result = modified(df_mass, df_rh, df_nh4_status=chem_result['NH4_status'])
303
+ """
304
+ # Validate input columns
305
+ validate_inputs(df_mass, REQUIRED_MASS_COLUMNS, 'modified', COLUMN_DESCRIPTIONS)
306
+
307
+ # Store original index for reindexing at the end
308
+ original_index = df_mass.index.copy()
309
+
310
+ # Filter out NH4 deficient samples if status provided
311
+ if df_nh4_status is not None:
312
+ if 'status' not in df_nh4_status.columns:
313
+ raise ValueError(
314
+ "\ndf_nh4_status 需要 'status' 欄位!\n"
315
+ " 應使用 reconstruction_basic() 的 'NH4_status' 輸出"
316
+ )
317
+ enough_mask = df_nh4_status['status'] == 'Enough'
318
+ df_mass = df_mass.loc[enough_mask].copy()
319
+ if df_rh is not None:
320
+ df_rh = df_rh.loc[enough_mask].copy()
321
+
322
+ df_mass, df_rh = union_index(df_mass, df_rh)
323
+ fRH_table = load_fRH()
324
+
325
+ def calculate_extinction(rh_data=None):
326
+ f_rh, f_rh_ss, f_rh_small, f_rh_large = get_fRH_factors(rh_data, fRH_table)
327
+
328
+ ext = DataFrame(index=df_mass.index)
329
+
330
+ # Modified IMPROVE coefficients (simpler version)
331
+ ext['AS'] = 3.0 * f_rh * df_mass['AS']
332
+ ext['AN'] = 3.0 * f_rh * df_mass['AN']
333
+ ext['OM'] = 4.0 * df_mass['OM']
334
+ ext['Soil'] = 1.0 * df_mass['Soil']
335
+ ext['SS'] = 1.7 * f_rh_ss * df_mass['SS']
336
+ ext['EC'] = 10.0 * df_mass['EC']
337
+
338
+ ext['total'] = ext.sum(axis=1)
339
+
340
+ return ext.dropna()
341
+
342
+ result = {'dry': calculate_extinction()}
343
+
344
+ if df_rh is not None:
345
+ result['wet'] = calculate_extinction(df_rh)
346
+
347
+ # Calculate ALWC contribution (wet - dry)
348
+ alwc = DataFrame(index=result['dry'].index)
349
+ alwc['AS'] = result['wet']['AS'] - result['dry']['AS']
350
+ alwc['AN'] = result['wet']['AN'] - result['dry']['AN']
351
+ alwc['SS'] = result['wet']['SS'] - result['dry']['SS']
352
+ alwc['total'] = result['wet']['total'] - result['dry']['total']
353
+ result['ALWC'] = alwc
354
+
355
+ # Calculate fRH (hygroscopic growth factor)
356
+ result['fRH'] = result['wet']['total'] / result['dry']['total']
357
+
358
+ # Reindex to original index (NH4 deficient rows will be NaN)
359
+ for key in result:
360
+ if isinstance(result[key], DataFrame):
361
+ result[key] = result[key].reindex(original_index)
362
+ else:
363
+ result[key] = result[key].reindex(original_index)
364
+
365
+ return result
366
+
367
+
368
+ def gas_extinction(df_no2, df_temp):
369
+ """
370
+ Calculate gas contribution to atmospheric extinction.
371
+
372
+ Parameters
373
+ ----------
374
+ df_no2 : DataFrame
375
+ NO2 concentration (ppb). Any column name accepted.
376
+ df_temp : DataFrame
377
+ Ambient temperature (Celsius). Any column name accepted.
378
+
379
+ Returns
380
+ -------
381
+ DataFrame
382
+ Gas extinction contributions (Mm-1) with columns:
383
+ - ScatteringByGas: Rayleigh scattering by air molecules
384
+ - AbsorptionByGas: Absorption by NO2
385
+ - ExtinctionByGas: Total gas extinction
386
+
387
+ Notes
388
+ -----
389
+ Rayleigh scattering coefficient: 11.4 Mm-1 at 293K
390
+ NO2 absorption cross-section: 0.33 Mm-1/ppb at 550nm
391
+
392
+ Examples
393
+ --------
394
+ >>> df_no2 = pd.DataFrame({'NO2': [20.0, 30.0]})
395
+ >>> df_temp = pd.DataFrame({'temp': [25.0, 28.0]})
396
+ >>> result = gas_extinction(df_no2, df_temp)
397
+ """
398
+ if df_no2 is None or df_no2.empty:
399
+ raise ValueError("gas_extinction() 需要 NO2 濃度資料 (ppb)")
400
+ if df_temp is None or df_temp.empty:
401
+ raise ValueError("gas_extinction() 需要溫度資料 (Celsius)")
402
+
403
+ df_no2, df_temp = union_index(df_no2, df_temp)
404
+
405
+ result = DataFrame(index=df_no2.index)
406
+
407
+ # Rayleigh scattering (temperature-dependent)
408
+ temp_kelvin = 273 + df_temp.iloc[:, 0]
409
+ result['ScatteringByGas'] = 11.4 * 293 / temp_kelvin
410
+
411
+ # NO2 absorption
412
+ result['AbsorptionByGas'] = 0.33 * df_no2.iloc[:, 0]
413
+
414
+ # Total gas extinction
415
+ result['ExtinctionByGas'] = result['ScatteringByGas'] + result['AbsorptionByGas']
416
+
417
+ return result
418
+
419
+
420
+ def get_required_columns():
421
+ """
422
+ Get the required column names for IMPROVE calculations.
423
+
424
+ Returns
425
+ -------
426
+ dict
427
+ Dictionary with function names as keys and required columns as values.
428
+
429
+ Examples
430
+ --------
431
+ >>> cols = get_required_columns()
432
+ >>> print(cols['revised'])
433
+ """
434
+ return {
435
+ 'revised': {
436
+ 'df_mass': REQUIRED_MASS_COLUMNS.copy(),
437
+ 'df_rh': 'Relative humidity (%) - optional',
438
+ 'df_nh4_status': "NH4 status from reconstruction_basic()['NH4_status'] - optional",
439
+ 'outputs': ['dry', 'wet', 'ALWC', 'fRH']
440
+ },
441
+ 'modified': {
442
+ 'df_mass': REQUIRED_MASS_COLUMNS.copy(),
443
+ 'df_rh': 'Relative humidity (%) - optional',
444
+ 'df_nh4_status': "NH4 status from reconstruction_basic()['NH4_status'] - optional",
445
+ 'outputs': ['dry', 'wet', 'ALWC', 'fRH']
446
+ },
447
+ 'gas_extinction': {
448
+ 'df_no2': 'NO2 concentration (ppb)',
449
+ 'df_temp': 'Temperature (Celsius)',
450
+ 'outputs': ['ScatteringByGas', 'AbsorptionByGas', 'ExtinctionByGas']
451
+ }
452
+ }