Claude Code Session

Session ID: 29260932-5341-42b4-a78a-8aa713b2ee06
Total Duration: 12m 27s
Claude Working Time: 2m 23s
Waiting for User: 7m 14s
let's implement a new option --fib_eff that gets a single value or a range, like 0.9 or 0.7-0.9 , in order to set the fiber efficiencies in the simulation. see fiber_efficiency.md how to do that. set the eff after loading the HDF file, dont modify it. Ranges like 0.7-0.9 are to be interpreted as a random value within that range, uniformly distributed. ask if this was unclear, othweise get crackin and iterate.
example command: uv run andes-sim flat-field --band H --subslit slitA --wl-min 1600 --wl-max 1602 --fib_eff 0.7-0.95
I'll start by reading the fiber efficiency documentation and exploring the codebase to understand the structure.
Read file_path='/Users/tom/ANDES/E2E/src/fiber_efficiency.md'
     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>
Click to show full content (214 lines)
     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>
Glob pattern='**/cli/*.py', path='/Users/tom/ANDES/E2E/src'
/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
Let me read the CLI main file and the core simulator to understand how things are structured.
Read file_path='/Users/tom/ANDES/E2E/src/andes_simulator/cli/main.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>
Click to show full content (541 lines)
     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>
Read file_path='/Users/tom/ANDES/E2E/src/andes_simulator/cli/utils.py'
     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>
Click to show full content (208 lines)
     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>
Let me look at the simulator to see how it loads the spectrograph and where I should apply the fiber efficiency.
Read file_path='/Users/tom/ANDES/E2E/src/andes_simulator/core/simulator.py'
     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>
Click to show full content (468 lines)
     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>
Read file_path='/Users/tom/ANDES/E2E/src/andes_simulator/core/config.py'
     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>
Click to show full content (454 lines)
     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>
