AeroViz 0.1.21__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. AeroViz/__init__.py +13 -0
  2. AeroViz/__pycache__/__init__.cpython-312.pyc +0 -0
  3. AeroViz/data/DEFAULT_DATA.csv +1417 -0
  4. AeroViz/data/DEFAULT_PNSD_DATA.csv +1417 -0
  5. AeroViz/data/hysplit_example_data.txt +101 -0
  6. AeroViz/dataProcess/Chemistry/__init__.py +149 -0
  7. AeroViz/dataProcess/Chemistry/__pycache__/__init__.cpython-312.pyc +0 -0
  8. AeroViz/dataProcess/Chemistry/_calculate.py +557 -0
  9. AeroViz/dataProcess/Chemistry/_isoropia.py +150 -0
  10. AeroViz/dataProcess/Chemistry/_mass_volume.py +487 -0
  11. AeroViz/dataProcess/Chemistry/_ocec.py +172 -0
  12. AeroViz/dataProcess/Chemistry/isrpia.cnf +21 -0
  13. AeroViz/dataProcess/Chemistry/isrpia2.exe +0 -0
  14. AeroViz/dataProcess/Optical/PyMieScatt_update.py +577 -0
  15. AeroViz/dataProcess/Optical/_IMPROVE.py +452 -0
  16. AeroViz/dataProcess/Optical/__init__.py +281 -0
  17. AeroViz/dataProcess/Optical/__pycache__/PyMieScatt_update.cpython-312.pyc +0 -0
  18. AeroViz/dataProcess/Optical/__pycache__/__init__.cpython-312.pyc +0 -0
  19. AeroViz/dataProcess/Optical/__pycache__/mie_theory.cpython-312.pyc +0 -0
  20. AeroViz/dataProcess/Optical/_derived.py +518 -0
  21. AeroViz/dataProcess/Optical/_extinction.py +123 -0
  22. AeroViz/dataProcess/Optical/_mie_sd.py +912 -0
  23. AeroViz/dataProcess/Optical/_retrieve_RI.py +243 -0
  24. AeroViz/dataProcess/Optical/coefficient.py +72 -0
  25. AeroViz/dataProcess/Optical/fRH.pkl +0 -0
  26. AeroViz/dataProcess/Optical/mie_theory.py +260 -0
  27. AeroViz/dataProcess/README.md +271 -0
  28. AeroViz/dataProcess/SizeDistr/__init__.py +245 -0
  29. AeroViz/dataProcess/SizeDistr/__pycache__/__init__.cpython-312.pyc +0 -0
  30. AeroViz/dataProcess/SizeDistr/__pycache__/_size_dist.cpython-312.pyc +0 -0
  31. AeroViz/dataProcess/SizeDistr/_size_dist.py +810 -0
  32. AeroViz/dataProcess/SizeDistr/merge/README.md +93 -0
  33. AeroViz/dataProcess/SizeDistr/merge/__init__.py +20 -0
  34. AeroViz/dataProcess/SizeDistr/merge/_merge_v0.py +251 -0
  35. AeroViz/dataProcess/SizeDistr/merge/_merge_v0_1.py +246 -0
  36. AeroViz/dataProcess/SizeDistr/merge/_merge_v1.py +255 -0
  37. AeroViz/dataProcess/SizeDistr/merge/_merge_v2.py +244 -0
  38. AeroViz/dataProcess/SizeDistr/merge/_merge_v3.py +518 -0
  39. AeroViz/dataProcess/SizeDistr/merge/_merge_v4.py +422 -0
  40. AeroViz/dataProcess/SizeDistr/prop.py +62 -0
  41. AeroViz/dataProcess/VOC/__init__.py +14 -0
  42. AeroViz/dataProcess/VOC/__pycache__/__init__.cpython-312.pyc +0 -0
  43. AeroViz/dataProcess/VOC/_potential_par.py +108 -0
  44. AeroViz/dataProcess/VOC/support_voc.json +446 -0
  45. AeroViz/dataProcess/__init__.py +66 -0
  46. AeroViz/dataProcess/__pycache__/__init__.cpython-312.pyc +0 -0
  47. AeroViz/dataProcess/core/__init__.py +272 -0
  48. AeroViz/dataProcess/core/__pycache__/__init__.cpython-312.pyc +0 -0
  49. AeroViz/mcp_server.py +352 -0
  50. AeroViz/plot/__init__.py +13 -0
  51. AeroViz/plot/__pycache__/__init__.cpython-312.pyc +0 -0
  52. AeroViz/plot/__pycache__/bar.cpython-312.pyc +0 -0
  53. AeroViz/plot/__pycache__/box.cpython-312.pyc +0 -0
  54. AeroViz/plot/__pycache__/pie.cpython-312.pyc +0 -0
  55. AeroViz/plot/__pycache__/radar.cpython-312.pyc +0 -0
  56. AeroViz/plot/__pycache__/regression.cpython-312.pyc +0 -0
  57. AeroViz/plot/__pycache__/scatter.cpython-312.pyc +0 -0
  58. AeroViz/plot/__pycache__/violin.cpython-312.pyc +0 -0
  59. AeroViz/plot/bar.py +126 -0
  60. AeroViz/plot/box.py +69 -0
  61. AeroViz/plot/distribution/__init__.py +1 -0
  62. AeroViz/plot/distribution/__pycache__/__init__.cpython-312.pyc +0 -0
  63. AeroViz/plot/distribution/__pycache__/distribution.cpython-312.pyc +0 -0
  64. AeroViz/plot/distribution/distribution.py +576 -0
  65. AeroViz/plot/meteorology/CBPF.py +295 -0
  66. AeroViz/plot/meteorology/__init__.py +3 -0
  67. AeroViz/plot/meteorology/__pycache__/CBPF.cpython-312.pyc +0 -0
  68. AeroViz/plot/meteorology/__pycache__/__init__.cpython-312.pyc +0 -0
  69. AeroViz/plot/meteorology/__pycache__/hysplit.cpython-312.pyc +0 -0
  70. AeroViz/plot/meteorology/__pycache__/wind_rose.cpython-312.pyc +0 -0
  71. AeroViz/plot/meteorology/hysplit.py +93 -0
  72. AeroViz/plot/meteorology/wind_rose.py +77 -0
  73. AeroViz/plot/optical/__init__.py +1 -0
  74. AeroViz/plot/optical/__pycache__/__init__.cpython-312.pyc +0 -0
  75. AeroViz/plot/optical/__pycache__/optical.cpython-312.pyc +0 -0
  76. AeroViz/plot/optical/optical.py +388 -0
  77. AeroViz/plot/pie.py +210 -0
  78. AeroViz/plot/radar.py +184 -0
  79. AeroViz/plot/regression.py +200 -0
  80. AeroViz/plot/scatter.py +174 -0
  81. AeroViz/plot/templates/__init__.py +6 -0
  82. AeroViz/plot/templates/__pycache__/__init__.cpython-312.pyc +0 -0
  83. AeroViz/plot/templates/__pycache__/ammonium_rich.cpython-312.pyc +0 -0
  84. AeroViz/plot/templates/__pycache__/contour.cpython-312.pyc +0 -0
  85. AeroViz/plot/templates/__pycache__/corr_matrix.cpython-312.pyc +0 -0
  86. AeroViz/plot/templates/__pycache__/diurnal_pattern.cpython-312.pyc +0 -0
  87. AeroViz/plot/templates/__pycache__/koschmieder.cpython-312.pyc +0 -0
  88. AeroViz/plot/templates/__pycache__/metal_heatmap.cpython-312.pyc +0 -0
  89. AeroViz/plot/templates/ammonium_rich.py +34 -0
  90. AeroViz/plot/templates/contour.py +47 -0
  91. AeroViz/plot/templates/corr_matrix.py +267 -0
  92. AeroViz/plot/templates/diurnal_pattern.py +61 -0
  93. AeroViz/plot/templates/koschmieder.py +95 -0
  94. AeroViz/plot/templates/metal_heatmap.py +164 -0
  95. AeroViz/plot/timeseries/__init__.py +2 -0
  96. AeroViz/plot/timeseries/__pycache__/__init__.cpython-312.pyc +0 -0
  97. AeroViz/plot/timeseries/__pycache__/template.cpython-312.pyc +0 -0
  98. AeroViz/plot/timeseries/__pycache__/timeseries.cpython-312.pyc +0 -0
  99. AeroViz/plot/timeseries/template.py +47 -0
  100. AeroViz/plot/timeseries/timeseries.py +446 -0
  101. AeroViz/plot/utils/__init__.py +4 -0
  102. AeroViz/plot/utils/__pycache__/__init__.cpython-312.pyc +0 -0
  103. AeroViz/plot/utils/__pycache__/_color.cpython-312.pyc +0 -0
  104. AeroViz/plot/utils/__pycache__/_unit.cpython-312.pyc +0 -0
  105. AeroViz/plot/utils/__pycache__/plt_utils.cpython-312.pyc +0 -0
  106. AeroViz/plot/utils/__pycache__/sklearn_utils.cpython-312.pyc +0 -0
  107. AeroViz/plot/utils/_color.py +71 -0
  108. AeroViz/plot/utils/_unit.py +55 -0
  109. AeroViz/plot/utils/fRH.json +390 -0
  110. AeroViz/plot/utils/plt_utils.py +92 -0
  111. AeroViz/plot/utils/sklearn_utils.py +49 -0
  112. AeroViz/plot/utils/units.json +89 -0
  113. AeroViz/plot/violin.py +80 -0
  114. AeroViz/rawDataReader/FLOW.md +138 -0
  115. AeroViz/rawDataReader/__init__.py +220 -0
  116. AeroViz/rawDataReader/__pycache__/__init__.cpython-312.pyc +0 -0
  117. AeroViz/rawDataReader/config/__init__.py +0 -0
  118. AeroViz/rawDataReader/config/__pycache__/__init__.cpython-312.pyc +0 -0
  119. AeroViz/rawDataReader/config/__pycache__/supported_instruments.cpython-312.pyc +0 -0
  120. AeroViz/rawDataReader/config/supported_instruments.py +135 -0
  121. AeroViz/rawDataReader/core/__init__.py +658 -0
  122. AeroViz/rawDataReader/core/__pycache__/__init__.cpython-312.pyc +0 -0
  123. AeroViz/rawDataReader/core/__pycache__/logger.cpython-312.pyc +0 -0
  124. AeroViz/rawDataReader/core/__pycache__/pre_process.cpython-312.pyc +0 -0
  125. AeroViz/rawDataReader/core/__pycache__/qc.cpython-312.pyc +0 -0
  126. AeroViz/rawDataReader/core/__pycache__/report.cpython-312.pyc +0 -0
  127. AeroViz/rawDataReader/core/logger.py +171 -0
  128. AeroViz/rawDataReader/core/pre_process.py +308 -0
  129. AeroViz/rawDataReader/core/qc.py +961 -0
  130. AeroViz/rawDataReader/core/report.py +579 -0
  131. AeroViz/rawDataReader/script/AE33.py +173 -0
  132. AeroViz/rawDataReader/script/AE43.py +151 -0
  133. AeroViz/rawDataReader/script/APS.py +339 -0
  134. AeroViz/rawDataReader/script/Aurora.py +191 -0
  135. AeroViz/rawDataReader/script/BAM1020.py +90 -0
  136. AeroViz/rawDataReader/script/BC1054.py +161 -0
  137. AeroViz/rawDataReader/script/EPA.py +79 -0
  138. AeroViz/rawDataReader/script/GRIMM.py +68 -0
  139. AeroViz/rawDataReader/script/IGAC.py +140 -0
  140. AeroViz/rawDataReader/script/MA350.py +179 -0
  141. AeroViz/rawDataReader/script/Minion.py +218 -0
  142. AeroViz/rawDataReader/script/NEPH.py +199 -0
  143. AeroViz/rawDataReader/script/OCEC.py +173 -0
  144. AeroViz/rawDataReader/script/Q-ACSM.py +12 -0
  145. AeroViz/rawDataReader/script/SMPS.py +389 -0
  146. AeroViz/rawDataReader/script/TEOM.py +181 -0
  147. AeroViz/rawDataReader/script/VOC.py +106 -0
  148. AeroViz/rawDataReader/script/Xact.py +244 -0
  149. AeroViz/rawDataReader/script/__init__.py +28 -0
  150. AeroViz/rawDataReader/script/__pycache__/AE33.cpython-312.pyc +0 -0
  151. AeroViz/rawDataReader/script/__pycache__/AE43.cpython-312.pyc +0 -0
  152. AeroViz/rawDataReader/script/__pycache__/APS.cpython-312.pyc +0 -0
  153. AeroViz/rawDataReader/script/__pycache__/Aurora.cpython-312.pyc +0 -0
  154. AeroViz/rawDataReader/script/__pycache__/BAM1020.cpython-312.pyc +0 -0
  155. AeroViz/rawDataReader/script/__pycache__/BC1054.cpython-312.pyc +0 -0
  156. AeroViz/rawDataReader/script/__pycache__/EPA.cpython-312.pyc +0 -0
  157. AeroViz/rawDataReader/script/__pycache__/GRIMM.cpython-312.pyc +0 -0
  158. AeroViz/rawDataReader/script/__pycache__/IGAC.cpython-312.pyc +0 -0
  159. AeroViz/rawDataReader/script/__pycache__/MA350.cpython-312.pyc +0 -0
  160. AeroViz/rawDataReader/script/__pycache__/Minion.cpython-312.pyc +0 -0
  161. AeroViz/rawDataReader/script/__pycache__/NEPH.cpython-312.pyc +0 -0
  162. AeroViz/rawDataReader/script/__pycache__/OCEC.cpython-312.pyc +0 -0
  163. AeroViz/rawDataReader/script/__pycache__/Q-ACSM.cpython-312.pyc +0 -0
  164. AeroViz/rawDataReader/script/__pycache__/SMPS.cpython-312.pyc +0 -0
  165. AeroViz/rawDataReader/script/__pycache__/TEOM.cpython-312.pyc +0 -0
  166. AeroViz/rawDataReader/script/__pycache__/VOC.cpython-312.pyc +0 -0
  167. AeroViz/rawDataReader/script/__pycache__/Xact.cpython-312.pyc +0 -0
  168. AeroViz/rawDataReader/script/__pycache__/__init__.cpython-312.pyc +0 -0
  169. AeroViz/tools/__init__.py +2 -0
  170. AeroViz/tools/__pycache__/__init__.cpython-312.pyc +0 -0
  171. AeroViz/tools/__pycache__/database.cpython-312.pyc +0 -0
  172. AeroViz/tools/__pycache__/dataclassifier.cpython-312.pyc +0 -0
  173. AeroViz/tools/database.py +95 -0
  174. AeroViz/tools/dataclassifier.py +117 -0
  175. AeroViz/tools/dataprinter.py +58 -0
  176. aeroviz-0.1.21.dist-info/METADATA +294 -0
  177. aeroviz-0.1.21.dist-info/RECORD +180 -0
  178. aeroviz-0.1.21.dist-info/WHEEL +5 -0
  179. aeroviz-0.1.21.dist-info/licenses/LICENSE +21 -0
  180. aeroviz-0.1.21.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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