SpecProc 1.0.0__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.
- specproc/__init__.py +9 -0
- specproc/calib_data/linelists/FeAr.dat +375 -0
- specproc/calib_data/linelists/README +79 -0
- specproc/calib_data/linelists/airvac.cl +61 -0
- specproc/calib_data/linelists/argon.dat +161 -0
- specproc/calib_data/linelists/ctiohear.dat +106 -0
- specproc/calib_data/linelists/ctiohenear.dat +146 -0
- specproc/calib_data/linelists/cuar.dat +459 -0
- specproc/calib_data/linelists/henear.dat +93 -0
- specproc/calib_data/linelists/henearhres.dat +300 -0
- specproc/calib_data/linelists/idhenear.dat +127 -0
- specproc/calib_data/linelists/krypton.dat +109 -0
- specproc/calib_data/linelists/lowskylines.dat +577 -0
- specproc/calib_data/linelists/ohlines.dat +268 -0
- specproc/calib_data/linelists/skylines.dat +1385 -0
- specproc/calib_data/linelists/thar-noao.dat +4169 -0
- specproc/calib_data/linelists/thar.dat +3057 -0
- specproc/calib_data/linelists/thorium.dat +3057 -0
- specproc/calib_data/linelists/vacidhenear.dat +127 -0
- specproc/calib_data/linelists/vacthar.dat +3057 -0
- specproc/calib_data/linelists/vacthorium.dat +3057 -0
- specproc/calib_data/linelists/xenon.dat +89 -0
- specproc/calib_data/telescopes/lijiang24hrs/lijiang24_hrs_thar_lines.csv +550 -0
- specproc/calib_data/telescopes/xinglong216bfosc/BFOSC/345/256/232/346/240/207/347/201/257/350/260/261/350/256/244/350/257/201/346/211/213/345/206/214v171118.pdf +0 -0
- specproc/calib_data/telescopes/xinglong216bfosc//346/212/245/345/221/212/345/233/276.docx +0 -0
- specproc/calib_data/telescopes/xinglong216hrs/216HRS /347/201/257/350/260/261/350/257/201/350/256/244/346/226/207/344/273/266.pdf +0 -0
- specproc/calib_data/telescopes/xinglong216hrs/wlcalib_20211123011_A.png +0 -0
- specproc/calib_data/telescopes/xinglong216hrs/xinglong216_thar_lines.csv +517 -0
- specproc/config/__init__.py +1 -0
- specproc/config/config_manager.py +431 -0
- specproc/core/__init__.py +1 -0
- specproc/core/basic_reduction.py +615 -0
- specproc/core/data_structures.py +525 -0
- specproc/core/de_blazing.py +210 -0
- specproc/core/extraction.py +375 -0
- specproc/core/flat_correction.py +1255 -0
- specproc/core/line_identification.py +302 -0
- specproc/core/order_stitching.py +187 -0
- specproc/core/order_tracing.py +1587 -0
- specproc/core/processing_pipeline.py +1183 -0
- specproc/core/scattered_light.py +654 -0
- specproc/core/slit_pipeline.py +762 -0
- specproc/core/wave_calibration.py +961 -0
- specproc/gui/__init__.py +1 -0
- specproc/gui/line_identifier.py +731 -0
- specproc/gui/main_window.py +2315 -0
- specproc/gui/settings_dialog.py +946 -0
- specproc/main.py +388 -0
- specproc/plotting/__init__.py +1 -0
- specproc/plotting/spectra_plotter.py +483 -0
- specproc/utils/__init__.py +1 -0
- specproc/utils/airvac.py +123 -0
- specproc/utils/fits_io.py +269 -0
- specproc/utils/image_processing.py +426 -0
- specproc/utils/overscan.py +600 -0
- specproc-1.0.0.dist-info/METADATA +668 -0
- specproc-1.0.0.dist-info/RECORD +61 -0
- specproc-1.0.0.dist-info/WHEEL +5 -0
- specproc-1.0.0.dist-info/entry_points.txt +5 -0
- specproc-1.0.0.dist-info/licenses/LICENSE +674 -0
- specproc-1.0.0.dist-info/top_level.txt +1 -0
specproc/main.py
ADDED
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SpecProc: Main application entry point.
|
|
3
|
+
|
|
4
|
+
PyQt-based GUI for echelle spectrograph FITS data reduction.
|
|
5
|
+
"""
|
|
6
|
+
# 提供图形界面与命令行程序入口。
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
import argparse
|
|
10
|
+
import logging
|
|
11
|
+
import shutil
|
|
12
|
+
import time
|
|
13
|
+
from logging.handlers import RotatingFileHandler
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Dict, List, Optional, Tuple
|
|
16
|
+
|
|
17
|
+
from astropy.io import fits
|
|
18
|
+
|
|
19
|
+
from specproc.config.config_manager import (
|
|
20
|
+
ConfigManager,
|
|
21
|
+
list_telescope_profiles,
|
|
22
|
+
resolve_telescope_profile,
|
|
23
|
+
)
|
|
24
|
+
from specproc.core.processing_pipeline import ProcessingPipeline
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
COPY_DEFAULT_CONFIG_SENTINEL = '__COPY_DEFAULT_CONFIG__'
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# 设置文件与控制台日志输出。
|
|
31
|
+
def setup_logging(log_file: str = 'specproc.log'):
|
|
32
|
+
"""Setup application logging."""
|
|
33
|
+
logger = logging.getLogger()
|
|
34
|
+
logger.setLevel(logging.INFO) # Set to INFO to reduce debug noise
|
|
35
|
+
|
|
36
|
+
# Reduce matplotlib logging - set more specific loggers
|
|
37
|
+
logging.getLogger('matplotlib').setLevel(logging.WARNING)
|
|
38
|
+
logging.getLogger('matplotlib.font_manager').setLevel(logging.WARNING)
|
|
39
|
+
logging.getLogger('matplotlib.pyplot').setLevel(logging.WARNING)
|
|
40
|
+
logging.getLogger('PIL').setLevel(logging.WARNING)
|
|
41
|
+
|
|
42
|
+
# Also disable font manager's verbose output
|
|
43
|
+
import matplotlib
|
|
44
|
+
matplotlib.rcParams['font.family'] = 'sans-serif'
|
|
45
|
+
matplotlib.rcParams['font.sans-serif'] = ['DejaVu Sans']
|
|
46
|
+
matplotlib.rcParams['axes.unicode_minus'] = False
|
|
47
|
+
|
|
48
|
+
# File handler
|
|
49
|
+
file_handler = RotatingFileHandler(
|
|
50
|
+
log_file, maxBytes=10*1024*1024, backupCount=5
|
|
51
|
+
)
|
|
52
|
+
file_handler.setLevel(logging.INFO) # File also at INFO level
|
|
53
|
+
|
|
54
|
+
# Console handler
|
|
55
|
+
console_handler = logging.StreamHandler(sys.stdout)
|
|
56
|
+
console_handler.setLevel(logging.INFO)
|
|
57
|
+
|
|
58
|
+
# Formatter
|
|
59
|
+
formatter = logging.Formatter(
|
|
60
|
+
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
61
|
+
)
|
|
62
|
+
file_handler.setFormatter(formatter)
|
|
63
|
+
console_handler.setFormatter(formatter)
|
|
64
|
+
|
|
65
|
+
# Add handlers
|
|
66
|
+
logger.addHandler(file_handler)
|
|
67
|
+
logger.addHandler(console_handler)
|
|
68
|
+
|
|
69
|
+
logger.info("=" * 60)
|
|
70
|
+
logger.info("SpecProc Application Started")
|
|
71
|
+
logger.info("=" * 60)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# 从输入文件或目录收集FITS图像。
|
|
75
|
+
def _collect_input_fits_files(inputs: List[str]) -> List[str]:
|
|
76
|
+
"""Collect FITS files from positional CLI inputs (files and/or directories)."""
|
|
77
|
+
paths = inputs if inputs else ['.']
|
|
78
|
+
discovered: List[Path] = []
|
|
79
|
+
|
|
80
|
+
for item in paths:
|
|
81
|
+
p = Path(item).expanduser()
|
|
82
|
+
if p.is_dir():
|
|
83
|
+
# Recursively scan directory and subdirectories for FITS files.
|
|
84
|
+
for pattern in ('*.fits', '*.fit', '*.FITS', '*.FIT'):
|
|
85
|
+
discovered.extend(sorted(p.rglob(pattern)))
|
|
86
|
+
elif p.is_file() and p.suffix.lower() in ('.fits', '.fit'):
|
|
87
|
+
discovered.append(p)
|
|
88
|
+
|
|
89
|
+
# Remove duplicates while preserving deterministic order.
|
|
90
|
+
unique_files: List[str] = []
|
|
91
|
+
seen = set()
|
|
92
|
+
for file_path in sorted(discovered):
|
|
93
|
+
key = str(file_path.resolve())
|
|
94
|
+
if key not in seen:
|
|
95
|
+
seen.add(key)
|
|
96
|
+
unique_files.append(str(file_path.resolve()))
|
|
97
|
+
|
|
98
|
+
return unique_files
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# 读取FITS头中的帧类型与曝光时间。
|
|
102
|
+
def _read_frame_type_hint(fits_path: str) -> Tuple[str, Optional[float]]:
|
|
103
|
+
"""Read frame-type hints and exposure time from FITS headers."""
|
|
104
|
+
collected: List[str] = []
|
|
105
|
+
exposure_value: Optional[float] = None
|
|
106
|
+
|
|
107
|
+
with fits.open(fits_path) as hdul:
|
|
108
|
+
for idx in range(len(hdul)):
|
|
109
|
+
header = getattr(hdul[idx], 'header', None)
|
|
110
|
+
if header is None:
|
|
111
|
+
continue
|
|
112
|
+
|
|
113
|
+
# Prioritize dedicated type keywords.
|
|
114
|
+
for key in ('OBSTYPE', 'IMAGETYP', 'OBJECT'):
|
|
115
|
+
value = str(header.get(key, '')).strip()
|
|
116
|
+
if value:
|
|
117
|
+
collected.append(value)
|
|
118
|
+
|
|
119
|
+
if exposure_value is None:
|
|
120
|
+
raw_exptime = header.get('EXPTIME', header.get('EXPOSURE', None))
|
|
121
|
+
try:
|
|
122
|
+
if raw_exptime is not None:
|
|
123
|
+
exposure_value = float(raw_exptime)
|
|
124
|
+
except (TypeError, ValueError):
|
|
125
|
+
pass
|
|
126
|
+
|
|
127
|
+
# Some observatories store frame type only in COMMENT.
|
|
128
|
+
comments = header.get('COMMENT', [])
|
|
129
|
+
if isinstance(comments, str):
|
|
130
|
+
if comments.strip():
|
|
131
|
+
collected.append(comments.strip())
|
|
132
|
+
else:
|
|
133
|
+
for comment in comments:
|
|
134
|
+
c = str(comment).strip()
|
|
135
|
+
if c:
|
|
136
|
+
collected.append(c)
|
|
137
|
+
|
|
138
|
+
# Deduplicate while preserving order.
|
|
139
|
+
seen = set()
|
|
140
|
+
unique_values: List[str] = []
|
|
141
|
+
for value in collected:
|
|
142
|
+
key = value.lower()
|
|
143
|
+
if key not in seen:
|
|
144
|
+
seen.add(key)
|
|
145
|
+
unique_values.append(value)
|
|
146
|
+
|
|
147
|
+
return ' | '.join(unique_values), exposure_value
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# 根据头信息判断观测帧类别。
|
|
151
|
+
def _classify_obstype(obstype: str) -> Optional[str]:
|
|
152
|
+
"""Map header type hints to one of: bias, flat, calib, science."""
|
|
153
|
+
normalized = obstype.strip().lower().replace('_', ' ')
|
|
154
|
+
if not normalized:
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
bias_tokens = {'bias', 'zero'}
|
|
158
|
+
flat_tokens = {'flat', 'quartz', 'halogen', 'continuum', 'tungsten'}
|
|
159
|
+
calib_tokens = {'thar', 'lamp', 'arc', 'comparison', 'comp', 'fear', 'ne', 'ar', 'he'}
|
|
160
|
+
science_tokens = {'science', 'object', 'target', 'star', 'source'}
|
|
161
|
+
|
|
162
|
+
if any(tok in normalized for tok in bias_tokens):
|
|
163
|
+
return 'bias'
|
|
164
|
+
if any(tok in normalized for tok in flat_tokens):
|
|
165
|
+
return 'flat'
|
|
166
|
+
if any(tok in normalized for tok in calib_tokens):
|
|
167
|
+
return 'calib'
|
|
168
|
+
if any(tok in normalized for tok in science_tokens):
|
|
169
|
+
return 'science'
|
|
170
|
+
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# 分类输入图像并运行命令行归约流程。
|
|
175
|
+
def run_cli_mode(config_path: Optional[str], inputs: List[str]) -> Dict:
|
|
176
|
+
"""Run full pipeline in CLI mode using OBSTYPE-based FITS classification."""
|
|
177
|
+
logger = logging.getLogger(__name__)
|
|
178
|
+
started_at = time.perf_counter()
|
|
179
|
+
fits_files = _collect_input_fits_files(inputs)
|
|
180
|
+
if not fits_files:
|
|
181
|
+
raise FileNotFoundError(
|
|
182
|
+
'No FITS files found. Provide FITS files or directories, e.g. '\
|
|
183
|
+
'specproc --mode cli 20241102_hrs'
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
config = ConfigManager(config_path)
|
|
187
|
+
grouped: Dict[str, List[str]] = {
|
|
188
|
+
'bias': [],
|
|
189
|
+
'flat': [],
|
|
190
|
+
'calib': [],
|
|
191
|
+
'science': [],
|
|
192
|
+
}
|
|
193
|
+
unknown: List[Tuple[str, str]] = []
|
|
194
|
+
by_filename = config.get('data', 'frame_type_source', 'header').strip().lower() == 'filename'
|
|
195
|
+
is_slit = config.get('telescope', 'instrument', '').strip().lower() == 'slit'
|
|
196
|
+
|
|
197
|
+
# 读取并规范化帧类型匹配关键词。
|
|
198
|
+
def _keywords(key: str, default: str) -> List[str]:
|
|
199
|
+
return [k.strip().lower() for k in config.get('data', key, default).split(',') if k.strip()]
|
|
200
|
+
|
|
201
|
+
kw = {
|
|
202
|
+
'bias': _keywords('bias_keywords', 'bias'),
|
|
203
|
+
'flat': _keywords('flat_keywords', 'flat'),
|
|
204
|
+
'calib': _keywords('lamp_keywords', 'fear'),
|
|
205
|
+
'dark': _keywords('dark_keywords', 'dark'),
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
for file_path in fits_files:
|
|
209
|
+
try:
|
|
210
|
+
if by_filename:
|
|
211
|
+
fname = Path(file_path).name.lower()
|
|
212
|
+
category = 'science'
|
|
213
|
+
for cat in ('bias', 'flat', 'calib', 'dark'):
|
|
214
|
+
if any(k in fname for k in kw[cat]):
|
|
215
|
+
category = cat
|
|
216
|
+
break
|
|
217
|
+
if category == 'dark':
|
|
218
|
+
logger.info(f"Dark frame not used: {Path(file_path).name}")
|
|
219
|
+
continue
|
|
220
|
+
grouped[category].append(file_path)
|
|
221
|
+
continue
|
|
222
|
+
|
|
223
|
+
obstype, exposure_value = _read_frame_type_hint(file_path)
|
|
224
|
+
category = _classify_obstype(obstype)
|
|
225
|
+
if category is None and exposure_value is not None and exposure_value > 0:
|
|
226
|
+
category = 'science'
|
|
227
|
+
|
|
228
|
+
if category is None:
|
|
229
|
+
unknown.append((file_path, obstype))
|
|
230
|
+
continue
|
|
231
|
+
grouped[category].append(file_path)
|
|
232
|
+
except Exception as exc:
|
|
233
|
+
logger.warning(f"Skip unreadable FITS file {file_path}: {exc}")
|
|
234
|
+
|
|
235
|
+
if unknown:
|
|
236
|
+
for file_path, obstype in unknown:
|
|
237
|
+
logger.warning(
|
|
238
|
+
f"Unrecognized frame type for {Path(file_path).name}: '{obstype}'. File is skipped."
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
required = ('bias', 'science') if is_slit else ('bias', 'flat', 'calib', 'science')
|
|
242
|
+
missing = [name for name in required if not grouped[name]]
|
|
243
|
+
if missing:
|
|
244
|
+
counts = ', '.join([f"{key}={len(val)}" for key, val in grouped.items()])
|
|
245
|
+
raise RuntimeError(
|
|
246
|
+
f"Frame classification incomplete ({counts}). Missing: {', '.join(missing)}."
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
logger.info(
|
|
250
|
+
"CLI classification result: "
|
|
251
|
+
f"bias={len(grouped['bias'])}, flat={len(grouped['flat'])}, "
|
|
252
|
+
f"calib={len(grouped['calib'])}, science={len(grouped['science'])}"
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
processed_files = grouped['bias'] + grouped['flat'] + grouped['calib'] + grouped['science']
|
|
256
|
+
total_bytes = sum(Path(file_path).stat().st_size for file_path in processed_files if Path(file_path).exists())
|
|
257
|
+
total_mb = total_bytes / (1024.0 * 1024.0)
|
|
258
|
+
|
|
259
|
+
pipeline = ProcessingPipeline(config)
|
|
260
|
+
result = pipeline.run_full_pipeline(
|
|
261
|
+
raw_image_paths=grouped['science'],
|
|
262
|
+
bias_filenames=grouped['bias'],
|
|
263
|
+
flat_filenames=grouped['flat'],
|
|
264
|
+
calib_filenames=grouped['calib'],
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
elapsed_minutes = (time.perf_counter() - started_at) / 60.0
|
|
268
|
+
speed = total_mb / elapsed_minutes if elapsed_minutes > 0 else 0.0
|
|
269
|
+
logger.info(
|
|
270
|
+
f"CLI processing summary: elapsed={elapsed_minutes:.2f} min, "
|
|
271
|
+
f"input_size={total_mb:.2f} MB, throughput={speed:.2f} MB/min"
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
return result
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
# 将所选默认配置复制到当前目录。
|
|
278
|
+
def copy_default_config_to_cwd(target_filename: str = 'specproc.cfg',
|
|
279
|
+
profile: Optional[str] = None) -> Path:
|
|
280
|
+
"""Copy a packaged default config into the current working directory.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
target_filename: Name of the file to create in the current directory.
|
|
284
|
+
profile: Optional telescope profile name (e.g. 'XL216_hrs' for the
|
|
285
|
+
Xinglong 2.16m HRS, 'MSTG193_slit' for the Muztagh-Ata 1.93m slit
|
|
286
|
+
spectrograph). If omitted, the generic default_config.cfg is used.
|
|
287
|
+
"""
|
|
288
|
+
if profile:
|
|
289
|
+
default_config = resolve_telescope_profile(profile)
|
|
290
|
+
if default_config is None:
|
|
291
|
+
available = ', '.join(sorted(list_telescope_profiles())) or '(none)'
|
|
292
|
+
raise FileNotFoundError(
|
|
293
|
+
f"Unknown telescope profile '{profile}'. Available profiles: {available}"
|
|
294
|
+
)
|
|
295
|
+
else:
|
|
296
|
+
default_config = Path(__file__).parent / 'config' / 'default_config.cfg'
|
|
297
|
+
if not default_config.exists():
|
|
298
|
+
raise FileNotFoundError(f'Default config file not found: {default_config}')
|
|
299
|
+
|
|
300
|
+
target_path = Path.cwd() / target_filename
|
|
301
|
+
if target_path.exists():
|
|
302
|
+
print(f'Config already exists: {target_path}')
|
|
303
|
+
return target_path
|
|
304
|
+
|
|
305
|
+
shutil.copyfile(default_config, target_path)
|
|
306
|
+
suffix = f' (telescope profile: {profile})' if profile else ''
|
|
307
|
+
print(f'Default config copied to: {target_path}{suffix}')
|
|
308
|
+
return target_path
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
# 解析启动参数并选择程序运行模式。
|
|
312
|
+
def main(config_path: Optional[str] = None, mode: Optional[str] = None):
|
|
313
|
+
"""Main application entry point.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
config_path: Path to configuration file (optional)
|
|
317
|
+
mode: Run mode ('gui' or 'cli')
|
|
318
|
+
"""
|
|
319
|
+
# Parse command line arguments first (before QApplication)
|
|
320
|
+
telescope_profiles = sorted(list_telescope_profiles())
|
|
321
|
+
profiles_hint = ', '.join(telescope_profiles) if telescope_profiles else '(none packaged)'
|
|
322
|
+
parser = argparse.ArgumentParser(
|
|
323
|
+
description='SpecProc - Spectral Processing Tool for Echelle Spectrographs',
|
|
324
|
+
epilog='Examples:\n specproc # Start GUI mode\n specproc 20241102_hrs # Auto CLI mode with directory input\n specproc --config # Copy default config to current directory\n specproc --mode cli 20241102_hrs\n specproc --mode cli --config custom.cfg ./raw_data\n specproc --mode cli --config XL216_hrs 20241102_hrs # Process with a packaged telescope profile\n specproc --config MSTG193_slit # Copy a packaged telescope profile to ./specproc.cfg\n\n'
|
|
325
|
+
f'Available telescope profiles: {profiles_hint}',
|
|
326
|
+
formatter_class=argparse.RawDescriptionHelpFormatter
|
|
327
|
+
)
|
|
328
|
+
parser.add_argument('--mode', choices=['gui', 'cli'], default=None,
|
|
329
|
+
help='Run mode: gui or cli (auto: cli when inputs are provided, otherwise gui)')
|
|
330
|
+
parser.add_argument('--config', nargs='?', const=COPY_DEFAULT_CONFIG_SENTINEL, default=None,
|
|
331
|
+
help='Path to configuration file, or the name of a packaged telescope '
|
|
332
|
+
f'profile ({profiles_hint}). With inputs, the profile/config is used '
|
|
333
|
+
'directly for processing; without inputs, it is copied to ./specproc.cfg. '
|
|
334
|
+
'Pass --config without a value to copy the generic default config.')
|
|
335
|
+
parser.add_argument('inputs', nargs='*',
|
|
336
|
+
help='FITS files or directories (CLI mode). If omitted, current directory is used.')
|
|
337
|
+
parser.add_argument('--version', action='version', version='SpecProc 1.0.0')
|
|
338
|
+
args = parser.parse_args()
|
|
339
|
+
|
|
340
|
+
# Override with function arguments if provided
|
|
341
|
+
if config_path is not None:
|
|
342
|
+
args.config = config_path
|
|
343
|
+
if mode is not None:
|
|
344
|
+
args.mode = mode
|
|
345
|
+
|
|
346
|
+
if args.config == COPY_DEFAULT_CONFIG_SENTINEL:
|
|
347
|
+
copy_default_config_to_cwd()
|
|
348
|
+
return
|
|
349
|
+
|
|
350
|
+
# If --config was given a value that is not an existing file path, try
|
|
351
|
+
# resolving it as a packaged telescope profile name (e.g. 'XL216_hrs',
|
|
352
|
+
# 'MSTG193_slit'). With inputs provided, use the profile directly for
|
|
353
|
+
# processing; without inputs, bootstrap ./specproc.cfg from it (mirrors
|
|
354
|
+
# the no-value --config behavior above, but for a specific telescope).
|
|
355
|
+
if args.config and not Path(args.config).expanduser().exists():
|
|
356
|
+
profile_path = resolve_telescope_profile(args.config)
|
|
357
|
+
if profile_path is not None:
|
|
358
|
+
if args.inputs:
|
|
359
|
+
args.config = str(profile_path)
|
|
360
|
+
else:
|
|
361
|
+
copy_default_config_to_cwd(profile=args.config)
|
|
362
|
+
return
|
|
363
|
+
|
|
364
|
+
# Auto mode selection:
|
|
365
|
+
# - If positional inputs are provided, default to CLI
|
|
366
|
+
# - Otherwise default to GUI
|
|
367
|
+
if args.mode is None:
|
|
368
|
+
args.mode = 'cli' if args.inputs else 'gui'
|
|
369
|
+
|
|
370
|
+
# Setup logging
|
|
371
|
+
setup_logging()
|
|
372
|
+
|
|
373
|
+
if args.mode == 'cli':
|
|
374
|
+
run_cli_mode(args.config, args.inputs)
|
|
375
|
+
else:
|
|
376
|
+
# GUI mode
|
|
377
|
+
from PyQt5.QtWidgets import QApplication
|
|
378
|
+
from specproc.gui.main_window import MainWindow
|
|
379
|
+
|
|
380
|
+
config = ConfigManager(args.config)
|
|
381
|
+
app = QApplication(sys.argv)
|
|
382
|
+
window = MainWindow(config)
|
|
383
|
+
window.show()
|
|
384
|
+
sys.exit(app.exec_())
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
if __name__ == '__main__':
|
|
388
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Plotting and visualization modules."""
|