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.
- AeroViz/__init__.py +13 -0
- AeroViz/__pycache__/__init__.cpython-312.pyc +0 -0
- AeroViz/data/DEFAULT_DATA.csv +1417 -0
- AeroViz/data/DEFAULT_PNSD_DATA.csv +1417 -0
- AeroViz/data/hysplit_example_data.txt +101 -0
- AeroViz/dataProcess/Chemistry/__init__.py +149 -0
- AeroViz/dataProcess/Chemistry/__pycache__/__init__.cpython-312.pyc +0 -0
- AeroViz/dataProcess/Chemistry/_calculate.py +557 -0
- AeroViz/dataProcess/Chemistry/_isoropia.py +150 -0
- AeroViz/dataProcess/Chemistry/_mass_volume.py +487 -0
- AeroViz/dataProcess/Chemistry/_ocec.py +172 -0
- AeroViz/dataProcess/Chemistry/isrpia.cnf +21 -0
- AeroViz/dataProcess/Chemistry/isrpia2.exe +0 -0
- AeroViz/dataProcess/Optical/PyMieScatt_update.py +577 -0
- AeroViz/dataProcess/Optical/_IMPROVE.py +452 -0
- AeroViz/dataProcess/Optical/__init__.py +281 -0
- AeroViz/dataProcess/Optical/__pycache__/PyMieScatt_update.cpython-312.pyc +0 -0
- AeroViz/dataProcess/Optical/__pycache__/__init__.cpython-312.pyc +0 -0
- AeroViz/dataProcess/Optical/__pycache__/mie_theory.cpython-312.pyc +0 -0
- AeroViz/dataProcess/Optical/_derived.py +518 -0
- AeroViz/dataProcess/Optical/_extinction.py +123 -0
- AeroViz/dataProcess/Optical/_mie_sd.py +912 -0
- AeroViz/dataProcess/Optical/_retrieve_RI.py +243 -0
- AeroViz/dataProcess/Optical/coefficient.py +72 -0
- AeroViz/dataProcess/Optical/fRH.pkl +0 -0
- AeroViz/dataProcess/Optical/mie_theory.py +260 -0
- AeroViz/dataProcess/README.md +271 -0
- AeroViz/dataProcess/SizeDistr/__init__.py +245 -0
- AeroViz/dataProcess/SizeDistr/__pycache__/__init__.cpython-312.pyc +0 -0
- AeroViz/dataProcess/SizeDistr/__pycache__/_size_dist.cpython-312.pyc +0 -0
- AeroViz/dataProcess/SizeDistr/_size_dist.py +810 -0
- AeroViz/dataProcess/SizeDistr/merge/README.md +93 -0
- AeroViz/dataProcess/SizeDistr/merge/__init__.py +20 -0
- AeroViz/dataProcess/SizeDistr/merge/_merge_v0.py +251 -0
- AeroViz/dataProcess/SizeDistr/merge/_merge_v0_1.py +246 -0
- AeroViz/dataProcess/SizeDistr/merge/_merge_v1.py +255 -0
- AeroViz/dataProcess/SizeDistr/merge/_merge_v2.py +244 -0
- AeroViz/dataProcess/SizeDistr/merge/_merge_v3.py +518 -0
- AeroViz/dataProcess/SizeDistr/merge/_merge_v4.py +422 -0
- AeroViz/dataProcess/SizeDistr/prop.py +62 -0
- AeroViz/dataProcess/VOC/__init__.py +14 -0
- AeroViz/dataProcess/VOC/__pycache__/__init__.cpython-312.pyc +0 -0
- AeroViz/dataProcess/VOC/_potential_par.py +108 -0
- AeroViz/dataProcess/VOC/support_voc.json +446 -0
- AeroViz/dataProcess/__init__.py +66 -0
- AeroViz/dataProcess/__pycache__/__init__.cpython-312.pyc +0 -0
- AeroViz/dataProcess/core/__init__.py +272 -0
- AeroViz/dataProcess/core/__pycache__/__init__.cpython-312.pyc +0 -0
- AeroViz/mcp_server.py +352 -0
- AeroViz/plot/__init__.py +13 -0
- AeroViz/plot/__pycache__/__init__.cpython-312.pyc +0 -0
- AeroViz/plot/__pycache__/bar.cpython-312.pyc +0 -0
- AeroViz/plot/__pycache__/box.cpython-312.pyc +0 -0
- AeroViz/plot/__pycache__/pie.cpython-312.pyc +0 -0
- AeroViz/plot/__pycache__/radar.cpython-312.pyc +0 -0
- AeroViz/plot/__pycache__/regression.cpython-312.pyc +0 -0
- AeroViz/plot/__pycache__/scatter.cpython-312.pyc +0 -0
- AeroViz/plot/__pycache__/violin.cpython-312.pyc +0 -0
- AeroViz/plot/bar.py +126 -0
- AeroViz/plot/box.py +69 -0
- AeroViz/plot/distribution/__init__.py +1 -0
- AeroViz/plot/distribution/__pycache__/__init__.cpython-312.pyc +0 -0
- AeroViz/plot/distribution/__pycache__/distribution.cpython-312.pyc +0 -0
- AeroViz/plot/distribution/distribution.py +576 -0
- AeroViz/plot/meteorology/CBPF.py +295 -0
- AeroViz/plot/meteorology/__init__.py +3 -0
- AeroViz/plot/meteorology/__pycache__/CBPF.cpython-312.pyc +0 -0
- AeroViz/plot/meteorology/__pycache__/__init__.cpython-312.pyc +0 -0
- AeroViz/plot/meteorology/__pycache__/hysplit.cpython-312.pyc +0 -0
- AeroViz/plot/meteorology/__pycache__/wind_rose.cpython-312.pyc +0 -0
- AeroViz/plot/meteorology/hysplit.py +93 -0
- AeroViz/plot/meteorology/wind_rose.py +77 -0
- AeroViz/plot/optical/__init__.py +1 -0
- AeroViz/plot/optical/__pycache__/__init__.cpython-312.pyc +0 -0
- AeroViz/plot/optical/__pycache__/optical.cpython-312.pyc +0 -0
- AeroViz/plot/optical/optical.py +388 -0
- AeroViz/plot/pie.py +210 -0
- AeroViz/plot/radar.py +184 -0
- AeroViz/plot/regression.py +200 -0
- AeroViz/plot/scatter.py +174 -0
- AeroViz/plot/templates/__init__.py +6 -0
- AeroViz/plot/templates/__pycache__/__init__.cpython-312.pyc +0 -0
- AeroViz/plot/templates/__pycache__/ammonium_rich.cpython-312.pyc +0 -0
- AeroViz/plot/templates/__pycache__/contour.cpython-312.pyc +0 -0
- AeroViz/plot/templates/__pycache__/corr_matrix.cpython-312.pyc +0 -0
- AeroViz/plot/templates/__pycache__/diurnal_pattern.cpython-312.pyc +0 -0
- AeroViz/plot/templates/__pycache__/koschmieder.cpython-312.pyc +0 -0
- AeroViz/plot/templates/__pycache__/metal_heatmap.cpython-312.pyc +0 -0
- AeroViz/plot/templates/ammonium_rich.py +34 -0
- AeroViz/plot/templates/contour.py +47 -0
- AeroViz/plot/templates/corr_matrix.py +267 -0
- AeroViz/plot/templates/diurnal_pattern.py +61 -0
- AeroViz/plot/templates/koschmieder.py +95 -0
- AeroViz/plot/templates/metal_heatmap.py +164 -0
- AeroViz/plot/timeseries/__init__.py +2 -0
- AeroViz/plot/timeseries/__pycache__/__init__.cpython-312.pyc +0 -0
- AeroViz/plot/timeseries/__pycache__/template.cpython-312.pyc +0 -0
- AeroViz/plot/timeseries/__pycache__/timeseries.cpython-312.pyc +0 -0
- AeroViz/plot/timeseries/template.py +47 -0
- AeroViz/plot/timeseries/timeseries.py +446 -0
- AeroViz/plot/utils/__init__.py +4 -0
- AeroViz/plot/utils/__pycache__/__init__.cpython-312.pyc +0 -0
- AeroViz/plot/utils/__pycache__/_color.cpython-312.pyc +0 -0
- AeroViz/plot/utils/__pycache__/_unit.cpython-312.pyc +0 -0
- AeroViz/plot/utils/__pycache__/plt_utils.cpython-312.pyc +0 -0
- AeroViz/plot/utils/__pycache__/sklearn_utils.cpython-312.pyc +0 -0
- AeroViz/plot/utils/_color.py +71 -0
- AeroViz/plot/utils/_unit.py +55 -0
- AeroViz/plot/utils/fRH.json +390 -0
- AeroViz/plot/utils/plt_utils.py +92 -0
- AeroViz/plot/utils/sklearn_utils.py +49 -0
- AeroViz/plot/utils/units.json +89 -0
- AeroViz/plot/violin.py +80 -0
- AeroViz/rawDataReader/FLOW.md +138 -0
- AeroViz/rawDataReader/__init__.py +220 -0
- AeroViz/rawDataReader/__pycache__/__init__.cpython-312.pyc +0 -0
- AeroViz/rawDataReader/config/__init__.py +0 -0
- AeroViz/rawDataReader/config/__pycache__/__init__.cpython-312.pyc +0 -0
- AeroViz/rawDataReader/config/__pycache__/supported_instruments.cpython-312.pyc +0 -0
- AeroViz/rawDataReader/config/supported_instruments.py +135 -0
- AeroViz/rawDataReader/core/__init__.py +658 -0
- AeroViz/rawDataReader/core/__pycache__/__init__.cpython-312.pyc +0 -0
- AeroViz/rawDataReader/core/__pycache__/logger.cpython-312.pyc +0 -0
- AeroViz/rawDataReader/core/__pycache__/pre_process.cpython-312.pyc +0 -0
- AeroViz/rawDataReader/core/__pycache__/qc.cpython-312.pyc +0 -0
- AeroViz/rawDataReader/core/__pycache__/report.cpython-312.pyc +0 -0
- AeroViz/rawDataReader/core/logger.py +171 -0
- AeroViz/rawDataReader/core/pre_process.py +308 -0
- AeroViz/rawDataReader/core/qc.py +961 -0
- AeroViz/rawDataReader/core/report.py +579 -0
- AeroViz/rawDataReader/script/AE33.py +173 -0
- AeroViz/rawDataReader/script/AE43.py +151 -0
- AeroViz/rawDataReader/script/APS.py +339 -0
- AeroViz/rawDataReader/script/Aurora.py +191 -0
- AeroViz/rawDataReader/script/BAM1020.py +90 -0
- AeroViz/rawDataReader/script/BC1054.py +161 -0
- AeroViz/rawDataReader/script/EPA.py +79 -0
- AeroViz/rawDataReader/script/GRIMM.py +68 -0
- AeroViz/rawDataReader/script/IGAC.py +140 -0
- AeroViz/rawDataReader/script/MA350.py +179 -0
- AeroViz/rawDataReader/script/Minion.py +218 -0
- AeroViz/rawDataReader/script/NEPH.py +199 -0
- AeroViz/rawDataReader/script/OCEC.py +173 -0
- AeroViz/rawDataReader/script/Q-ACSM.py +12 -0
- AeroViz/rawDataReader/script/SMPS.py +389 -0
- AeroViz/rawDataReader/script/TEOM.py +181 -0
- AeroViz/rawDataReader/script/VOC.py +106 -0
- AeroViz/rawDataReader/script/Xact.py +244 -0
- AeroViz/rawDataReader/script/__init__.py +28 -0
- AeroViz/rawDataReader/script/__pycache__/AE33.cpython-312.pyc +0 -0
- AeroViz/rawDataReader/script/__pycache__/AE43.cpython-312.pyc +0 -0
- AeroViz/rawDataReader/script/__pycache__/APS.cpython-312.pyc +0 -0
- AeroViz/rawDataReader/script/__pycache__/Aurora.cpython-312.pyc +0 -0
- AeroViz/rawDataReader/script/__pycache__/BAM1020.cpython-312.pyc +0 -0
- AeroViz/rawDataReader/script/__pycache__/BC1054.cpython-312.pyc +0 -0
- AeroViz/rawDataReader/script/__pycache__/EPA.cpython-312.pyc +0 -0
- AeroViz/rawDataReader/script/__pycache__/GRIMM.cpython-312.pyc +0 -0
- AeroViz/rawDataReader/script/__pycache__/IGAC.cpython-312.pyc +0 -0
- AeroViz/rawDataReader/script/__pycache__/MA350.cpython-312.pyc +0 -0
- AeroViz/rawDataReader/script/__pycache__/Minion.cpython-312.pyc +0 -0
- AeroViz/rawDataReader/script/__pycache__/NEPH.cpython-312.pyc +0 -0
- AeroViz/rawDataReader/script/__pycache__/OCEC.cpython-312.pyc +0 -0
- AeroViz/rawDataReader/script/__pycache__/Q-ACSM.cpython-312.pyc +0 -0
- AeroViz/rawDataReader/script/__pycache__/SMPS.cpython-312.pyc +0 -0
- AeroViz/rawDataReader/script/__pycache__/TEOM.cpython-312.pyc +0 -0
- AeroViz/rawDataReader/script/__pycache__/VOC.cpython-312.pyc +0 -0
- AeroViz/rawDataReader/script/__pycache__/Xact.cpython-312.pyc +0 -0
- AeroViz/rawDataReader/script/__pycache__/__init__.cpython-312.pyc +0 -0
- AeroViz/tools/__init__.py +2 -0
- AeroViz/tools/__pycache__/__init__.cpython-312.pyc +0 -0
- AeroViz/tools/__pycache__/database.cpython-312.pyc +0 -0
- AeroViz/tools/__pycache__/dataclassifier.cpython-312.pyc +0 -0
- AeroViz/tools/database.py +95 -0
- AeroViz/tools/dataclassifier.py +117 -0
- AeroViz/tools/dataprinter.py +58 -0
- aeroviz-0.1.21.dist-info/METADATA +294 -0
- aeroviz-0.1.21.dist-info/RECORD +180 -0
- aeroviz-0.1.21.dist-info/WHEEL +5 -0
- aeroviz-0.1.21.dist-info/licenses/LICENSE +21 -0
- 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
|
+
}
|