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,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