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