1→# Fiber Efficiency in PyEchelle HDF Files
2→
3→## The Warning Message
4→
5→When running simulations, you may see:
6→```
7→root - WARNING - No spectrograph efficiency data found for fiber 5.
8→```
9→
10→This means PyEchelle cannot find fiber efficiency data in the HDF model file.
... (content hidden) ...
<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>
1→# Fiber Efficiency in PyEchelle HDF Files
2→
3→## The Warning Message
4→
5→When running simulations, you may see:
6→```
7→root - WARNING - No spectrograph efficiency data found for fiber 5.
8→```
9→
10→This means PyEchelle cannot find fiber efficiency data in the HDF model file.
11→
12→## What PyEchelle Expects
13→
14→PyEchelle looks for fiber efficiency data stored in the HDF file at:
15→- **Path**: `CCD_{ccd_index}/fiber_{fiber_num}` (as an attribute)
16→- **Attribute name**: `"efficiency"`
17→- **Format**: A tuple of `(wavelength_array, efficiency_array)`
18→ - Wavelengths in microns
19→ - Efficiency values between 0 and 1
20→ - Both arrays must have the same length
21→
22→### Code Reference
23→
24→From PyEchelle's ZEMAX class (`pyechelle/spectrograph.py`):
25→
26→```python
27→def get_efficiency(self, fiber: int, ccd_index: int) -> SystemEfficiency:
28→ try:
29→ # Try to load grating efficiency from HDF attributes
30→ ge = GratingEfficiency(...)
31→ except KeyError:
32→ ge = ConstantEfficiency("Spectrograph", eff=1.0)
33→
34→ try:
35→ # Try to load fiber efficiency from HDF attributes
36→ self._efficiency[ccd_index][fiber] = SystemEfficiency(
37→ [
38→ ge,
39→ TabulatedEfficiency(
40→ "System",
41→ *self.h5f[f"CCD_{ccd_index}/fiber_{fiber}"].attrs["efficiency"],
42→ ),
43→ ],
44→ "System",
45→ )
46→ except KeyError:
47→ # This is where the warning comes from
48→ logging.warning(f"No spectrograph efficiency data found for fiber {fiber}.")
49→ self._efficiency[ccd_index][fiber] = SystemEfficiency([ge], "System")
50→```
51→
52→## Expected HDF Structure
53→
54→```
55→HDF File
56→└── CCD_1/
57→ ├── Spectrograph (group)
58→ │ └── attrs: blaze, gpmm
59→ ├── fiber_1/
60→ │ ├── attrs: field_shape, efficiency
61→ │ ├── order109/ (transformation data)
62→ │ ├── psf_order_109/ (PSF data)
63→ │ └── ...
64→ ├── fiber_2/
65→ │ └── attrs: efficiency
66→ └── ...
67→```
68→
69→## What Happens Without Efficiency Data
70→
71→If the `efficiency` attribute is missing, PyEchelle:
72→1. Issues a warning for each fiber
73→2. Falls back to grating efficiency only
74→3. Continues simulation (doesn't crash)
75→4. May produce less realistic results
76→
77→## How to Add Efficiency Data
78→
79→### Option 1: Manually Patch Existing HDF Files
80→
81→Use h5py to add efficiency data to existing HDF files:
82→
83→```python
84→import h5py
85→import numpy as np
86→
87→# Open HDF file in read-write mode
88→with h5py.File('src/HDF/ANDES_123_R3.hdf', 'r+') as h5f:
89→ for fiber_num in range(1, 67): # 66 fibers for optical bands
90→ # Define wavelength range for this band
91→ # R-band example: 0.6-0.8 microns
92→ wavelengths = np.linspace(0.6, 0.8, 50)
93→
94→ # Define efficiency values
95→ # Option A: Constant efficiency
96→ efficiency = np.ones(50) * 0.85 # 85% constant
97→
98→ # Option B: Wavelength-dependent efficiency
99→ # efficiency = 0.5 + 0.4 * np.exp(-((wavelengths - 0.7)**2) / 0.01)
100→
101→ # Save as attribute (tuple of two arrays)
102→ fiber_group = h5f[f'CCD_1/fiber_{fiber_num}']
103→ fiber_group.attrs['efficiency'] = (wavelengths, efficiency)
104→
105→print("Efficiency data added to all fibers")
106→```
107→
108→### Option 2: Add During HDF Generation from ZEMAX
109→
110→Modify the HDF builder code to include efficiency when creating HDF files from ZEMAX models. This requires modifying PyEchelle's `HDFBuilder` class or post-processing the generated HDF.
111→
112→## Wavelength Ranges by Band
113→
114→When adding efficiency data, use appropriate wavelength ranges:
115→
116→| Band | Wavelength Range (microns) | Wavelength Range (nm) |
117→|------|---------------------------|----------------------|
118→| U | 0.30 - 0.40 | 300 - 400 |
119→| B | 0.40 - 0.50 | 400 - 500 |
120→| V | 0.50 - 0.60 | 500 - 600 |
121→| R | 0.60 - 0.80 | 600 - 800 |
122→| IZ | 0.80 - 1.00 | 800 - 1000 |
123→| Y | 0.95 - 1.15 | 950 - 1150 |
124→| J | 1.15 - 1.35 | 1150 - 1350 |
125→| H | 1.50 - 1.80 | 1500 - 1800 |
126→
127→## Sources of Efficiency Data
128→
129→Efficiency data can come from:
130→
131→1. **ZEMAX ray-tracing**: Transmission calculations from optical model
132→2. **Fiber manufacturer specifications**: Typical fiber transmission curves
133→3. **Measured calibration data**: From actual instrument calibrations
134→4. **Constant values**: Simple approximation (e.g., 0.85 for all wavelengths)
135→5. **Model curves**: Gaussian or polynomial fits to expected performance
136→
137→## Example: Inspect Existing Efficiency Data
138→
139→The Y-band file `ANDES_Y01_wFiberEff.hdf` contains efficiency data. Inspect it:
140→
141→```python
142→import h5py
143→import matplotlib.pyplot as plt
144→
145→with h5py.File('src/HDF/ANDES_Y01_wFiberEff.hdf', 'r') as h5f:
146→ # Check if fiber 1 has efficiency data
147→ fiber_group = h5f['CCD_1/fiber_1']
148→
149→ if 'efficiency' in fiber_group.attrs:
150→ wavelengths, efficiency = fiber_group.attrs['efficiency']
151→
152→ print(f"Number of wavelength points: {len(wavelengths)}")
153→ print(f"Wavelength range: {wavelengths[0]:.3f} - {wavelengths[-1]:.3f} microns")
154→ print(f"Efficiency range: {efficiency.min():.3f} - {efficiency.max():.3f}")
155→
156→ # Plot efficiency curve
157→ plt.plot(wavelengths * 1000, efficiency) # Convert to nm
158→ plt.xlabel('Wavelength (nm)')
159→ plt.ylabel('Efficiency')
160→ plt.title('Fiber 1 Efficiency Curve')
161→ plt.grid(True)
162→ plt.show()
163→ else:
164→ print("No efficiency data found")
165→```
166→
167→## Quick Fix for Testing
168→
169→To eliminate warnings quickly without detailed efficiency curves:
170→
171→```python
172→import h5py
173→import numpy as np
174→
175→# Add constant 85% efficiency to all fibers in R-band
176→with h5py.File('src/HDF/ANDES_123_R3.hdf', 'r+') as h5f:
177→ wavelengths = np.array([0.6, 0.7, 0.8]) # R-band in microns
178→ efficiency = np.array([0.85, 0.85, 0.85]) # Constant 85%
179→
180→ for fiber_num in range(1, 67): # 66 fibers
181→ h5f[f'CCD_1/fiber_{fiber_num}'].attrs['efficiency'] = (wavelengths, efficiency)
182→```
183→
184→## Available HDF Files
185→
186→In `src/HDF/`:
187→- `ANDES_Y01_wFiberEff.hdf` - **WITH** fiber efficiency (Y-band)
188→- `ANDES_Y01.hdf` - without fiber efficiency
189→- `ANDES_75fibre_Y.hdf`, `ANDES_75fibre_J.hdf`, `ANDES_75fibre_H.hdf` - IR bands
190→- `ANDES_123_R3.hdf`, `ANDES_123_IZ3.hdf` - optical bands
191→
192→## Configuration
193→
194→To use HDF files with fiber efficiency in your simulations:
195→
196→```yaml
197→# In your config YAML
198→hdf_model: "with_fiber_eff" # Instead of "default"
199→```
200→
201→Currently only Y-band has a `with_fiber_eff` variant configured in `instruments.py`.
202→
203→## Notes
204→
205→- Efficiency is optional but provides more realistic simulations
206→- Each fiber can have different efficiency (important for multi-fiber instruments)
207→- If you don't have measured data, constant efficiency is better than none
208→- The simulator will work without efficiency data, just with warnings
209→
<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>
/Users/tom/ANDES/E2E/src/andes_simulator/cli/__init__.py
/Users/tom/ANDES/E2E/src/.venv/lib/python3.13/site-packages/charset_normalizer/cli/__init__.py
/Users/tom/ANDES/E2E/src/.venv/lib/python3.13/site-packages/charset_normalizer/cli/__main__.py
/Users/tom/ANDES/E2E/src/.venv/lib/python3.13/site-packages/setuptools/_vendor/wheel/cli/__init__.py
/Users/tom/ANDES/E2E/src/.venv/lib/python3.13/site-packages/setuptools/_vendor/wheel/cli/convert.py
/Users/tom/ANDES/E2E/src/.venv/lib/python3.13/site-packages/setuptools/_vendor/wheel/cli/pack.py
/Users/tom/ANDES/E2E/src/.venv/lib/python3.13/site-packages/setuptools/_vendor/wheel/cli/tags.py
/Users/tom/ANDES/E2E/src/.venv/lib/python3.13/site-packages/setuptools/_vendor/wheel/cli/unpack.py
/Users/tom/ANDES/E2E/src/andes_simulator/cli/main.py
/Users/tom/ANDES/E2E/src/andes_simulator/cli/utils.py
1→"""
2→Main command-line interface for ANDES simulation framework.
3→
4→Provides unified CLI with subcommands for all simulation types
5→including flat field, Fabry-Perot, stellar observations, and post-processing.
6→"""
7→
8→import sys
9→import logging
10→from pathlib import Path
... (content hidden) ...
<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>
1→"""
2→Main command-line interface for ANDES simulation framework.
3→
4→Provides unified CLI with subcommands for all simulation types
5→including flat field, Fabry-Perot, stellar observations, and post-processing.
6→"""
7→
8→import sys
9→import logging
10→from pathlib import Path
11→from typing import Optional, List
12→try:
13→ import click
14→except ImportError:
15→ print("Error: click package required for CLI. Install with: pip install click")
16→ sys.exit(1)
17→
18→from .utils import (
19→ run_simulation_command,
20→ build_config_from_options,
21→ format_dry_run_output,
22→ setup_logging
23→)
24→
25→# Lightweight band list for CLI validation (avoids importing instruments module)
26→ANDES_BANDS = ['U', 'B', 'V', 'R', 'IZ', 'Y', 'J', 'H']
27→
28→SUBSLIT_CHOICES = ['all', 'single', 'even_odd', 'slitA', 'slitB', 'cal',
29→ 'ifu', 'ring0', 'ring1', 'ring2', 'ring3', 'ring4']
30→
31→
32→def common_options(f):
33→ """Common options for simulation commands."""
34→ f = click.option('--dry-run', is_flag=True, help='Show what would be done without running')(f)
35→ f = click.option('--output-dir', type=click.Path(path_type=Path), help='Output directory')(f)
36→ f = click.option('--exposure', default=1.0, type=float, help='Exposure time in seconds')(f)
37→ f = click.option('--hdf', type=click.Path(exists=True, path_type=Path),
38→ help='HDF model file (infers band if --band not given)')(f)
39→ f = click.option('--wl-min', type=float, help='Minimum wavelength in nm')(f)
40→ f = click.option('--wl-max', type=float, help='Maximum wavelength in nm')(f)
41→ return f
42→
43→
44→def resolve_band_and_hdf(
45→ band: Optional[str],
46→ hdf: Optional[Path],
47→ project_root: Optional[Path],
48→ wl_min: Optional[float] = None,
49→ wl_max: Optional[float] = None
50→) -> tuple:
51→ """Resolve band and HDF model path, inferring band from HDF or wavelengths if needed."""
52→ from ..core.instruments import infer_band_from_hdf, infer_band_from_wavelengths, get_hdf_model_path
53→
54→ if hdf:
55→ inferred_band = infer_band_from_hdf(hdf)
56→ if band and band != inferred_band:
57→ raise click.UsageError(
58→ f"--band {band} conflicts with HDF file (contains {inferred_band} data)")
59→ return inferred_band, str(hdf)
60→
61→ if not band:
62→ # Try to infer from wavelength limits
63→ if wl_min is not None or wl_max is not None:
64→ try:
65→ band = infer_band_from_wavelengths(wl_min, wl_max)
66→ click.echo(f"Inferred band: {band} (from wavelength limits)")
67→ except ValueError as e:
68→ raise click.UsageError(str(e))
69→ else:
70→ raise click.UsageError("Either --band, --hdf, or wavelength limits (--wl-min/--wl-max) required")
71→
72→ # Use default HDF for band
73→ if project_root is None:
74→ project_root = Path(__file__).parent.parent.parent
75→ default_hdf = get_hdf_model_path(band, 'default', project_root)
76→ return band, str(default_hdf) if default_hdf.exists() else None
77→
78→
79→def subslit_options(f):
80→ """Subslit/fiber selection options."""
81→ f = click.option('--fiber', type=int, help='Specific fiber number (for single subslit)')(f)
82→ f = click.option('--subslit', default='all', type=click.Choice(SUBSLIT_CHOICES),
83→ help='Fiber selection (ifu/ringN only for YJH bands)')(f)
84→ return f
85→
86→
87→def flux_options(default_scaling=1e5):
88→ """Flux/scaling options with configurable default scaling."""
89→ def decorator(f):
90→ f = click.option('--scaling', type=float, default=default_scaling,
91→ help='Base scaling factor')(f)
92→ f = click.option('--flux', default=1.0, type=float,
93→ help='Flux multiplier (multiplied with scaling)')(f)
94→ return f
95→ return decorator
96→
97→
98→@click.group()
99→@click.option('--verbose', '-v', is_flag=True, help='Enable verbose logging')
100→@click.option('--project-root', type=click.Path(exists=True, path_type=Path),
101→ help='Project root directory (auto-detected if not specified)')
102→@click.pass_context
103→def cli(ctx, verbose, project_root):
104→ """ANDES E2E Simulation Framework - Unified simulation and analysis tools."""
105→ setup_logging(verbose)
106→
107→ # Store context
108→ ctx.ensure_object(dict)
109→ ctx.obj['verbose'] = verbose
110→ ctx.obj['project_root'] = project_root
111→
112→
113→@cli.command()
114→@click.option('--band', type=click.Choice(ANDES_BANDS), help='Spectral band (inferred from --hdf-model if not given)')
115→@subslit_options
116→@flux_options(default_scaling=2e5)
117→@common_options
118→@click.option('--config', type=click.Path(exists=True, path_type=Path),
119→ help='YAML configuration file')
120→@click.pass_context
121→def flat_field(ctx, band, subslit, fiber, flux, scaling, exposure, output_dir, hdf, wl_min, wl_max, config, dry_run):
122→ """Generate flat field calibration frames."""
123→ from ..core.config import SimulationConfig
124→
125→ if config:
126→ sim_config = SimulationConfig.from_yaml(config)
127→ else:
128→ band, hdf_path = resolve_band_and_hdf(band, hdf, ctx.obj['project_root'], wl_min, wl_max)
129→ sim_config = build_config_from_options(
130→ simulation_type="flat_field",
131→ band=band,
132→ exposure=exposure,
133→ source_type="constant",
134→ fiber_mode=subslit,
135→ output_dir=output_dir,
136→ fiber=fiber,
137→ flux=flux,
138→ scaling=scaling,
139→ flux_unit="ph/s/AA",
140→ hdf=hdf_path,
141→ wl_min=wl_min,
142→ wl_max=wl_max
143→ )
144→
145→ run_simulation_command(
146→ sim_config,
147→ dry_run,
148→ lambda: format_dry_run_output(sim_config),
149→ f"Flat field simulation completed ({subslit} subslit)"
150→ )
151→
152→
153→@cli.command()
154→@click.option('--band', type=click.Choice(ANDES_BANDS), help='Spectral band (inferred from --hdf-model if not given)')
155→@subslit_options
156→@flux_options(default_scaling=1e5)
157→@common_options
158→@click.option('--velocity-shift', type=float, help='Velocity shift in m/s')
159→@click.option('--config', type=click.Path(exists=True, path_type=Path),
160→ help='YAML configuration file')
161→@click.pass_context
162→def fabry_perot(ctx, band, subslit, fiber, flux, scaling, exposure, output_dir, hdf, wl_min, wl_max, velocity_shift, config, dry_run):
163→ """Generate Fabry-Perot wavelength calibration frames."""
164→ from ..core.config import SimulationConfig
165→
166→ if config:
167→ sim_config = SimulationConfig.from_yaml(config)
168→ else:
169→ band, hdf_path = resolve_band_and_hdf(band, hdf, ctx.obj['project_root'], wl_min, wl_max)
170→ sim_config = build_config_from_options(
171→ simulation_type="fabry_perot",
172→ band=band,
173→ exposure=exposure,
174→ source_type="fabry_perot",
175→ fiber_mode=subslit,
176→ output_dir=output_dir,
177→ fiber=fiber,
178→ flux=flux,
179→ scaling=scaling,
180→ velocity_shift=velocity_shift,
181→ hdf=hdf_path,
182→ wl_min=wl_min,
183→ wl_max=wl_max
184→ )
185→
186→ run_simulation_command(
187→ sim_config,
188→ dry_run,
189→ lambda: format_dry_run_output(sim_config),
190→ "Fabry-Perot simulation completed"
191→ )
192→
193→
194→@cli.command()
195→@click.option('--band', type=click.Choice(ANDES_BANDS), help='Spectral band (inferred from --hdf-model if not given)')
196→@subslit_options
197→@flux_options(default_scaling=1e5)
198→@common_options
199→@click.pass_context
200→def lfc(ctx, band, subslit, fiber, flux, scaling, exposure, output_dir, hdf, wl_min, wl_max, dry_run):
201→ """Generate Laser Frequency Comb wavelength calibration frames.
202→
203→ LFC produces unresolved emission lines equidistant in velocity,
204→ with approximately 100 lines per spectral order.
205→ """
206→ band, hdf_path = resolve_band_and_hdf(band, hdf, ctx.obj['project_root'], wl_min, wl_max)
207→ sim_config = build_config_from_options(
208→ simulation_type="lfc",
209→ band=band,
210→ exposure=exposure,
211→ source_type="lfc",
212→ fiber_mode=subslit,
213→ output_dir=output_dir,
214→ fiber=fiber,
215→ flux=flux,
216→ scaling=scaling,
217→ hdf=hdf_path,
218→ wl_min=wl_min,
219→ wl_max=wl_max
220→ )
221→
222→ run_simulation_command(
223→ sim_config,
224→ dry_run,
225→ lambda: format_dry_run_output(sim_config),
226→ "LFC simulation completed"
227→ )
228→
229→
230→@cli.command()
231→@click.option('--band', required=True, type=click.Choice(ANDES_BANDS),
232→ help='Spectral band')
233→@click.option('--spectrum', required=True, type=click.Path(exists=True, path_type=Path),
234→ help='CSV spectrum file')
235→@click.option('--fiber', required=True, type=int, help='Fiber number to illuminate')
236→@click.option('--scaling', default=5e3, type=float, help='Spectrum flux scaling factor')
237→@click.option('--exposure', default=30.0, type=float, help='Exposure time in seconds')
238→@click.option('--output-dir', type=click.Path(path_type=Path), help='Output directory')
239→@click.option('--config', type=click.Path(exists=True, path_type=Path),
240→ help='YAML configuration file')
241→@click.option('--dry-run', is_flag=True, help='Show what would be done without running')
242→@click.pass_context
243→def spectrum(ctx, band, spectrum, fiber, scaling, exposure, output_dir, config, dry_run):
244→ """Generate stellar spectrum observations."""
245→ from ..core.config import SimulationConfig
246→
247→ if config:
248→ sim_config = SimulationConfig.from_yaml(config)
249→ else:
250→ sim_config = build_config_from_options(
251→ simulation_type="spectrum",
252→ band=band,
253→ exposure=exposure,
254→ source_type="csv",
255→ fiber_mode="single",
256→ output_dir=output_dir,
257→ fiber=fiber,
258→ scaling=scaling,
259→ spectrum_path=spectrum
260→ )
261→
262→ run_simulation_command(
263→ sim_config,
264→ dry_run,
265→ lambda: format_dry_run_output(sim_config),
266→ f"Spectrum simulation completed for fiber {fiber}"
267→ )
268→
269→
270→@cli.command()
271→@click.option('--band', required=True, type=click.Choice(ANDES_BANDS),
272→ help='Spectral band')
273→@click.option('--zemax-file', type=str, help='ZEMAX file name (uses default if not specified)')
274→@click.option('--output', type=click.Path(path_type=Path), help='Output HDF file path')
275→@click.option('--n-transform', default=15, type=int, help='Transformations per order')
276→@click.option('--n-psf', default=15, type=int, help='PSFs per order')
277→@click.option('--dry-run', is_flag=True, help='Show what would be done without running')
278→@click.pass_context
279→def generate_hdf(ctx, band, zemax_file, output, n_transform, n_psf, dry_run):
280→ """Generate HDF instrument model files from ZEMAX."""
281→ from ..models.hdf_builder import AndesHDFBuilder
282→
283→ if dry_run:
284→ click.echo("Dry run - would execute:")
285→ click.echo(f" Band: {band}")
286→ click.echo(f" ZEMAX file: {zemax_file or 'default'}")
287→ click.echo(f" Output: {output or 'default path'}")
288→ click.echo(f" Samples: {n_transform} transforms, {n_psf} PSFs per order")
289→ click.echo(" WARNING: This would take a very long time!")
290→ return
291→
292→ click.echo(f"Generating HDF model for {band}-band...")
293→ click.echo("WARNING: This will take a very long time!")
294→
295→ if not click.confirm("Do you want to continue?"):
296→ return
297→
298→ # Create HDF builder
299→ builder = AndesHDFBuilder(band, ctx.obj['project_root'])
300→
301→ try:
302→ output_path = builder.build_complete_model(
303→ output_path=output,
304→ zemax_file=zemax_file,
305→ n_transformation_per_order=n_transform,
306→ n_psfs_per_order=n_psf
307→ )
308→ click.echo(f"HDF model generated: {output_path}")
309→ except Exception as e:
310→ click.echo(f"Error generating HDF model: {e}", err=True)
311→ sys.exit(1)
312→
313→
314→@cli.command()
315→@click.option('--band', required=True, type=click.Choice(ANDES_BANDS),
316→ help='Spectral band')
317→@click.option('--input-pattern', required=True, type=str,
318→ help='Input file pattern (e.g., "{band}_FP_fiber{fib:02d}_shift*.fits")')
319→@click.option('--kernel-size', default='4,4', type=str, help='Kernel size as "width,height"')
320→@click.option('--fwhm', default=3.2, type=float, help='FWHM in arcseconds')
321→@click.option('--edge-blank', default='left',
322→ type=click.Choice(['none', 'top', 'bottom', 'left', 'right', 'random']),
323→ help='Edge blanking mode')
324→@click.option('--output-dir', type=click.Path(path_type=Path), help='Output directory')
325→@click.option('--visualize', is_flag=True, help='Create kernel visualization')
326→@click.option('--dry-run', is_flag=True, help='Show what would be done without running')
327→@click.pass_context
328→def psf_process(ctx, band, input_pattern, kernel_size, fwhm, edge_blank, output_dir, visualize, dry_run):
329→ """Apply PSF convolution to simulation outputs."""
330→ from ..postprocess.psf import PSFProcessor
331→
332→ # Parse kernel size
333→ try:
334→ dimx, dimy = map(int, kernel_size.split(','))
335→ except ValueError:
336→ raise click.BadParameter("kernel-size must be 'width,height' format")
337→
338→ if dry_run:
339→ click.echo("Dry run - would execute:")
340→ click.echo(f" Band: {band}")
341→ click.echo(f" Input pattern: {input_pattern}")
342→ click.echo(f" Kernel: {dimx}x{dimy}, FWHM={fwhm}\", edge={edge_blank}")
343→ click.echo(f" Output dir: {output_dir or 'default'}")
344→ return
345→
346→ # Create PSF processor
347→ processor = PSFProcessor(band, ctx.obj['project_root'])
348→
349→ # Set up kernel parameters
350→ kernel_params = (dimx, dimy, fwhm, edge_blank)
351→
352→ try:
353→ # Process files
354→ output_path = processor.process_fabry_perot_files(
355→ kernel_params=kernel_params,
356→ output_dir=output_dir,
357→ input_pattern=input_pattern
358→ )
359→ click.echo(f"PSF processing completed: {output_path}")
360→
361→ # Create visualization if requested
362→ if visualize:
363→ viz_path = processor.create_kernel_visualization(kernel_params)
364→ if viz_path:
365→ click.echo(f"Kernel visualization saved: {viz_path}")
366→
367→ except Exception as e:
368→ click.echo(f"Error in PSF processing: {e}", err=True)
369→ sys.exit(1)
370→
371→
372→@cli.command()
373→@click.option('--band', required=True, type=click.Choice(ANDES_BANDS),
374→ help='Spectral band')
375→@click.option('--input-pattern', required=True, type=str,
376→ help='Input file pattern (e.g., "{band}_FP_fiber{fib:02d}_shift*.fits")')
377→@click.option('--mode', default='all',
378→ type=click.Choice(['all', 'even_odd', 'slits', 'custom']),
379→ help='Combination mode')
380→@click.option('--fibers', type=str, help='Fiber list for custom mode (e.g., "1,5,10-15")')
381→@click.option('--output', type=str, help='Output filename')
382→@click.option('--output-dir', type=click.Path(path_type=Path), help='Output directory')
383→@click.option('--report', is_flag=True, help='Generate combination report')
384→@click.option('--dry-run', is_flag=True, help='Show what would be done without running')
385→@click.pass_context
386→def combine(ctx, band, input_pattern, mode, fibers, output, output_dir, report, dry_run):
387→ """Combine individual fiber outputs."""
388→ from ..postprocess.combine import FiberCombiner
389→
390→ if dry_run:
391→ click.echo("Dry run - would execute:")
392→ click.echo(f" Band: {band}")
393→ click.echo(f" Input pattern: {input_pattern}")
394→ click.echo(f" Mode: {mode}")
395→ if fibers:
396→ click.echo(f" Fibers: {fibers}")
397→ return
398→
399→ # Create fiber combiner (use output_dir as input_dir for finding files)
400→ combiner = FiberCombiner(band, ctx.obj['project_root'], input_dir=output_dir)
401→
402→ try:
403→ if mode == 'all':
404→ combined_image = combiner.combine_all_fibers(input_pattern)
405→ output_filename = output or f"{band}_combined_all.fits"
406→
407→ elif mode == 'even_odd':
408→ results = combiner.combine_even_odd_fibers(input_pattern)
409→ for eo_mode, image_data in results.items():
410→ eo_filename = output or f"{band}_combined_{eo_mode}.fits"
411→ if output_dir:
412→ eo_path = output_dir / eo_filename
413→ else:
414→ eo_path = combiner.project_root.parent / band / eo_filename
415→ combiner.save_combined_image(image_data, eo_path, {'mode': eo_mode})
416→ click.echo(f"Saved {eo_mode} combination: {eo_path}")
417→
418→ if report:
419→ report_path = combiner.create_combination_report(input_pattern, output_dir)
420→ click.echo(f"Report created: {report_path}")
421→ return
422→
423→ elif mode == 'slits':
424→ if band in ['Y', 'J', 'H']:
425→ click.echo("Slit mode not applicable to YJH bands", err=True)
426→ return
427→
428→ results = combiner.combine_pseudo_slits(input_pattern)
429→ for slit_name, image_data in results.items():
430→ slit_filename = output or f"{band}_combined_{slit_name}.fits"
431→ if output_dir:
432→ slit_path = output_dir / slit_filename
433→ else:
434→ slit_path = combiner.project_root.parent / band / slit_filename
435→ combiner.save_combined_image(image_data, slit_path, {'mode': slit_name})
436→ click.echo(f"Saved {slit_name} combination: {slit_path}")
437→
438→ if report:
439→ report_path = combiner.create_combination_report(input_pattern, output_dir)
440→ click.echo(f"Report created: {report_path}")
441→ return
442→
443→ elif mode == 'custom':
444→ if not fibers:
445→ raise click.BadParameter("--fibers required for custom mode")
446→
447→ # Parse fiber list (simple implementation)
448→ fiber_nums = []
449→ for part in fibers.split(','):
450→ if '-' in part:
451→ start, end = map(int, part.split('-'))
452→ fiber_nums.extend(range(start, end + 1))
453→ else:
454→ fiber_nums.append(int(part))
455→
456→ combined_image = combiner.combine_fiber_subset(fiber_nums, input_pattern)
457→ output_filename = output or f"{band}_combined_custom.fits"
458→
459→ # Save result (for all, custom modes)
460→ if mode in ['all', 'custom']:
461→ if output_dir:
462→ output_path = output_dir / output_filename
463→ else:
464→ output_path = combiner.project_root.parent / band / output_filename
465→
466→ combiner.save_combined_image(combined_image, output_path, {'mode': mode})
467→ click.echo(f"Combined image saved: {output_path}")
468→
469→ # Generate report if requested
470→ if report:
471→ report_path = combiner.create_combination_report(input_pattern, output_dir)
472→ click.echo(f"Report created: {report_path}")
473→
474→ except Exception as e:
475→ click.echo(f"Error in fiber combination: {e}", err=True)
476→ sys.exit(1)
477→
478→
479→@cli.command()
480→@click.option('--config', required=True, type=click.Path(exists=True, path_type=Path),
481→ help='YAML configuration file')
482→@click.option('--dry-run', is_flag=True, help='Show what would be done without running')
483→@click.pass_context
484→def run_config(ctx, config, dry_run):
485→ """Run simulation from YAML configuration file."""
486→ from ..core.config import SimulationConfig
487→
488→ sim_config = SimulationConfig.from_yaml(config)
489→
490→ run_simulation_command(
491→ sim_config,
492→ dry_run,
493→ lambda: format_dry_run_output(sim_config, [f"Config: {config}"]),
494→ f"Simulation completed: {sim_config.simulation_type}"
495→ )
496→
497→
498→@cli.command()
499→@click.option('--output-dir', default='configs/examples', type=click.Path(path_type=Path),
500→ help='Output directory for template files')
501→@click.pass_context
502→def create_templates(ctx, output_dir):
503→ """Create template configuration files."""
504→ from ..core.config import create_template_configs
505→
506→ if not output_dir.is_absolute():
507→ if ctx.obj['project_root']:
508→ output_dir = ctx.obj['project_root'] / output_dir
509→ else:
510→ output_dir = Path.cwd() / output_dir
511→
512→ try:
513→ create_template_configs(output_dir)
514→ click.echo(f"Template configurations created in: {output_dir}")
515→ except Exception as e:
516→ click.echo(f"Error creating templates: {e}", err=True)
517→ sys.exit(1)
518→
519→
520→@cli.command()
521→@click.pass_context
522→def list_bands(ctx):
523→ """List available spectral bands."""
524→ bands = ANDES_BANDS
525→ click.echo("Available spectral bands:")
526→ for band in bands:
527→ click.echo(f" {band}")
528→
529→
530→def main():
531→ """Main entry point for the CLI."""
532→ cli()
533→
534→
535→if __name__ == '__main__':
536→ main()
<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>
1→"""
2→CLI utilities for ANDES simulation commands.
3→
4→Provides shared functionality to reduce code duplication across CLI commands.
5→"""
6→
7→import sys
8→import logging
9→from pathlib import Path
10→from typing import Optional, Callable, Any
... (content hidden) ...
<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>
1→"""
2→CLI utilities for ANDES simulation commands.
3→
4→Provides shared functionality to reduce code duplication across CLI commands.
5→"""
6→
7→import sys
8→import logging
9→from pathlib import Path
10→from typing import Optional, Callable, Any
11→
12→try:
13→ import click
14→except ImportError:
15→ print("Error: click package required for CLI. Install with: pip install click")
16→ sys.exit(1)
17→
18→
19→def run_simulation_command(
20→ config,
21→ dry_run: bool,
22→ dry_run_callback: Callable[[], None],
23→ success_message: str = "Simulation completed"
24→) -> Any:
25→ """
26→ Execute a simulation with standard dry-run and error handling.
27→
28→ Parameters
29→ ----------
30→ config : SimulationConfig
31→ The simulation configuration
32→ dry_run : bool
33→ If True, only show what would be done
34→ dry_run_callback : callable
35→ Function to call for dry-run output (should use click.echo)
36→ success_message : str
37→ Message to display on successful completion
38→
39→ Returns
40→ -------
41→ Any
42→ Simulation result, or None if dry_run
43→ """
44→ if dry_run:
45→ dry_run_callback()
46→ return None
47→
48→ # Lazy import to speed up --help
49→ from ..core.simulator import AndesSimulator
50→
51→ try:
52→ simulator = AndesSimulator(config)
53→ result = simulator.run_simulation()
54→ click.echo(success_message)
55→ return result
56→ except Exception as e:
57→ click.echo(f"Simulation failed: {e}", err=True)
58→ sys.exit(1)
59→
60→
61→def build_config_from_options(
62→ simulation_type: str,
63→ band: str,
64→ exposure: float,
65→ source_type: str,
66→ fiber_mode: str,
67→ output_dir: Optional[Path] = None,
68→ fiber: Optional[int] = None,
69→ flux: float = 1.0,
70→ scaling: Optional[float] = None,
71→ velocity_shift: Optional[float] = None,
72→ spectrum_path: Optional[Path] = None,
73→ flux_unit: str = "ph/s/AA",
74→ hdf: Optional[str] = None,
75→ wl_min: Optional[float] = None,
76→ wl_max: Optional[float] = None
77→):
78→ """
79→ Build a SimulationConfig from CLI options.
80→
81→ This centralizes the config-building logic shared across commands.
82→
83→ Parameters
84→ ----------
85→ simulation_type : str
86→ Type of simulation (flat_field, fabry_perot, spectrum)
87→ band : str
88→ Spectral band
89→ exposure : float
90→ Exposure time in seconds
91→ source_type : str
92→ Source type (constant, fabry_perot, csv)
93→ fiber_mode : str
94→ Fiber illumination mode
95→ output_dir : Path, optional
96→ Output directory
97→ fiber : int, optional
98→ Specific fiber number for single mode
99→ flux : float
100→ Flux level
101→ scaling : float, optional
102→ Scaling factor (overrides flux-based calculation if provided)
103→ velocity_shift : float, optional
104→ Velocity shift in m/s
105→ spectrum_path : Path, optional
106→ Path to spectrum file for CSV sources
107→ flux_unit : str
108→ Unit for flux values
109→
110→ Returns
111→ -------
112→ SimulationConfig
113→ Complete simulation configuration
114→ """
115→ from ..core.config import SimulationConfig, SourceConfig, FiberConfig, OutputConfig
116→
117→ # Determine fibers based on mode
118→ if fiber_mode == 'single':
119→ if fiber is None:
120→ raise click.BadParameter("--fiber required for single mode")
121→ fibers = [fiber]
122→ else:
123→ fibers = "all"
124→
125→ # Build source config
126→ source_kwargs = {'type': source_type, 'flux': flux, 'flux_unit': flux_unit}
127→ if source_type in ('fabry_perot', 'constant'):
128→ # For FP and constant sources, total scaling = flux × scaling
129→ effective_scaling = flux * (scaling if scaling is not None else 1.0)
130→ source_kwargs['scaling_factor'] = effective_scaling
131→ if source_type == 'constant':
132→ # For constant sources, the flux IS the scaling factor
133→ source_kwargs['flux'] = effective_scaling
134→ elif scaling is not None:
135→ source_kwargs['scaling_factor'] = scaling
136→ if spectrum_path is not None:
137→ source_kwargs['filepath'] = str(spectrum_path)
138→
139→ # Build output config (default to current working directory)
140→ output_directory = str(output_dir) if output_dir else str(Path.cwd())
141→
142→ return SimulationConfig(
143→ simulation_type=simulation_type,
144→ band=band,
145→ exposure_time=exposure,
146→ velocity_shift=velocity_shift,
147→ hdf_model=hdf,
148→ wl_min=wl_min,
149→ wl_max=wl_max,
150→ source=SourceConfig(**source_kwargs),
151→ fibers=FiberConfig(mode=fiber_mode, fibers=fibers),
152→ output=OutputConfig(directory=output_directory)
153→ )
154→
155→
156→def format_dry_run_output(config, extra_lines: Optional[list] = None) -> None:
157→ """
158→ Print standardized dry-run output.
159→
160→ Parameters
161→ ----------
162→ config : SimulationConfig
163→ The simulation configuration
164→ extra_lines : list, optional
165→ Additional lines to print
166→ """
167→ click.echo("Dry run - would execute:")
168→ click.echo(f" Type: {config.simulation_type}")
169→ click.echo(f" Band: {config.band}")
170→ if config.hdf_model:
171→ click.echo(f" HDF model: {config.hdf_model}")
172→ click.echo(f" Mode: {config.fibers.mode}")
173→ click.echo(f" Exposure: {config.exposure_time}s")
174→
175→ if config.source.type == "constant":
176→ click.echo(f" Flux: {config.source.flux}")
177→ elif config.source.type == "fabry_perot":
178→ click.echo(f" Scaling: {config.source.scaling_factor:.2e}")
179→ elif config.source.type == "lfc":
180→ click.echo(f" LFC flux per line: {config.source.scaling_factor:.2e} ph/s")
181→ elif config.source.type == "csv":
182→ click.echo(f" Spectrum: {config.source.filepath}")
183→ click.echo(f" Scaling: {config.source.scaling_factor}")
184→
185→ if config.velocity_shift:
186→ click.echo(f" Velocity shift: {config.velocity_shift} m/s")
187→ if config.wl_min or config.wl_max:
188→ wl_range = f"{config.wl_min or '...'}-{config.wl_max or '...'} nm"
189→ click.echo(f" Wavelength range: {wl_range}")
190→
191→ if extra_lines:
192→ for line in extra_lines:
193→ click.echo(f" {line}")
194→
195→
196→def setup_logging(verbose: bool) -> None:
197→ """Configure logging based on verbosity."""
198→ level = logging.DEBUG if verbose else logging.INFO
199→ logging.basicConfig(
200→ level=level,
201→ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
202→ )
203→
<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>
1→"""
2→Main ANDES simulation engine.
3→
4→Coordinates all aspects of ANDES spectrograph simulations including
5→source setup, instrument configuration, and output generation.
6→"""
7→
8→import logging
9→import numpy as np
10→from pathlib import Path
... (content hidden) ...
<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>
1→"""
2→Main ANDES simulation engine.
3→
4→Coordinates all aspects of ANDES spectrograph simulations including
5→source setup, instrument configuration, and output generation.
6→"""
7→
8→import logging
9→import numpy as np
10→from pathlib import Path
11→from typing import List, Dict, Any, Optional, Union
12→
13→# Import pyechelle components
14→from pyechelle.simulator import Simulator
15→from pyechelle.sources import ConstantPhotonFlux, CSVSource
16→from pyechelle.telescope import Telescope
17→from pyechelle.spectrograph import ZEMAX, LocalDisturber
18→from pyechelle.CCD import CCD
19→from pyechelle.hdfbuilder import HDFBuilder
20→from pyechelle.spectrograph import InteractiveZEMAX
21→
22→from .config import SimulationConfig
23→from .instruments import get_instrument_config, get_hdf_model_path, get_sed_path
24→from .sources import SourceFactory, SPEED_OF_LIGHT
25→
26→
27→class AndesSimulator:
28→ """
29→ Main ANDES simulation engine.
30→
31→ Handles all types of ANDES simulations including flat field calibrations,
32→ Fabry-Perot wavelength calibrations, stellar observations, and HDF model generation.
33→ """
34→
35→ def __init__(self, config: SimulationConfig):
36→ """
37→ Initialize the simulator with a configuration.
38→
39→ Parameters
40→ ----------
41→ config : SimulationConfig
42→ Complete simulation configuration
43→ """
44→ self.config = config
45→ self.instrument_config = config.instrument_config
46→ self.project_root = Path(__file__).parent.parent.parent
47→
48→ # Initialize logging
49→ logging.basicConfig(level=logging.INFO)
50→ self.logger = logging.getLogger(__name__)
51→
52→ # Initialize source factory
53→ self.source_factory = SourceFactory(self.project_root)
54→
55→ # Will be set during simulation setup
56→ self.simulator = None
57→ self.sources = None
58→
59→ def cleanup(self) -> None:
60→ """Clean up resources (temporary files, etc.)."""
61→ self.source_factory.cleanup()
62→
63→ def __del__(self):
64→ """Ensure cleanup on deletion."""
65→ self.cleanup()
66→
67→ def setup_simulator(self) -> None:
68→ """Set up the pyechelle simulator with instrument configuration."""
69→ # Get HDF model path
70→ hdf_model = self.config.hdf_model or 'default'
71→ if hdf_model != 'default' and (hdf_model.endswith('.hdf') or '/' in hdf_model):
72→ # Already a full path
73→ hdf_path = Path(hdf_model)
74→ if not hdf_path.is_absolute():
75→ hdf_path = self.project_root / hdf_path
76→ else:
77→ hdf_path = get_hdf_model_path(
78→ self.config.band,
79→ hdf_model,
80→ self.project_root
81→ )
82→
83→ if not hdf_path.exists():
84→ raise FileNotFoundError(f"HDF model not found: {hdf_path}")
85→
86→ self.logger.info(f"Using HDF model: {hdf_path}")
87→
88→ # Create spectrograph with optional velocity shift
89→ if self.config.velocity_shift is not None:
90→ tx = self.config.velocity_shift / SPEED_OF_LIGHT
91→ spec = LocalDisturber(ZEMAX(str(hdf_path)), d_tx=tx)
92→ self.logger.info(f"Applied velocity shift: {self.config.velocity_shift} m/s")
93→ else:
94→ spec = ZEMAX(str(hdf_path))
95→
96→ # Create simulator
97→ self.simulator = Simulator(spec)
98→ self.simulator.set_ccd(1)
99→
100→ # Set fiber configuration
101→ fiber_range = range(1, self.instrument_config['n_fibers'] + 1)
102→ self.simulator.set_fibers(fiber_range)
103→
104→ # Set telescope
105→ tel_config = self.instrument_config['telescope']
106→ telescope = Telescope(
107→ tel_config['primary_diameter'],
108→ tel_config['central_obstruction']
109→ )
110→ self.simulator.set_telescope(telescope)
111→
112→ # Set computation parameters
113→ self.simulator.set_cuda(self.config.use_cuda)
114→ self.simulator.max_cpu = self.config.max_cpu
115→ self.simulator.set_exposure_time(self.config.exposure_time)
116→
117→ self.logger.info(f"Simulator configured for {self.config.band}-band")
118→
119→ def setup_sources(self) -> List[Any]:
120→ """
121→ Set up source configuration based on simulation type and fiber configuration.
122→
123→ Returns
124→ -------
125→ List
126→ List of source objects for each fiber
127→ """
128→ n_fibers = self.instrument_config['n_fibers']
129→ illuminated_fibers = self.config.get_fiber_list()
130→
131→ # Handle even/odd mode specially
132→ if self.config.fibers.mode == "even_odd":
133→ sources = self.source_factory.create_even_odd_sources(
134→ self.config.source,
135→ n_fibers,
136→ self.config.band,
137→ wl_min=self.config.wl_min,
138→ wl_max=self.config.wl_max
139→ )
140→ return sources
141→
142→ # Standard illumination pattern
143→ self.sources = self.source_factory.create_fiber_sources(
144→ self.config.source,
145→ n_fibers,
146→ illuminated_fibers,
147→ self.config.band,
148→ wl_min=self.config.wl_min,
149→ wl_max=self.config.wl_max
150→ )
151→
152→ return self.sources
153→
154→ def run_simulation(self, output_path: Optional[Path] = None) -> Any:
155→ """
156→ Run the complete simulation.
157→
158→ Parameters
159→ ----------
160→ output_path : Path, optional
161→ Override output path. If None, uses config default.
162→
163→ Returns
164→ -------
165→ Any
166→ Simulation result (usually image data)
167→ """
168→ if self.config.simulation_type == "hdf_generation":
169→ return self.run_hdf_generation(output_path)
170→ else:
171→ return self.run_standard_simulation(output_path)
172→
173→ def run_hdf_generation(self, output_path: Optional[Path] = None) -> None:
174→ """
175→ Generate HDF model files from ZEMAX.
176→
177→ Parameters
178→ ----------
179→ output_path : Path, optional
180→ Override output path
181→ """
182→ self.logger.info(f"Starting HDF generation for {self.config.band}-band")
183→
184→ # Get ZEMAX file path (this would need to be configured)
185→ band = self.config.band
186→ if band not in self.instrument_config.get('zemax_files', {}):
187→ raise ValueError(f"No ZEMAX file configured for band {band}")
188→
189→ zemax_file = self.instrument_config['zemax_files'][band]
190→
191→ # Create InteractiveZEMAX instance
192→ zmx = InteractiveZEMAX(
193→ name=f'ANDES_{band}',
194→ zemax_filepath=zemax_file
195→ )
196→
197→ # Set grating specifications
198→ zmx.set_grating(surface='ECHELLE', blaze=76)
199→
200→ # Add CCD information
201→ detector_size = self.instrument_config['detector_size']
202→ pixel_size = self.instrument_config['pixel_size']
203→ zmx.add_ccd(1, CCD(detector_size[0], detector_size[1], pixelsize=pixel_size))
204→
205→ # Set up fibers
206→ n_fibers = self.instrument_config['n_fibers']
207→ fiber_size = self.instrument_config['fiber_config']['fiber_size']
208→
209→ # Fiber field positions (vertical distribution)
210→ y_field = (np.arange(n_fibers) - n_fibers/2 + 0.5) * fiber_size / 1000
211→ x_field = np.zeros(n_fibers) # Vertical slit
212→
213→ # Add fibers and set diffraction orders
214→ diffraction_orders = self.instrument_config.get('diffraction_orders')
215→ for i in range(n_fibers):
216→ zmx.add_field(
217→ x_field[i], y_field[i],
218→ fiber_size, fiber_size,
219→ shape='circular',
220→ name='Science fiber'
221→ )
222→ if diffraction_orders:
223→ zmx.set_orders(1, i+1, diffraction_orders)
224→
225→ # Set PSF settings
226→ zmx.psf_settings(
227→ image_delta=3,
228→ image_sampling="128x128",
229→ pupil_sampling="64x64"
230→ )
231→
232→ # Generate HDF file
233→ if output_path is None:
234→ output_path = self.config.get_output_path()
235→
236→ self.logger.info(f"Generating HDF file: {output_path}")
237→ hdf = HDFBuilder(zmx, str(output_path))
238→ hdf.save_to_hdf(n_transformation_per_order=15, n_psfs_per_order=15)
239→ hdf.close()
240→
241→ self.logger.info("HDF generation completed")
242→
243→ def run_standard_simulation(self, output_path: Optional[Path] = None) -> Any:
244→ """
245→ Run standard pyechelle simulation.
246→
247→ Parameters
248→ ----------
249→ output_path : Path, optional
250→ Override output path
251→
252→ Returns
253→ -------
254→ Any
255→ Simulation result
256→ """
257→ # Set up simulator and sources
258→ self.setup_simulator()
259→
260→ # Handle even/odd mode specially
261→ if self.config.fibers.mode == "even_odd":
262→ return self.run_even_odd_simulation(output_path)
263→
264→ # Standard simulation
265→ sources = self.setup_sources()
266→ self.simulator.set_sources(sources)
267→
268→ # Set output path
269→ if output_path is None:
270→ output_path = self.config.get_output_path()
271→
272→ output_path.parent.mkdir(parents=True, exist_ok=True)
273→ self.simulator.set_output(str(output_path), overwrite=self.config.output.overwrite)
274→
275→ self.logger.info(f"Running {self.config.simulation_type} simulation")
276→ self.logger.info(f"Output: {output_path}")
277→
278→ # Run simulation
279→ result = self.simulator.run()
280→
281→ self.logger.info("Simulation completed")
282→ return result
283→
284→ def run_even_odd_simulation(self, output_path: Optional[Path] = None) -> Dict[str, Any]:
285→ """
286→ Run even/odd fiber simulation (creates two output files).
287→
288→ Parameters
289→ ----------
290→ output_path : Path, optional
291→ Base output path
292→
293→ Returns
294→ -------
295→ Dict
296→ Results for even and odd fiber configurations
297→ """
298→ sources_dict = self.setup_sources()
299→ results = {}
300→
301→ for mode in ['even', 'odd']:
302→ self.logger.info(f"Running {mode} fiber simulation")
303→
304→ # Create fresh simulator for each run to avoid state contamination
305→ self.setup_simulator()
306→
307→ # Set sources for this mode
308→ self.simulator.set_sources(sources_dict[mode])
309→
310→ # Set output path
311→ if output_path is None:
312→ out_path = self.config.get_output_path(suffix=mode)
313→ else:
314→ out_path = output_path.parent / f"{output_path.stem}_{mode}{output_path.suffix}"
315→
316→ out_path.parent.mkdir(parents=True, exist_ok=True)
317→ self.simulator.set_output(str(out_path), overwrite=self.config.output.overwrite)
318→
319→ # Run simulation
320→ result = self.simulator.run()
321→ results[mode] = result
322→
323→ self.logger.info(f"{mode.capitalize()} simulation completed: {out_path}")
324→
325→ return results
326→
327→ def run_single_fiber_batch(self, output_dir: Optional[Path] = None) -> Dict[int, Any]:
328→ """
329→ Run single fiber simulations for all fibers in the configuration.
330→
331→ Parameters
332→ ----------
333→ output_dir : Path, optional
334→ Override output directory
335→
336→ Returns
337→ -------
338→ Dict
339→ Results indexed by fiber number
340→ """
341→ if self.config.fibers.mode != "single":
342→ raise ValueError("Single fiber batch requires fibers.mode = 'single'")
343→
344→ self.setup_simulator()
345→ results = {}
346→ n_fibers = self.instrument_config['n_fibers']
347→
348→ # Get list of fibers to simulate
349→ if isinstance(self.config.fibers.fibers, list):
350→ fiber_list = self.config.fibers.fibers
351→ else:
352→ fiber_list = range(1, n_fibers + 1)
353→
354→ for fiber_num in fiber_list:
355→ if fiber_num in self.config.fibers.skip_fibers:
356→ continue
357→
358→ self.logger.info(f"Simulating fiber {fiber_num}")
359→
360→ # Use source factory to create sources for this single fiber
361→ sources = self.source_factory.create_fiber_sources(
362→ self.config.source,
363→ n_fibers,
364→ [fiber_num],
365→ self.config.band,
366→ wl_min=self.config.wl_min,
367→ wl_max=self.config.wl_max
368→ )
369→ self.simulator.set_sources(sources)
370→
371→ # Set output path
372→ if output_dir is None:
373→ output_path = self.config.get_output_path(fiber_num=fiber_num)
374→ else:
375→ filename = f"{self.config.band}_fiber{fiber_num:02d}_{int(self.config.exposure_time)}s.fits"
376→ output_path = output_dir / filename
377→
378→ output_path.parent.mkdir(parents=True, exist_ok=True)
379→ self.simulator.set_output(str(output_path), overwrite=self.config.output.overwrite)
380→
381→ # Run simulation
382→ result = self.simulator.run()
383→ results[fiber_num] = result
384→
385→ self.logger.info(f"Fiber {fiber_num} completed: {output_path}")
386→
387→ return results
388→
389→ @classmethod
390→ def from_config_file(cls, config_path: Union[str, Path]) -> 'AndesSimulator':
391→ """
392→ Create simulator from a YAML configuration file.
393→
394→ Parameters
395→ ----------
396→ config_path : str or Path
397→ Path to YAML configuration file
398→
399→ Returns
400→ -------
401→ AndesSimulator
402→ Configured simulator instance
403→ """
404→ config = SimulationConfig.from_yaml(config_path)
405→ return cls(config)
406→
407→ @classmethod
408→ def quick_flat_field(cls, band: str, fiber_mode: str = "all", **kwargs) -> 'AndesSimulator':
409→ """
410→ Quick setup for flat field simulations.
411→
412→ Parameters
413→ ----------
414→ band : str
415→ Spectral band
416→ fiber_mode : str
417→ Fiber illumination mode
418→ **kwargs
419→ Additional configuration parameters
420→
421→ Returns
422→ -------
423→ AndesSimulator
424→ Configured simulator
425→ """
426→ from .config import SimulationConfig, SourceConfig, FiberConfig
427→
428→ config = SimulationConfig(
429→ simulation_type="flat_field",
430→ band=band,
431→ source=SourceConfig(type="constant", flux=0.001),
432→ fibers=FiberConfig(mode=fiber_mode),
433→ **kwargs
434→ )
435→ return cls(config)
436→
437→ @classmethod
438→ def quick_fabry_perot(cls, band: str, **kwargs) -> 'AndesSimulator':
439→ """
440→ Quick setup for Fabry-Perot simulations.
441→
442→ Parameters
443→ ----------
444→ band : str
445→ Spectral band
446→ **kwargs
447→ Additional configuration parameters
448→
449→ Returns
450→ -------
451→ AndesSimulator
452→ Configured simulator
453→ """
454→ from .config import SimulationConfig, SourceConfig, FiberConfig
455→
456→ config = SimulationConfig(
457→ simulation_type="fabry_perot",
458→ band=band,
459→ source=SourceConfig(type="fabry_perot", scaling_factor=5e9),
460→ fibers=FiberConfig(mode="all"),
461→ **kwargs
462→ )
463→ return cls(config)
<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>
1→"""
2→Configuration management for ANDES simulations.
3→
4→Handles loading and validation of simulation configurations from YAML files
5→and command-line parameters.
6→"""
7→
8→import yaml
9→import logging
10→from pathlib import Path
... (content hidden) ...
<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>
1→"""
2→Configuration management for ANDES simulations.
3→
4→Handles loading and validation of simulation configurations from YAML files
5→and command-line parameters.
6→"""
7→
8→import yaml
9→import logging
10→from pathlib import Path
11→from typing import Dict, List, Any, Union, Optional
12→from dataclasses import dataclass, field
13→
14→from .instruments import get_instrument_config, get_all_bands, FIBER_CONFIG, validate_wavelength_range
15→
16→
17→@dataclass
18→class OutputConfig:
19→ """Configuration for simulation outputs."""
20→ directory: str = "."
21→ filename_template: str = "{band}_{type}_{exposure}s.fits"
22→ overwrite: bool = True
23→
24→
25→@dataclass
26→class SourceConfig:
27→ """Configuration for source spectrum."""
28→ type: str = "constant" # constant, csv, fabry_perot
29→ flux: float = 1.0
30→ scaling_factor: float = 1.0
31→ filepath: Optional[str] = None # For CSV sources
32→ wavelength_unit: str = "nm"
33→ flux_unit: str = "ph/s"
34→
35→
36→@dataclass
37→class FiberConfig:
38→ """Configuration for fiber illumination."""
39→ mode: str = "all" # all, single, even_odd, first_slit, second_slit, custom
40→ fibers: Union[str, List[int]] = "all" # "all", fiber numbers, or ranges
41→ skip_fibers: List[int] = field(default_factory=list)
42→
43→
44→@dataclass
45→class PSFConfig:
46→ """Configuration for PSF convolution."""
47→ enabled: bool = False
48→ kernel_size: tuple = (4, 4) # (dimx, dimy)
49→ sigma: float = 1.0
50→ edge_blank: str = "random" # none, top, bottom, left, right, random
51→
52→
53→@dataclass
54→class SimulationConfig:
55→ """Main simulation configuration."""
56→ # Simulation type and basic parameters
57→ simulation_type: str = "flat_field" # flat_field, fabry_perot, spectrum, hdf_generation
58→ band: str = "Y"
59→ exposure_time: float = 30.0
60→
61→ # Instrument configuration
62→ hdf_model: Optional[str] = None # Use default if None
63→ use_cuda: bool = False
64→ max_cpu: int = 1 # Use performance cores only (M4 has 10 perf + 4 efficiency cores)
65→
66→ # Source configuration
67→ source: SourceConfig = field(default_factory=SourceConfig)
68→
69→ # Fiber configuration
70→ fibers: FiberConfig = field(default_factory=FiberConfig)
71→
72→ # Special features
73→ velocity_shift: Optional[float] = None # m/s for Doppler shifts
74→ thermal_model: Optional[str] = None # For thermal variations
75→ wl_min: Optional[float] = None # Minimum wavelength in nm
76→ wl_max: Optional[float] = None # Maximum wavelength in nm
77→
78→ # Post-processing
79→ psf: PSFConfig = field(default_factory=PSFConfig)
80→ combine_fibers: bool = False
81→
82→ # Output configuration
83→ output: OutputConfig = field(default_factory=OutputConfig)
84→
85→ # Validation and derived properties
86→ _instrument_config: Optional[Dict] = field(default=None, init=False)
87→
88→ def __post_init__(self):
89→ """Validate configuration after initialization."""
90→ self.validate()
91→ self._instrument_config = get_instrument_config(self.band)
92→
93→ def validate(self):
94→ """Validate the configuration parameters."""
95→ # Validate band
96→ if self.band not in get_all_bands():
97→ raise ValueError(f"Invalid band '{self.band}'. Available: {get_all_bands()}")
98→
99→ # Validate wavelength limits against band
100→ validate_wavelength_range(self.band, self.wl_min, self.wl_max)
101→
102→ # Validate simulation type
103→ valid_sim_types = ["flat_field", "fabry_perot", "spectrum", "lfc", "hdf_generation"]
104→ if self.simulation_type not in valid_sim_types:
105→ raise ValueError(f"Invalid simulation_type '{self.simulation_type}'. Available: {valid_sim_types}")
106→
107→ # Validate fiber mode
108→ valid_fiber_modes = ["all", "single", "even_odd", "slitA", "slitB", "cal",
109→ "ifu", "ring0", "ring1", "ring2", "ring3", "ring4", "custom"]
110→ if self.fibers.mode not in valid_fiber_modes:
111→ raise ValueError(f"Invalid fiber mode '{self.fibers.mode}'. Available: {valid_fiber_modes}")
112→
113→ # Validate source type
114→ valid_source_types = ["constant", "csv", "fabry_perot", "lfc"]
115→ if self.source.type not in valid_source_types:
116→ raise ValueError(f"Invalid source type '{self.source.type}'. Available: {valid_source_types}")
117→
118→ # Check for required parameters based on simulation type
119→ if self.simulation_type == "spectrum" and self.source.type == "csv":
120→ if self.source.filepath is None:
121→ raise ValueError("CSV filepath required for spectrum simulations")
122→
123→ if self.fibers.mode == "single" and isinstance(self.fibers.fibers, str):
124→ raise ValueError("Specific fiber number required for single fiber mode")
125→
126→ @property
127→ def instrument_config(self) -> Dict:
128→ """Get the instrument configuration for this band."""
129→ if self._instrument_config is None:
130→ self._instrument_config = get_instrument_config(self.band)
131→ return self._instrument_config
132→
133→ @property
134→ def n_fibers(self) -> int:
135→ """Get the number of fibers for this band."""
136→ return self.instrument_config['n_fibers']
137→
138→ @property
139→ def detector_size(self) -> tuple:
140→ """Get the detector size for this band."""
141→ return self.instrument_config['detector_size']
142→
143→ def get_fiber_list(self) -> List[int]:
144→ """
145→ Get the list of fiber numbers to illuminate based on configuration.
146→
147→ Returns
148→ -------
149→ List[int]
150→ List of 1-based fiber numbers to illuminate
151→ """
152→ n_fibers = self.n_fibers
153→
154→ if self.fibers.mode == "all":
155→ if isinstance(self.fibers.fibers, str) and self.fibers.fibers == "all":
156→ fibers = list(range(1, n_fibers + 1))
157→ elif isinstance(self.fibers.fibers, list):
158→ fibers = self.fibers.fibers
159→ else:
160→ raise ValueError("Invalid fiber specification for 'all' mode")
161→
162→ elif self.fibers.mode == "single":
163→ if isinstance(self.fibers.fibers, int):
164→ fibers = [self.fibers.fibers]
165→ elif isinstance(self.fibers.fibers, list) and len(self.fibers.fibers) == 1:
166→ fibers = self.fibers.fibers
167→ else:
168→ raise ValueError("Single fiber mode requires exactly one fiber number")
169→
170→ elif self.fibers.mode == "even_odd":
171→ # This mode requires special handling in the simulator
172→ # Return all fibers, but the simulator will handle even/odd logic
173→ fibers = list(range(1, n_fibers + 1))
174→
175→ elif self.fibers.mode in ("slitA", "slitB", "cal"):
176→ # Slit modes: use appropriate config based on band
177→ if self.band in ['Y', 'J', 'H']:
178→ fiber_cfg = FIBER_CONFIG['YJH_SL']['slits']
179→ else:
180→ fiber_cfg = FIBER_CONFIG['UBVRIZ']['slits']
181→ key = 'cal_fibers' if self.fibers.mode == 'cal' else self.fibers.mode
182→ fibers = list(fiber_cfg[key])
183→
184→ elif self.fibers.mode in ("ifu", "ring0", "ring1", "ring2", "ring3", "ring4"):
185→ # IFU modes: only for YJH bands
186→ if self.band not in ['Y', 'J', 'H']:
187→ raise ValueError("IFU/ring modes only applicable to YJH bands")
188→ fiber_cfg = FIBER_CONFIG['YJH_IFU']['slits']
189→ if self.fibers.mode == "ifu":
190→ # All IFU fibers (all rings combined)
191→ fibers = []
192→ for key in ('ring0', 'ring1', 'ring2', 'ring3', 'ring4'):
193→ fibers.extend(fiber_cfg[key])
194→ else:
195→ fibers = list(fiber_cfg[self.fibers.mode])
196→
197→ elif self.fibers.mode == "custom":
198→ if isinstance(self.fibers.fibers, list):
199→ fibers = self.fibers.fibers
200→ else:
201→ raise ValueError("Custom mode requires explicit fiber list")
202→ else:
203→ raise ValueError(f"Unknown fiber mode: {self.fibers.mode}")
204→
205→ # Remove skipped fibers
206→ if self.fibers.skip_fibers:
207→ fibers = [f for f in fibers if f not in self.fibers.skip_fibers]
208→
209→ return fibers
210→
211→ def get_output_path(self, fiber_num: Optional[int] = None, suffix: str = "") -> Path:
212→ """
213→ Generate output file path based on configuration.
214→
215→ Parameters
216→ ----------
217→ fiber_num : int, optional
218→ Fiber number for single-fiber outputs
219→ suffix : str, optional
220→ Additional suffix for filename
221→
222→ Returns
223→ -------
224→ Path
225→ Complete output file path
226→ """
227→ # Format directory path
228→ directory = self.output.directory.format(
229→ band=self.band,
230→ type=self.simulation_type
231→ )
232→
233→ # Format filename
234→ filename_parts = {
235→ 'band': self.band,
236→ 'type': self.simulation_type,
237→ 'exposure': int(self.exposure_time),
238→ 'fiber': f"{fiber_num:02d}" if fiber_num is not None else "",
239→ 'mode': self.fibers.mode
240→ }
241→
242→ if suffix:
243→ filename_parts['suffix'] = suffix
244→
245→ # Create filename based on simulation type
246→ wl_suffix = self._format_wavelength_suffix()
247→ if self.simulation_type == "flat_field":
248→ if self.fibers.mode == "single" and fiber_num is not None:
249→ filename = f"{self.band}_FF_fiber{fiber_num:02d}_{int(self.exposure_time)}s{wl_suffix}.fits"
250→ elif self.fibers.mode == "even_odd":
251→ filename = f"{self.band}_FF_{suffix}_{int(self.exposure_time)}s{wl_suffix}.fits"
252→ else:
253→ filename = f"{self.band}_FF_{self.fibers.mode}_{int(self.exposure_time)}s{wl_suffix}.fits"
254→ elif self.simulation_type == "fabry_perot":
255→ if fiber_num is not None:
256→ shift_str = f"_shift{suffix}" if suffix else ""
257→ filename = f"{self.band}_FP_fiber{fiber_num:02d}{shift_str}{wl_suffix}.fits"
258→ else:
259→ filename = f"{self.band}_FP_{self.fibers.mode}_{int(self.exposure_time)}s{wl_suffix}.fits"
260→ elif self.simulation_type == "spectrum":
261→ if fiber_num is not None:
262→ filename = f"{self.band}_spectrum_fiber{fiber_num:02d}{wl_suffix}.fits"
263→ else:
264→ filename = f"{self.band}_spectrum_{int(self.exposure_time)}s{wl_suffix}.fits"
265→ elif self.simulation_type == "lfc":
266→ if fiber_num is not None:
267→ filename = f"{self.band}_LFC_fiber{fiber_num:02d}{wl_suffix}.fits"
268→ else:
269→ filename = f"{self.band}_LFC_{self.fibers.mode}_{int(self.exposure_time)}s{wl_suffix}.fits"
270→ elif self.simulation_type == "hdf_generation":
271→ filename = f"ANDES_75fibre_{self.band}.hdf" if self.band in ['Y', 'J', 'H'] else f"ANDES_123_{self.band}3.hdf"
272→ else:
273→ # Use template
274→ filename = self.output.filename_template.format(**filename_parts)
275→ filename = self._append_suffix(filename, wl_suffix)
276→
277→ return Path(directory) / filename
278→
279→ def _format_wavelength_suffix(self) -> str:
280→ if self.wl_min is None and self.wl_max is None:
281→ return ""
282→
283→ def fmt(value: Optional[float]) -> str:
284→ if value is None:
285→ return "x"
286→ text = f"{value:.3f}"
287→ text = text.rstrip('0').rstrip('.')
288→ if not text:
289→ text = "0"
290→ return text.replace('.', 'p')
291→
292→ return f"_wl{fmt(self.wl_min)}-{fmt(self.wl_max)}"
293→
294→ def _append_suffix(self, filename: str, suffix: str) -> str:
295→ if not suffix:
296→ return filename
297→ if '.' in filename:
298→ stem, ext = filename.rsplit('.', 1)
299→ return f"{stem}{suffix}.{ext}"
300→ return f"{filename}{suffix}"
301→
302→ @classmethod
303→ def from_yaml(cls, yaml_path: Union[str, Path]) -> 'SimulationConfig':
304→ """
305→ Load configuration from a YAML file.
306→
307→ Parameters
308→ ----------
309→ yaml_path : str or Path
310→ Path to YAML configuration file
311→
312→ Returns
313→ -------
314→ SimulationConfig
315→ Loaded configuration object
316→ """
317→ yaml_path = Path(yaml_path)
318→ if not yaml_path.exists():
319→ raise FileNotFoundError(f"Configuration file not found: {yaml_path}")
320→
321→ with open(yaml_path, 'r') as f:
322→ data = yaml.safe_load(f)
323→
324→ return cls.from_dict(data)
325→
326→ @classmethod
327→ def from_dict(cls, data: Dict[str, Any]) -> 'SimulationConfig':
328→ """
329→ Create configuration from a dictionary.
330→
331→ Parameters
332→ ----------
333→ data : dict
334→ Configuration dictionary
335→
336→ Returns
337→ -------
338→ SimulationConfig
339→ Configuration object
340→ """
341→ # Handle nested configurations
342→ if 'source' in data:
343→ data['source'] = SourceConfig(**data['source'])
344→ if 'fibers' in data:
345→ data['fibers'] = FiberConfig(**data['fibers'])
346→ if 'psf' in data:
347→ data['psf'] = PSFConfig(**data['psf'])
348→ if 'output' in data:
349→ data['output'] = OutputConfig(**data['output'])
350→
351→ return cls(**data)
352→
353→ def to_yaml(self, yaml_path: Union[str, Path]) -> None:
354→ """
355→ Save configuration to a YAML file.
356→
357→ Parameters
358→ ----------
359→ yaml_path : str or Path
360→ Output path for YAML file
361→ """
362→ yaml_path = Path(yaml_path)
363→ yaml_path.parent.mkdir(parents=True, exist_ok=True)
364→
365→ # Convert to dictionary, handling dataclasses
366→ data = {}
367→ for field_name, field_obj in self.__dataclass_fields__.items():
368→ value = getattr(self, field_name)
369→ if hasattr(value, '__dataclass_fields__'):
370→ # Convert nested dataclass to dict
371→ data[field_name] = {k: getattr(value, k) for k in value.__dataclass_fields__.keys()}
372→ elif not field_name.startswith('_'):
373→ data[field_name] = value
374→
375→ with open(yaml_path, 'w') as f:
376→ yaml.dump(data, f, default_flow_style=False, sort_keys=False)
377→
378→
379→def create_template_configs(output_dir: Path) -> None:
380→ """
381→ Create template configuration files for different simulation types.
382→
383→ Parameters
384→ ----------
385→ output_dir : Path
386→ Directory to save template files
387→ """
388→ output_dir.mkdir(parents=True, exist_ok=True)
389→
390→ # Flat field single fiber template
391→ ff_single = SimulationConfig(
392→ simulation_type="flat_field",
393→ band="Y",
394→ exposure_time=1.0,
395→ source=SourceConfig(type="constant", flux=0.001),
396→ fibers=FiberConfig(mode="single", fibers=[1]),
397→ output=OutputConfig(
398→ directory="../{band}/",
399→ filename_template="{band}_FF_fiber{fiber:02d}_{exposure}s.fits"
400→ )
401→ )
402→ ff_single.to_yaml(output_dir / "flat_field_single_fiber.yaml")
403→
404→ # Flat field even/odd template
405→ ff_even_odd = SimulationConfig(
406→ simulation_type="flat_field",
407→ band="R",
408→ exposure_time=30.0,
409→ source=SourceConfig(type="constant", flux=0.001),
410→ fibers=FiberConfig(mode="even_odd"),
411→ output=OutputConfig(directory="../{band}/")
412→ )
413→ ff_even_odd.to_yaml(output_dir / "flat_field_even_odd.yaml")
414→
415→ # Fabry-Perot template
416→ fp_config = SimulationConfig(
417→ simulation_type="fabry_perot",
418→ band="Y",
419→ exposure_time=30.0,
420→ source=SourceConfig(type="fabry_perot", scaling_factor=5e9),
421→ fibers=FiberConfig(mode="all"),
422→ )
423→ fp_config.to_yaml(output_dir / "fabry_perot_all_fibers.yaml")
424→
425→ # Spectrum simulation template
426→ spectrum_config = SimulationConfig(
427→ simulation_type="spectrum",
428→ band="J",
429→ exposure_time=30.0,
430→ source=SourceConfig(
431→ type="csv",
432→ filepath="SED/star_spectrum.csv",
433→ scaling_factor=5e3
434→ ),
435→ fibers=FiberConfig(mode="single", fibers=[33])
436→ )
437→ spectrum_config.to_yaml(output_dir / "spectrum_simulation.yaml")
438→
439→ # HDF generation template
440→ hdf_config = SimulationConfig(
441→ simulation_type="hdf_generation",
442→ band="Y",
443→ source=SourceConfig(type="zemax"), # Special type for HDF generation
444→ output=OutputConfig(directory="HDF/")
445→ )
446→ hdf_config.to_yaml(output_dir / "hdf_generation.yaml")
447→
448→ logging.info(f"Created template configurations in {output_dir}")
449→
<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>
Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable
fib_eff field to the config and the CLI option, then apply it in the simulator.Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable
fib_eff and pass it through. Let me update flat_field, fabry_perot, and lfc commands.Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable
Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable
1→"""
2→Instrument configurations for ANDES spectrograph bands.
3→
4→Defines the fiber counts, detector sizes, HDF models, and other
5→band-specific parameters for all ANDES spectral channels.
6→"""
7→
8→from pathlib import Path
9→from typing import Dict, List, Any
10→
... (content hidden) ...
<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>
1→"""
2→Instrument configurations for ANDES spectrograph bands.
3→
4→Defines the fiber counts, detector sizes, HDF models, and other
5→band-specific parameters for all ANDES spectral channels.
6→"""
7→
8→from pathlib import Path
9→from typing import Dict, List, Any
10→
11→# Instrument configurations for each spectral band
12→INSTRUMENTS = {
13→ # Near-infrared bands (75 fibers)
14→ 'Y': {
15→ 'n_fibers': 75,
16→ 'detector_size': (4096, 4096),
17→ 'pixel_size': 15, # microns
18→ 'hdf_models': {
19→ 'default': 'ANDES_75fibre_Y',
20→ 'with_fiber_eff': 'ANDES_Y01_wFiberEff'
21→ },
22→ 'zemax_files': {
23→ 'Y': 'HIRES_Y_21jan2022_sconf_noap.zmx'
24→ },
25→ 'diffraction_orders': list(range(109, 127)),
26→ 'fp_spectrum': 'FP_simulation_YJH_finesse_26.csv',
27→ 'sampling': 2.1,
28→ 'skip_fibers': [3, 4, 36, 37, 39, 40, 72, 73]
29→ },
30→ 'J': {
31→ 'n_fibers': 75,
32→ 'detector_size': (4096, 4096),
33→ 'pixel_size': 15,
34→ 'hdf_models': {
35→ 'default': 'ANDES_75fibre_J'
36→ },
37→ 'zemax_files': {
38→ 'J': 'HIRES_J_21jan2022_sconf_noap.zmx'
39→ },
40→ 'diffraction_orders': list(range(90, 108)),
41→ 'fp_spectrum': 'FP_simulation_YJH_finesse_26.csv',
42→ 'sampling': 2.1,
43→ 'skip_fibers': [3, 4, 36, 37, 39, 40, 72, 73]
44→ },
45→ 'H': {
46→ 'n_fibers': 75,
47→ 'detector_size': (4096, 4096),
48→ 'pixel_size': 15,
49→ 'hdf_models': {
50→ 'default': 'ANDES_75fibre_H'
51→ },
52→ 'zemax_files': {
53→ 'H': 'HIRES_H_21jan2022_sconf.zmx'
54→ },
55→ 'diffraction_orders': list(range(68, 83)),
56→ 'fp_spectrum': 'FP_simulation_YJH_finesse_26.csv',
57→ 'sampling': 2.1,
58→ 'skip_fibers': [3, 4, 36, 37, 39, 40, 72, 73]
59→ },
60→
61→ # Optical bands (66 fibers)
62→ 'R': {
63→ 'n_fibers': 66,
64→ 'detector_size': (9216, 9232),
65→ 'pixel_size': 15,
66→ 'hdf_models': {
67→ 'default': 'ANDES_123_R3',
68→ 'thermal_variants': [
69→ 'ANDES_full_F18A33_win_jmr_MC_T0019_Rband_p0',
70→ 'Andes_full_F18A33_win_jmr_MC_T0108_Rband_P0_cfg1'
71→ ]
72→ },
73→ 'fp_spectrum': 'FP_simulation_RIZ_finesse_23.csv',
74→ 'sampling': 4.0,
75→ 'skip_fibers': [32, 35]
76→ },
77→ 'IZ': {
78→ 'n_fibers': 66,
79→ 'detector_size': (9216, 9232),
80→ 'pixel_size': 15,
81→ 'hdf_models': {
82→ 'default': 'ANDES_123_IZ3',
83→ 'thermal_variants': [
84→ 'Andes_F18A33_VM246aa_win_jmr9_MC_T0045_IZband_P0_cf1',
85→ 'Andes_full_F18A33_win_jmr_MC_T0028_IZband_P0'
86→ ]
87→ },
88→ 'fp_spectrum': 'FP_simulation_RIZ_finesse_23.csv',
89→ 'sampling': 4.0,
90→ 'skip_fibers': [32, 35]
91→ },
92→ 'U': {
93→ 'n_fibers': 66,
94→ 'detector_size': (9216, 9232),
95→ 'pixel_size': 15,
96→ 'hdf_models': {
97→ 'default': 'ANDES_123_U3'
98→ },
99→ 'fp_spectrum': 'FP_simulation_UBV_finesse_23.csv',
100→ 'sampling': 4.0,
101→ 'skip_fibers': [32, 35]
102→ },
103→ 'B': {
104→ 'n_fibers': 66,
105→ 'detector_size': (9216, 9232),
106→ 'pixel_size': 15,
107→ 'hdf_models': {
108→ 'default': 'ANDES_123_B3'
109→ },
110→ 'fp_spectrum': 'FP_simulation_UBV_finesse_26.csv',
111→ 'sampling': 4.0,
112→ 'skip_fibers': [32, 35]
113→ },
114→ 'V': {
115→ 'n_fibers': 66,
116→ 'detector_size': (9216, 9232),
117→ 'pixel_size': 15,
118→ 'hdf_models': {
119→ 'default': 'ANDES_123_V3'
120→ },
121→ 'fp_spectrum': 'FP_simulation_UBV_finesse_26.csv',
122→ 'sampling': 4.0,
123→ 'skip_fibers': [32, 35]
124→ }
125→}
126→
127→# Band wavelength ranges in nm
128→BAND_WAVELENGTH_RANGES = {
129→ 'U': (310, 390),
130→ 'B': (390, 490),
131→ 'V': (490, 620),
132→ 'R': (620, 800),
133→ 'IZ': (800, 1000),
134→ 'Y': (1000, 1150),
135→ 'J': (1150, 1350),
136→ 'H': (1450, 1800),
137→}
138→
139→# Telescope configuration (ELT)
140→TELESCOPE = {
141→ 'primary_diameter': 39.3, # meters
142→ 'central_obstruction': 4.09 # meters
143→}
144→
145→# Standard fiber sizes and configurations
146→FIBER_CONFIG = {
147→ 'UBVRIZ': {
148→ 'n_fibers': 66,
149→ 'slits': {
150→ 'slitA': list(range(1, 32)),
151→ 'slitB': list(range(35, 66)),
152→ 'cal_fibers': [33, 34]
153→ }
154→ },
155→ 'YJH_SL': {
156→ 'fiber_size': 474, # micrometers
157→ 'n_fibers': 75,
158→ 'slits': {
159→ 'slitA': list(range(4, 35)),
160→ 'slitB': list(range(42, 73)),
161→ 'cal_fibers': [1,37,38,39,75]
162→ }
163→ },
164→ 'YJH_IFU': {
165→ 'fiber_size': 474, # micrometers
166→ 'n_fibers': 75,
167→ 'slits': {
168→ 'ring0': [3],
169→ 'ring1': [6,8,10,12,14,16],
170→ 'ring2': list(range(18, 18+12)),
171→ 'ring3': list(range(31, 31+18)),
172→ 'ring4': list(range(50, 50+24)),
173→ 'cal_fibers': [1,75]
174→ }
175→ }
176→}
177→
178→
179→def get_instrument_config(band: str) -> Dict[str, Any]:
180→ """
181→ Get instrument configuration for a specific band.
182→
183→ Parameters
184→ ----------
185→ band : str
186→ Spectral band name (Y, J, H, R, IZ, U, B, V)
187→
188→ Returns
189→ -------
190→ dict
191→ Complete instrument configuration including telescope and fiber setup
192→ """
193→ if band not in INSTRUMENTS:
194→ raise ValueError(f"Unknown band '{band}'. Available bands: {list(INSTRUMENTS.keys())}")
195→
196→ config = INSTRUMENTS[band].copy()
197→ config['telescope'] = TELESCOPE.copy()
198→
199→ # Add appropriate fiber configuration
200→ if band in ['Y', 'J', 'H']:
201→ config['fiber_config'] = FIBER_CONFIG['YJH_SL'].copy()
202→ else:
203→ config['fiber_config'] = FIBER_CONFIG['UBVRIZ'].copy()
204→
205→ return config
206→
207→
208→def get_all_bands() -> List[str]:
209→ """Get list of all available spectral bands."""
210→ return list(INSTRUMENTS.keys())
211→
212→
213→def get_nir_bands() -> List[str]:
214→ """Get list of near-infrared bands (YJH)."""
215→ return ['Y', 'J', 'H']
216→
217→
218→def get_optical_bands() -> List[str]:
219→ """Get list of optical bands (RIUZV)."""
220→ return ['R', 'IZ', 'U', 'B', 'V']
221→
222→
223→def get_hdf_model_path(band: str, model_type: str = 'default', project_root: Path = None) -> Path:
224→ """
225→ Get the full path to an HDF model file.
226→
227→ Parameters
228→ ----------
229→ band : str
230→ Spectral band name
231→ model_type : str
232→ Type of model ('default', 'with_fiber_eff', or specific thermal variant)
233→ project_root : Path, optional
234→ Project root directory. If None, uses current file location.
235→
236→ Returns
237→ -------
238→ Path
239→ Full path to HDF model file
240→ """
241→ if project_root is None:
242→ project_root = Path(__file__).parent.parent.parent
243→
244→ config = get_instrument_config(band)
245→
246→ if model_type == 'default':
247→ model_name = config['hdf_models']['default']
248→ elif model_type in config['hdf_models']:
249→ if isinstance(config['hdf_models'][model_type], list):
250→ # For thermal variants, return the first one as default
251→ model_name = config['hdf_models'][model_type][0]
252→ else:
253→ model_name = config['hdf_models'][model_type]
254→ else:
255→ # Assume it's a specific model name
256→ model_name = model_type
257→
258→ return project_root / 'HDF' / f'{model_name}.hdf'
259→
260→
261→def infer_band_from_hdf(hdf_path: Path) -> str:
262→ """
263→ Infer spectral band from HDF file by reading wavelength coverage.
264→
265→ Parameters
266→ ----------
267→ hdf_path : Path
268→ Path to HDF model file
269→
270→ Returns
271→ -------
272→ str
273→ Inferred band name
274→
275→ Raises
276→ ------
277→ ValueError
278→ If band cannot be determined from wavelength range
279→ """
280→ import h5py
281→
282→ with h5py.File(hdf_path, 'r') as f:
283→ ccd = f['CCD_1']
284→ fiber_key = next(k for k in ccd.keys() if k.startswith('fiber_'))
285→ fiber = ccd[fiber_key]
286→
287→ # Collect wavelengths from PSF entries
288→ wavelengths = []
289→ for key in fiber.keys():
290→ if key.startswith('psf_order'):
291→ psf_grp = fiber[key]
292→ for wl_key in psf_grp.keys():
293→ if wl_key.startswith('wavelength_'):
294→ # Parse wavelength from key name (in micrometers)
295→ wl = float(wl_key.replace('wavelength_', ''))
296→ wavelengths.append(wl)
297→ break # One sample per order is enough
298→
299→ if not wavelengths:
300→ raise ValueError(f"No wavelength data found in {hdf_path}")
301→
302→ # Wavelengths in micrometers, convert to nm
303→ wl_min = min(wavelengths) * 1000
304→ wl_max = max(wavelengths) * 1000
305→ wl_center = (wl_min + wl_max) / 2
306→
307→ for band, (low, high) in BAND_WAVELENGTH_RANGES.items():
308→ if low <= wl_center <= high:
309→ return band
310→
311→ raise ValueError(f"Cannot infer band from wavelength range {wl_min:.0f}-{wl_max:.0f}nm")
312→
313→
314→def infer_band_from_wavelengths(wl_min: float = None, wl_max: float = None) -> str:
315→ """
316→ Infer spectral band from wavelength limits.
317→
318→ Parameters
319→ ----------
320→ wl_min : float, optional
321→ Minimum wavelength in nm
322→ wl_max : float, optional
323→ Maximum wavelength in nm
324→
325→ Returns
326→ -------
327→ str
328→ Inferred band name
329→
330→ Raises
331→ ------
332→ ValueError
333→ If band cannot be uniquely determined from wavelength range
334→ """
335→ if wl_min is None and wl_max is None:
336→ raise ValueError("At least one of wl_min or wl_max must be provided")
337→
338→ matching_bands = []
339→ for band, (low, high) in BAND_WAVELENGTH_RANGES.items():
340→ # Check if any part of the requested range overlaps with this band
341→ req_min = wl_min if wl_min is not None else low
342→ req_max = wl_max if wl_max is not None else high
343→ if req_min <= high and req_max >= low:
344→ matching_bands.append(band)
345→
346→ if len(matching_bands) == 0:
347→ raise ValueError(
348→ f"Wavelength range {wl_min}-{wl_max}nm does not match any band")
349→ if len(matching_bands) > 1:
350→ raise ValueError(
351→ f"Wavelength range {wl_min}-{wl_max}nm is ambiguous, "
352→ f"could match: {', '.join(matching_bands)}. Use --band to specify.")
353→
354→ return matching_bands[0]
355→
356→
357→def validate_wavelength_range(band: str, wl_min: float = None, wl_max: float = None) -> None:
358→ """
359→ Validate that wavelength limits fall within the band's range.
360→
361→ Parameters
362→ ----------
363→ band : str
364→ Spectral band name
365→ wl_min : float, optional
366→ Minimum wavelength in nm
367→ wl_max : float, optional
368→ Maximum wavelength in nm
369→
370→ Raises
371→ ------
372→ ValueError
373→ If wavelength limits are outside the band's range
374→ """
375→ if wl_min is None and wl_max is None:
376→ return
377→
378→ if band not in BAND_WAVELENGTH_RANGES:
379→ raise ValueError(f"Unknown band '{band}'")
380→
381→ band_min, band_max = BAND_WAVELENGTH_RANGES[band]
382→
383→ if wl_min is not None and wl_min < band_min:
384→ raise ValueError(
385→ f"wl_min={wl_min}nm is below {band}-band range ({band_min}-{band_max}nm)")
386→ if wl_max is not None and wl_max > band_max:
387→ raise ValueError(
388→ f"wl_max={wl_max}nm is above {band}-band range ({band_min}-{band_max}nm)")
389→ if wl_min is not None and wl_max is not None and wl_min >= wl_max:
390→ raise ValueError(f"wl_min={wl_min}nm must be less than wl_max={wl_max}nm")
391→
392→
393→def get_sed_path(band: str, project_root: Path = None) -> Path:
394→ """
395→ Get the path to the Fabry-Perot SED file for a band.
396→
397→ Parameters
398→ ----------
399→ band : str
400→ Spectral band name
401→ project_root : Path, optional
402→ Project root directory
403→
404→ Returns
405→ -------
406→ Path
407→ Full path to SED file
408→ """
409→ if project_root is None:
410→ project_root = Path(__file__).parent.parent.parent
411→
412→ config = get_instrument_config(band)
413→ return project_root / 'SED' / config['fp_spectrum']
<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>
setup_simulator() to apply the efficiency after loading the spectrograph._apply_fiber_efficiency method in setup_simulator() after creating the spectrograph.Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable
Dry run - would execute:
Type: flat_field
Band: H
Dry run - would execute:
Type: flat_field
Band: H
HDF model: /Users/tom/ANDES/E2E/src/HDF/ANDES_75fibre_H.hdf
Mode: slitA
Exposure: 1.0s
Flux: 200000.0
Wavelength range: 1600.0-1602.0 nm
Fiber efficiency: 0.7-0.95
2025-12-16 11:03:51,196 - andes_simulator.core.simulator - INFO - Using HDF model: /Users/tom/ANDES/E2E/src/HDF/ANDES_75fibre_H.hdf
2025-12-16 11:03:51,196 - root - INFO - Using the model file /Users/tom/ANDES/E2E/src/HDF/ANDES_75fibre_H.hdf
2025-12-16 11:03:51,196 - andes_simulator.core.simulator - INFO - Applying fiber efficiency: 0.7-0.95
2025-12-16 11:03:51,196 - andes_simulator.core.simulator - INFO - Using HDF model: /Users/tom/ANDES/E2E/src/HDF/ANDES_75fibre_H.hdf
2025-12-16 11:03:51,196 - root - INFO - Using the model file /Users/tom/ANDES/E2E/src/HDF/ANDES_75fibre_H.hdf
2025-12-16 11:03:51,196 - andes_simulator.core.simulator - INFO - Applying fiber efficiency: 0.7-0.95
Simulation failed: Unable to synchronously create attribute (no write intent on file)
2025-12-16 11:04:39,006 - andes_simulator.core.simulator - INFO - Using HDF model: /Users/tom/ANDES/E2E/src/HDF/ANDES_75fibre_H.hdf
2025-12-16 11:04:39,006 - root - INFO - Using the model file /Users/tom/ANDES/E2E/src/HDF/ANDES_75fibre_H.hdf
2025-12-16 11:04:39,006 - andes_simulator.core.simulator - INFO - Applying fiber efficiency: 0.7-0.95
2025-12-16 11:04:39,006 - andes_simulator.core.simulator - INFO - Using HDF model: /Users/tom/ANDES/E2E/src/HDF/ANDES_75fibre_H.hdf
2025-12-16 11:04:39,006 - root - INFO - Using the model file /Users/tom/ANDES/E2E/src/HDF/ANDES_75fibre_H.hdf
2025-12-16 11:04:39,006 - andes_simulator.core.simulator - INFO - Applying fiber efficiency: 0.7-0.95
2025-12-16 11:04:39,017 - andes_simulator.core.simulator - INFO - Simulator configured for H-band
2025-12-16 11:04:39,018 - andes_simulator.core.simulator - INFO - Running flat_field simulation
2025-12-16 11:04:39,018 - andes_simulator.core.simulator - INFO - Output: ../H/H_FF_single_1s_wl1600-1602.fits
2025-12-16 11:04:39,018 - Simulator - INFO - It was not explicitly specified whether to consider atmospheric transmission. It is set to False
2025-12-16 11:04:39,018 - Simulator - INFO - Atmospheric conditions were not specified. The default atmospheric conditions apply. (e.g. airmass 1)
2025-12-16 11:04:39,018 - Simulator - INFO - Radial velocities are not specified explicitly. They are therefore set to 0.0
Order 68: 1757.58nm - 1801.76nm, 0 photons
Order 69: 1732.34nm - 1775.73nm, 0 photons
Order 70: 1707.81nm - 1750.45nm, 0 photons
Order 71: 1683.95nm - 1725.88nm, 0 photons
Order 72: 1660.74nm - 1701.99nm, 0 photons
Order 73: 1638.16nm - 1678.75nm, 0 photons
Order 74: 1616.17nm - 1656.15nm, 0 photons
Order 75: 1594.76nm - 1634.14nm, 0 photons
Order 76: 1573.91nm - 1612.71nm, 0 photons
Order 77: 1553.58nm - 1591.84nm, 0 photons
Order 78: 1533.77nm - 1571.51nm, 0 photons
Order 79: 1514.45nm - 1551.69nm, 0 photons
Order 80: 1495.61nm - 1532.36nm, 0 photons
Order 81: 1477.23nm - 1513.51nm, 0 photons
Order 82: 1459.29nm - 1495.12nm, 0 photons
Order 68: 1757.59nm - 1801.75nm, 0 photons
Order 69: 1732.35nm - 1775.73nm, 0 photons
Order 70: 1707.81nm - 1750.45nm, 0 photons
Order 71: 1683.95nm - 1725.88nm, 0 photons
Order 72: 1660.74nm - 1701.99nm, 0 photons
Order 73: 1638.16nm - 1678.75nm, 0 photons
2025-12-16 11:04:47,530 - andes_simulator.core.simulator - INFO - Using HDF model: /Users/tom/ANDES/E2E/src/HDF/ANDES_75fibre_H.hdf
2025-12-16 11:04:47,530 - root - INFO - Using the model file /Users/tom/ANDES/E2E/src/HDF/ANDES_75fibre_H.hdf
2025-12-16 11:04:47,530 - andes_simulator.core.simulator - INFO - Applying fiber efficiency: 0.7-0.95
2025-12-16 11:04:47,530 - andes_simulator.core.simulator - INFO - Using HDF model: /Users/tom/ANDES/E2E/src/HDF/ANDES_75fibre_H.hdf
2025-12-16 11:04:47,530 - root - INFO - Using the model file /Users/tom/ANDES/E2E/src/HDF/ANDES_75fibre_H.hdf
2025-12-16 11:04:47,530 - andes_simulator.core.simulator - INFO - Applying fiber efficiency: 0.7-0.95
2025-12-16 11:04:47,539 - andes_simulator.core.simulator - INFO - Simulator configured for H-band
2025-12-16 11:04:47,539 - andes_simulator.core.simulator - INFO - Running flat_field simulation
2025-12-16 11:04:47,539 - andes_simulator.core.simulator - INFO - Output: ../H/H_FF_single_1s_wl1600-1602.fits
2025-12-16 11:04:47,539 - Simulator - INFO - It was not explicitly specified whether to consider atmospheric transmission. It is set to False
2025-12-16 11:04:47,539 - Simulator - INFO - Atmospheric conditions were not specified. The default atmospheric conditions apply. (e.g. airmass 1)
2025-12-16 11:04:47,539 - Simulator - INFO - Radial velocities are not specified explicitly. They are therefore set to 0.0
2025-12-16 11:04:51,761 - Simulator - INFO - Finish up simulation and save...
2025-12-16 11:04:51,806 - Simulator - INFO - Total time for simulation: 4.225s.
2025-12-16 11:04:51,806 - Simulator - INFO - Total simulated photons: 7031507
2025-12-16 11:04:51,806 - andes_simulator.core.simulator - INFO - Simulation completed
Order 68: 1757.58nm - 1801.76nm, 0 photons
Order 69: 1732.34nm - 1775.73nm, 0 photons
Order 70: 1707.81nm - 1750.45nm, 0 photons
Order 71: 1683.95nm - 1725.88nm, 0 photons
Order 72: 1660.74nm - 1701.99nm, 0 photons
Order 73: 1638.16nm - 1678.75nm, 0 photons
Order 74: 1616.17nm - 1656.15nm, 0 photons
Order 75: 1594.76nm - 1634.14nm, 0 photons
Order 76: 1573.91nm - 1612.71nm, 0 photons
Order 77: 1553.58nm - 1591.84nm, 0 photons
Order 78: 1533.77nm - 1571.51nm, 0 photons
Order 79: 1514.45nm - 1551.69nm, 0 photons
Order 80: 1495.61nm - 1532.36nm, 0 photons
Order 81: 1477.23nm - 1513.51nm, 0 photons
Order 82: 1459.29nm - 1495.12nm, 0 photons
Order 68: 1757.59nm - 1801.75nm, 0 photons
Order 69: 1732.35nm - 1775.73nm, 0 photons
Order 70: 1707.81nm - 1750.45nm, 0 photons
Order 71: 1683.95nm - 1725.88nm, 0 photons
Order 72: 1660.74nm - 1701.99nm, 0 photons
Order 73: 1638.16nm - 1678.75nm, 0 photons
Order 74: 1616.17nm - 1656.15nm, 0 photons
Order 75: 1594.76nm - 1634.14nm, 0 photons
Order 76: 1573.91nm - 1612.71nm, 0 photons
Order 77: 1553.58nm - 1591.84nm, 0 photons
Order 78: 1533.77nm - 1571.51nm, 0 photons
Order 79: 1514.45nm - 1551.69nm, 0 photons
Order 80: 1495.61nm - 1532.36nm, 0 photons
Order 81: 1477.23nm - 1513.51nm, 0 photons
Order 82: 1459.29nm - 1495.12nm, 0 photons
Order 68: 1757.59nm - 1801.75nm, 0 photons
Order 69: 1732.35nm - 1775.73nm, 0 photons
Order 70: 1707.81nm - 1750.45nm, 0 photons
Order 71: 1683.95nm - 1725.87nm, 0 photons
Order 72: 1660.75nm - 1701.99nm, 0 photons
Order 73: 1638.16nm - 1678.75nm, 0 photons
Order 74: 1616.18nm - 1656.14nm, 0 photons
Order 75: 1594.77nm - 1634.14nm, 0 photons
Order 76: 1573.91nm - 1612.71nm, 0 photons
Order 77: 1553.58nm - 1591.84nm, 0 photons
Order 78: 1533.77nm - 1571.51nm, 0 photons
Order 79: 1514.45nm - 1551.68nm, 0 photons
Order 80: 1495.61nm - 1532.36nm, 0 photons
Order 81: 1477.23nm - 1513.51nm, 0 photons
Order 82: 1459.29nm - 1495.12nm, 0 photons
Order 68: 1757.59nm - 1801.75nm, 0 photons
Order 69: 1732.35nm - 1775.73nm, 0 photons
Order 70: 1707.81nm - 1750.44nm, 0 photons
Order 71: 1683.96nm - 1725.87nm, 0 photons
Order 72: 1660.75nm - 1701.98nm, 0 photons
Order 73: 1638.16nm - 1678.75nm, 0 photons
Order 74: 1616.18nm - 1656.14nm, 0 photons
Order 75: 1594.77nm - 1634.14nm, 0 photons
Order 76: 1573.91nm - 1612.71nm, 0 photons
Order 77: 1553.59nm - 1591.84nm, 0 photons
Order 78: 1533.77nm - 1571.50nm, 0 photons
Order 79: 1514.45nm - 1551.68nm, 0 photons
Order 80: 1495.61nm - 1532.36nm, 0 photons
Order 81: 1477.23nm - 1513.51nm, 0 photons
Order 82: 1459.29nm - 1495.11nm, 0 photons
Order 68: 1757.59nm - 1801.75nm, 0 photons
Order 69: 1732.35nm - 1775.72nm, 0 photons
Order 70: 1707.82nm - 1750.44nm, 0 photons
Order 71: 1683.96nm - 1725.87nm, 0 photons
Order 72: 1660.75nm - 1701.98nm, 0 photons
Order 73: 1638.17nm - 1678.75nm, 0 photons
Order 74: 1616.18nm - 1656.14nm, 0 photons
Order 75: 1594.77nm - 1634.14nm, 0 photons
Order 76: 1573.91nm - 1612.71nm, 0 photons
Order 77: 1553.59nm - 1591.84nm, 0 photons
Order 78: 1533.77nm - 1571.50nm, 0 photons
Order 79: 1514.46nm - 1551.68nm, 0 photons
Order 80: 1495.61nm - 1532.35nm, 0 photons
Order 81: 1477.23nm - 1513.50nm, 0 photons
Order 82: 1459.29nm - 1495.11nm, 0 photons
Order 68: 1757.60nm - 1801.75nm, 0 photons
Order 69: 1732.35nm - 1775.72nm, 0 photons
Order 70: 1707.82nm - 1750.44nm, 0 photons
Order 71: 1683.96nm - 1725.87nm, 0 photons
Order 72: 1660.75nm - 1701.98nm, 0 photons
Order 73: 1638.17nm - 1678.75nm, 0 photons
Order 74: 1616.18nm - 1656.14nm, 0 photons
Order 75: 1594.77nm - 1634.13nm, 0 photons
Order 76: 1573.91nm - 1612.71nm, 0 photons
Order 77: 1553.59nm - 1591.84nm, 0 photons
Order 78: 1533.78nm - 1571.50nm, 0 photons
Order 79: 1514.46nm - 1551.68nm, 0 photons
Order 80: 1495.61nm - 1532.35nm, 0 photons
Order 81: 1477.23nm - 1513.50nm, 0 photons
Order 82: 1459.29nm - 1495.11nm, 0 photons
Order 68: 1757.60nm - 1801.74nm, 0 photons
Order 69: 1732.36nm - 1775.72nm, 0 photons
Order 70: 1707.82nm - 1750.44nm, 0 photons
Order 71: 1683.96nm - 1725.87nm, 0 photons
Order 72: 1660.75nm - 1701.98nm, 0 photons
Order 73: 1638.17nm - 1678.74nm, 0 photons
Order 74: 1616.18nm - 1656.14nm, 0 photons
Order 75: 1594.77nm - 1634.13nm, 0 photons
Order 76: 1573.91nm - 1612.71nm, 0 photons
Order 77: 1553.59nm - 1591.84nm, 0 photons
Order 78: 1533.78nm - 1571.50nm, 0 photons
Order 79: 1514.46nm - 1551.68nm, 0 photons
Order 80: 1495.62nm - 1532.35nm, 0 photons
Order 81: 1477.23nm - 1513.50nm, 0 photons
Order 82: 1459.29nm - 1495.11nm, 0 photons
Order 68: 1757.60nm - 1801.74nm, 0 photons
Order 69: 1732.36nm - 1775.72nm, 0 photons
Order 70: 1707.82nm - 1750.44nm, 0 photons
Order 71: 1683.96nm - 1725.87nm, 0 photons
Order 72: 1660.75nm - 1701.98nm, 0 photons
Order 73: 1638.17nm - 1678.74nm, 0 photons
Order 74: 1616.18nm - 1656.14nm, 0 photons
Order 75: 1594.77nm - 1634.13nm, 0 photons
Order 76: 1573.92nm - 1612.70nm, 0 photons
Order 77: 1553.59nm - 1591.83nm, 0 photons
Order 78: 1533.78nm - 1571.50nm, 0 photons
Order 79: 1514.46nm - 1551.68nm, 0 photons
Order 80: 1495.62nm - 1532.35nm, 0 photons
Order 81: 1477.23nm - 1513.50nm, 0 photons
Order 82: 1459.29nm - 1495.11nm, 0 photons
Order 68: 1757.60nm - 1801.74nm, 0 photons
Order 69: 1732.36nm - 1775.72nm, 0 photons
Order 70: 1707.82nm - 1750.43nm, 0 photons
Order 71: 1683.97nm - 1725.86nm, 0 photons
Order 72: 1660.76nm - 1701.97nm, 0 photons
Order 73: 1638.17nm - 1678.74nm, 0 photons
Order 74: 1616.18nm - 1656.13nm, 0 photons
Order 75: 1594.77nm - 1634.13nm, 0 photons
Order 76: 1573.92nm - 1612.70nm, 0 photons
Order 77: 1553.59nm - 1591.83nm, 0 photons
Order 78: 1533.78nm - 1571.50nm, 0 photons
Order 79: 1514.46nm - 1551.68nm, 0 photons
Order 80: 1495.62nm - 1532.35nm, 0 photons
Order 81: 1477.23nm - 1513.50nm, 0 photons
Order 82: 1459.29nm - 1495.11nm, 0 photons
Order 68: 1757.60nm - 1801.74nm, 0 photons
Order 69: 1732.36nm - 1775.71nm, 0 photons
Order 70: 1707.83nm - 1750.43nm, 0 photons
Order 71: 1683.97nm - 1725.86nm, 0 photons
Order 72: 1660.76nm - 1701.97nm, 0 photons
Order 73: 1638.17nm - 1678.74nm, 0 photons
Order 74: 1616.19nm - 1656.13nm, 0 photons
Order 75: 1594.78nm - 1634.13nm, 0 photons
Order 76: 1573.92nm - 1612.70nm, 0 photons
Order 77: 1553.59nm - 1591.83nm, 0 photons
Order 78: 1533.78nm - 1571.49nm, 0 photons
Order 79: 1514.46nm - 1551.67nm, 0 photons
Order 80: 1495.62nm - 1532.35nm, 0 photons
Order 81: 1477.23nm - 1513.50nm, 0 photons
Order 82: 1459.29nm - 1495.11nm, 0 photons
Order 68: 1757.61nm - 1801.74nm, 0 photons
Order 69: 1732.36nm - 1775.71nm, 0 photons
Order 70: 1707.83nm - 1750.43nm, 0 photons
Order 71: 1683.97nm - 1725.86nm, 0 photons
Order 72: 1660.76nm - 1701.97nm, 0 photons
Order 73: 1638.17nm - 1678.74nm, 0 photons
Order 74: 1616.19nm - 1656.13nm, 0 photons
Order 75: 1594.78nm - 1634.12nm, 0 photons
Order 76: 1573.92nm - 1612.70nm, 0 photons
Order 77: 1553.59nm - 1591.83nm, 0 photons
Order 78: 1533.78nm - 1571.49nm, 0 photons
Order 79: 1514.46nm - 1551.67nm, 0 photons
Order 80: 1495.62nm - 1532.35nm, 0 photons
Order 81: 1477.24nm - 1513.50nm, 0 photons
Order 82: 1459.29nm - 1495.10nm, 0 photons
Order 68: 1757.61nm - 1801.73nm, 0 photons
Order 69: 1732.37nm - 1775.71nm, 0 photons
Order 70: 1707.83nm - 1750.43nm, 0 photons
Order 71: 1683.97nm - 1725.86nm, 0 photons
Order 72: 1660.76nm - 1701.97nm, 0 photons
Order 73: 1638.18nm - 1678.73nm, 0 photons
Order 74: 1616.19nm - 1656.13nm, 0 photons
Order 75: 1594.78nm - 1634.12nm, 0 photons
Order 76: 1573.92nm - 1612.70nm, 0 photons
Order 77: 1553.59nm - 1591.83nm, 0 photons
Order 78: 1533.78nm - 1571.49nm, 0 photons
Order 79: 1514.46nm - 1551.67nm, 0 photons
Order 80: 1495.62nm - 1532.34nm, 0 photons
Order 81: 1477.24nm - 1513.49nm, 0 photons
Order 82: 1459.29nm - 1495.10nm, 0 photons
Order 68: 1757.61nm - 1801.73nm, 0 photons
Order 69: 1732.37nm - 1775.71nm, 0 photons
Order 70: 1707.83nm - 1750.43nm, 0 photons
Order 71: 1683.97nm - 1725.86nm, 0 photons
Order 72: 1660.76nm - 1701.97nm, 0 photons
Order 73: 1638.18nm - 1678.73nm, 0 photons
Order 74: 1616.19nm - 1656.13nm, 0 photons
Order 75: 1594.78nm - 1634.12nm, 0 photons
Order 76: 1573.92nm - 1612.70nm, 0 photons
Order 77: 1553.60nm - 1591.83nm, 0 photons
Order 78: 1533.78nm - 1571.49nm, 0 photons
Order 79: 1514.46nm - 1551.67nm, 0 photons
Order 80: 1495.62nm - 1532.34nm, 0 photons
Order 81: 1477.24nm - 1513.49nm, 0 photons
Order 82: 1459.30nm - 1495.10nm, 0 photons
Order 68: 1757.61nm - 1801.73nm, 0 photons
Order 69: 1732.37nm - 1775.71nm, 0 photons
Order 70: 1707.83nm - 1750.42nm, 0 photons
Order 71: 1683.97nm - 1725.85nm, 0 photons
Order 72: 1660.76nm - 1701.97nm, 0 photons
Order 73: 1638.18nm - 1678.73nm, 0 photons
Order 74: 1616.19nm - 1656.12nm, 0 photons
Order 75: 1594.78nm - 1634.12nm, 0 photons
Order 76: 1573.92nm - 1612.69nm, 0 photons
Order 77: 1553.60nm - 1591.82nm, 0 photons
Order 78: 1533.78nm - 1571.49nm, 0 photons
Order 79: 1514.46nm - 1551.67nm, 0 photons
Order 80: 1495.62nm - 1532.34nm, 0 photons
Order 81: 1477.24nm - 1513.49nm, 0 photons
Order 82: 1459.30nm - 1495.10nm, 0 photons
Order 68: 1757.62nm - 1801.73nm, 0 photons
Order 69: 1732.37nm - 1775.70nm, 0 photons
Order 70: 1707.84nm - 1750.42nm, 0 photons
Order 71: 1683.98nm - 1725.85nm, 0 photons
Order 72: 1660.77nm - 1701.96nm, 0 photons
Order 73: 1638.18nm - 1678.73nm, 0 photons
Order 74: 1616.19nm - 1656.12nm, 0 photons
Order 75: 1594.78nm - 1634.12nm, 0 photons
Order 76: 1573.92nm - 1612.69nm, 0 photons
Order 77: 1553.60nm - 1591.82nm, 0 photons
Order 78: 1533.79nm - 1571.49nm, 0 photons
Order 79: 1514.47nm - 1551.67nm, 0 photons
Order 80: 1495.62nm - 1532.34nm, 0 photons
Order 81: 1477.24nm - 1513.49nm, 0 photons
Order 82: 1459.30nm - 1495.10nm, 0 photons
Order 68: 1757.62nm - 1801.73nm, 0 photons
Order 69: 1732.37nm - 1775.70nm, 0 photons
Order 70: 1707.84nm - 1750.42nm, 0 photons
Order 71: 1683.98nm - 1725.85nm, 0 photons
Order 72: 1660.77nm - 1701.96nm, 0 photons
Order 73: 1638.18nm - 1678.73nm, 0 photons
Order 74: 1616.19nm - 1656.12nm, 0 photons
Order 75: 1594.78nm - 1634.12nm, 0 photons
Order 76: 1573.92nm - 1612.69nm, 0 photons
Order 77: 1553.60nm - 1591.82nm, 0 photons
Order 78: 1533.79nm - 1571.49nm, 0 photons
Order 79: 1514.47nm - 1551.66nm, 0 photons
Order 80: 1495.62nm - 1532.34nm, 0 photons
Order 81: 1477.24nm - 1513.49nm, 0 photons
Order 82: 1459.30nm - 1495.10nm, 0 photons
Order 68: 1757.62nm - 1801.72nm, 0 photons
Order 69: 1732.38nm - 1775.70nm, 0 photons
Order 70: 1707.84nm - 1750.42nm, 0 photons
Order 71: 1683.98nm - 1725.85nm, 0 photons
Order 72: 1660.77nm - 1701.96nm, 0 photons
Order 73: 1638.18nm - 1678.73nm, 0 photons
Order 74: 1616.20nm - 1656.12nm, 0 photons
Order 75: 1594.78nm - 1634.11nm, 0 photons
Order 76: 1573.93nm - 1612.69nm, 0 photons
Order 77: 1553.60nm - 1591.82nm, 0 photons
Order 78: 1533.79nm - 1571.48nm, 0 photons
Order 79: 1514.47nm - 1551.66nm, 0 photons
Order 80: 1495.62nm - 1532.34nm, 0 photons
Order 81: 1477.24nm - 1513.49nm, 0 photons
Order 82: 1459.30nm - 1495.10nm, 0 photons
Order 68: 1757.62nm - 1801.72nm, 0 photons
Order 69: 1732.38nm - 1775.70nm, 0 photons
Order 70: 1707.84nm - 1750.42nm, 0 photons
Order 71: 1683.98nm - 1725.85nm, 0 photons
Order 72: 1660.77nm - 1701.96nm, 0 photons
Order 73: 1638.19nm - 1678.72nm, 0 photons
Order 74: 1616.20nm - 1656.12nm, 0 photons
Order 75: 1594.79nm - 1634.11nm, 0 photons
Order 76: 1573.93nm - 1612.69nm, 0 photons
Order 77: 1553.60nm - 1591.82nm, 0 photons
Order 78: 1533.79nm - 1571.48nm, 0 photons
Order 79: 1514.47nm - 1551.66nm, 0 photons
Order 80: 1495.63nm - 1532.34nm, 0 photons
Order 81: 1477.24nm - 1513.49nm, 0 photons
Order 82: 1459.30nm - 1495.09nm, 0 photons
Order 68: 1757.62nm - 1801.72nm, 0 photons
Order 69: 1732.38nm - 1775.70nm, 0 photons
Order 70: 1707.84nm - 1750.41nm, 0 photons
Order 71: 1683.98nm - 1725.84nm, 0 photons
Order 72: 1660.77nm - 1701.96nm, 0 photons
Order 73: 1638.19nm - 1678.72nm, 0 photons
Order 74: 1616.20nm - 1656.12nm, 0 photons
Order 75: 1594.79nm - 1634.11nm, 0 photons
Order 76: 1573.93nm - 1612.69nm, 0 photons
Order 77: 1553.60nm - 1591.82nm, 0 photons
Order 78: 1533.79nm - 1571.48nm, 0 photons
Order 79: 1514.47nm - 1551.66nm, 0 photons
Order 80: 1495.63nm - 1532.33nm, 0 photons
Order 81: 1477.24nm - 1513.48nm, 0 photons
Order 82: 1459.30nm - 1495.09nm, 0 photons
Order 68: 1757.63nm - 1801.72nm, 0 photons
Order 69: 1732.38nm - 1775.69nm, 0 photons
Order 70: 1707.84nm - 1750.41nm, 0 photons
Order 71: 1683.98nm - 1725.84nm, 0 photons
Order 72: 1660.77nm - 1701.95nm, 0 photons
Order 73: 1638.19nm - 1678.72nm, 0 photons
Order 74: 1616.20nm - 1656.11nm, 0 photons
Order 75: 1594.79nm - 1634.11nm, 0 photons
Order 76: 1573.93nm - 1612.68nm, 0 photons
Order 77: 1553.60nm - 1591.81nm, 0 photons
Order 78: 1533.79nm - 1571.48nm, 0 photons
Order 79: 1514.47nm - 1551.66nm, 0 photons
Order 80: 1495.63nm - 1532.33nm, 0 photons
Order 81: 1477.24nm - 1513.48nm, 0 photons
Order 82: 1459.30nm - 1495.09nm, 0 photons
Order 68: 1757.63nm - 1801.72nm, 0 photons
Order 69: 1732.38nm - 1775.69nm, 0 photons
Order 70: 1707.85nm - 1750.41nm, 0 photons
Order 71: 1683.99nm - 1725.84nm, 0 photons
Order 72: 1660.78nm - 1701.95nm, 0 photons
Order 73: 1638.19nm - 1678.72nm, 0 photons
Order 74: 1616.20nm - 1656.11nm, 0 photons
Order 75: 1594.79nm - 1634.11nm, 0 photons
Order 76: 1573.93nm - 1612.68nm, 0 photons
Order 77: 1553.60nm - 1591.81nm, 0 photons
Order 78: 1533.79nm - 1571.48nm, 0 photons
Order 79: 1514.47nm - 1551.66nm, 0 photons
Order 80: 1495.63nm - 1532.33nm, 0 photons
Order 81: 1477.24nm - 1513.48nm, 0 photons
Order 82: 1459.30nm - 1495.09nm, 0 photons
Order 68: 1757.63nm - 1801.71nm, 0 photons
Order 69: 1732.39nm - 1775.69nm, 0 photons
Order 70: 1707.85nm - 1750.41nm, 0 photons
Order 71: 1683.99nm - 1725.84nm, 0 photons
Order 72: 1660.78nm - 1701.95nm, 0 photons
Order 73: 1638.19nm - 1678.72nm, 0 photons
Order 74: 1616.20nm - 1656.11nm, 0 photons
Order 75: 1594.79nm - 1634.11nm, 0 photons
Order 76: 1573.93nm - 1612.68nm, 0 photons
Order 77: 1553.61nm - 1591.81nm, 0 photons
Order 78: 1533.79nm - 1571.48nm, 0 photons
Order 79: 1514.47nm - 1551.65nm, 0 photons
Order 80: 1495.63nm - 1532.33nm, 0 photons
Order 81: 1477.24nm - 1513.48nm, 0 photons
Order 82: 1459.30nm - 1495.09nm, 0 photons
Order 68: 1757.63nm - 1801.71nm, 0 photons
Order 69: 1732.39nm - 1775.69nm, 0 photons
Order 70: 1707.85nm - 1750.41nm, 0 photons
Order 71: 1683.99nm - 1725.84nm, 0 photons
Order 72: 1660.78nm - 1701.95nm, 0 photons
Order 73: 1638.19nm - 1678.71nm, 0 photons
Order 74: 1616.20nm - 1656.11nm, 0 photons
Order 75: 1594.79nm - 1634.10nm, 0 photons
Order 76: 1573.93nm - 1612.68nm, 0 photons
Order 77: 1553.61nm - 1591.81nm, 0 photons
Order 78: 1533.79nm - 1571.47nm, 0 photons
Order 79: 1514.47nm - 1551.65nm, 0 photons
Order 80: 1495.63nm - 1532.33nm, 0 photons
Order 81: 1477.24nm - 1513.48nm, 0 photons
Order 82: 1459.30nm - 1495.09nm, 0 photons
Order 68: 1757.63nm - 1801.71nm, 0 photons
Order 69: 1732.39nm - 1775.69nm, 0 photons
Order 70: 1707.85nm - 1750.40nm, 0 photons
Order 71: 1683.99nm - 1725.83nm, 0 photons
Order 72: 1660.78nm - 1701.95nm, 0 photons
Order 73: 1638.19nm - 1678.71nm, 0 photons
Order 74: 1616.21nm - 1656.11nm, 0 photons
Order 75: 1594.79nm - 1634.10nm, 0 photons
Order 76: 1573.93nm - 1612.68nm, 0 photons
Order 77: 1553.61nm - 1591.81nm, 0 photons
Order 78: 1533.79nm - 1571.47nm, 0 photons
Order 79: 1514.47nm - 1551.65nm, 0 photons
Order 80: 1495.63nm - 1532.33nm, 0 photons
Order 81: 1477.25nm - 1513.48nm, 0 photons
Order 82: 1459.30nm - 1495.09nm, 0 photons
Order 68: 1757.64nm - 1801.71nm, 0 photons
Order 69: 1732.39nm - 1775.68nm, 0 photons
Order 70: 1707.85nm - 1750.40nm, 0 photons
Order 71: 1683.99nm - 1725.83nm, 0 photons
Order 72: 1660.78nm - 1701.95nm, 0 photons
Order 73: 1638.20nm - 1678.71nm, 0 photons
Order 74: 1616.21nm - 1656.10nm, 0 photons
Order 75: 1594.79nm - 1634.10nm, 0 photons
Order 76: 1573.94nm - 1612.68nm, 0 photons
Order 77: 1553.61nm - 1591.81nm, 0 photons
Order 78: 1533.80nm - 1571.47nm, 0 photons
Order 79: 1514.48nm - 1551.65nm, 0 photons
Order 80: 1495.63nm - 1532.32nm, 0 photons
Order 81: 1477.25nm - 1513.47nm, 0 photons
Order 82: 1459.30nm - 1495.08nm, 0 photons
Order 68: 1757.64nm - 1801.71nm, 0 photons
Order 69: 1732.39nm - 1775.68nm, 0 photons
Order 70: 1707.86nm - 1750.40nm, 0 photons
Order 71: 1683.99nm - 1725.83nm, 0 photons
Order 72: 1660.78nm - 1701.94nm, 0 photons
Order 73: 1638.20nm - 1678.71nm, 0 photons
Order 74: 1616.21nm - 1656.10nm, 0 photons
Order 75: 1594.80nm - 1634.10nm, 0 photons
Order 76: 1573.94nm - 1612.67nm, 0 photons
Order 77: 1553.61nm - 1591.80nm, 0 photons
Order 78: 1533.80nm - 1571.47nm, 0 photons
Order 79: 1514.48nm - 1551.65nm, 0 photons
Order 80: 1495.63nm - 1532.32nm, 0 photons
Order 81: 1477.25nm - 1513.47nm, 0 photons
Order 82: 1459.30nm - 1495.08nm, 0 photons
Order 68: 1757.64nm - 1801.70nm, 0 photons
Order 69: 1732.40nm - 1775.68nm, 0 photons
Order 70: 1707.86nm - 1750.40nm, 0 photons
Order 71: 1684.00nm - 1725.83nm, 0 photons
Order 72: 1660.79nm - 1701.94nm, 0 photons
Order 73: 1638.20nm - 1678.71nm, 0 photons
Order 74: 1616.21nm - 1656.10nm, 0 photons
Order 75: 1594.80nm - 1634.10nm, 0 photons
Order 76: 1573.94nm - 1612.67nm, 0 photons
Order 77: 1553.61nm - 1591.80nm, 0 photons
Order 78: 1533.80nm - 1571.47nm, 0 photons
Order 79: 1514.48nm - 1551.65nm, 0 photons
Order 80: 1495.63nm - 1532.32nm, 0 photons
Order 81: 1477.25nm - 1513.47nm, 0 photons
Order 82: 1459.31nm - 1495.08nm, 0 photons
Order 68: 1757.64nm - 1801.70nm, 0 photons
Order 69: 1732.40nm - 1775.68nm, 0 photons
Order 70: 1707.86nm - 1750.40nm, 0 photons
Order 71: 1684.00nm - 1725.83nm, 0 photons
Order 72: 1660.79nm - 1701.94nm, 0 photons
Order 73: 1638.20nm - 1678.71nm, 0 photons
Order 74: 1616.21nm - 1656.10nm, 0 photons
Order 75: 1594.80nm - 1634.10nm, 0 photons
Order 76: 1573.94nm - 1612.67nm, 0 photons
Order 77: 1553.61nm - 1591.80nm, 0 photons
Order 78: 1533.80nm - 1571.47nm, 0 photons
Order 79: 1514.48nm - 1551.65nm, 0 photons
Order 80: 1495.63nm - 1532.32nm, 0 photons
Order 81: 1477.25nm - 1513.47nm, 0 photons
Order 82: 1459.31nm - 1495.08nm, 0 photons
Order 68: 1757.64nm - 1801.70nm, 0 photons
Order 69: 1732.40nm - 1775.68nm, 0 photons
Order 70: 1707.86nm - 1750.39nm, 0 photons
Order 71: 1684.00nm - 1725.82nm, 0 photons
Order 72: 1660.79nm - 1701.94nm, 0 photons
Order 73: 1638.20nm - 1678.70nm, 0 photons
Order 74: 1616.21nm - 1656.10nm, 0 photons
Order 75: 1594.80nm - 1634.09nm, 0 photons
Order 76: 1573.94nm - 1612.67nm, 0 photons
Order 77: 1553.61nm - 1591.80nm, 0 photons
Order 78: 1533.80nm - 1571.46nm, 0 photons
Order 79: 1514.48nm - 1551.64nm, 0 photons
Order 80: 1495.63nm - 1532.32nm, 0 photons
Order 81: 1477.25nm - 1513.47nm, 0 photons
Order 82: 1459.31nm - 1495.08nm, 0 photons
Order 68: 1757.65nm - 1801.70nm, 0 photons
Order 69: 1732.40nm - 1775.67nm, 0 photons
Order 70: 1707.86nm - 1750.39nm, 0 photons
Order 71: 1684.00nm - 1725.82nm, 0 photons
Order 72: 1660.79nm - 1701.94nm, 0 photons
Order 73: 1638.20nm - 1678.70nm, 0 photons
Order 74: 1616.21nm - 1656.10nm, 0 photons
Order 75: 1594.80nm - 1634.09nm, 0 photons
Order 76: 1573.94nm - 1612.67nm, 0 photons
Order 77: 1553.61nm - 1591.80nm, 0 photons
Order 78: 1533.80nm - 1571.46nm, 0 photons
Order 79: 1514.48nm - 1551.64nm, 0 photons
Order 80: 1495.64nm - 1532.32nm, 0 photons
Order 81: 1477.25nm - 1513.47nm, 0 photons
Order 82: 1459.31nm - 1495.08nm, 0 photons
Order 68: 1757.65nm - 1801.69nm, 0 photons
Order 69: 1732.40nm - 1775.67nm, 0 photons
Order 70: 1707.86nm - 1750.39nm, 0 photons
Order 71: 1684.00nm - 1725.82nm, 0 photons
Order 72: 1660.79nm - 1701.93nm, 0 photons
Order 73: 1638.20nm - 1678.70nm, 0 photons
Order 74: 1616.21nm - 1656.09nm, 0 photons
Order 75: 1594.80nm - 1634.09nm, 0 photons
Order 76: 1573.94nm - 1612.66nm, 0 photons
Order 77: 1553.61nm - 1591.80nm, 0 photons
Order 78: 1533.80nm - 1571.46nm, 0 photons
Order 79: 1514.48nm - 1551.64nm, 0 photons
Order 80: 1495.64nm - 1532.31nm, 0 photons
Order 81: 1477.25nm - 1513.47nm, 0 photons
Order 82: 1459.31nm - 1495.07nm, 0 photons
Order 68: 1757.65nm - 1801.69nm, 0 photons
Order 69: 1732.40nm - 1775.67nm, 0 photons
Order 70: 1707.87nm - 1750.39nm, 0 photons
Order 71: 1684.00nm - 1725.82nm, 0 photons
Order 72: 1660.79nm - 1701.93nm, 0 photons
Order 73: 1638.21nm - 1678.70nm, 0 photons
Order 74: 1616.22nm - 1656.09nm, 0 photons
Order 75: 1594.80nm - 1634.09nm, 0 photons
Order 76: 1573.94nm - 1612.66nm, 0 photons
Order 77: 1553.62nm - 1591.79nm, 0 photons
Order 78: 1533.80nm - 1571.46nm, 0 photons
Order 79: 1514.48nm - 1551.64nm, 0 photons
Order 80: 1495.64nm - 1532.31nm, 0 photons
Order 81: 1477.25nm - 1513.46nm, 0 photons
Order 82: 1459.31nm - 1495.07nm, 0 photons
Order 68: 1757.65nm - 1801.69nm, 0 photons
Order 69: 1732.41nm - 1775.67nm, 0 photons
Order 70: 1707.87nm - 1750.39nm, 0 photons
Order 71: 1684.01nm - 1725.82nm, 0 photons
Order 72: 1660.79nm - 1701.93nm, 0 photons
Order 73: 1638.21nm - 1678.70nm, 0 photons
Order 74: 1616.22nm - 1656.09nm, 0 photons
Order 75: 1594.80nm - 1634.09nm, 0 photons
Order 76: 1573.94nm - 1612.66nm, 0 photons
Order 77: 1553.62nm - 1591.79nm, 0 photons
Order 78: 1533.80nm - 1571.46nm, 0 photons
Order 79: 1514.48nm - 1551.64nm, 0 photons
Order 80: 1495.64nm - 1532.31nm, 0 photons
Order 81: 1477.25nm - 1513.46nm, 0 photons
Order 82: 1459.31nm - 1495.07nm, 0 photons
Order 68: 1757.65nm - 1801.69nm, 0 photons
Order 69: 1732.41nm - 1775.66nm, 0 photons
Order 70: 1707.87nm - 1750.38nm, 0 photons
Order 71: 1684.01nm - 1725.81nm, 0 photons
Order 72: 1660.80nm - 1701.93nm, 0 photons
Order 73: 1638.21nm - 1678.69nm, 0 photons
Order 74: 1616.22nm - 1656.09nm, 0 photons
Order 75: 1594.81nm - 1634.08nm, 0 photons
Order 76: 1573.95nm - 1612.66nm, 0 photons
Order 77: 1553.62nm - 1591.79nm, 0 photons
Order 78: 1533.80nm - 1571.46nm, 0 photons
Order 79: 1514.48nm - 1551.64nm, 0 photons
Order 80: 1495.64nm - 1532.31nm, 0 photons
Order 81: 1477.25nm - 1513.46nm, 0 photons
Order 82: 1459.31nm - 1495.07nm, 0 photons
Order 68: 1757.66nm - 1801.69nm, 0 photons
Order 69: 1732.41nm - 1775.66nm, 0 photons
Order 70: 1707.87nm - 1750.38nm, 0 photons
Order 71: 1684.01nm - 1725.81nm, 0 photons
Order 72: 1660.80nm - 1701.93nm, 0 photons
Order 73: 1638.21nm - 1678.69nm, 0 photons
Order 74: 1616.22nm - 1656.09nm, 0 photons
Order 75: 1594.81nm - 1634.08nm, 0 photons
Order 76: 1573.95nm - 1612.66nm, 0 photons
Order 77: 1553.62nm - 1591.79nm, 0 photons
Order 78: 1533.80nm - 1571.45nm, 0 photons
Order 79: 1514.48nm - 1551.63nm, 0 photons
Order 80: 1495.64nm - 1532.31nm, 0 photons
Order 81: 1477.25nm - 1513.46nm, 0 photons
Order 82: 1459.31nm - 1495.07nm, 0 photons
Order 68: 1757.66nm - 1801.68nm, 0 photons
Order 69: 1732.41nm - 1775.66nm, 0 photons
Order 70: 1707.87nm - 1750.38nm, 0 photons
Order 71: 1684.01nm - 1725.81nm, 0 photons
Order 72: 1660.80nm - 1701.92nm, 0 photons
Order 73: 1638.21nm - 1678.69nm, 0 photons
Order 74: 1616.22nm - 1656.08nm, 0 photons
Order 75: 1594.81nm - 1634.08nm, 0 photons
Order 76: 1573.95nm - 1612.66nm, 0 photons
Order 77: 1553.62nm - 1591.79nm, 0 photons
Order 78: 1533.80nm - 1571.45nm, 0 photons
Order 79: 1514.48nm - 1551.63nm, 0 photons
Order 80: 1495.64nm - 1532.31nm, 0 photons
Order 81: 1477.25nm - 1513.46nm, 0 photons
Order 82: 1459.31nm - 1495.07nm, 0 photons
Order 68: 1757.66nm - 1801.68nm, 0 photons
Order 69: 1732.41nm - 1775.66nm, 0 photons
Order 70: 1707.87nm - 1750.38nm, 0 photons
Order 71: 1684.01nm - 1725.81nm, 0 photons
Order 72: 1660.80nm - 1701.92nm, 0 photons
Order 73: 1638.21nm - 1678.69nm, 0 photons
Order 74: 1616.22nm - 1656.08nm, 0 photons
Order 75: 1594.81nm - 1634.08nm, 0 photons
Order 76: 1573.95nm - 1612.65nm, 0 photons
Order 77: 1553.62nm - 1591.78nm, 0 photons
Order 78: 1533.81nm - 1571.45nm, 0 photons
Order 79: 1514.49nm - 1551.63nm, 0 photons
Order 80: 1495.64nm - 1532.30nm, 0 photons
Order 81: 1477.25nm - 1513.46nm, 0 photons
Order 82: 1459.31nm - 1495.07nm, 0 photons
Order 68: 1757.66nm - 1801.68nm, 0 photons
Order 69: 1732.42nm - 1775.66nm, 0 photons
Order 70: 1707.88nm - 1750.38nm, 0 photons
Order 71: 1684.01nm - 1725.81nm, 0 photons
Order 72: 1660.80nm - 1701.92nm, 0 photons
Order 73: 1638.21nm - 1678.69nm, 0 photons
Order 74: 1616.22nm - 1656.08nm, 0 photons
Order 75: 1594.81nm - 1634.08nm, 0 photons
Order 76: 1573.95nm - 1612.65nm, 0 photons
Order 77: 1553.62nm - 1591.78nm, 0 photons
Order 78: 1533.81nm - 1571.45nm, 0 photons
Order 79: 1514.49nm - 1551.63nm, 0 photons
Order 80: 1495.64nm - 1532.30nm, 0 photons
Order 81: 1477.26nm - 1513.45nm, 0 photons
Order 82: 1459.31nm - 1495.06nm, 0 photons
Order 68: 1757.66nm - 1801.68nm, 0 photons
Order 69: 1732.42nm - 1775.65nm, 0 photons
Order 70: 1707.88nm - 1750.37nm, 0 photons
Order 71: 1684.02nm - 1725.80nm, 0 photons
Order 72: 1660.80nm - 1701.92nm, 0 photons
Order 73: 1638.21nm - 1678.68nm, 0 photons
Order 74: 1616.22nm - 1656.08nm, 0 photons
Order 75: 1594.81nm - 1634.07nm, 0 photons
Order 76: 1573.95nm - 1612.65nm, 0 photons
Order 77: 1553.62nm - 1591.78nm, 0 photons
Order 78: 1533.81nm - 1571.45nm, 0 photons
Order 79: 1514.49nm - 1551.63nm, 0 photons
Order 80: 1495.64nm - 1532.30nm, 0 photons
Order 81: 1477.26nm - 1513.45nm, 0 photons
Order 82: 1459.31nm - 1495.06nm, 0 photons
Order 68: 1757.67nm - 1801.67nm, 0 photons
Order 69: 1732.42nm - 1775.65nm, 0 photons
Order 70: 1707.88nm - 1750.37nm, 0 photons
Order 71: 1684.02nm - 1725.80nm, 0 photons
Order 72: 1660.80nm - 1701.92nm, 0 photons
Order 73: 1638.22nm - 1678.68nm, 0 photons
Order 74: 1616.23nm - 1656.08nm, 0 photons
Order 75: 1594.81nm - 1634.07nm, 0 photons
Order 76: 1573.95nm - 1612.65nm, 0 photons
Order 77: 1553.62nm - 1591.78nm, 0 photons
Order 78: 1533.81nm - 1571.44nm, 0 photons
Order 79: 1514.49nm - 1551.62nm, 0 photons
Order 80: 1495.64nm - 1532.30nm, 0 photons
Order 81: 1477.26nm - 1513.45nm, 0 photons
Order 82: 1459.31nm - 1495.06nm, 0 photons
Order 68: 1757.67nm - 1801.67nm, 0 photons
Order 69: 1732.42nm - 1775.65nm, 0 photons
Order 70: 1707.88nm - 1750.37nm, 0 photons
Order 71: 1684.02nm - 1725.80nm, 0 photons
Order 72: 1660.81nm - 1701.91nm, 0 photons
Order 73: 1638.22nm - 1678.68nm, 0 photons
Order 74: 1616.23nm - 1656.07nm, 0 photons
Order 75: 1594.81nm - 1634.07nm, 0 photons
Order 76: 1573.95nm - 1612.65nm, 0 photons
Order 77: 1553.62nm - 1591.78nm, 0 photons
Order 78: 1533.81nm - 1571.44nm, 0 photons
Order 79: 1514.49nm - 1551.62nm, 0 photons
Order 80: 1495.64nm - 1532.30nm, 0 photons
Order 81: 1477.26nm - 1513.45nm, 0 photons
Order 82: 1459.31nm - 1495.06nm, 0 photons
Order 68: 1757.67nm - 1801.67nm, 0 photons
Order 69: 1732.42nm - 1775.65nm, 0 photons
Order 70: 1707.88nm - 1750.37nm, 0 photons
Order 71: 1684.02nm - 1725.80nm, 0 photons
Order 72: 1660.81nm - 1701.91nm, 0 photons
Order 73: 1638.22nm - 1678.68nm, 0 photons
Order 74: 1616.23nm - 1656.07nm, 0 photons
Order 75: 1594.81nm - 1634.07nm, 3515544 photons
Order 76: 1573.95nm - 1612.64nm, 3515963 photons
Order 77: 1553.63nm - 1591.78nm, 0 photons
Order 78: 1533.81nm - 1571.44nm, 0 photons
Order 79: 1514.49nm - 1551.62nm, 0 photons
Order 80: 1495.64nm - 1532.30nm, 0 photons
Order 81: 1477.26nm - 1513.45nm, 0 photons
Order 82: 1459.31nm - 1495.06nm, 0 photons
Order 68: 1757.67nm - 1801.67nm, 0 photons
Order 69: 1732.42nm - 1775.64nm, 0 photons
Order 70: 1707.88nm - 1750.36nm, 0 photons
Order 71: 1684.02nm - 1725.80nm, 0 photons
Order 72: 1660.81nm - 1701.91nm, 0 photons
Order 73: 1638.22nm - 1678.68nm, 0 photons
Order 74: 1616.23nm - 1656.07nm, 0 photons
Order 75: 1594.82nm - 1634.07nm, 0 photons
Order 76: 1573.95nm - 1612.64nm, 0 photons
Order 77: 1553.63nm - 1591.77nm, 0 photons
Order 78: 1533.81nm - 1571.44nm, 0 photons
Order 79: 1514.49nm - 1551.62nm, 0 photons
Order 80: 1495.64nm - 1532.29nm, 0 photons
Order 81: 1477.26nm - 1513.45nm, 0 photons
Order 82: 1459.32nm - 1495.06nm, 0 photons
Order 68: 1757.67nm - 1801.67nm, 0 photons
Order 69: 1732.43nm - 1775.64nm, 0 photons
Order 70: 1707.89nm - 1750.36nm, 0 photons
Order 71: 1684.02nm - 1725.79nm, 0 photons
Order 72: 1660.81nm - 1701.91nm, 0 photons
Orde
... [476 lines truncated] ...
Dry run - would execute:
Type: flat_field
Band: H
Dry run - would execute:
Type: flat_field
Band: H
HDF model: /Users/tom/ANDES/E2E/src/HDF/ANDES_75fibre_H.hdf
Mode: single
Exposure: 1.0s
Flux: 200000.0
Wavelength range: 1600.0-1602.0 nm
Fiber efficiency: 0.5
Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable
andes_simulator/core/config.py - Added fib_eff: Optional[str] = None field to SimulationConfigandes_simulator/cli/main.py - Added --fib-eff option to common_options decorator and updated flat_field, fabry_perot, and lfc commands to accept and pass through the parameterandes_simulator/cli/utils.py - Updated build_config_from_options() to accept fib_eff and pass it to SimulationConfig, plus added dry-run output for the parameterandes_simulator/core/simulator.py - Added two methods:_parse_fib_eff() - parses "0.9" or "0.7-0.9" format_apply_fiber_efficiency() - sets fiber efficiency in PyEchelle's internal cache using TabulatedEfficiency and SystemEfficiency objects# Constant efficiency
uv run andes-sim flat-field --band H --subslit slitA --fib-eff 0.85
# Random efficiency in range (uniform distribution per fiber)
uv run andes-sim flat-field --band H --subslit slitA --fib-eff 0.7-0.95
On branch master
Your branch is up to date with 'origin/master'.
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: andes_simulator/cli/main.py
modified: andes_simulator/cli/utils.py
modified: andes_simulator/core/config.py
modified: andes_simulator/core/simulator.py
no changes added to commit (use "git add" and/or "git commit -a")
andes_simulator/cli/main.py | 16 +++++---
andes_simulator/cli/utils.py | 6 ++-
andes_simulator/core/config.py | 1 +
andes_simulator/cli/main.py | 16 +++++---
andes_simulator/cli/utils.py | 6 ++-
andes_simulator/core/config.py | 1 +
andes_simulator/core/simulator.py | 84 ++++++++++++++++++++++++++++++++++++++-
4 files changed, 98 insertions(+), 9 deletions(-)
689b273 check --band against --wl-minmax, and allow to omit --band. also fix default outpath
32eabd6 md cleanup
8f426e0 Add wavelength clipping support and annotate outputs
689b273 check --band against --wl-minmax, and allow to omit --band. also fix default outpath
32eabd6 md cleanup
8f426e0 Add wavelength clipping support and annotate outputs
4df4b4a Add --hdf option with band inference from wavelengths
82e6b1e Refactor FIBER_CONFIG: add UBVRIZ, YJH_SL, YJH_IFU configs
[master 23bedec] Add --fib-eff option for fiber efficiency control
4 files changed, 98 insertions(+), 9 deletions(-)
23bedec Add --fib-eff option for fiber efficiency control
689b273 check --band against --wl-minmax, and allow to omit --band. also fix default outpath
32eabd6 md cleanup
23bedec.