TodoWrite todos=[{'content': 'Add fib_eff field to SimulationConfig', 'status': 'in_progress', 'activeForm': 'Adding fib_eff field to SimulationConfig'}, {'content': 'Add --fib_eff CLI option to common_options', 'status': 'pending', 'activeForm': 'Adding --fib_eff CLI option'}, {'content': 'Pass fib_eff through build_config_from_options', 'status': 'pending', 'activeForm': 'Passing fib_eff through build_config_from_options'}, {'content': 'Apply fiber efficiency in simulator after loading HDF', 'status': 'pending', 'activeForm': 'Applying fiber efficiency in simulator'}, {'content': 'Test the implementation', 'status': 'pending', 'activeForm': 'Testing the implementation'}]
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
Let me add the fib_eff field to the config and the CLI option, then apply it in the simulator.
Edit file_path='/Users/tom/ANDES/E2E/src/andes_simulator/core/config.py', old_string=' # Special features\n velocity_shift: Optional[float] = None # m/s for Doppler shifts\n ther...', new_string=' # Special features\n velocity_shift: Optional[float] = None # m/s for Doppler shifts\n ther...'
• Edit /Users/tom/ANDES/E2E/src/andes_simulator/core/config.py
2 2 velocity_shift: Optional[float] = None # m/s for Doppler shifts
3 3 thermal_model: Optional[str] = None # For thermal variations
4 4 wl_min: Optional[float] = None # Minimum wavelength in nm
5 - wl_max: Optional[float] = None # Maximum wavelength in nm
5 + wl_max: Optional[float] = None # Maximum wavelength in nm
6 + fib_eff: Optional[str] = None # Fiber efficiency: "0.9" or "0.7-0.9" (random)
TodoWrite todos=[{'content': 'Add fib_eff field to SimulationConfig', 'status': 'completed', 'activeForm': 'Adding fib_eff field to SimulationConfig'}, {'content': 'Add --fib_eff CLI option to common_options', 'status': 'in_progress', 'activeForm': 'Adding --fib_eff CLI option'}, {'content': 'Pass fib_eff through build_config_from_options', 'status': 'pending', 'activeForm': 'Passing fib_eff through build_config_from_options'}, {'content': 'Apply fiber efficiency in simulator after loading HDF', 'status': 'pending', 'activeForm': 'Applying fiber efficiency in simulator'}, {'content': 'Test the implementation', 'status': 'pending', 'activeForm': 'Testing the implementation'}]
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
Edit file_path='/Users/tom/ANDES/E2E/src/andes_simulator/cli/main.py', old_string='def common_options(f):\n """Common options for simulation commands."""\n f = click.option(\'--dry...', new_string='def common_options(f):\n """Common options for simulation commands."""\n f = click.option(\'--dry...'
• Edit /Users/tom/ANDES/E2E/src/andes_simulator/cli/main.py
7 7 help='HDF model file (infers band if --band not given)')(f)
8 8 f = click.option('--wl-min', type=float, help='Minimum wavelength in nm')(f)
9 9 f = click.option('--wl-max', type=float, help='Maximum wavelength in nm')(f)
10 + f = click.option('--fib-eff', type=str, help='Fiber efficiency: single value (0.9) or range (0.7-0.9)')(f)
10 11 return f
Now I need to update the commands to accept fib_eff and pass it through. Let me update flat_field, fabry_perot, and lfc commands.
Edit file_path='/Users/tom/ANDES/E2E/src/andes_simulator/cli/main.py', old_string="@cli.command()\n@click.option('--band', type=click.Choice(ANDES_BANDS), help='Spectral band (inferred...", new_string="@cli.command()\n@click.option('--band', type=click.Choice(ANDES_BANDS), help='Spectral band (inferred..."
• Edit /Users/tom/ANDES/E2E/src/andes_simulator/cli/main.py
6 6 @click.option('--config', type=click.Path(exists=True, path_type=Path),
7 7 help='YAML configuration file')
8 8 @click.pass_context
9 - def flat_field(ctx, band, subslit, fiber, flux, scaling, exposure, output_dir, hdf, wl_min, wl_max, config, dry_run):
9 + def flat_field(ctx, band, subslit, fiber, flux, scaling, exposure, output_dir, hdf, wl_min, wl_max, fib_eff, config, dry_run):
6 6 @click.option('--config', type=click.Path(exists=True, path_type=Path),
7 7 help='YAML configuration file')
8 8 @click.pass_context
9 - def flat_field(ctx, band, subslit, fiber, flux, scaling, exposure, output_dir, hdf, wl_min, wl_max, config, dry_run):
9 + def flat_field(ctx, band, subslit, fiber, flux, scaling, exposure, output_dir, hdf, wl_min, wl_max, fib_eff, config, dry_run):
10 10 """Generate flat field calibration frames."""
11 11 from ..core.config import SimulationConfig
12 12
27 27 flux_unit="ph/s/AA",
28 28 hdf=hdf_path,
29 29 wl_min=wl_min,
30 - wl_max=wl_max
30 + wl_max=wl_max,
31 + fib_eff=fib_eff
31 32 )
32 33
33 34 run_simulation_command(
Edit file_path='/Users/tom/ANDES/E2E/src/andes_simulator/cli/main.py', old_string="@cli.command()\n@click.option('--band', type=click.Choice(ANDES_BANDS), help='Spectral band (inferred...", new_string="@cli.command()\n@click.option('--band', type=click.Choice(ANDES_BANDS), help='Spectral band (inferred..."
• Edit /Users/tom/ANDES/E2E/src/andes_simulator/cli/main.py
7 7 @click.option('--config', type=click.Path(exists=True, path_type=Path),
8 8 help='YAML configuration file')
9 9 @click.pass_context
10 - def fabry_perot(ctx, band, subslit, fiber, flux, scaling, exposure, output_dir, hdf, wl_min, wl_max, velocity_shift, config, dry_run):
10 + def fabry_perot(ctx, band, subslit, fiber, flux, scaling, exposure, output_dir, hdf, wl_min, wl_max, fib_eff, velocity_shift, config, dry_run):
7 7 @click.option('--config', type=click.Path(exists=True, path_type=Path),
8 8 help='YAML configuration file')
9 9 @click.pass_context
10 - def fabry_perot(ctx, band, subslit, fiber, flux, scaling, exposure, output_dir, hdf, wl_min, wl_max, velocity_shift, config, dry_run):
10 + def fabry_perot(ctx, band, subslit, fiber, flux, scaling, exposure, output_dir, hdf, wl_min, wl_max, fib_eff, velocity_shift, config, dry_run):
11 11 """Generate Fabry-Perot wavelength calibration frames."""
12 12 from ..core.config import SimulationConfig
13 13
28 28 velocity_shift=velocity_shift,
29 29 hdf=hdf_path,
30 30 wl_min=wl_min,
31 - wl_max=wl_max
31 + wl_max=wl_max,
32 + fib_eff=fib_eff
32 33 )
33 34
34 35 run_simulation_command(
Edit file_path='/Users/tom/ANDES/E2E/src/andes_simulator/cli/main.py', old_string="@cli.command()\n@click.option('--band', type=click.Choice(ANDES_BANDS), help='Spectral band (inferred...", new_string="@cli.command()\n@click.option('--band', type=click.Choice(ANDES_BANDS), help='Spectral band (inferred..."
• Edit /Users/tom/ANDES/E2E/src/andes_simulator/cli/main.py
4 4 @flux_options(default_scaling=1e5)
5 5 @common_options
6 6 @click.pass_context
7 - def lfc(ctx, band, subslit, fiber, flux, scaling, exposure, output_dir, hdf, wl_min, wl_max, dry_run):
7 + def lfc(ctx, band, subslit, fiber, flux, scaling, exposure, output_dir, hdf, wl_min, wl_max, fib_eff, dry_run):
4 4 @flux_options(default_scaling=1e5)
5 5 @common_options
6 6 @click.pass_context
7 - def lfc(ctx, band, subslit, fiber, flux, scaling, exposure, output_dir, hdf, wl_min, wl_max, dry_run):
7 + def lfc(ctx, band, subslit, fiber, flux, scaling, exposure, output_dir, hdf, wl_min, wl_max, fib_eff, dry_run):
8 8 """Generate Laser Frequency Comb wavelength calibration frames.
9 9
10 10 LFC produces unresolved emission lines equidistant in velocity,
23 23 scaling=scaling,
24 24 hdf=hdf_path,
25 25 wl_min=wl_min,
26 - wl_max=wl_max
26 + wl_max=wl_max,
27 + fib_eff=fib_eff
27 28 )
28 29
29 30 run_simulation_command(
TodoWrite todos=[{'content': 'Add fib_eff field to SimulationConfig', 'status': 'completed', 'activeForm': 'Adding fib_eff field to SimulationConfig'}, {'content': 'Add --fib_eff CLI option to common_options', 'status': 'completed', 'activeForm': 'Adding --fib_eff CLI option'}, {'content': 'Pass fib_eff through build_config_from_options', 'status': 'in_progress', 'activeForm': 'Passing fib_eff through build_config_from_options'}, {'content': 'Apply fiber efficiency in simulator after loading HDF', 'status': 'pending', 'activeForm': 'Applying fiber efficiency in simulator'}, {'content': 'Test the implementation', 'status': 'pending', 'activeForm': 'Testing the implementation'}]
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
Edit file_path='/Users/tom/ANDES/E2E/src/andes_simulator/cli/utils.py', old_string='def build_config_from_options(\n simulation_type: str,\n band: str,\n exposure: float,\n sou...', new_string='def build_config_from_options(\n simulation_type: str,\n band: str,\n exposure: float,\n sou...'
• Edit /Users/tom/ANDES/E2E/src/andes_simulator/cli/utils.py
13 13 flux_unit: str = "ph/s/AA",
14 14 hdf: Optional[str] = None,
15 15 wl_min: Optional[float] = None,
16 - wl_max: Optional[float] = None
16 + wl_max: Optional[float] = None,
17 + fib_eff: Optional[str] = None
17 18 ):
Edit file_path='/Users/tom/ANDES/E2E/src/andes_simulator/cli/utils.py', old_string=' return SimulationConfig(\n simulation_type=simulation_type,\n band=band,\n exp...', new_string=' return SimulationConfig(\n simulation_type=simulation_type,\n band=band,\n exp...'
• Edit /Users/tom/ANDES/E2E/src/andes_simulator/cli/utils.py
6 6 hdf_model=hdf,
7 7 wl_min=wl_min,
8 8 wl_max=wl_max,
9 + fib_eff=fib_eff,
9 10 source=SourceConfig(**source_kwargs),
10 11 fibers=FiberConfig(mode=fiber_mode, fibers=fibers),
11 12 output=OutputConfig(directory=output_directory)
Edit file_path='/Users/tom/ANDES/E2E/src/andes_simulator/cli/utils.py', old_string=' if config.velocity_shift:\n click.echo(f" Velocity shift: {config.velocity_shift} m/s")\n ...', new_string=' if config.velocity_shift:\n click.echo(f" Velocity shift: {config.velocity_shift} m/s")\n ...'
• Edit /Users/tom/ANDES/E2E/src/andes_simulator/cli/utils.py
2 2 click.echo(f" Velocity shift: {config.velocity_shift} m/s")
3 3 if config.wl_min or config.wl_max:
4 4 wl_range = f"{config.wl_min or '...'}-{config.wl_max or '...'} nm"
5 - click.echo(f" Wavelength range: {wl_range}")
5 + click.echo(f" Wavelength range: {wl_range}")
6 + if config.fib_eff:
7 + click.echo(f" Fiber efficiency: {config.fib_eff}")
TodoWrite todos=[{'content': 'Add fib_eff field to SimulationConfig', 'status': 'completed', 'activeForm': 'Adding fib_eff field to SimulationConfig'}, {'content': 'Add --fib_eff CLI option to common_options', 'status': 'completed', 'activeForm': 'Adding --fib_eff CLI option'}, {'content': 'Pass fib_eff through build_config_from_options', 'status': 'completed', 'activeForm': 'Passing fib_eff through build_config_from_options'}, {'content': 'Apply fiber efficiency in simulator after loading HDF', 'status': 'in_progress', 'activeForm': 'Applying fiber efficiency in simulator'}, {'content': 'Test the implementation', 'status': 'pending', 'activeForm': 'Testing the implementation'}]
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
Now I need to apply the fiber efficiency in the simulator. Let me check what instruments data we have for wavelength ranges.
Read file_path='/Users/tom/ANDES/E2E/src/andes_simulator/core/instruments.py'
     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>
Click to show full content (418 lines)
     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>
Now I'll add the fiber efficiency application logic to the simulator. I need to modify setup_simulator() to apply the efficiency after loading the spectrograph.
Edit file_path='/Users/tom/ANDES/E2E/src/andes_simulator/core/simulator.py', old_string='from .config import SimulationConfig\nfrom .instruments import get_instrument_config, get_hdf_model_p...', new_string='from .config import SimulationConfig\nfrom .instruments import get_instrument_config, get_hdf_model_p...'
• Edit /Users/tom/ANDES/E2E/src/andes_simulator/core/simulator.py
1 1 from .config import SimulationConfig
2 - from .instruments import get_instrument_config, get_hdf_model_path, get_sed_path
2 + from .instruments import get_instrument_config, get_hdf_model_path, get_sed_path, BAND_WAVELENGTH_RANGES
3 3 from .sources import SourceFactory, SPEED_OF_LIGHT
Edit file_path='/Users/tom/ANDES/E2E/src/andes_simulator/core/simulator.py', old_string=' def cleanup(self) -> None:\n """Clean up resources (temporary files, etc.)."""\n sel...', new_string=' def cleanup(self) -> None:\n """Clean up resources (temporary files, etc.)."""\n sel...'
• Edit /Users/tom/ANDES/E2E/src/andes_simulator/core/simulator.py
1 1 def cleanup(self) -> None:
2 2 """Clean up resources (temporary files, etc.)."""
3 - self.source_factory.cleanup()
3 + self.source_factory.cleanup()
4 +
1 1 def cleanup(self) -> None:
2 2 """Clean up resources (temporary files, etc.)."""
3 - self.source_factory.cleanup()
3 + self.source_factory.cleanup()
4 +
5 + def _parse_fib_eff(self, fib_eff: str) -> tuple:
6 + """
7 + Parse fiber efficiency specification.
8 +
9 + Parameters
10 + ----------
11 + fib_eff : str
12 + Efficiency value as "0.9" or range as "0.7-0.9"
13 +
14 + Returns
15 + -------
16 + tuple
17 + (min_eff, max_eff) - if equal, use constant; if different, use random
18 + """
19 + if '-' in fib_eff:
20 + parts = fib_eff.split('-')
21 + if len(parts) != 2:
22 + raise ValueError(f"Invalid fib_eff format: {fib_eff}")
23 + return float(parts[0]), float(parts[1])
24 + else:
25 + val = float(fib_eff)
26 + return val, val
27 +
28 + def _apply_fiber_efficiency(self, spec, fib_eff: str) -> None:
29 + """
30 + Apply fiber efficiency to spectrograph HDF data in memory.
31 +
32 + Parameters
33 + ----------
34 + spec : ZEMAX or LocalDisturber
35 + The spectrograph object (has h5f attribute if ZEMAX, or wrapped spec)
36 + fib_eff : str
37 + Efficiency specification ("0.9" or "0.7-0.9")
38 + """
39 + eff_min, eff_max = self._parse_fib_eff(fib_eff)
40 +
41 + # Get the ZEMAX object (may be wrapped in LocalDisturber)
42 + if hasattr(spec, 'spectrograph'):
43 + zemax = spec.spectrograph
44 + else:
45 + zemax = spec
46 +
47 + h5f = zemax.h5f
48 + n_fibers = self.instrument_config['n_fibers']
49 +
50 + # Get wavelength range for this band (convert nm to microns)
51 + wl_min_nm, wl_max_nm = BAND_WAVELENGTH_RANGES[self.config.band]
52 + wl_min_um = wl_min_nm / 1000.0
53 + wl_max_um = wl_max_nm / 1000.0
54 + wavelengths = np.array([wl_min_um, (wl_min_um + wl_max_um) / 2, wl_max_um])
55 +
56 + self.logger.info(f"Applying fiber efficiency: {fib_eff}")
57 +
58 + for fiber_num in range(1, n_fibers + 1):
59 + fiber_path = f'CCD_1/fiber_{fiber_num}'
60 + if fiber_path not in h5f:
61 + continue
62 +
63 + fiber_group = h5f[fiber_path]
64 +
65 + # Determine efficiency value for this fiber
66 + if eff_min == eff_max:
67 + eff_value = eff_min
68 + else:
69 + eff_value = np.random.uniform(eff_min, eff_max)
70 +
71 + efficiency = np.array([eff_value, eff_value, eff_value])
72 +
73 + # Set the efficiency attribute (overwrites if exists)
74 + fiber_group.attrs['efficiency'] = (wavelengths, efficiency)
Now I need to call the _apply_fiber_efficiency method in setup_simulator() after creating the spectrograph.
Edit file_path='/Users/tom/ANDES/E2E/src/andes_simulator/core/simulator.py', old_string=' # Create spectrograph with optional velocity shift\n if self.config.velocity_shift is ...', new_string=' # Create spectrograph with optional velocity shift\n if self.config.velocity_shift is ...'
• Edit /Users/tom/ANDES/E2E/src/andes_simulator/core/simulator.py
5 5 self.logger.info(f"Applied velocity shift: {self.config.velocity_shift} m/s")
6 6 else:
7 7 spec = ZEMAX(str(hdf_path))
8 -
8 +
5 5 self.logger.info(f"Applied velocity shift: {self.config.velocity_shift} m/s")
6 6 else:
7 7 spec = ZEMAX(str(hdf_path))
8 -
8 +
9 + # Apply fiber efficiency if specified
10 + if self.config.fib_eff:
11 + self._apply_fiber_efficiency(spec, self.config.fib_eff)
12 +
9 13 # Create simulator
10 14 self.simulator = Simulator(spec)
TodoWrite todos=[{'content': 'Add fib_eff field to SimulationConfig', 'status': 'completed', 'activeForm': 'Adding fib_eff field to SimulationConfig'}, {'content': 'Add --fib_eff CLI option to common_options', 'status': 'completed', 'activeForm': 'Adding --fib_eff CLI option'}, {'content': 'Pass fib_eff through build_config_from_options', 'status': 'completed', 'activeForm': 'Passing fib_eff through build_config_from_options'}, {'content': 'Apply fiber efficiency in simulator after loading HDF', 'status': 'completed', 'activeForm': 'Applying fiber efficiency in simulator'}, {'content': 'Test the implementation', 'status': 'in_progress', 'activeForm': 'Testing the implementation'}]
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
Let me test the implementation with a dry run.
Bash command='uv run andes-sim flat-field --band H --subslit slitA --wl-min 1600 --wl-max 1602 --fib-eff 0.7-0.95 ...', description='Test --fib-eff option with dry run'
Dry run - would execute:
  Type: flat_field
  Band: H
Click to show full output (9 lines)
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
The CLI is working. Now let me run a quick actual simulation to verify the fiber efficiency gets applied.
Bash command='uv run andes-sim flat-field --band H --subslit single --fiber 42 --wl-min 1600 --wl-max 1602 --fib-e...', description='Test actual simulation with fiber efficiency', timeout=120000
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
Click to show full output (4 lines)
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)
The HDF file is opened read-only by PyEchelle. I need to modify the approach - I'll set the efficiency directly in PyEchelle's internal cache instead of modifying the HDF file.
Edit file_path='/Users/tom/ANDES/E2E/src/andes_simulator/core/simulator.py', old_string=' def _apply_fiber_efficiency(self, spec, fib_eff: str) -> None:\n """\n Apply fiber e...', new_string=' def _apply_fiber_efficiency(self, spec, fib_eff: str) -> None:\n """\n Apply fiber e...'
• Edit /Users/tom/ANDES/E2E/src/andes_simulator/core/simulator.py
1 1 def _apply_fiber_efficiency(self, spec, fib_eff: str) -> None:
2 2 """
3 - Apply fiber efficiency to spectrograph HDF data in memory.
3 + Apply fiber efficiency to spectrograph by setting PyEchelle's internal cache.
4 4
1 1 def _apply_fiber_efficiency(self, spec, fib_eff: str) -> None:
2 2 """
3 - Apply fiber efficiency to spectrograph HDF data in memory.
3 + Apply fiber efficiency to spectrograph by setting PyEchelle's internal cache.
4 4
5 5 Parameters
6 6 ----------
7 7 spec : ZEMAX or LocalDisturber
8 - The spectrograph object (has h5f attribute if ZEMAX, or wrapped spec)
8 + The spectrograph object
9 9 fib_eff : str
10 10 Efficiency specification ("0.9" or "0.7-0.9")
11 11 """
12 + from pyechelle.efficiency import TabulatedEfficiency, SystemEfficiency, ConstantEfficiency
13 +
12 14 eff_min, eff_max = self._parse_fib_eff(fib_eff)
13 15
14 16 # Get the ZEMAX object (may be wrapped in LocalDisturber)
17 19 else:
18 20 zemax = spec
19 21
20 - h5f = zemax.h5f
21 22 n_fibers = self.instrument_config['n_fibers']
23 + ccd_index = 1
22 24
23 25 # Get wavelength range for this band (convert nm to microns)
24 26 wl_min_nm, wl_max_nm = BAND_WAVELENGTH_RANGES[self.config.band]
28 30
29 31 self.logger.info(f"Applying fiber efficiency: {fib_eff}")
30 32
33 + # Ensure the efficiency cache dict exists
34 + if not hasattr(zemax, '_efficiency') or zemax._efficiency is None:
35 + zemax._efficiency = {}
36 + if ccd_index not in zemax._efficiency:
37 + zemax._efficiency[ccd_index] = {}
38 +
31 39 for fiber_num in range(1, n_fibers + 1):
32 - fiber_path = f'CCD_1/fiber_{fiber_num}'
33 - if fiber_path not in h5f:
34 - continue
35 -
36 - fiber_group = h5f[fiber_path]
37 -
38 40 # Determine efficiency value for this fiber
39 41 if eff_min == eff_max:
40 42 eff_value = eff_min
43 45
44 46 efficiency = np.array([eff_value, eff_value, eff_value])
45 47
46 - # Set the efficiency attribute (overwrites if exists)
47 - fiber_group.attrs['efficiency'] = (wavelengths, efficiency)
48 + # Create efficiency object and set in cache
49 + fiber_eff_obj = TabulatedEfficiency("FiberEff", wavelengths, efficiency)
50 + zemax._efficiency[ccd_index][fiber_num] = SystemEfficiency(
51 + [fiber_eff_obj], "System"
52 + )
Bash command='uv run andes-sim flat-field --band H --subslit single --fiber 42 --wl-min 1600 --wl-max 1602 --fib-e...', description='Test simulation with fiber efficiency fix', timeout=120000
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
Click to show full output (30 lines)
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
Bash command='uv run andes-sim flat-field --band H --subslit single --fiber 42 --wl-min 1600 --wl-max 1602 --fib-e...', description='Complete simulation with fiber efficiency', timeout=300000
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
Click to show full output (666 lines)
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] ...
Simulation completed successfully. Let me also test with a constant efficiency value.
Bash command='uv run andes-sim flat-field --band H --subslit single --fiber 42 --wl-min 1600 --wl-max 1602 --fib-e...', description='Test constant fiber efficiency dry run'
Dry run - would execute:
  Type: flat_field
  Band: H
