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,150 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from subprocess import Popen, PIPE
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
from pandas import concat, DataFrame, to_numeric, read_csv
|
|
6
|
+
|
|
7
|
+
from ._calculate import convert_mass_to_molar_concentration
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _basic(df_che, path_out, nam_lst):
|
|
11
|
+
"""
|
|
12
|
+
Run ISORROPIA II thermodynamic model to calculate aerosol pH, liquid water content (ALWC),
|
|
13
|
+
and gas-particle partitioning of semi-volatile inorganic species.
|
|
14
|
+
|
|
15
|
+
Parameters
|
|
16
|
+
----------
|
|
17
|
+
df_che : list of pandas.DataFrame
|
|
18
|
+
List of DataFrames containing chemical species concentrations and meteorological data.
|
|
19
|
+
These DataFrames will be concatenated along columns.
|
|
20
|
+
|
|
21
|
+
path_out : pathlib.Path
|
|
22
|
+
Output directory path where temporary files will be created and results stored.
|
|
23
|
+
|
|
24
|
+
nam_lst : list of str
|
|
25
|
+
List of column names to be assigned to the concatenated DataFrame.
|
|
26
|
+
Should include: 'NH4+', 'NH3', 'HNO3', 'NO3-', 'HCl', 'Cl-', 'Na+',
|
|
27
|
+
'SO42-', 'Ca2+', 'K+', 'Mg2+', 'RH', 'temp'
|
|
28
|
+
|
|
29
|
+
Returns
|
|
30
|
+
-------
|
|
31
|
+
dict
|
|
32
|
+
Dictionary containing two DataFrames:
|
|
33
|
+
- 'input': DataFrame with processed input data for ISORROPIA II
|
|
34
|
+
- 'output': DataFrame with model results including:
|
|
35
|
+
* 'pH': Aerosol pH (only consider data RH between 20% and 95%)
|
|
36
|
+
* 'ALWC': Aerosol liquid water content (μg/m³)
|
|
37
|
+
* 'NH3', 'HNO3', 'HCl': Gas phase concentrations (μmol/m³)
|
|
38
|
+
* 'NH4+', 'NO3-', 'Cl-': Aerosol phase concentrations (μmol/m³)
|
|
39
|
+
|
|
40
|
+
Notes
|
|
41
|
+
-----
|
|
42
|
+
This function:
|
|
43
|
+
1. Converts mass concentrations to molar concentrations
|
|
44
|
+
2. Prepares input for ISORROPIA II in required format
|
|
45
|
+
3. Executes the ISORROPIA II model in forward mode and metastable state
|
|
46
|
+
4. Processes model output to calculate pH and aerosol composition
|
|
47
|
+
|
|
48
|
+
The function creates temporary files during execution which are removed afterward.
|
|
49
|
+
|
|
50
|
+
Examples
|
|
51
|
+
--------
|
|
52
|
+
>>> import pandas as pd
|
|
53
|
+
>>> from AeroViz import DataProcess
|
|
54
|
+
>>>
|
|
55
|
+
>>> path_out = Path("./results")
|
|
56
|
+
>>> df = pd.read_csv('your_data.csv')
|
|
57
|
+
>>> column_names = ['NH4+', 'NH3', 'HNO3', 'NO3-', 'HCl', 'Cl-', 'Na+',
|
|
58
|
+
>>> 'SO42-', 'Ca2+', 'K+', 'Mg2+', 'RH', 'temp']
|
|
59
|
+
>>> chem_prcs = DataProcess('Chemistry', path_out, excel=False, csv=True)
|
|
60
|
+
>>> run_iso = chem_prcs.ISOROPIA(df[column_names])
|
|
61
|
+
"""
|
|
62
|
+
df_all = concat(df_che, axis=1)
|
|
63
|
+
index = df_all.index.copy()
|
|
64
|
+
df_all.columns = nam_lst
|
|
65
|
+
|
|
66
|
+
df_umol = convert_mass_to_molar_concentration(df_all)
|
|
67
|
+
|
|
68
|
+
# output
|
|
69
|
+
# Na, SO4, NH3, NO3, Cl, Ca, K, Mg, RH, TEMP
|
|
70
|
+
df_input = DataFrame(index=index)
|
|
71
|
+
df_out = DataFrame(index=index)
|
|
72
|
+
|
|
73
|
+
pth_input = path_out / '_temp_input.txt'
|
|
74
|
+
pth_output = path_out / '_temp_input.dat'
|
|
75
|
+
|
|
76
|
+
pth_input.unlink(missing_ok=True)
|
|
77
|
+
pth_output.unlink(missing_ok=True)
|
|
78
|
+
|
|
79
|
+
# header
|
|
80
|
+
_header = 'Input units (0=umol/m3, 1=ug/m3)\n' + '0\n\n' + \
|
|
81
|
+
'Problem type (0=forward, 1=reverse); Phase state (0=solid+liquid, 1=metastable)\n' + '0, 1\n\n' + \
|
|
82
|
+
'NH4-SO4 system case\n'
|
|
83
|
+
|
|
84
|
+
# software
|
|
85
|
+
path_iso = Path(__file__).parent / 'isrpia2.exe'
|
|
86
|
+
|
|
87
|
+
# make input file and output temp input (without index)
|
|
88
|
+
# NH3
|
|
89
|
+
df_input['NH3'] = df_umol['NH4+'].fillna(0).copy() + df_umol['NH3']
|
|
90
|
+
|
|
91
|
+
# NO3
|
|
92
|
+
df_input['NO3'] = df_umol['HNO3'].fillna(0).copy() + df_umol['NO3-']
|
|
93
|
+
|
|
94
|
+
# Cl
|
|
95
|
+
df_input['Cl'] = df_umol['HCl'].fillna(0).copy() + df_umol['Cl-']
|
|
96
|
+
|
|
97
|
+
# temp, RH
|
|
98
|
+
df_input['RH'] = df_all['RH'] / 100
|
|
99
|
+
df_input['TEMP'] = df_all['temp'] + 273.15
|
|
100
|
+
|
|
101
|
+
df_input[['Na', 'SO4', 'Ca', 'K', 'Mg']] = df_umol[['Na+', 'SO42-', 'Ca2+', 'K+', 'Mg2+']].copy()
|
|
102
|
+
|
|
103
|
+
df_input = df_input[['Na', 'SO4', 'NH3', 'NO3', 'Cl', 'Ca', 'K', 'Mg', 'RH', 'TEMP']].fillna('-').copy()
|
|
104
|
+
|
|
105
|
+
# output the input data
|
|
106
|
+
df_input.to_csv(pth_input, index=False)
|
|
107
|
+
with (pth_input).open('r+', encoding='utf-8', errors='ignore') as _f:
|
|
108
|
+
_cont = _f.read()
|
|
109
|
+
_f.seek(0)
|
|
110
|
+
|
|
111
|
+
_f.write(_header)
|
|
112
|
+
_f.write(_cont)
|
|
113
|
+
|
|
114
|
+
# use ISOROPIA2
|
|
115
|
+
run = Popen([path_iso], stdin=PIPE, stdout=PIPE, stderr=PIPE)
|
|
116
|
+
scrn_res, run_res = run.communicate(input=str(pth_input.resolve()).encode())
|
|
117
|
+
|
|
118
|
+
# read dat file and transform to the normal name
|
|
119
|
+
cond_idx = df_all[['SO42-', 'NH4+', 'NO3-']].dropna().index
|
|
120
|
+
|
|
121
|
+
with pth_output.open('r', encoding='utf-8', errors='ignore') as f:
|
|
122
|
+
df_res = read_csv(f, delimiter=r'\s+').apply(to_numeric, errors='coerce').set_index(index)
|
|
123
|
+
|
|
124
|
+
df_out['H'] = df_res['HLIQ'] / (df_res['WATER'] / 1000)
|
|
125
|
+
|
|
126
|
+
df_out.loc[cond_idx, 'pH'] = -np.log10(df_out['H'].loc[cond_idx])
|
|
127
|
+
df_out['pH'] = df_out['pH'].where((df_all['RH'] <= 95) & (df_all['RH'] >= 20))
|
|
128
|
+
|
|
129
|
+
cond_idx = df_out['pH'].dropna().index
|
|
130
|
+
df_out.loc[cond_idx, 'ALWC'] = df_res['WATER'].loc[cond_idx]
|
|
131
|
+
|
|
132
|
+
df_out[['NH3', 'HNO3', 'HCl', 'NH4+', 'NO3-', 'Cl-']] = df_res[
|
|
133
|
+
['GNH3', 'GHNO3', 'GHCL', 'NH4AER', 'NO3AER', 'CLAER']]
|
|
134
|
+
|
|
135
|
+
# calculate partition
|
|
136
|
+
# df_out['epls_NO3-'] = df_umol['NO3-'] / (df_umol['NO3-'] + df_umol['HNO3'])
|
|
137
|
+
# df_out['epls_NH4+'] = df_umol['NH4+'] / (df_umol['NH4+'] + df_umol['NH3'])
|
|
138
|
+
# df_out['epls_Cl-'] = df_umol['Cl-'] / (df_umol['Cl-'] + df_umol['HCl'])
|
|
139
|
+
|
|
140
|
+
# remove _temp file (input and output)
|
|
141
|
+
pth_input.unlink(missing_ok=True)
|
|
142
|
+
pth_output.unlink(missing_ok=True)
|
|
143
|
+
|
|
144
|
+
# output input and output
|
|
145
|
+
out = {
|
|
146
|
+
'input': df_input,
|
|
147
|
+
'output': df_out,
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return out
|
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Mass and volume reconstruction for aerosol chemical composition.
|
|
3
|
+
|
|
4
|
+
This module reconstructs aerosol mass and volume from ionic species measurements,
|
|
5
|
+
handling both ammonium-sufficient and ammonium-deficient conditions.
|
|
6
|
+
|
|
7
|
+
Required Input Columns
|
|
8
|
+
----------------------
|
|
9
|
+
- NH4+ : Ammonium (ug/m3)
|
|
10
|
+
- SO42-: Sulfate (ug/m3)
|
|
11
|
+
- NO3- : Nitrate (ug/m3)
|
|
12
|
+
- Fe : Iron (ug/m3) - for Soil calculation
|
|
13
|
+
- Na+ : Sodium (ug/m3) - for Sea Salt calculation
|
|
14
|
+
- OC : Organic Carbon (ug/m3)
|
|
15
|
+
- EC : Elemental Carbon (ug/m3)
|
|
16
|
+
|
|
17
|
+
Output Species
|
|
18
|
+
--------------
|
|
19
|
+
- AS : Ammonium Sulfate (NH4)2SO4
|
|
20
|
+
- AN : Ammonium Nitrate NH4NO3
|
|
21
|
+
- OM : Organic Matter
|
|
22
|
+
- Soil : Soil/Crustal matter
|
|
23
|
+
- SS : Sea Salt
|
|
24
|
+
- EC : Elemental Carbon
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from pandas import concat, DataFrame
|
|
28
|
+
|
|
29
|
+
from AeroViz.dataProcess.core import validate_inputs
|
|
30
|
+
|
|
31
|
+
# =============================================================================
|
|
32
|
+
# Constants
|
|
33
|
+
# =============================================================================
|
|
34
|
+
|
|
35
|
+
# Required input columns
|
|
36
|
+
REQUIRED_COLUMNS = ['NH4+', 'SO42-', 'NO3-', 'Fe', 'Na+', 'OC', 'EC']
|
|
37
|
+
|
|
38
|
+
# Input column descriptions 輸入欄位說明
|
|
39
|
+
COLUMN_DESCRIPTIONS = {
|
|
40
|
+
'NH4+': 'Particulate Ammonium (μg/m³) 顆粒態銨鹽',
|
|
41
|
+
'SO42-': 'Particulate Sulfate (μg/m³) 顆粒態硫酸鹽',
|
|
42
|
+
'NO3-': 'Particulate Nitrate (μg/m³) 顆粒態硝酸鹽',
|
|
43
|
+
'Fe': 'Iron (μg/m³) 鐵 - 用於計算土壤/地殼物質 Soil',
|
|
44
|
+
'Na+': 'Sodium (μg/m³) 鈉 - 用於計算海鹽 Sea Salt',
|
|
45
|
+
'OC': 'Organic Carbon (μg/m³) 有機碳 - 用於計算有機物 OM',
|
|
46
|
+
'EC': 'Elemental Carbon (μg/m³) 元素碳',
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# Molecular weights (g/mol)
|
|
50
|
+
MOLECULAR_WEIGHT = {
|
|
51
|
+
'NH4+': 18,
|
|
52
|
+
'SO42-': 96,
|
|
53
|
+
'NO3-': 62,
|
|
54
|
+
'AS': 132, # (NH4)2SO4
|
|
55
|
+
'AN': 80, # NH4NO3
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# Conversion: raw species -> reconstructed species
|
|
59
|
+
SPECIES_MAPPING = {
|
|
60
|
+
'AS': 'SO42-',
|
|
61
|
+
'AN': 'NO3-',
|
|
62
|
+
'OM': 'OC',
|
|
63
|
+
'Soil': 'Fe',
|
|
64
|
+
'SS': 'Na+',
|
|
65
|
+
'EC': 'EC',
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# Mass reconstruction coefficients
|
|
69
|
+
# AS: (NH4)2SO4 / SO4 = 132/96 = 1.375
|
|
70
|
+
# AN: NH4NO3 / NO3 = 80/62 = 1.29
|
|
71
|
+
MASS_COEFFICIENTS = {
|
|
72
|
+
'AS': 1.375,
|
|
73
|
+
'AN': 1.29,
|
|
74
|
+
'OM': 1.8,
|
|
75
|
+
'Soil': 28.57,
|
|
76
|
+
'SS': 2.54,
|
|
77
|
+
'EC': 1.0,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
# Density for volume calculation (g/cm3)
|
|
81
|
+
DENSITY_COEFFICIENTS = {
|
|
82
|
+
'AS': 1.76,
|
|
83
|
+
'AN': 1.73,
|
|
84
|
+
'OM': 1.4,
|
|
85
|
+
'Soil': 2.6,
|
|
86
|
+
'SS': 2.16,
|
|
87
|
+
'EC': 1.5,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# Refractive index at different wavelengths (n + kj)
|
|
91
|
+
REFRACTIVE_INDEX = {
|
|
92
|
+
'550': {
|
|
93
|
+
'ALWC': 1.333 + 0j,
|
|
94
|
+
'AS': 1.53 + 0j,
|
|
95
|
+
'AN': 1.55 + 0j,
|
|
96
|
+
'OM': 1.55 + 0.0163j,
|
|
97
|
+
'Soil': 1.56 + 0.006j,
|
|
98
|
+
'SS': 1.54 + 0j,
|
|
99
|
+
'EC': 1.80 + 0.72j,
|
|
100
|
+
},
|
|
101
|
+
'450': {
|
|
102
|
+
'ALWC': 1.333 + 0j,
|
|
103
|
+
'AS': 1.57 + 0j,
|
|
104
|
+
'AN': 1.57 + 0j,
|
|
105
|
+
'OM': 1.58 + 0.056j,
|
|
106
|
+
'Soil': 1.56 + 0.009j,
|
|
107
|
+
'SS': 1.54 + 0j,
|
|
108
|
+
'EC': 1.80 + 0.79j,
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# =============================================================================
|
|
114
|
+
# Helper Functions
|
|
115
|
+
# =============================================================================
|
|
116
|
+
|
|
117
|
+
def calculate_molar_concentrations(df):
|
|
118
|
+
"""Calculate molar concentrations from mass concentrations."""
|
|
119
|
+
mol_NH4 = df['NH4+'] / MOLECULAR_WEIGHT['NH4+']
|
|
120
|
+
mol_SO4 = df['SO42-'] / MOLECULAR_WEIGHT['SO42-']
|
|
121
|
+
mol_NO3 = df['NO3-'] / MOLECULAR_WEIGHT['NO3-']
|
|
122
|
+
return mol_NH4, mol_SO4, mol_NO3
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def calculate_nh4_status(mol_NH4, mol_SO4, mol_NO3, index):
|
|
126
|
+
"""
|
|
127
|
+
Calculate ammonium status (neutralization ratio).
|
|
128
|
+
|
|
129
|
+
NH4 status = mol_NH4 / (2 * mol_SO4 + mol_NO3)
|
|
130
|
+
- >= 1: Ammonium sufficient (Enough)
|
|
131
|
+
- < 1: Ammonium deficient (Deficiency)
|
|
132
|
+
"""
|
|
133
|
+
ratio = mol_NH4 / (2 * mol_SO4 + mol_NO3)
|
|
134
|
+
|
|
135
|
+
df_status = DataFrame(index=index)
|
|
136
|
+
df_status['ratio'] = ratio
|
|
137
|
+
df_status['status'] = ratio.apply(lambda x: 'Enough' if x >= 1 else 'Deficiency')
|
|
138
|
+
|
|
139
|
+
return df_status, ratio
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def reconstruct_mass_enough(df, mol_NH4, mol_SO4, mol_NO3):
|
|
143
|
+
"""
|
|
144
|
+
Reconstruct mass for NH4-sufficient conditions.
|
|
145
|
+
|
|
146
|
+
When NH4 is sufficient:
|
|
147
|
+
- AS = SO42- * 1.375 (full neutralization)
|
|
148
|
+
- AN = NO3- * 1.29 (full neutralization)
|
|
149
|
+
"""
|
|
150
|
+
df_mass = DataFrame(index=df.index)
|
|
151
|
+
|
|
152
|
+
for species, coef in MASS_COEFFICIENTS.items():
|
|
153
|
+
raw_col = SPECIES_MAPPING[species]
|
|
154
|
+
df_mass[species] = df[raw_col] * coef
|
|
155
|
+
|
|
156
|
+
return df_mass
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def adjust_mass_deficiency(df_mass, mol_NH4, mol_SO4, mol_NO3, status_ratio):
|
|
160
|
+
"""
|
|
161
|
+
Adjust AS and AN mass for NH4-deficient conditions.
|
|
162
|
+
|
|
163
|
+
When NH4 is deficient (ratio < 1):
|
|
164
|
+
1. Calculate residual NH4 after neutralizing SO4: residual = mol_NH4 - 2*mol_SO4
|
|
165
|
+
2. If residual > 0: Some NH4 left to neutralize NO3
|
|
166
|
+
- AN = min(residual, mol_NO3) * 80
|
|
167
|
+
3. If residual <= 0: Not enough NH4 even for SO4
|
|
168
|
+
- AN = 0
|
|
169
|
+
- AS = mol_NH4/2 * 132 (partial neutralization)
|
|
170
|
+
"""
|
|
171
|
+
deficient_mask = status_ratio < 1
|
|
172
|
+
if not deficient_mask.any():
|
|
173
|
+
return df_mass
|
|
174
|
+
|
|
175
|
+
residual = mol_NH4 - 2 * mol_SO4
|
|
176
|
+
|
|
177
|
+
# Case 1: residual > 0 (some NH4 left for NO3)
|
|
178
|
+
pos_residual = residual > 0
|
|
179
|
+
if pos_residual.any():
|
|
180
|
+
# AN limited by residual or available NO3
|
|
181
|
+
cond = pos_residual & (residual <= mol_NO3)
|
|
182
|
+
df_mass.loc[cond, 'AN'] = residual.loc[cond] * MOLECULAR_WEIGHT['AN']
|
|
183
|
+
|
|
184
|
+
cond = pos_residual & (residual > mol_NO3)
|
|
185
|
+
df_mass.loc[cond, 'AN'] = mol_NO3.loc[cond] * MOLECULAR_WEIGHT['AN']
|
|
186
|
+
|
|
187
|
+
# Case 2: residual <= 0 (not enough NH4 for SO4)
|
|
188
|
+
neg_residual = residual <= 0
|
|
189
|
+
if neg_residual.any():
|
|
190
|
+
df_mass.loc[neg_residual, 'AN'] = 0
|
|
191
|
+
|
|
192
|
+
# Partial AS neutralization
|
|
193
|
+
cond = neg_residual & (mol_NH4 <= 2 * mol_SO4)
|
|
194
|
+
df_mass.loc[cond, 'AS'] = mol_NH4.loc[cond] / 2 * MOLECULAR_WEIGHT['AS']
|
|
195
|
+
|
|
196
|
+
cond = neg_residual & (mol_NH4 > 2 * mol_SO4)
|
|
197
|
+
df_mass.loc[cond, 'AS'] = mol_SO4.loc[cond] * MOLECULAR_WEIGHT['AS']
|
|
198
|
+
|
|
199
|
+
return df_mass
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def calculate_volume(df_mass, df_water=None):
|
|
203
|
+
"""
|
|
204
|
+
Calculate species volume concentrations from mass using density coefficients.
|
|
205
|
+
|
|
206
|
+
Output columns:
|
|
207
|
+
- {species}_volume: Volume concentration for each species (μm³/m³)
|
|
208
|
+
- total_dry: Total dry aerosol volume concentration (μm³/m³)
|
|
209
|
+
- ALWC: Aerosol liquid water content volume (μm³/m³), if df_water provided
|
|
210
|
+
- total_wet: Total wet aerosol volume (μm³/m³), if df_water provided
|
|
211
|
+
"""
|
|
212
|
+
df_vol = DataFrame(index=df_mass.index)
|
|
213
|
+
|
|
214
|
+
# Calculate dry volumes (μg/m³ / g/cm³ = μm³/m³)
|
|
215
|
+
for species, density in DENSITY_COEFFICIENTS.items():
|
|
216
|
+
if species in df_mass.columns:
|
|
217
|
+
df_vol[f'{species}_volume'] = df_mass[species] / density
|
|
218
|
+
|
|
219
|
+
# Total dry aerosol volume concentration
|
|
220
|
+
volume_cols = [f'{sp}_volume' for sp in DENSITY_COEFFICIENTS.keys() if f'{sp}_volume' in df_vol.columns]
|
|
221
|
+
df_vol['total_dry'] = df_vol[volume_cols].sum(axis=1, min_count=6)
|
|
222
|
+
|
|
223
|
+
# Add ALWC (Aerosol Liquid Water Content) if provided
|
|
224
|
+
if df_water is not None:
|
|
225
|
+
df_vol['ALWC'] = df_water.copy()
|
|
226
|
+
df_vol = df_vol.dropna()
|
|
227
|
+
df_vol['total_wet'] = df_vol['total_dry'] + df_vol['ALWC']
|
|
228
|
+
|
|
229
|
+
return df_vol
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def calculate_refractive_index(df_vol, df_water=None):
|
|
233
|
+
"""
|
|
234
|
+
Calculate volume-weighted refractive index at 550nm and 450nm.
|
|
235
|
+
|
|
236
|
+
Output:
|
|
237
|
+
- RI_dry: Dry aerosol refractive index (complex: n + kj)
|
|
238
|
+
- RI_wet: Wet aerosol refractive index (if ALWC provided)
|
|
239
|
+
"""
|
|
240
|
+
ri_results = {}
|
|
241
|
+
|
|
242
|
+
for wavelength, ri_coef in REFRACTIVE_INDEX.items():
|
|
243
|
+
df_ri = DataFrame(index=df_vol.index)
|
|
244
|
+
|
|
245
|
+
# Calculate RI contribution from each species (volume * RI)
|
|
246
|
+
available_species = []
|
|
247
|
+
for species in DENSITY_COEFFICIENTS.keys():
|
|
248
|
+
vol_col = f'{species}_volume'
|
|
249
|
+
if vol_col in df_vol.columns:
|
|
250
|
+
df_ri[species] = df_vol[vol_col] * ri_coef[species]
|
|
251
|
+
available_species.append(species)
|
|
252
|
+
|
|
253
|
+
# Dry RI (volume-weighted average): sum(Vi * RIi) / total_V
|
|
254
|
+
df_ri['RI_dry'] = (df_ri[available_species] / df_vol['total_dry'].values.reshape(-1, 1)).sum(axis=1)
|
|
255
|
+
|
|
256
|
+
# Wet RI (if ALWC provided)
|
|
257
|
+
df_ri['RI_wet'] = None
|
|
258
|
+
if df_water is not None and 'total_wet' in df_vol.columns:
|
|
259
|
+
df_ri['ALWC'] = df_vol['ALWC'] * ri_coef['ALWC']
|
|
260
|
+
all_species = available_species + ['ALWC']
|
|
261
|
+
df_ri['RI_wet'] = (df_ri[all_species] / df_vol['total_wet'].values.reshape(-1, 1)).sum(axis=1)
|
|
262
|
+
|
|
263
|
+
ri_results[f'RI_{wavelength}'] = df_ri[['RI_dry', 'RI_wet']]
|
|
264
|
+
|
|
265
|
+
return ri_results
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def calculate_density(df_mass, df_vol, df_all, df_density=None):
|
|
269
|
+
"""Calculate aerosol density (reconstructed and measured)."""
|
|
270
|
+
# Reconstructed density
|
|
271
|
+
density_rec = df_mass['total'] / df_vol['total_dry']
|
|
272
|
+
|
|
273
|
+
# Measured density (if density data provided)
|
|
274
|
+
if df_density is not None:
|
|
275
|
+
df_den_all = concat([
|
|
276
|
+
df_all[['SO42-', 'NO3-', 'NH4+', 'EC']],
|
|
277
|
+
df_density,
|
|
278
|
+
df_mass['OM']
|
|
279
|
+
], axis=1).dropna()
|
|
280
|
+
|
|
281
|
+
vol_cal = (
|
|
282
|
+
df_den_all[['SO42-', 'NO3-', 'NH4+']].sum(axis=1) / 1.75 +
|
|
283
|
+
df_den_all['Cl-'] / 1.52 +
|
|
284
|
+
df_den_all['OM'] / 1.4 +
|
|
285
|
+
df_den_all['EC'] / 1.77
|
|
286
|
+
)
|
|
287
|
+
density_mat = df_den_all.sum(axis=1, min_count=6) / vol_cal
|
|
288
|
+
else:
|
|
289
|
+
vol_cal = DataFrame()
|
|
290
|
+
density_mat = density_rec
|
|
291
|
+
|
|
292
|
+
return density_mat, density_rec, vol_cal
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def calculate_equivalents(mol_NH4, mol_SO4, mol_NO3):
|
|
296
|
+
"""Calculate molar and equivalent concentrations."""
|
|
297
|
+
df_eq = concat([
|
|
298
|
+
mol_NH4, mol_SO4, mol_NO3,
|
|
299
|
+
mol_NH4 * 1, # eq_NH4 (charge = 1)
|
|
300
|
+
mol_SO4 * 2, # eq_SO4 (charge = 2)
|
|
301
|
+
mol_NO3 * 1 # eq_NO3 (charge = 1)
|
|
302
|
+
], axis=1)
|
|
303
|
+
df_eq.columns = ['mol_NH4', 'mol_SO4', 'mol_NO3', 'eq_NH4', 'eq_SO4', 'eq_NO3']
|
|
304
|
+
return df_eq
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
# =============================================================================
|
|
308
|
+
# Main Function
|
|
309
|
+
# =============================================================================
|
|
310
|
+
|
|
311
|
+
def reconstruction_basic(df_che, df_ref, df_water=None, df_density=None, nam_lst=None):
|
|
312
|
+
"""
|
|
313
|
+
Reconstruct aerosol mass and volume from chemical composition.
|
|
314
|
+
|
|
315
|
+
This function converts ionic species (NH4+, SO42-, NO3-, etc.) to
|
|
316
|
+
reconstructed species (AS, AN, OM, Soil, SS, EC) considering the
|
|
317
|
+
ammonium neutralization status.
|
|
318
|
+
|
|
319
|
+
Parameters
|
|
320
|
+
----------
|
|
321
|
+
df_che : tuple of DataFrames
|
|
322
|
+
Chemical composition data. Will be concatenated and renamed to nam_lst.
|
|
323
|
+
df_ref : DataFrame or Series
|
|
324
|
+
Reference mass (e.g., PM2.5) for quality control.
|
|
325
|
+
df_water : DataFrame or None, optional
|
|
326
|
+
Aerosol liquid water content (ALWC).
|
|
327
|
+
df_density : DataFrame or None, optional
|
|
328
|
+
Measured density data (requires 'Cl-' column).
|
|
329
|
+
nam_lst : list, optional
|
|
330
|
+
Column names for df_che after concatenation.
|
|
331
|
+
Default: ['NH4+', 'SO42-', 'NO3-', 'Fe', 'Na+', 'OC', 'EC']
|
|
332
|
+
|
|
333
|
+
Returns
|
|
334
|
+
-------
|
|
335
|
+
dict
|
|
336
|
+
Dictionary containing:
|
|
337
|
+
- 'mass': Reconstructed mass (AS, AN, OM, Soil, SS, EC, total)
|
|
338
|
+
- 'volume': Reconstructed volume (species + total_dry, total_wet)
|
|
339
|
+
- 'vol_cal': Calculated volume for density
|
|
340
|
+
- 'eq': Molar and equivalent concentrations
|
|
341
|
+
- 'NH4_status': Ammonium status ('ratio' and 'status')
|
|
342
|
+
- 'density_mat': Measured density
|
|
343
|
+
- 'density_rec': Reconstructed density
|
|
344
|
+
- 'RI_550': Refractive index at 550nm
|
|
345
|
+
- 'RI_450': Refractive index at 450nm
|
|
346
|
+
|
|
347
|
+
Raises
|
|
348
|
+
------
|
|
349
|
+
ValueError
|
|
350
|
+
If required columns are missing.
|
|
351
|
+
|
|
352
|
+
Examples
|
|
353
|
+
--------
|
|
354
|
+
>>> result = reconstruction_basic(
|
|
355
|
+
... df_che=(df_ions, df_carbon),
|
|
356
|
+
... df_ref=df_pm25,
|
|
357
|
+
... df_water=df_alwc,
|
|
358
|
+
... nam_lst=['NH4+', 'SO42-', 'NO3-', 'Fe', 'Na+', 'OC', 'EC']
|
|
359
|
+
... )
|
|
360
|
+
>>> result['mass'] # Reconstructed mass
|
|
361
|
+
>>> result['NH4_status'] # Ammonium status
|
|
362
|
+
"""
|
|
363
|
+
# Default column names
|
|
364
|
+
if nam_lst is None:
|
|
365
|
+
nam_lst = REQUIRED_COLUMNS
|
|
366
|
+
|
|
367
|
+
# Prepare input data
|
|
368
|
+
df_all = concat(df_che, axis=1)
|
|
369
|
+
original_index = df_all.index.copy()
|
|
370
|
+
df_all.columns = nam_lst
|
|
371
|
+
|
|
372
|
+
# Validate required columns
|
|
373
|
+
validate_inputs(df_all, REQUIRED_COLUMNS, 'reconstruction_basic', COLUMN_DESCRIPTIONS)
|
|
374
|
+
|
|
375
|
+
# Step 1: Calculate molar concentrations
|
|
376
|
+
mol_NH4, mol_SO4, mol_NO3 = calculate_molar_concentrations(df_all)
|
|
377
|
+
|
|
378
|
+
# Step 2: Calculate NH4 status
|
|
379
|
+
df_nh4_status, status_ratio = calculate_nh4_status(mol_NH4, mol_SO4, mol_NO3, original_index)
|
|
380
|
+
|
|
381
|
+
# Step 3: Reconstruct mass (assuming NH4 sufficient)
|
|
382
|
+
df_mass = reconstruct_mass_enough(df_all, mol_NH4, mol_SO4, mol_NO3)
|
|
383
|
+
|
|
384
|
+
# Step 4: Adjust for NH4 deficiency
|
|
385
|
+
df_mass = adjust_mass_deficiency(df_mass, mol_NH4, mol_SO4, mol_NO3, status_ratio)
|
|
386
|
+
df_mass['total'] = df_mass.sum(axis=1, min_count=6)
|
|
387
|
+
|
|
388
|
+
# Quality control ratio
|
|
389
|
+
qc_ratio = df_mass['total'] / df_ref
|
|
390
|
+
qc_valid = (qc_ratio >= 0.5) & (qc_ratio <= 1.5)
|
|
391
|
+
|
|
392
|
+
# Step 5: Calculate volume
|
|
393
|
+
df_mass_valid = df_mass.dropna().copy()
|
|
394
|
+
df_vol = calculate_volume(df_mass_valid, df_water)
|
|
395
|
+
|
|
396
|
+
# Step 6: Calculate density
|
|
397
|
+
density_mat, density_rec, vol_cal = calculate_density(df_mass, df_vol, df_all, df_density)
|
|
398
|
+
|
|
399
|
+
# Step 7: Calculate refractive index
|
|
400
|
+
ri_results = calculate_refractive_index(df_vol, df_water)
|
|
401
|
+
|
|
402
|
+
# Step 8: Calculate equivalents
|
|
403
|
+
df_eq = calculate_equivalents(mol_NH4, mol_SO4, mol_NO3)
|
|
404
|
+
|
|
405
|
+
# Compile output
|
|
406
|
+
out = {
|
|
407
|
+
'mass': df_mass,
|
|
408
|
+
'volume': df_vol,
|
|
409
|
+
'vol_cal': vol_cal,
|
|
410
|
+
'eq': df_eq,
|
|
411
|
+
'NH4_status': df_nh4_status,
|
|
412
|
+
'density_mat': density_mat,
|
|
413
|
+
'density_rec': density_rec,
|
|
414
|
+
}
|
|
415
|
+
out.update(ri_results)
|
|
416
|
+
|
|
417
|
+
# Reindex all outputs to original index
|
|
418
|
+
for key, value in out.items():
|
|
419
|
+
if hasattr(value, 'reindex'):
|
|
420
|
+
out[key] = value.reindex(original_index)
|
|
421
|
+
|
|
422
|
+
return out
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
# =============================================================================
|
|
426
|
+
# Utility Functions
|
|
427
|
+
# =============================================================================
|
|
428
|
+
|
|
429
|
+
def mass_ratio(df):
|
|
430
|
+
"""
|
|
431
|
+
Calculate mass ratios relative to PM2.5.
|
|
432
|
+
|
|
433
|
+
Parameters
|
|
434
|
+
----------
|
|
435
|
+
df : Series
|
|
436
|
+
Must contain 'PM25' and 'total_mass' values.
|
|
437
|
+
|
|
438
|
+
Returns
|
|
439
|
+
-------
|
|
440
|
+
Series
|
|
441
|
+
Mass ratios for each species.
|
|
442
|
+
"""
|
|
443
|
+
if df['PM25'] >= df['total_mass']:
|
|
444
|
+
df['others'] = df['PM25'] - df['total_mass']
|
|
445
|
+
else:
|
|
446
|
+
df['others'] = 0
|
|
447
|
+
|
|
448
|
+
for val, species in zip(df.values, df.index):
|
|
449
|
+
df[f'{species}_ratio'] = round(val / df['PM25'], 3)
|
|
450
|
+
|
|
451
|
+
return df['others':].drop(labels=['PM25_ratio', 'total_mass_ratio'])
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def get_required_columns():
|
|
455
|
+
"""
|
|
456
|
+
Get required column names and output descriptions.
|
|
457
|
+
|
|
458
|
+
Returns
|
|
459
|
+
-------
|
|
460
|
+
dict
|
|
461
|
+
Documentation for reconstruction_basic inputs and outputs.
|
|
462
|
+
"""
|
|
463
|
+
return {
|
|
464
|
+
'reconstruction_basic': {
|
|
465
|
+
'required_columns': REQUIRED_COLUMNS.copy(),
|
|
466
|
+
'column_descriptions': COLUMN_DESCRIPTIONS.copy(),
|
|
467
|
+
'outputs': {
|
|
468
|
+
'mass': 'Reconstructed mass (AS, AN, OM, Soil, SS, EC, total)',
|
|
469
|
+
'volume': 'Reconstructed volume with ALWC',
|
|
470
|
+
'eq': 'Molar and equivalent concentrations',
|
|
471
|
+
'NH4_status': "Ammonium status: 'ratio' and 'status' (Enough/Deficiency)",
|
|
472
|
+
'density_mat': 'Measured density',
|
|
473
|
+
'density_rec': 'Reconstructed density',
|
|
474
|
+
'RI_550': 'Refractive index at 550nm (RI_dry, RI_wet)',
|
|
475
|
+
'RI_450': 'Refractive index at 450nm (RI_dry, RI_wet)',
|
|
476
|
+
},
|
|
477
|
+
'coefficients': {
|
|
478
|
+
'mass': MASS_COEFFICIENTS,
|
|
479
|
+
'density': DENSITY_COEFFICIENTS,
|
|
480
|
+
}
|
|
481
|
+
},
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
# Backward compatibility
|
|
486
|
+
_basic = reconstruction_basic
|
|
487
|
+
DEFAULT_REQUIRED_COLUMNS = REQUIRED_COLUMNS
|