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,557 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Chemical calculations for aerosol analysis.
|
|
3
|
+
|
|
4
|
+
This module provides functions for:
|
|
5
|
+
- Molar concentration conversion
|
|
6
|
+
- Volume-average mixing refractive index
|
|
7
|
+
- Kappa (hygroscopicity parameter)
|
|
8
|
+
- Growth factor (gRH)
|
|
9
|
+
- Gas-particle partitioning ratios (SOR, NOR, NTR, epsilon)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from pandas import concat, DataFrame
|
|
13
|
+
|
|
14
|
+
from AeroViz.dataProcess.core import validate_inputs
|
|
15
|
+
|
|
16
|
+
# =============================================================================
|
|
17
|
+
# Constants
|
|
18
|
+
# =============================================================================
|
|
19
|
+
|
|
20
|
+
# Molecular weights in g/mol
|
|
21
|
+
MOLECULAR_WEIGHTS = {
|
|
22
|
+
'SO42-': 96.06,
|
|
23
|
+
'NO3-': 62.00,
|
|
24
|
+
'Cl-': 35.4,
|
|
25
|
+
'Ca2+': 40.078,
|
|
26
|
+
'K+': 39.098,
|
|
27
|
+
'Mg2+': 24.305,
|
|
28
|
+
'Na+': 22.99,
|
|
29
|
+
'NH4+': 18.04,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# Gas molecular weights for partition calculations
|
|
33
|
+
GAS_MOLECULAR_WEIGHTS = {
|
|
34
|
+
'SO2': 64.07,
|
|
35
|
+
'NO2': 46.01,
|
|
36
|
+
'NH3': 17.03,
|
|
37
|
+
'HNO3': 63.01,
|
|
38
|
+
'HCl': 36.46,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# =============================================================================
|
|
42
|
+
# Required columns definitions
|
|
43
|
+
# =============================================================================
|
|
44
|
+
|
|
45
|
+
VOLUME_MIXING_REQUIRED = ['total_dry']
|
|
46
|
+
VOLUME_MIXING_SPECIES = ['AS', 'AN', 'OM', 'Soil', 'SS', 'EC']
|
|
47
|
+
|
|
48
|
+
KAPPA_REQUIRED = ['gRH', 'AT', 'RH']
|
|
49
|
+
GRH_VOLUME_REQUIRED = ['total_dry']
|
|
50
|
+
GRH_ALWC_REQUIRED = ['ALWC']
|
|
51
|
+
|
|
52
|
+
# Partition calculation required columns
|
|
53
|
+
PARTITION_REQUIRED = ['temp'] # Temperature is required for molar conversion
|
|
54
|
+
PARTITION_SPECIES = {
|
|
55
|
+
'SOR': ['SO42-', 'SO2'], # Sulfur Oxidation Ratio
|
|
56
|
+
'NOR': ['NO3-', 'NO2'], # Nitrogen Oxidation Ratio
|
|
57
|
+
'NOR_2': ['NO3-', 'NO2', 'HNO3'], # NOR including HNO3
|
|
58
|
+
'NTR': ['NH4+', 'NH3'], # Nitrogen Transformation Ratio
|
|
59
|
+
'epls_NO3': ['NO3-', 'HNO3'], # NO3 partitioning
|
|
60
|
+
'epls_NH4': ['NH4+', 'NH3'], # NH4 partitioning
|
|
61
|
+
'epls_Cl': ['Cl-', 'HCl'], # Cl partitioning
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# =============================================================================
|
|
65
|
+
# Column descriptions (Chinese/English) - 欄位說明
|
|
66
|
+
# =============================================================================
|
|
67
|
+
|
|
68
|
+
# Volume-average mixing 體積平均混合計算所需欄位
|
|
69
|
+
VOLUME_COLUMN_DESCRIPTIONS = {
|
|
70
|
+
'AS_volume': 'Ammonium Sulfate volume (μm³/m³) 硫酸銨體積濃度',
|
|
71
|
+
'AN_volume': 'Ammonium Nitrate volume (μm³/m³) 硝酸銨體積濃度',
|
|
72
|
+
'OM_volume': 'Organic Matter volume (μm³/m³) 有機物體積濃度',
|
|
73
|
+
'Soil_volume': 'Soil/Crustal volume (μm³/m³) 土壤/地殼物質體積濃度',
|
|
74
|
+
'SS_volume': 'Sea Salt volume (μm³/m³) 海鹽體積濃度',
|
|
75
|
+
'EC_volume': 'Elemental Carbon volume (μm³/m³) 元素碳體積濃度',
|
|
76
|
+
'total_dry': 'Total dry aerosol volume (μm³/m³) 乾氣膠總體積濃度',
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# Kappa 吸濕參數計算所需欄位
|
|
80
|
+
KAPPA_COLUMN_DESCRIPTIONS = {
|
|
81
|
+
'gRH': 'Hygroscopic growth factor (Dp_wet/Dp_dry) 吸濕成長因子 (濕粒徑/乾粒徑)',
|
|
82
|
+
'AT': 'Ambient Temperature (°C) 環境溫度',
|
|
83
|
+
'RH': 'Relative Humidity (%) 相對濕度',
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# gRH 成長因子計算所需欄位
|
|
87
|
+
GRH_COLUMN_DESCRIPTIONS = {
|
|
88
|
+
'total_dry': 'Total dry aerosol volume (μm³/m³) 乾氣膠總體積濃度',
|
|
89
|
+
'ALWC': 'Aerosol Liquid Water Content (μg/m³) 氣膠液態水含量',
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
# Partition 氣固分配比計算所需欄位
|
|
93
|
+
PARTITION_COLUMN_DESCRIPTIONS = {
|
|
94
|
+
'temp': 'Ambient Temperature (°C) 環境溫度',
|
|
95
|
+
'SO42-': 'Particulate Sulfate (μg/m³) 顆粒態硫酸鹽',
|
|
96
|
+
'SO2': 'Gaseous Sulfur Dioxide (μg/m³) 氣態二氧化硫',
|
|
97
|
+
'NO3-': 'Particulate Nitrate (μg/m³) 顆粒態硝酸鹽',
|
|
98
|
+
'NO2': 'Gaseous Nitrogen Dioxide (μg/m³) 氣態二氧化氮',
|
|
99
|
+
'HNO3': 'Gaseous Nitric Acid (μg/m³) 氣態硝酸',
|
|
100
|
+
'NH4+': 'Particulate Ammonium (μg/m³) 顆粒態銨鹽',
|
|
101
|
+
'NH3': 'Gaseous Ammonia (μg/m³) 氣態氨',
|
|
102
|
+
'Cl-': 'Particulate Chloride (μg/m³) 顆粒態氯鹽',
|
|
103
|
+
'HCl': 'Gaseous Hydrochloric Acid (μg/m³) 氣態鹽酸',
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def convert_mass_to_molar_concentration(df):
|
|
108
|
+
"""
|
|
109
|
+
Convert mass concentration (μg/m³) to molar concentration (μmol/m³ for particles, ppm for gases).
|
|
110
|
+
|
|
111
|
+
This function identifies ionic species based on the MOLECULAR_WEIGHTS dictionary
|
|
112
|
+
and converts them from mass to molar units. Gaseous species are converted using
|
|
113
|
+
the ideal gas law with temperature data from the input DataFrame.
|
|
114
|
+
|
|
115
|
+
Parameters
|
|
116
|
+
----------
|
|
117
|
+
df : pandas.DataFrame
|
|
118
|
+
DataFrame containing concentration data with column names matching ions
|
|
119
|
+
in MOLECULAR_WEIGHTS. Must include 'temp' column in Celsius.
|
|
120
|
+
|
|
121
|
+
Returns
|
|
122
|
+
-------
|
|
123
|
+
pandas.DataFrame
|
|
124
|
+
DataFrame with all concentrations converted to molar units:
|
|
125
|
+
- Ionic species: μg/m³ → μmol/m³
|
|
126
|
+
- Gaseous species: μg/m³ → ppm (using ideal gas law)
|
|
127
|
+
|
|
128
|
+
Notes
|
|
129
|
+
-----
|
|
130
|
+
- The function assumes temperature ('temp') is in Celsius and converts it to Kelvin
|
|
131
|
+
- Uses the ideal gas constant of 0.082 L·atm/(mol·K)
|
|
132
|
+
- Non-matched columns (except 'temp' and 'RH') are treated as gaseous species
|
|
133
|
+
|
|
134
|
+
Examples
|
|
135
|
+
--------
|
|
136
|
+
>>> import pandas as pd
|
|
137
|
+
>>> data = pd.DataFrame({
|
|
138
|
+
... 'SO42-': [10.0, 15.0],
|
|
139
|
+
... 'NO3-': [5.0, 7.5],
|
|
140
|
+
... 'O3': [30.0, 45.0],
|
|
141
|
+
... 'temp': [25.0, 30.0],
|
|
142
|
+
... 'RH': [60.0, 70.0]
|
|
143
|
+
... })
|
|
144
|
+
>>> convert_mass_to_molar_concentration(data)
|
|
145
|
+
"""
|
|
146
|
+
# Identify which columns are particulate ions vs. gases
|
|
147
|
+
particle_keys = list(set(df.keys()) & set(MOLECULAR_WEIGHTS.keys()))
|
|
148
|
+
gas_keys = list(set(df.keys()) - set(MOLECULAR_WEIGHTS.keys()) - {'temp', 'RH'})
|
|
149
|
+
|
|
150
|
+
# Calculate gas constant * temperature factor for gas conversion (ideal gas law)
|
|
151
|
+
temperature_factor = (df['temp'].to_frame() + 273.15) * 0.082
|
|
152
|
+
|
|
153
|
+
# Convert particulate species (μg/m³ → μmol/m³)
|
|
154
|
+
df_particles = concat([
|
|
155
|
+
(df[key] / MOLECULAR_WEIGHTS[key]).copy() for key in particle_keys
|
|
156
|
+
], axis=1)
|
|
157
|
+
|
|
158
|
+
# Convert gaseous species (μg/m³ → ppm)
|
|
159
|
+
df_gases = df[gas_keys] / temperature_factor.values
|
|
160
|
+
|
|
161
|
+
# Combine results
|
|
162
|
+
return concat([df_particles, df_gases], axis=1)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def volume_average_mixing(df_volume, df_alwc=None):
|
|
166
|
+
"""
|
|
167
|
+
Calculate volume-average refractive index using mixing rule.
|
|
168
|
+
|
|
169
|
+
This function calculates the dry and ambient refractive indices
|
|
170
|
+
based on volume-weighted mixing of individual species at 550 nm.
|
|
171
|
+
|
|
172
|
+
Parameters
|
|
173
|
+
----------
|
|
174
|
+
df_volume : DataFrame
|
|
175
|
+
Volume concentration data (μm³/m³) with columns:
|
|
176
|
+
- total_dry : Total dry aerosol volume concentration (required)
|
|
177
|
+
- At least one of: AS_volume, AN_volume, OM_volume, Soil_volume, SS_volume, EC_volume
|
|
178
|
+
df_alwc : DataFrame, optional
|
|
179
|
+
Aerosol liquid water content (μg/m³) with 'ALWC' column.
|
|
180
|
+
|
|
181
|
+
Returns
|
|
182
|
+
-------
|
|
183
|
+
DataFrame
|
|
184
|
+
Refractive index data with columns:
|
|
185
|
+
- n_dry : Real part of dry aerosol RI (dimensionless)
|
|
186
|
+
- k_dry : Imaginary part of dry aerosol RI (dimensionless)
|
|
187
|
+
- n_amb : Real part of ambient (wet) aerosol RI
|
|
188
|
+
- k_amb : Imaginary part of ambient aerosol RI
|
|
189
|
+
- gRH : Hygroscopic growth factor (Dp_wet/Dp_dry)
|
|
190
|
+
|
|
191
|
+
Raises
|
|
192
|
+
------
|
|
193
|
+
ValueError
|
|
194
|
+
If required columns are missing.
|
|
195
|
+
|
|
196
|
+
Notes
|
|
197
|
+
-----
|
|
198
|
+
Volume-average mixing rule: RI_mix = Σ(Vi * RIi) / V_total
|
|
199
|
+
"""
|
|
200
|
+
import numpy as np
|
|
201
|
+
from pandas import DataFrame
|
|
202
|
+
|
|
203
|
+
# Validate required columns
|
|
204
|
+
validate_inputs(df_volume, VOLUME_MIXING_REQUIRED, 'volume_average_mixing', VOLUME_COLUMN_DESCRIPTIONS)
|
|
205
|
+
|
|
206
|
+
# Check that at least one volume species exists
|
|
207
|
+
available_species = [col for col in VOLUME_MIXING_SPECIES if f'{col}_volume' in df_volume.columns]
|
|
208
|
+
if not available_species:
|
|
209
|
+
volume_cols = [f'{sp}_volume' for sp in VOLUME_MIXING_SPECIES]
|
|
210
|
+
raise ValueError(
|
|
211
|
+
f"\nvolume_average_mixing() 至少需要一個體積欄位!\n"
|
|
212
|
+
f" 可用欄位: {volume_cols}\n"
|
|
213
|
+
f" 現有欄位: {sorted(df_volume.columns.tolist())}"
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
if df_alwc is not None:
|
|
217
|
+
validate_inputs(df_alwc, GRH_ALWC_REQUIRED, 'volume_average_mixing (df_alwc)', GRH_COLUMN_DESCRIPTIONS)
|
|
218
|
+
|
|
219
|
+
# Refractive index values at 550 nm
|
|
220
|
+
RI_values = {
|
|
221
|
+
'n': {'AS': 1.53, 'AN': 1.55, 'OM': 1.55, 'Soil': 1.56, 'SS': 1.54, 'EC': 1.80, 'ALWC': 1.33},
|
|
222
|
+
'k': {'AS': 0.00, 'AN': 0.00, 'OM': 0.00, 'Soil': 0.01, 'SS': 0.00, 'EC': 0.54, 'ALWC': 0.00}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
volume_cols = ['AS', 'AN', 'OM', 'Soil', 'SS', 'EC']
|
|
226
|
+
volume_ratio = DataFrame(index=df_volume.index)
|
|
227
|
+
|
|
228
|
+
for col in volume_cols:
|
|
229
|
+
if f'{col}_volume' in df_volume.columns:
|
|
230
|
+
volume_ratio[f'{col}_volume_ratio'] = df_volume[f'{col}_volume'] / df_volume['total_dry']
|
|
231
|
+
|
|
232
|
+
result = DataFrame(index=df_volume.index)
|
|
233
|
+
|
|
234
|
+
result['n_dry'] = sum(
|
|
235
|
+
RI_values['n'][col] * volume_ratio[f'{col}_volume_ratio']
|
|
236
|
+
for col in volume_cols if f'{col}_volume_ratio' in volume_ratio.columns
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
result['k_dry'] = sum(
|
|
240
|
+
RI_values['k'][col] * volume_ratio[f'{col}_volume_ratio']
|
|
241
|
+
for col in volume_cols if f'{col}_volume_ratio' in volume_ratio.columns
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
if df_alwc is not None and 'ALWC' in df_alwc.columns:
|
|
245
|
+
v_dry = df_volume['total_dry']
|
|
246
|
+
v_wet = df_volume['total_dry'] + df_alwc['ALWC']
|
|
247
|
+
|
|
248
|
+
multiplier = v_dry / v_wet
|
|
249
|
+
alwc_ratio = 1 - multiplier
|
|
250
|
+
|
|
251
|
+
result['ALWC_volume_ratio'] = alwc_ratio
|
|
252
|
+
|
|
253
|
+
result['n_amb'] = (
|
|
254
|
+
sum(
|
|
255
|
+
RI_values['n'][col] * volume_ratio[f'{col}_volume_ratio']
|
|
256
|
+
for col in volume_cols if f'{col}_volume_ratio' in volume_ratio.columns
|
|
257
|
+
) * multiplier +
|
|
258
|
+
RI_values['n']['ALWC'] * alwc_ratio
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
result['k_amb'] = (
|
|
262
|
+
sum(
|
|
263
|
+
RI_values['k'][col] * volume_ratio[f'{col}_volume_ratio']
|
|
264
|
+
for col in volume_cols if f'{col}_volume_ratio' in volume_ratio.columns
|
|
265
|
+
) * multiplier
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
result['gRH'] = (v_wet / v_dry) ** (1 / 3)
|
|
269
|
+
else:
|
|
270
|
+
result['n_amb'] = np.nan
|
|
271
|
+
result['k_amb'] = np.nan
|
|
272
|
+
result['gRH'] = np.nan
|
|
273
|
+
|
|
274
|
+
return result
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def kappa_calculate(df_data, diameter=0.5):
|
|
278
|
+
"""
|
|
279
|
+
Calculate the hygroscopicity parameter kappa.
|
|
280
|
+
|
|
281
|
+
Parameters
|
|
282
|
+
----------
|
|
283
|
+
df_data : DataFrame
|
|
284
|
+
Data containing:
|
|
285
|
+
- gRH : Hygroscopic growth factor
|
|
286
|
+
- AT : Ambient temperature (Celsius)
|
|
287
|
+
- RH : Relative humidity (%)
|
|
288
|
+
diameter : float, default=0.5
|
|
289
|
+
Particle dry diameter in micrometers.
|
|
290
|
+
|
|
291
|
+
Returns
|
|
292
|
+
-------
|
|
293
|
+
DataFrame
|
|
294
|
+
Kappa values with 'kappa_chem' column.
|
|
295
|
+
|
|
296
|
+
Raises
|
|
297
|
+
------
|
|
298
|
+
ValueError
|
|
299
|
+
If required columns (gRH, AT, RH) are missing.
|
|
300
|
+
|
|
301
|
+
Examples
|
|
302
|
+
--------
|
|
303
|
+
>>> cols = get_required_columns()['kappa_calculate']
|
|
304
|
+
>>> print(cols)
|
|
305
|
+
['gRH', 'AT', 'RH']
|
|
306
|
+
"""
|
|
307
|
+
import numpy as np
|
|
308
|
+
from pandas import DataFrame
|
|
309
|
+
|
|
310
|
+
# Validate required columns
|
|
311
|
+
validate_inputs(df_data, KAPPA_REQUIRED, 'kappa_calculate', KAPPA_COLUMN_DESCRIPTIONS)
|
|
312
|
+
|
|
313
|
+
surface_tension = 0.072
|
|
314
|
+
Mw = 18
|
|
315
|
+
density = 1
|
|
316
|
+
R = 8.314
|
|
317
|
+
|
|
318
|
+
result = DataFrame(index=df_data.index)
|
|
319
|
+
|
|
320
|
+
T = df_data['AT'] + 273
|
|
321
|
+
A = 4 * (surface_tension * Mw) / (density * R * T)
|
|
322
|
+
power = A / (diameter * 1e-6)
|
|
323
|
+
|
|
324
|
+
a_w = (df_data['RH'] / 100) * np.exp(-power)
|
|
325
|
+
|
|
326
|
+
gRH = df_data['gRH']
|
|
327
|
+
result['kappa_chem'] = (gRH ** 3 - 1) * (1 - a_w) / a_w
|
|
328
|
+
|
|
329
|
+
return result
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def gRH_calculate(df_volume, df_alwc):
|
|
333
|
+
"""
|
|
334
|
+
Calculate the hygroscopic growth factor gRH.
|
|
335
|
+
|
|
336
|
+
Parameters
|
|
337
|
+
----------
|
|
338
|
+
df_volume : DataFrame
|
|
339
|
+
Volume data with 'total_dry' column.
|
|
340
|
+
df_alwc : DataFrame
|
|
341
|
+
Aerosol liquid water content with 'ALWC' column.
|
|
342
|
+
|
|
343
|
+
Returns
|
|
344
|
+
-------
|
|
345
|
+
DataFrame
|
|
346
|
+
Growth factor data with 'gRH' column.
|
|
347
|
+
|
|
348
|
+
Raises
|
|
349
|
+
------
|
|
350
|
+
ValueError
|
|
351
|
+
If required columns are missing.
|
|
352
|
+
|
|
353
|
+
Examples
|
|
354
|
+
--------
|
|
355
|
+
>>> cols = get_required_columns()['gRH_calculate']
|
|
356
|
+
>>> print(cols)
|
|
357
|
+
"""
|
|
358
|
+
from pandas import DataFrame
|
|
359
|
+
|
|
360
|
+
# Validate required columns
|
|
361
|
+
validate_inputs(df_volume, GRH_VOLUME_REQUIRED, 'gRH_calculate (df_volume)', GRH_COLUMN_DESCRIPTIONS)
|
|
362
|
+
validate_inputs(df_alwc, GRH_ALWC_REQUIRED, 'gRH_calculate (df_alwc)', GRH_COLUMN_DESCRIPTIONS)
|
|
363
|
+
|
|
364
|
+
result = DataFrame(index=df_volume.index)
|
|
365
|
+
|
|
366
|
+
v_dry = df_volume['total_dry']
|
|
367
|
+
v_wet = v_dry + df_alwc['ALWC']
|
|
368
|
+
|
|
369
|
+
result['gRH'] = (v_wet / v_dry) ** (1 / 3)
|
|
370
|
+
|
|
371
|
+
return result
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def get_required_columns():
|
|
375
|
+
"""
|
|
376
|
+
Get required column names for calculation functions.
|
|
377
|
+
|
|
378
|
+
Returns
|
|
379
|
+
-------
|
|
380
|
+
dict
|
|
381
|
+
Dictionary with function names as keys and required columns as values.
|
|
382
|
+
|
|
383
|
+
Examples
|
|
384
|
+
--------
|
|
385
|
+
>>> cols = get_required_columns()
|
|
386
|
+
>>> print(cols['kappa_calculate'])
|
|
387
|
+
['gRH', 'AT', 'RH']
|
|
388
|
+
"""
|
|
389
|
+
return {
|
|
390
|
+
'convert_mass_to_molar_concentration': {
|
|
391
|
+
'required': ['temp'],
|
|
392
|
+
'ionic_species': list(MOLECULAR_WEIGHTS.keys()),
|
|
393
|
+
'description': 'Converts mass to molar concentration. Ionic species are optional.'
|
|
394
|
+
},
|
|
395
|
+
'volume_average_mixing': {
|
|
396
|
+
'required': VOLUME_MIXING_REQUIRED.copy(),
|
|
397
|
+
'species': [f'{sp}_volume' for sp in VOLUME_MIXING_SPECIES],
|
|
398
|
+
'optional': ['ALWC (for ambient RI calculation)']
|
|
399
|
+
},
|
|
400
|
+
'kappa_calculate': KAPPA_REQUIRED.copy(),
|
|
401
|
+
'gRH_calculate': {
|
|
402
|
+
'df_volume': GRH_VOLUME_REQUIRED.copy(),
|
|
403
|
+
'df_alwc': GRH_ALWC_REQUIRED.copy()
|
|
404
|
+
},
|
|
405
|
+
'partition_ratios': {
|
|
406
|
+
'required': PARTITION_REQUIRED.copy(),
|
|
407
|
+
'species': PARTITION_SPECIES.copy(),
|
|
408
|
+
'description': 'Calculate gas-particle partitioning ratios'
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
# =============================================================================
|
|
414
|
+
# Gas-Particle Partitioning Functions
|
|
415
|
+
# =============================================================================
|
|
416
|
+
|
|
417
|
+
def partition_ratios(df_data):
|
|
418
|
+
"""
|
|
419
|
+
Calculate gas-particle partitioning ratios.
|
|
420
|
+
|
|
421
|
+
Calculates oxidation ratios and equilibrium partitioning coefficients
|
|
422
|
+
to assess the degree of secondary aerosol formation.
|
|
423
|
+
|
|
424
|
+
Parameters
|
|
425
|
+
----------
|
|
426
|
+
df_data : DataFrame
|
|
427
|
+
Data containing particle and gas concentrations (μg/m³).
|
|
428
|
+
Required column: 'temp' (temperature in Celsius)
|
|
429
|
+
Optional species columns (at least one pair needed):
|
|
430
|
+
- SO42-, SO2 : for SOR (Sulfur Oxidation Ratio)
|
|
431
|
+
- NO3-, NO2 : for NOR (Nitrogen Oxidation Ratio)
|
|
432
|
+
- NO3-, NO2, HNO3 : for NOR_2 (complete nitrogen)
|
|
433
|
+
- NH4+, NH3 : for NTR (Nitrogen Transformation Ratio)
|
|
434
|
+
- Cl-, HCl : for chloride partitioning
|
|
435
|
+
|
|
436
|
+
Returns
|
|
437
|
+
-------
|
|
438
|
+
DataFrame
|
|
439
|
+
Partitioning ratios with columns:
|
|
440
|
+
- SOR : SO₄²⁻ / (SO₄²⁻ + SO₂) - Sulfur oxidation ratio
|
|
441
|
+
- NOR : NO₃⁻ / (NO₃⁻ + NO₂) - Nitrogen oxidation ratio
|
|
442
|
+
- NOR_2 : (NO₃⁻ + HNO₃) / (NO₃⁻ + NO₂ + HNO₃) - Complete NOR
|
|
443
|
+
- NTR : NH₄⁺ / (NH₄⁺ + NH₃) - Nitrogen transformation ratio
|
|
444
|
+
- epls_SO42- : Same as SOR (epsilon for sulfate)
|
|
445
|
+
- epls_NO3- : NO₃⁻ / (NO₃⁻ + HNO₃) - Nitrate partitioning
|
|
446
|
+
- epls_NH4+ : Same as NTR (epsilon for ammonium)
|
|
447
|
+
- epls_Cl- : Cl⁻ / (Cl⁻ + HCl) - Chloride partitioning
|
|
448
|
+
|
|
449
|
+
Notes
|
|
450
|
+
-----
|
|
451
|
+
**Physical Meaning:**
|
|
452
|
+
|
|
453
|
+
- **SOR (Sulfur Oxidation Ratio)**: Indicates the degree of SO₂ → SO₄²⁻
|
|
454
|
+
conversion. Higher values suggest more aged/processed aerosols.
|
|
455
|
+
SOR > 0.1 typically indicates secondary sulfate formation.
|
|
456
|
+
|
|
457
|
+
- **NOR (Nitrogen Oxidation Ratio)**: Indicates the degree of NOₓ → NO₃⁻
|
|
458
|
+
conversion. Higher values suggest photochemical aging.
|
|
459
|
+
|
|
460
|
+
- **NTR (Nitrogen Transformation Ratio)**: Indicates the conversion of
|
|
461
|
+
gaseous NH₃ to particulate NH₄⁺. Related to acid-base neutralization.
|
|
462
|
+
|
|
463
|
+
- **Epsilon (ε)**: Equilibrium partitioning coefficient. Represents the
|
|
464
|
+
fraction in particle phase at thermodynamic equilibrium.
|
|
465
|
+
|
|
466
|
+
**Interpretation:**
|
|
467
|
+
- Values near 1.0: Nearly complete conversion to particle phase
|
|
468
|
+
- Values near 0.0: Gas phase dominant
|
|
469
|
+
- Values 0.3-0.7: Active gas-particle partitioning
|
|
470
|
+
|
|
471
|
+
Examples
|
|
472
|
+
--------
|
|
473
|
+
>>> import pandas as pd
|
|
474
|
+
>>> data = pd.DataFrame({
|
|
475
|
+
... 'SO42-': [10.0, 15.0],
|
|
476
|
+
... 'SO2': [5.0, 3.0],
|
|
477
|
+
... 'NO3-': [8.0, 12.0],
|
|
478
|
+
... 'NO2': [20.0, 15.0],
|
|
479
|
+
... 'temp': [25.0, 30.0]
|
|
480
|
+
... })
|
|
481
|
+
>>> result = partition_ratios(data)
|
|
482
|
+
>>> print(result['SOR']) # [0.67, 0.83]
|
|
483
|
+
"""
|
|
484
|
+
# Validate temperature column exists
|
|
485
|
+
validate_inputs(df_data, PARTITION_REQUIRED, 'partition_ratios', PARTITION_COLUMN_DESCRIPTIONS)
|
|
486
|
+
|
|
487
|
+
# Convert to molar concentrations
|
|
488
|
+
df_mol = convert_mass_to_molar_concentration(df_data)
|
|
489
|
+
|
|
490
|
+
result = DataFrame(index=df_data.index)
|
|
491
|
+
|
|
492
|
+
# Helper function to safely calculate ratio
|
|
493
|
+
def safe_ratio(numerator, denominator):
|
|
494
|
+
"""Calculate ratio, returning NaN for division by zero."""
|
|
495
|
+
import numpy as np
|
|
496
|
+
with np.errstate(divide='ignore', invalid='ignore'):
|
|
497
|
+
ratio = numerator / denominator
|
|
498
|
+
ratio = ratio.replace([np.inf, -np.inf], np.nan)
|
|
499
|
+
return ratio
|
|
500
|
+
|
|
501
|
+
# SOR: Sulfur Oxidation Ratio
|
|
502
|
+
# SO₄²⁻ / (SO₄²⁻ + SO₂)
|
|
503
|
+
if 'SO42-' in df_mol.columns and 'SO2' in df_mol.columns:
|
|
504
|
+
result['SOR'] = safe_ratio(
|
|
505
|
+
df_mol['SO42-'],
|
|
506
|
+
df_mol['SO42-'] + df_mol['SO2']
|
|
507
|
+
)
|
|
508
|
+
result['epls_SO42-'] = result['SOR']
|
|
509
|
+
|
|
510
|
+
# NOR: Nitrogen Oxidation Ratio
|
|
511
|
+
# NO₃⁻ / (NO₃⁻ + NO₂)
|
|
512
|
+
if 'NO3-' in df_mol.columns and 'NO2' in df_mol.columns:
|
|
513
|
+
result['NOR'] = safe_ratio(
|
|
514
|
+
df_mol['NO3-'],
|
|
515
|
+
df_mol['NO3-'] + df_mol['NO2']
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
# NOR_2: Complete NOR including HNO3
|
|
519
|
+
# (NO₃⁻ + HNO₃) / (NO₃⁻ + NO₂ + HNO₃)
|
|
520
|
+
if all(col in df_mol.columns for col in ['NO3-', 'NO2', 'HNO3']):
|
|
521
|
+
result['NOR_2'] = safe_ratio(
|
|
522
|
+
df_mol['NO3-'] + df_mol['HNO3'],
|
|
523
|
+
df_mol['NO3-'] + df_mol['NO2'] + df_mol['HNO3']
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
# NTR: Nitrogen Transformation Ratio (also called NTR+)
|
|
527
|
+
# NH₄⁺ / (NH₄⁺ + NH₃)
|
|
528
|
+
if 'NH4+' in df_mol.columns and 'NH3' in df_mol.columns:
|
|
529
|
+
result['NTR'] = safe_ratio(
|
|
530
|
+
df_mol['NH4+'],
|
|
531
|
+
df_mol['NH4+'] + df_mol['NH3']
|
|
532
|
+
)
|
|
533
|
+
result['epls_NH4+'] = result['NTR']
|
|
534
|
+
|
|
535
|
+
# Epsilon (ε) for NO3: NO₃⁻ / (NO₃⁻ + HNO₃)
|
|
536
|
+
if 'NO3-' in df_mol.columns and 'HNO3' in df_mol.columns:
|
|
537
|
+
result['epls_NO3-'] = safe_ratio(
|
|
538
|
+
df_mol['NO3-'],
|
|
539
|
+
df_mol['NO3-'] + df_mol['HNO3']
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
# Epsilon (ε) for Cl: Cl⁻ / (Cl⁻ + HCl)
|
|
543
|
+
if 'Cl-' in df_mol.columns and 'HCl' in df_mol.columns:
|
|
544
|
+
result['epls_Cl-'] = safe_ratio(
|
|
545
|
+
df_mol['Cl-'],
|
|
546
|
+
df_mol['Cl-'] + df_mol['HCl']
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
if result.empty:
|
|
550
|
+
raise ValueError(
|
|
551
|
+
"\npartition_ratios() 需要至少一組氣-固物種對!\n"
|
|
552
|
+
f" 可用物種對: {list(PARTITION_SPECIES.keys())}\n"
|
|
553
|
+
f" 現有欄位: {sorted(df_data.columns.tolist())}\n"
|
|
554
|
+
" 例如: SO42- + SO2, NO3- + NO2, NH4+ + NH3"
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
return result
|