Click to show full output (9 lines)
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
TodoWrite todos=[{'content': 'Add fib_eff field to SimulationConfig', 'status': 'completed', 'activeForm': 'Adding fib_eff field to SimulationConfig'}, {'content': 'Add --fib_eff CLI option to common_options', 'status': 'completed', 'activeForm': 'Adding --fib_eff CLI option'}, {'content': 'Pass fib_eff through build_config_from_options', 'status': 'completed', 'activeForm': 'Passing fib_eff through build_config_from_options'}, {'content': 'Apply fiber efficiency in simulator after loading HDF', 'status': 'completed', 'activeForm': 'Applying fiber efficiency in simulator'}, {'content': 'Test the implementation', 'status': 'completed', 'activeForm': 'Testing the implementation'}]
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
Implementation complete. Here's a summary of the changes:

Files modified:

1. andes_simulator/core/config.py - Added fib_eff: Optional[str] = None field to SimulationConfig

2. andes_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 parameter

3. andes_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 parameter

4. andes_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

Usage:
# 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

The efficiency is applied after loading the HDF file by directly populating PyEchelle's efficiency cache, avoiding the need to modify the read-only HDF file.
great thx! commit pls
Bash command='git status', description='Check git status'
On branch master
Your branch is up to date with 'origin/master'.
Click to show full output (12 lines)
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")
Bash command='git diff --stat', description='Show diff statistics'
 andes_simulator/cli/main.py       | 16 +++++---
 andes_simulator/cli/utils.py      |  6 ++-
 andes_simulator/core/config.py    |  1 +
Click to show full output (5 lines)
 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(-)
Bash command='git log --oneline -5', description='Show recent commits'
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
Click to show full output (5 lines)
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
Bash command='git add andes_simulator/cli/main.py andes_simulator/cli/utils.py andes_simulator/core/config.py ande...', description='Commit fiber efficiency changes'
[master 23bedec] Add --fib-eff option for fiber efficiency control
 4 files changed, 98 insertions(+), 9 deletions(-)
Bash command='git log --oneline -3', description='Verify commit'
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
Done. Committed as 23bedec.