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,171 @@
1
+ import codecs
2
+ import logging
3
+ import os
4
+ import platform
5
+ import re
6
+ import sys
7
+ from pathlib import Path
8
+
9
+
10
+ class ReaderLogger:
11
+ def __init__(self, name: str, log_path: Path, log_level: str = 'INFO'):
12
+ self.name = name
13
+ self.log_path = log_path
14
+ self._log_level = getattr(logging, log_level)
15
+
16
+ # 檢查是否支持顏色輸出
17
+ self.color_support = self._check_color_support()
18
+
19
+ # 設置顏色代碼
20
+ if self.color_support:
21
+ self.CYAN = '\033[96m'
22
+ self.BLUE = '\033[94m'
23
+ self.GREEN = '\033[92m'
24
+ self.YELLOW = '\033[93m'
25
+ self.RED = '\033[91m'
26
+ self.RESET = '\033[0m'
27
+ else:
28
+ self.CYAN = ''
29
+ self.BLUE = ''
30
+ self.GREEN = ''
31
+ self.YELLOW = ''
32
+ self.RED = ''
33
+ self.RESET = ''
34
+
35
+ # 檢查 Unicode 支持
36
+ self.unicode_support = self._check_unicode_support()
37
+
38
+ # 設置框架字符
39
+ if self.unicode_support:
40
+ self.BOX_TOP_LEFT = "╭"
41
+ self.BOX_TOP_RIGHT = "╮"
42
+ self.BOX_BOTTOM_LEFT = "╰"
43
+ self.BOX_BOTTOM_RIGHT = "╯"
44
+ self.BOX_HORIZONTAL = "─"
45
+ self.BOX_VERTICAL = "│"
46
+ self.ARROW = "▶"
47
+ else:
48
+ self.BOX_TOP_LEFT = "+"
49
+ self.BOX_TOP_RIGHT = "+"
50
+ self.BOX_BOTTOM_LEFT = "+"
51
+ self.BOX_BOTTOM_RIGHT = "+"
52
+ self.BOX_HORIZONTAL = "-"
53
+ self.BOX_VERTICAL = "|"
54
+ self.ARROW = ">"
55
+
56
+ self.logger = self._setup_logger()
57
+
58
+ def _check_color_support(self) -> bool:
59
+ """檢查環境是否支持顏色輸出"""
60
+ # 檢查是否在 Spyder 或其他 IDE 中運行
61
+ if any(IDE in os.environ.get('PYTHONPATH', '') for IDE in ['spyder', 'jupyter']):
62
+ return False
63
+
64
+ # 檢查是否強制啟用或禁用顏色
65
+ if 'FORCE_COLOR' in os.environ:
66
+ return os.environ['FORCE_COLOR'].lower() in ('1', 'true', 'yes')
67
+
68
+ # Windows 檢查
69
+ if platform.system().lower() == 'windows':
70
+ return ('ANSICON' in os.environ or
71
+ 'WT_SESSION' in os.environ or # Windows Terminal
72
+ 'ConEmuANSI' in os.environ or
73
+ os.environ.get('TERM_PROGRAM', '').lower() == 'vscode')
74
+
75
+ # 其他系統檢查
76
+ return hasattr(sys.stdout, 'isatty') and sys.stdout.isatty()
77
+
78
+ def _check_unicode_support(self) -> bool:
79
+ """設置 Unicode 支持"""
80
+ if platform.system().lower() == 'windows':
81
+ try:
82
+ if hasattr(sys.stdout, 'reconfigure'):
83
+ sys.stdout.reconfigure(encoding='utf-8')
84
+ elif hasattr(sys.stdout, 'buffer'):
85
+ sys.stdout = codecs.getwriter('utf-8')(sys.stdout.buffer)
86
+ else:
87
+ return False
88
+ return True
89
+ except Exception:
90
+ return False
91
+ return True
92
+
93
+ def _setup_logger(self) -> logging.Logger:
94
+ """設置logger"""
95
+ logger = logging.getLogger(self.name)
96
+ logger.setLevel(self._log_level)
97
+
98
+ # 移除現有的 handlers
99
+ for handler in logger.handlers[:]:
100
+ handler.close()
101
+ logger.removeHandler(handler)
102
+
103
+ # 清理 ANSI 格式化器
104
+ class CleanFormatter(logging.Formatter):
105
+ def format(self, record):
106
+ formatted_msg = super().format(record)
107
+ return re.sub(r'\033\[[0-9;]*m', '', formatted_msg)
108
+
109
+ # 設置檔案處理器
110
+ try:
111
+ log_dir = Path(self.log_path)
112
+ log_dir.mkdir(parents=True, exist_ok=True)
113
+ file_handler = logging.FileHandler(
114
+ log_dir / f'{self.name}.log',
115
+ encoding='utf-8',
116
+ errors='replace'
117
+ )
118
+ file_handler.setFormatter(
119
+ CleanFormatter('%(asctime)s - %(message)s',
120
+ datefmt='%Y-%m-%d %H:%M:%S')
121
+ )
122
+ logger.addHandler(file_handler)
123
+ except Exception as e:
124
+ print(f"Warning: Could not set up file logging: {e}")
125
+
126
+ # 設置控制台處理器
127
+ console_handler = logging.StreamHandler(sys.stdout)
128
+ console_handler.setFormatter(logging.Formatter('%(message)s'))
129
+ logger.addHandler(console_handler)
130
+
131
+ return logger
132
+
133
+ def _safe_print(self, text: str) -> str:
134
+ """安全打印,處理編碼問題"""
135
+ if not self.unicode_support:
136
+ text = text.encode('ascii', 'replace').decode('ascii')
137
+ return text
138
+
139
+ def debug(self, msg: str):
140
+ self.logger.debug(self._safe_print(msg))
141
+
142
+ def info(self, msg: str):
143
+ self.logger.info(self._safe_print(msg))
144
+
145
+ def warning(self, msg: str):
146
+ self.logger.warning(self._safe_print(msg))
147
+
148
+ def error(self, msg: str):
149
+ self.logger.error(self._safe_print(msg))
150
+
151
+ def info_box(self, text: str, color_part: str = None, width: int = 80):
152
+ """創建帶框的消息,可選擇性地為部分文本著色"""
153
+ # 處理文本
154
+ display_text = text.replace(color_part, " " * len(color_part)) if color_part else text
155
+
156
+ # 計算padding
157
+ left_padding = " " * ((width - len(display_text)) // 2)
158
+ right_padding = " " * (width - len(display_text) - len(left_padding))
159
+
160
+ # 處理顏色
161
+ if color_part and self.color_support:
162
+ content = text.replace(color_part, f"{self.CYAN}{color_part}{self.RESET}")
163
+ else:
164
+ content = text
165
+
166
+ __content__ = f"{left_padding}{content}{right_padding}"
167
+
168
+ # 使用當前設置的框架字符
169
+ self.info(f"{self.BOX_TOP_LEFT}{self.BOX_HORIZONTAL * width}{self.BOX_TOP_RIGHT}")
170
+ self.info(f"{self.BOX_VERTICAL}{__content__}{self.BOX_VERTICAL}")
171
+ self.info(f"{self.BOX_BOTTOM_LEFT}{self.BOX_HORIZONTAL * width}{self.BOX_BOTTOM_RIGHT}")
@@ -0,0 +1,308 @@
1
+ import numba
2
+ import numpy as np
3
+ import pandas as pd
4
+
5
+
6
+ @numba.jit(nopython=True)
7
+ def _angstrom_fit_numba(log_wavelengths, log_values):
8
+ """
9
+ Fast implementation of linear fit for Ångström exponent calculation using numba.
10
+
11
+ Parameters
12
+ ----------
13
+ log_wavelengths : numpy.ndarray
14
+ Log of wavelengths
15
+ log_values : numpy.ndarray
16
+ Log of measurement values
17
+
18
+ Returns
19
+ -------
20
+ tuple
21
+ Slope and intercept of the linear fit
22
+ """
23
+ n = len(log_wavelengths)
24
+ sum_x = np.sum(log_wavelengths)
25
+ sum_y = np.sum(log_values)
26
+ sum_xy = np.sum(log_wavelengths * log_values)
27
+ sum_xx = np.sum(log_wavelengths * log_wavelengths)
28
+
29
+ # Calculate slope and intercept
30
+ slope = (n * sum_xy - sum_x * sum_y) / (n * sum_xx - sum_x * sum_x)
31
+ intercept = (sum_y - slope * sum_x) / n
32
+
33
+ return slope, intercept
34
+
35
+
36
+ @numba.jit(nopython=True)
37
+ def calculate_bulk_angstrom_numba(abs_values, log_wavelengths):
38
+ """
39
+ JIT-compiled function to calculate Ångström exponents for multiple rows.
40
+
41
+ Parameters
42
+ ----------
43
+ abs_values : numpy.ndarray
44
+ 2D array of absorption values [n_samples, n_wavelengths]
45
+ log_wavelengths : numpy.ndarray
46
+ Log of wavelengths
47
+
48
+ Returns
49
+ -------
50
+ numpy.ndarray
51
+ Array of [slope, intercept] pairs for each row
52
+ """
53
+ n_samples = abs_values.shape[0]
54
+ results = np.empty((n_samples, 2))
55
+
56
+ for i in range(n_samples):
57
+ row = abs_values[i]
58
+
59
+ # Skip rows with zero or negative values
60
+ if np.any(row <= 0):
61
+ results[i, 0] = np.nan
62
+ results[i, 1] = np.nan
63
+ continue
64
+
65
+ log_values = np.log(row)
66
+ results[i, 0], results[i, 1] = _angstrom_fit_numba(log_wavelengths, log_values)
67
+
68
+ return results
69
+
70
+
71
+ @numba.jit(nopython=True)
72
+ def calculate_specific_wavelengths_numba(ref_values, ae_values, ratio_factor):
73
+ """
74
+ JIT-compiled function to calculate values at specific wavelengths using Ångström relation.
75
+
76
+ Parameters
77
+ ----------
78
+ ref_values : numpy.ndarray
79
+ Reference values at reference wavelength
80
+ ae_values : numpy.ndarray
81
+ Ångström exponent values (positive)
82
+ ratio_factor : float
83
+ Wavelength ratio factor (target_wl / ref_wl)
84
+
85
+ Returns
86
+ -------
87
+ numpy.ndarray
88
+ Calculated values at target wavelength
89
+
90
+ Notes
91
+ -----
92
+ This function implements the Ångström power law relationship:
93
+ X(λ₂) = X(λ₁) × (λ₂/λ₁)^(-AE)
94
+
95
+ where:
96
+ - X is either absorption or scattering coefficient
97
+ - AE is the Ångström exponent (AAE for absorption, SAE for scattering)
98
+ - The negative sign in the exponent reflects that both absorption and
99
+ scattering coefficients typically decrease with increasing wavelength
100
+
101
+ By convention, both AAE and SAE are reported as positive values in the literature,
102
+ with the negative sign included in the formula. This function expects positive
103
+ AE values and applies the negative sign internally.
104
+
105
+ Typical values:
106
+ - AAE: 1-2 for black carbon, higher for brown carbon and dust
107
+ - SAE: 0-4, with ~4 for small particles and ~0 for large particles
108
+
109
+ """
110
+ n_samples = len(ref_values)
111
+ results = np.empty(n_samples)
112
+
113
+ for i in range(n_samples):
114
+ if np.isnan(ae_values[i]):
115
+ results[i] = np.nan
116
+ else:
117
+ # Note the negative sign to follow the Ångström relation
118
+ results[i] = ref_values[i] * (ratio_factor ** -ae_values[i])
119
+
120
+ return results
121
+
122
+
123
+ def _scaCoe(df, instru, specified_band: list):
124
+ """
125
+ Calculate scattering coefficients and Ångström exponent for scattering.
126
+
127
+ Parameters
128
+ ----------
129
+ df : pandas.DataFrame
130
+ Data frame containing scattering measurements
131
+ instru : str
132
+ Instrument type ('Neph' or 'Aurora')
133
+ specified_band : list
134
+ List of wavelengths to calculate scattering coefficients for
135
+
136
+ Returns
137
+ -------
138
+ pandas.DataFrame
139
+ Data frame with scattering coefficients and Ångström exponent
140
+ """
141
+ band_Neph = np.array([450, 550, 700])
142
+ band_Aurora = np.array([450, 525, 635])
143
+
144
+ band = band_Neph if instru == 'Neph' else band_Aurora
145
+
146
+ # Create mask for valid rows to avoid copying data
147
+ mask = ~df[['B', 'G', 'R']].isna().any(axis=1)
148
+
149
+ # Pre-allocate output DataFrame
150
+ result_columns = [f'sca_{_band}' for _band in specified_band] + ['SAE']
151
+ result_df = pd.DataFrame(np.nan, index=df.index, columns=result_columns)
152
+
153
+ # Calculate only for valid rows
154
+ if mask.any():
155
+ if instru == 'Neph':
156
+ # For Nephelometer, directly use G column
157
+ if len(specified_band) == 1 and specified_band[0] == 550:
158
+ # Common case optimization
159
+ result_df.loc[mask, f'sca_550'] = df.loc[mask, 'G']
160
+ else:
161
+ # Need to extrapolate to other wavelengths
162
+ bgr_values = df.loc[mask, ['B', 'G', 'R']].values
163
+ log_band = np.log(band)
164
+
165
+ # Calculate SAE using numba function
166
+ sae_results = calculate_bulk_angstrom_numba(bgr_values, log_band)
167
+
168
+ # Use the calculated SAE to get scattering at specified wavelengths
169
+ for i, wl in enumerate(specified_band):
170
+ closest_idx = np.abs(band - wl).argmin()
171
+ ref_wl = band[closest_idx]
172
+ ref_idx = ['B', 'G', 'R'][closest_idx]
173
+
174
+ # Get reference measurements
175
+ ref_values = df.loc[mask, ref_idx].values
176
+
177
+ # Calculate scattering at target wavelength
178
+ ratio = wl / ref_wl
179
+ result_df.loc[mask, f'sca_{wl}'] = ref_values * (ratio ** -sae_results[:, 0])
180
+
181
+ # Store SAE values
182
+ result_df.loc[mask, 'SAE'] = sae_results[:, 0]
183
+ else:
184
+ # For Aurora, calculate using numba-optimized function instead of get_species_wavelength
185
+ bgr_values = df.loc[mask, ['B', 'G', 'R']].values
186
+ log_band = np.log(band)
187
+
188
+ # Calculate SAE using numba function
189
+ sae_results = calculate_bulk_angstrom_numba(bgr_values, log_band)
190
+
191
+ # Store SAE values
192
+ result_df.loc[mask, 'SAE'] = sae_results[:, 0]
193
+
194
+ # Calculate scattering at specified wavelengths
195
+ for i, wl in enumerate(specified_band):
196
+ closest_idx = np.abs(band - wl).argmin()
197
+ ref_wl = band[closest_idx]
198
+ ref_idx = ['B', 'G', 'R'][closest_idx]
199
+
200
+ # Get reference measurements
201
+ ref_values = df.loc[mask, ref_idx].values
202
+
203
+ # Calculate using the same function as for absorption, but with negative SAE
204
+ ratio = wl / ref_wl
205
+ # Note the negative sign for SAE
206
+ neg_sae_values = -sae_results[:, 0] # Negative SAE for scattering
207
+ result_df.loc[mask, f'sca_{wl}'] = calculate_specific_wavelengths_numba(
208
+ ref_values, neg_sae_values, ratio)
209
+
210
+ # Combine with original data
211
+ return pd.concat([df, result_df], axis=1)
212
+
213
+
214
+ def _absCoe(df, instru, specified_band: list):
215
+ """
216
+ Calculate absorption coefficients and Ångström exponent for absorption.
217
+
218
+ Parameters
219
+ ----------
220
+ df : pandas.DataFrame
221
+ Data frame containing black carbon measurements
222
+ instru : str
223
+ Instrument type ('AE33', 'BC1054', or 'MA350')
224
+ specified_band : list
225
+ List of wavelengths to calculate absorption coefficients for
226
+
227
+ Returns
228
+ -------
229
+ pandas.DataFrame
230
+ Data frame with original data, absorption coefficients,
231
+ coefficients at specified wavelengths, and Ångström exponent
232
+ """
233
+ config = {
234
+ 'AE33': {
235
+ 'band': np.array([370, 470, 520, 590, 660, 880, 950]),
236
+ 'MAE': np.array([18.47, 14.54, 13.14, 11.58, 10.35, 7.77, 7.19]) * 1e-3,
237
+ 'eBC': 'BC6'
238
+ },
239
+ 'BC1054': {
240
+ 'band': np.array([370, 430, 470, 525, 565, 590, 660, 700, 880, 950]),
241
+ 'MAE': np.array([18.48, 15.90, 14.55, 13.02, 12.10, 11.59, 10.36, 9.77, 7.77, 7.20]) * 1e-3,
242
+ 'eBC': 'BC9'
243
+ },
244
+ 'MA350': {
245
+ 'band': np.array([375, 470, 528, 625, 880]),
246
+ 'MAE': np.array([24.069, 19.070, 17.028, 14.091, 10.120]) * 1e-3,
247
+ 'eBC': 'BC5'
248
+ }
249
+ }
250
+
251
+ # Get configuration for the instrument
252
+ band_config = config[instru]
253
+
254
+ # Create mask for valid rows - non-zero and non-NaN
255
+ mask = ~((df == 0).all(axis=1) | df.isna().any(axis=1))
256
+
257
+ # Pre-allocate output columns
258
+ result_columns = ([f'abs_{_band}' for _band in band_config['band']] +
259
+ [f'abs_{_band}' for _band in specified_band] +
260
+ ['eBC', 'AAE'])
261
+ result_df = pd.DataFrame(np.nan, index=df.index, columns=result_columns)
262
+
263
+ # Exit early if no valid data
264
+ if not mask.any():
265
+ return pd.concat([df, result_df], axis=1)
266
+
267
+ # Get valid rows for processing
268
+ df_valid = df[mask]
269
+
270
+ # Calculate absorption coefficients (vectorized)
271
+ for i, wl in enumerate(band_config['band']):
272
+ col_name = f'abs_{wl}'
273
+ if col_name not in result_df.columns:
274
+ continue
275
+ result_df.loc[mask, col_name] = df_valid[df_valid.columns[i]] * band_config['MAE'][i]
276
+
277
+ # Extract absorption values as array for AAE calculation
278
+ abs_cols = [f'abs_{wl}' for wl in band_config['band']]
279
+ abs_values = result_df.loc[mask, abs_cols].values
280
+
281
+ # Calculate AAE with numba
282
+ log_wavelengths = np.log(band_config['band'])
283
+ aae_results = calculate_bulk_angstrom_numba(abs_values, log_wavelengths)
284
+
285
+ # Store AAE values
286
+ result_df.loc[mask, 'AAE'] = aae_results[:, 0]
287
+
288
+ # Calculate absorption at specified wavelengths
289
+ for target_wl in specified_band:
290
+ # Find the closest reference wavelength
291
+ closest_idx = np.abs(band_config['band'] - target_wl).argmin()
292
+ ref_wl = band_config['band'][closest_idx]
293
+ ref_col = f'abs_{ref_wl}'
294
+
295
+ # Get reference values and AAE
296
+ ref_values = result_df.loc[mask, ref_col].values
297
+ aae_values = result_df.loc[mask, 'AAE'].values
298
+
299
+ # Calculate using ratio
300
+ ratio = target_wl / ref_wl
301
+ result_df.loc[mask, f'abs_{target_wl}'] = calculate_specific_wavelengths_numba(
302
+ ref_values, aae_values, ratio)
303
+
304
+ # Set eBC values
305
+ result_df.loc[mask, 'eBC'] = df_valid[band_config['eBC']]
306
+
307
+ # Combine with original data
308
+ return pd.concat([df, result_df], axis=1)