Source code for exosim.tasks.radiometric.utils.compute_noise_column

from collections import OrderedDict

import numpy as np
from astropy.table import QTable

from exosim.log import with_logger
from exosim.utils.klass_factory import find_task


@with_logger
[docs] def compute_noise_column( table: QTable, payloadConfig: dict, noise_key: str, task_key: str, default_task, signal_col: str, gain_col: str, output_col: str, logger=None, ) -> QTable: """ Generalized function to compute and add a noise column to the table. Parameters ---------- table : QTable The radiometric table to add noise columns to payloadConfig : dict Payload configuration containing channel specifications noise_key : str Key in the radiometric configuration (e.g., 'dark_current', 'read_noise') task_key : str Key for custom task specification (e.g., 'dark_current_task') default_task : class Default task class to use if no custom task is specified signal_col : str Column name containing the signal data gain_col : str or None Column name containing gain data (None for noise types that don't use gain) output_col : str or None Name of the main output column to create. If None, only adds individual noise components from the noise_table logger : Logger Logger instance for debugging Returns ------- QTable Updated table with new noise columns """ logger.debug( f"Entered compute_noise_column() for {noise_key} with {len(table)} rows" ) channels = payloadConfig["channel"] if isinstance(channels, OrderedDict): channel_items = channels.items() else: channel_items = [("single_channel", channels)] for ch_name, ch_description in channel_items: # Skip if noise configuration is not present if noise_key not in ch_description["radiometric"]: logger.debug(f"Channel {ch_name!r}: no {noise_key} configuration, skipping") continue # Skip if noise is disabled if not ch_description["radiometric"][noise_key]: logger.debug(f"Channel {ch_name!r}: {noise_key} is disabled, skipping") continue logger.info(f"Estimating {noise_key} on {ch_name}") # Get the noise computation task (custom or default) compute_noise_task = ( find_task( ch_description["radiometric"].get(task_key), default_task, ) if task_key in ch_description["radiometric"] else default_task ) computeNoise = compute_noise_task() # Get rows for this channel mask = table["ch_name"] == ch_name # Handle different return patterns from noise computation tasks if gain_col is not None: # Standard pattern: tasks that need gain and return (noise_table, noise) noise_table, noise = computeNoise( signal=table[signal_col][mask], # Pass all values, not just [0] aperture_table=table[mask], description=ch_description, multiaccum_gain=table[gain_col][mask], # Pass all values, not just [0] ) else: # Custom noise pattern: tasks that don't need gain noise_table, noise = computeNoise( wavelength=table["wavelength"][ mask ], # Pass all wavelengths, not just [0] description=ch_description, radiometric_table=table[mask], ) # Add the main noise column to the table (if output_col is specified) if output_col is not None: if output_col not in table.colnames: table[output_col] = np.nan * noise.unit table[output_col][mask] = noise # Assign all values, remove [0]! logger.debug( f"Channel {ch_name!r}: {output_col} min={np.min(noise)!r}, max={np.max(noise)!r}" ) # Add all individual noise components to the table for col_name in noise_table.colnames: # Skip the output column if it's already been handled above if output_col is not None and col_name == output_col: continue # Instead of skipping, update only the rows for this channel if col_name in table.colnames: table[col_name][mask] = noise_table[col_name] else: table[col_name] = 0 * noise_table[col_name].unit table[col_name][mask] = noise_table[col_name] logger.debug( f"Channel {ch_name!r}: updated component {col_name} min={np.min(noise_table[col_name])!r}, max={np.max(noise_table[col_name])!r}" ) if output_col is None: logger.info( f"Channel {ch_name}: added {len(noise_table.colnames)} noise components" ) logger.info( f"Finished compute_noise_column for {noise_key} on all {len(table)} rows" ) return table