kartikmandar's picture
feat: add lazy loading for large FITS files
869b08d
# Standard Imports
import os
import stat
import copy
import logging
import numpy as np
import warnings
import tempfile
import traceback
import requests
from bokeh.models import Tooltip
# HoloViz Imports
import panel as pn
# Stingray Imports
from stingray.events import EventList
# Dashboard Classes and State Management Imports
from utils.app_context import AppContext
from utils.error_handler import ErrorHandler
from utils.DashboardClasses import (
MainHeader,
MainArea,
OutputBox,
WarningBox,
HelpBox,
WarningHandler,
PlotsContainer,
)
def create_warning_handler():
"""
Create an instance of WarningHandler and redirect warnings to this custom handler.
Returns:
WarningHandler: An instance of WarningHandler to handle warnings.
Side effects:
Overrides the default warning handler with a custom one.
Example:
>>> warning_handler = create_warning_handler()
>>> warning_handler.warn("Test warning", category=RuntimeWarning)
"""
warning_handler = WarningHandler()
warnings.showwarning = warning_handler.warn
return warning_handler
""" Header Section """
def create_eventlist_header(context: AppContext):
"""
Create the header for the EventList section.
Args:
context (AppContext): The application context containing containers and state.
Returns:
MainHeader: An instance of MainHeader with the specified heading.
Example:
>>> header = create_eventlist_header(context)
>>> header.heading.value
'QuickLook EventList'
"""
home_heading_input = pn.widgets.TextInput(
name="Heading", value="QuickLook EventList"
)
return MainHeader(heading=home_heading_input)
def create_eventlist_output_box(content):
"""
Create an output box to display messages.
Args:
content (str): The content to be displayed in the output box.
Returns:
OutputBox: An instance of OutputBox with the specified content.
Example:
>>> output_box = create_loadingdata_output_box("File loaded successfully.")
>>> output_box.output_content
'File loaded successfully.'
"""
return OutputBox(output_content=content)
""" Warning Box Section """
def create_eventlist_warning_box(content):
"""
Create a warning box to display warnings.
Args:
content (str): The content to be displayed in the warning box.
Returns:
WarningBox: An instance of WarningBox with the specified content.
Example:
>>> warning_box = create_loadingdata_warning_box("Invalid file format.")
>>> warning_box.warning_content
'Invalid file format.'
"""
return WarningBox(warning_content=content)
# TODO: ADD better comments, error handlling and docstrings
def create_event_list(
event,
times_input,
energy_input,
pi_input,
gti_input,
mjdref_input,
dt_input,
high_precision_checkbox,
mission_input,
instr_input,
detector_id_input,
header_input,
timeref_input,
timesys_input,
ephem_input,
rmf_file_input,
skip_checks_checkbox,
notes_input,
name_input,
context: AppContext,
warning_handler,
):
"""
Create an event list from user input with all parameters of the EventList class.
Args:
See above function for argument details.
Side effects:
- Creates a new EventList object and adds it to `loaded_event_data`.
- Updates the output and warning containers with messages.
Exceptions:
- Displays exceptions in the warning box if event list creation fails.
"""
try:
# Mandatory input validation
if not times_input.value:
context.update_container('output_box',
create_eventlist_output_box(
"Error: Photon Arrival Times is a mandatory field."
)
)
context.update_container('warning_box',
create_eventlist_warning_box(
"Warning: Mandatory fields are missing. Please provide required inputs."
)
)
return
# Clean and parse inputs, ignoring empty values
times = [float(t) for t in times_input.value.split(",") if t.strip()]
mjdref = (
float(mjdref_input.value.strip()) if mjdref_input.value.strip() else 0.0
)
energy = (
[float(e) for e in energy_input.value.split(",") if e.strip()]
if energy_input.value.strip()
else None
)
pi = (
[int(p) for p in pi_input.value.split(",") if p.strip()]
if pi_input.value.strip()
else None
)
gti = (
[
[float(g) for g in interval.split()]
for interval in gti_input.value.split(";")
if interval.strip()
]
if gti_input.value.strip()
else None
)
dt = float(dt_input.value.strip()) if dt_input.value.strip() else 0.0
high_precision = high_precision_checkbox.value
mission = mission_input.value.strip() or None
instr = instr_input.value.strip() or None
detector_id = (
[int(d) for d in detector_id_input.value.split(",") if d.strip()]
if detector_id_input.value.strip()
else None
)
header = header_input.value.strip() or None
timeref = timeref_input.value.strip() or None
timesys = timesys_input.value.strip() or None
ephem = ephem_input.value.strip() or None
rmf_file = rmf_file_input.value.strip() or None
skip_checks = skip_checks_checkbox.value
notes = notes_input.value.strip() or None
name = name_input.value.strip() or f"event_list_{len(context.state.get_event_data())}"
# Check for duplicates
if context.state.has_event_data(name):
context.update_container('output_box',
create_eventlist_output_box(
f"A file with the name '{name}' already exists in memory. Please provide a different name."
)
)
return
# Create EventList
event_list = EventList(
time=times,
energy=energy,
pi=pi,
gti=gti,
mjdref=mjdref,
dt=dt,
high_precision=high_precision,
mission=mission,
instr=instr,
detector_id=detector_id,
header=header,
timeref=timeref,
timesys=timesys,
ephem=ephem,
rmf_file=rmf_file,
skip_checks=skip_checks,
notes=notes,
)
# Store the EventList
context.state.add_event_data(name, event_list)
context.update_container('output_box',
create_eventlist_output_box(
f"Event List created successfully!\nSaved as: {name}\nDetails:\n"
f"Times: {event_list.time}\nMJDREF: {event_list.mjdref}\nGTI: {event_list.gti}\n"
f"Energy: {event_list.energy if energy else 'Not provided'}\nPI: {event_list.pi if pi else 'Not provided'}\n"
f"Mission: {event_list.mission if mission else 'Not provided'}\nInstrument: {event_list.instr if instr else 'Not provided'}"
)
)
except ValueError as ve:
user_msg, tech_msg = ErrorHandler.handle_error(
ve,
context="Creating custom event list",
log_level=logging.WARNING
)
warning_handler.warn(tech_msg, category=ValueError)
context.update_container('output_box',
create_eventlist_output_box(f"Error: {user_msg}")
)
except Exception as e:
user_msg, tech_msg = ErrorHandler.handle_error(
e,
context="Creating custom event list"
)
warning_handler.warn(tech_msg, category=RuntimeError)
context.update_container('output_box',
create_eventlist_output_box(f"Error: {user_msg}")
)
if warning_handler.warnings:
context.update_container('warning_box',
create_eventlist_warning_box("\n".join(warning_handler.warnings))
)
else:
context.update_container('warning_box', create_eventlist_warning_box("No warnings."))
warning_handler.warnings.clear()
# TODO: ADD better comments, error handlling and docstrings
def simulate_event_list(
event,
time_bins_input,
max_counts_input,
dt_input,
name_input,
method_selector,
seed_input,
simulate_energies_checkbox,
energy_bins_input,
energy_counts_input,
context: AppContext,
warning_handler,
):
"""
Simulate an event list based on user-defined parameters.
Args:
event: The event object triggering the function.
time_bins_input: The input for the number of time bins.
max_counts_input: The input for the maximum counts per bin.
dt_input: The input for delta time (dt).
name_input: The input widget for the simulated event list name.
method_selector: Radio button group for simulation method selection.
seed_input: Input for random seed (optional).
simulate_energies_checkbox: Checkbox to enable energy simulation.
energy_bins_input: Energy bins input (comma-separated keV values).
energy_counts_input: Counts per bin input (comma-separated values).
context: Application context.
warning_handler: The handler for warnings.
Side effects:
- Creates a simulated EventList object and adds it to `loaded_event_data`.
- Updates the output and warning containers with messages.
Exceptions:
- Displays exceptions in the warning box if simulation fails.
Restrictions:
- Requires a unique name for the simulated event list.
Example:
>>> simulate_event_list(event, time_bins_input, max_counts_input, dt_input, name_input, method_selector, seed_input, ...)
"Event List simulated successfully!"
"""
# Clear previous warnings
warning_handler.warnings.clear()
warnings.resetwarnings()
try:
if not name_input.value:
context.update_container('output_box',
create_eventlist_output_box(
"Please provide a name for the simulated event list."
)
)
return
if context.state.has_event_data(name_input.value):
context.update_container('output_box',
create_eventlist_output_box(
f"A file with the name '{name_input.value}' already exists in memory. Please provide a different name."
)
)
return
# Parse inputs from IntInput and FloatInput widgets
time_bins = time_bins_input.value
max_counts = max_counts_input.value
dt = dt_input.value
# Simulate the light curve using lightcurve service
times = np.arange(time_bins)
counts = np.random.randint(0, max_counts, size=time_bins)
lc_result = context.services.lightcurve.create_lightcurve_from_arrays(
times=times,
counts=counts,
dt=dt
)
if not lc_result["success"]:
context.update_container('output_box',
create_eventlist_output_box(f"Error: {lc_result['message']}")
)
return
lc = lc_result["data"]
# Map radio button value to method string
method_map = {
'Probabilistic (Recommended)': 'probabilistic',
'Deterministic (Legacy)': 'deterministic'
}
method = method_map.get(method_selector.value, 'probabilistic')
# Get seed value (None if empty)
seed = seed_input.value if seed_input.value is not None else None
# Simulate EventList from lightcurve using new method
event_list_result = context.services.lightcurve.simulate_event_list_from_lightcurve(
lightcurve=lc,
method=method,
seed=seed
)
if not event_list_result["success"]:
context.update_container('output_box',
create_eventlist_output_box(f"Error: {event_list_result['message']}")
)
return
event_list = event_list_result["data"]
metadata = event_list_result.get("metadata", {})
name = name_input.value
# Simulate energies if requested
energy_metadata = {}
if simulate_energies_checkbox.value:
# Parse energy spectrum inputs
energy_bins_str = energy_bins_input.value.strip()
energy_counts_str = energy_counts_input.value.strip()
if not energy_bins_str or not energy_counts_str:
context.update_container('output_box',
create_eventlist_output_box(
"Error: Energy simulation enabled but spectrum not provided.\n"
"Please provide both energy bins and counts."
)
)
return
try:
# Parse comma-separated values
energy_bins = [float(e.strip()) for e in energy_bins_str.split(',')]
energy_counts = [float(c.strip()) for c in energy_counts_str.split(',')]
# Create spectrum
spectrum = [energy_bins, energy_counts]
# Simulate energies
energy_result = context.services.lightcurve.simulate_energies_for_event_list(
event_list=event_list,
spectrum=spectrum
)
if not energy_result["success"]:
context.update_container('output_box',
create_eventlist_output_box(f"Error simulating energies: {energy_result['message']}")
)
return
event_list = energy_result["data"]
energy_metadata = energy_result.get("metadata", {})
except ValueError as ve:
context.update_container('output_box',
create_eventlist_output_box(
f"Error parsing energy spectrum: {str(ve)}\n"
"Make sure to use comma-separated numbers."
)
)
return
context.state.add_event_data(name, event_list)
# Build output message with method, seed, and energy info
output_message = (
f"Event List simulated successfully!\n"
f"Saved as: {name}\n"
f"Method: {metadata.get('method', 'unknown').capitalize()}\n"
f"Seed: {metadata.get('seed', 'random')}\n"
f"Number of events: {metadata.get('n_events', len(event_list.time))}\n"
f"Time range: {metadata.get('time_range', (event_list.time[0], event_list.time[-1]))}\n"
f"Original lightcurve counts: {counts}"
)
if energy_metadata:
output_message += (
f"\n\nEnergy simulation:\n"
f"Energy range: {energy_metadata.get('energy_range', 'N/A')} keV\n"
f"Mean energy: {energy_metadata.get('mean_energy', 'N/A'):.2f} keV\n"
f"Number of energy bins: {energy_metadata.get('n_energy_bins', 'N/A')}"
)
context.update_container('output_box',
create_eventlist_output_box(output_message)
)
except Exception as e:
user_msg, tech_msg = ErrorHandler.handle_error(
e,
context="Simulating event list from lightcurve",
time_bins=time_bins,
max_counts=max_counts,
dt=dt
)
warning_handler.warn(tech_msg, category=RuntimeError)
context.update_container('output_box',
create_eventlist_output_box(f"Error: {user_msg}")
)
if warning_handler.warnings:
context.update_container('warning_box',
create_eventlist_warning_box("\n".join(warning_handler.warnings))
)
else:
context.update_container('warning_box', create_eventlist_warning_box("No warnings."))
warning_handler.warnings.clear()
# TODO: ADD better comments, error handlling and docstrings
def create_event_list_tab(context: AppContext, warning_handler):
"""
Create the tab for creating an event list with all parameters of the EventList class.
Args:
context (AppContext): The application context containing all containers and state.
warning_handler (WarningHandler): The handler for warnings.
Returns:
Column: A Panel Column containing the widgets and layout for the event list creation tab.
"""
# Mandatory parameters
times_input = pn.widgets.TextInput(
name="Photon Arrival Times", placeholder="e.g., 0.5, 1.1, 2.2, 3.7"
)
mjdref_input = pn.widgets.TextInput(
name="Reference MJD", placeholder="e.g., 58000."
)
# Optional parameters
energy_input = pn.widgets.TextInput(
name="Energy (optional)", placeholder="e.g., 0., 3., 4., 20."
)
pi_input = pn.widgets.TextInput(
name="PI (optional)", placeholder="e.g., 100, 200, 300, 400"
)
gti_input = pn.widgets.TextInput(
name="GTIs (optional)", placeholder="e.g., 0 4; 5 10"
)
dt_input = pn.widgets.TextInput(
name="Time Resolution (optional)", placeholder="e.g., 0.01"
)
high_precision_checkbox = pn.widgets.Checkbox(
name="Use High Precision (float128)", value=False
)
mission_input = pn.widgets.TextInput(
name="Mission (optional)", placeholder="e.g., NICER"
)
instr_input = pn.widgets.TextInput(
name="Instrument (optional)", placeholder="e.g., XTI"
)
detector_id_input = pn.widgets.TextInput(
name="Detector ID (optional)", placeholder="e.g., 1, 2, 3"
)
header_input = pn.widgets.TextAreaInput(
name="Header (optional)", placeholder="Provide FITS header if available"
)
timeref_input = pn.widgets.TextInput(
name="Time Reference (optional)", placeholder="e.g., SOLARSYSTEM"
)
timesys_input = pn.widgets.TextInput(
name="Time System (optional)", placeholder="e.g., TDB"
)
ephem_input = pn.widgets.TextInput(
name="Ephemeris (optional)", placeholder="e.g., DE430"
)
rmf_file_input = pn.widgets.TextInput(
name="RMF File (optional)", placeholder="e.g., test.rmf"
)
skip_checks_checkbox = pn.widgets.Checkbox(name="Skip Validity Checks", value=False)
notes_input = pn.widgets.TextAreaInput(
name="Notes (optional)", placeholder="Any useful annotations"
)
name_input = pn.widgets.TextInput(
name="Event List Name", placeholder="e.g., my_event_list"
)
# Create button
create_button = pn.widgets.Button(name="Create Event List", button_type="primary")
def on_create_button_click(event):
# Clear previous output and warnings
context.clear_container('output_box')
context.clear_container('warning_box')
warning_handler.warnings.clear()
warnings.resetwarnings()
create_event_list(
event,
times_input,
energy_input,
pi_input,
gti_input,
mjdref_input,
dt_input,
high_precision_checkbox,
mission_input,
instr_input,
detector_id_input,
header_input,
timeref_input,
timesys_input,
ephem_input,
rmf_file_input,
skip_checks_checkbox,
notes_input,
name_input,
context,
warning_handler,
)
create_button.on_click(on_create_button_click)
tab_content = pn.Column(
pn.pane.Markdown("# Create Event List"),
pn.Row(
pn.Column(
name_input,
times_input,
mjdref_input,
energy_input,
pi_input,
gti_input,
dt_input,
high_precision_checkbox,
mission_input,
),
pn.Column(
instr_input,
detector_id_input,
header_input,
timeref_input,
timesys_input,
ephem_input,
rmf_file_input,
skip_checks_checkbox,
notes_input,
),
),
create_button,
)
return tab_content
# TODO: ADD better comments, error handlling and docstrings
def create_simulate_event_list_tab(context: AppContext, warning_handler):
"""
Create the tab for simulating event lists.
Args:
context (AppContext): The application context containing all containers and state.
warning_handler (WarningHandler): The handler for warnings.
Returns:
Column: A Panel Column containing the widgets and layout for the event list simulation tab.
Example:
>>> tab = create_simulate_event_list_tab(context, warning_handler)
>>> isinstance(tab, pn.Column)
True
"""
simulation_title = pn.pane.Markdown("# Simulating Random Event Lists")
time_bins_input = pn.widgets.IntInput(
name="Number of Time Bins", value=10, step=1, start=1, end=1000000
)
max_counts_input = pn.widgets.IntInput(
name="Max Possible Counts per Bin", value=5, step=1, start=1, end=100000
)
dt_input = pn.widgets.FloatInput(
name="Delta Time (dt)", value=1.0, step=0.1, start=0.001, end=10000.0
)
sim_name_input = pn.widgets.TextInput(
name="Simulated Event List Name", placeholder="e.g., my_sim_event_list"
)
method_selector = pn.widgets.RadioButtonGroup(
name="Simulation Method",
options=['Probabilistic (Recommended)', 'Deterministic (Legacy)'],
value='Probabilistic (Recommended)',
button_type='default'
)
method_tooltip = pn.widgets.TooltipIcon(
value=Tooltip(
content="""Probabilistic (Recommended): Uses inverse CDF sampling for statistically realistic events. Each run produces different results (use seed for reproducibility).
Deterministic (Legacy): Creates exact count matching. Same results every time. Not suitable for scientific simulations.""",
position="bottom",
)
)
seed_input = pn.widgets.IntInput(
name="Random Seed (optional, for reproducibility)",
value=None,
start=0,
end=2147483647,
placeholder="Leave empty for random"
)
seed_tooltip = pn.widgets.TooltipIcon(
value=Tooltip(
content="""Set a random seed to make probabilistic simulations reproducible. Same seed = same result. Leave empty for truly random simulation.""",
position="bottom",
)
)
simulate_energies_checkbox = pn.widgets.Checkbox(
name="Simulate photon energies (optional)",
value=False
)
simulate_energies_tooltip = pn.widgets.TooltipIcon(
value=Tooltip(
content="""Simulate realistic photon energies based on a spectral distribution. The spectrum defines energy bins (keV) and counts in each bin. Uses inverse CDF sampling.""",
position="bottom",
)
)
energy_bins_input = pn.widgets.TextInput(
name="Energy bins (keV, comma-separated)",
placeholder="e.g., 1, 2, 3, 4, 5, 6",
visible=False
)
energy_counts_input = pn.widgets.TextInput(
name="Counts per bin (comma-separated)",
placeholder="e.g., 1000, 2040, 1000, 3000, 4020, 2070",
visible=False
)
def toggle_energy_inputs(event):
"""Show/hide energy input fields based on checkbox."""
energy_bins_input.visible = simulate_energies_checkbox.value
energy_counts_input.visible = simulate_energies_checkbox.value
simulate_energies_checkbox.param.watch(toggle_energy_inputs, 'value')
simulate_button = pn.widgets.Button(
name="Simulate Event List", button_type="primary"
)
simulate_button_tooltip = pn.widgets.TooltipIcon(
value=Tooltip(
content="""Simulate a random light curve and then use it to get the EventList from the specified parameters.""",
position="bottom",
)
)
def on_simulate_button_click(event):
# Clear previous output and warnings
context.update_container('output_box', create_eventlist_output_box("N.A."))
context.update_container('warning_box', create_eventlist_warning_box("N.A."))
warning_handler.warnings.clear()
warnings.resetwarnings()
# Simulate the event list
simulate_event_list(
event,
time_bins_input,
max_counts_input,
dt_input,
sim_name_input,
method_selector,
seed_input,
simulate_energies_checkbox,
energy_bins_input,
energy_counts_input,
context,
warning_handler,
)
simulate_button.on_click(on_simulate_button_click)
tab_content = pn.Column(
simulation_title,
time_bins_input,
max_counts_input,
dt_input,
sim_name_input,
pn.pane.Markdown("---"),
pn.Row(method_selector, method_tooltip),
pn.Row(seed_input, seed_tooltip),
pn.pane.Markdown("---"),
pn.Row(simulate_energies_checkbox, simulate_energies_tooltip),
energy_bins_input,
energy_counts_input,
pn.pane.Markdown("---"),
simulate_button,
)
return tab_content
# TODO: ADD better comments, error handlling and docstrings
def create_eventlist_operations_tab(context: AppContext, warning_handler):
"""
Create the EventList Operations tab with buttons for operations like applying deadtime,
filtering energy ranges, and converting PI to energy.
Args:
context (AppContext): The application context containing all containers and state.
warning_handler: The custom warning handler.
Returns:
Panel layout for the tab.
"""
# Define widgets for input
multi_event_list_select = pn.widgets.MultiSelect(
name="Select Event List(s)",
options={name: i for i, (name, event) in enumerate(context.state.get_event_data())},
size=8,
)
event_list_properties_box = pn.pane.Markdown(
"**Select an EventList to view its properties.**"
)
multi_light_curve_select = pn.widgets.MultiSelect(
name="Select Light Curve(s)",
options={name: i for i, (name, lc) in enumerate(context.state.get_light_curve())},
size=8,
)
light_curve_properties_box = pn.pane.Markdown(
"**Select a LightCurve to view its properties.**"
)
deadtime_input = pn.widgets.FloatInput(
name="Deadtime", value=0.01, step=0.001, start=0.001, end=10000.0
)
deadtime_inplace_checkbox = pn.widgets.Checkbox(
name="If True, apply the deadtime to the current event list. Otherwise, return a new event list.",
value=False,
)
apply_deadtime_button = pn.widgets.Button(
name="Apply Deadtime", button_type="primary"
)
## TODO: additional_output: Only returned if return_all checbox is True. See get_deadtime_mask for more details.
rmf_file_input = pn.widgets.TextInput(
name="RMF File Path", placeholder="Path to RMF file for PI to Energy conversion"
)
rmf_newEventList_checkbox = pn.widgets.Checkbox(
name="If True, create a new event list with the converted energy values. Otherwise, modify the existing event list in place.",
value=True,
)
convert_pi_button = pn.widgets.Button(
name="Convert PI to Energy", button_type="primary"
)
energy_range_input = pn.widgets.TextInput(
name="Energy Range in (keV) or PI channel if use_pi is True",
placeholder="e.g., 0.3, 10",
)
filterEnergy_use_pi_checkbox = pn.widgets.Checkbox(
name="Use PI channel instead of energy for filtering", value=False
)
filterEnergy_inplace_checkbox = pn.widgets.Checkbox(
name="If True, filter the current event list in place. Otherwise, return a new event list.",
value=False,
)
filter_energy_button = pn.widgets.Button(
name="Filter by Energy Range", button_type="primary"
)
energy_ranges_input = pn.widgets.TextInput(
name="Energy Ranges",
placeholder="e.g., [[0.3, 2], [2, 10]]",
)
segment_size_input = pn.widgets.FloatInput(
name="Segment Size", value=0.5, step=0.1, start=0.0, end=1e6
)
color_use_pi_checkbox = pn.widgets.Checkbox(
name="Use PI channel instead of energy", value=False
)
compute_color_button = pn.widgets.Button(
name="Compute Color Evolution", button_type="primary"
)
energy_mask_input = pn.widgets.TextInput(
name="Energy Range (keV or PI if use_pi=True)", placeholder="e.g., 0.3, 10"
)
energy_mask_use_pi_checkbox = pn.widgets.Checkbox(
name="Use PI channel instead of energy", value=False
)
get_energy_mask_button = pn.widgets.Button(
name="Get Energy Mask", button_type="primary"
)
# Widgets for Intensity Evolution
intensity_energy_range_input = pn.widgets.TextInput(
name="Energy Range (keV or PI if use_pi=True)", placeholder="e.g., 0.3, 10"
)
intensity_segment_size_input = pn.widgets.FloatInput(
name="Segment Size", value=0.5, step=0.1, start=0.0, end=1e6
)
intensity_use_pi_checkbox = pn.widgets.Checkbox(
name="Use PI channel instead of energy", value=False
)
compute_intensity_button = pn.widgets.Button(
name="Compute Intensity Evolution", button_type="primary"
)
# Widgets for Joining EventLists
join_strategy_select = pn.widgets.Select(
name="Join Strategy",
options=["infer", "intersection", "union", "append", "none"],
value="infer",
)
join_button = pn.widgets.Button(name="Join EventLists", button_type="primary")
# Widgets for Sorting EventLists
sort_inplace_checkbox = pn.widgets.Checkbox(name="Sort in place", value=False)
sort_button = pn.widgets.Button(name="Sort EventLists", button_type="primary")
# Widgets for Astropy Export
astropy_export_path_input = pn.widgets.TextInput(
name="Output file path",
placeholder="/path/to/output.ecsv"
)
astropy_export_format_select = pn.widgets.Select(
name="Export format",
options=["ascii.ecsv", "fits", "votable", "hdf5"],
value="ascii.ecsv"
)
export_astropy_button = pn.widgets.Button(
name="Export to Astropy Table",
button_type="primary"
)
# Widgets for Astropy Import
astropy_import_path_input = pn.widgets.TextInput(
name="Input file path",
placeholder="/path/to/input.ecsv"
)
astropy_import_format_select = pn.widgets.Select(
name="Import format",
options=["ascii.ecsv", "fits", "votable", "hdf5"],
value="ascii.ecsv"
)
astropy_import_name_input = pn.widgets.TextInput(
name="EventList name",
placeholder="imported_eventlist"
)
import_astropy_button = pn.widgets.Button(
name="Import from Astropy Table",
button_type="primary"
)
# Callback to update the properties box
def update_event_list_properties(event):
selected_indices = multi_event_list_select.value
if not selected_indices:
event_list_properties_box.object = "**No EventList selected.**"
return
properties = []
for selected_index in selected_indices:
event_list_name, event_list = context.state.get_event_data()[selected_index]
gti_count = len(event_list.gti) if hasattr(event_list, "gti") else "N/A"
time_span = (
f"{event_list.time[0]:.2f} - {event_list.time[-1]:.2f}"
if hasattr(event_list, "time") and len(event_list.time) > 0
else "N/A"
)
energy_info = (
"Available"
if hasattr(event_list, "energy") and event_list.energy is not None
else "Not available"
)
pi_info = (
"Available"
if hasattr(event_list, "pi") and event_list.pi is not None
else "Not available"
)
properties.append(
f"### EventList: {event_list_name}\n"
f"- **GTI Count**: {gti_count}\n"
f"- **Time Span**: {time_span}\n"
f"- **Energy Data**: {energy_info}\n"
f"- **PI Data**: {pi_info}\n"
)
event_list_properties_box.object = "\n".join(properties)
# Callback to update the lightcurve properties box
def update_light_curve_properties(event):
selected_indices = multi_light_curve_select.value
if not selected_indices:
light_curve_properties_box.object = "**No LightCurve selected.**"
return
properties = []
for selected_index in selected_indices:
light_curve_name, light_curve = context.state.get_light_curve()[selected_index]
properties.append(
f"### LightCurve: {light_curve_name}\n"
f"- **Counts**: {light_curve.counts}\n"
f"- **Time Span**: {light_curve.time[0]:.2f} - {light_curve.time[-1]:.2f}\n"
f"- **Time Resolution**: {light_curve.dt:.2f}\n"
)
light_curve_properties_box.object = "\n".join(properties)
# Callback: Apply Deadtime
def apply_deadtime_callback(event):
selected_indices = multi_event_list_select.value
if selected_indices is None:
output_box_container[:] = [
create_eventlist_output_box("No event list selected.")
]
return
deadtime = deadtime_input.value
inplace = deadtime_inplace_checkbox.value
results = []
for index in selected_indices:
try:
event_list_name, event_list = state_manager.get_event_data()[index]
if inplace:
event_list.apply_deadtime(deadtime, inplace=True)
results.append(
f"Modified EventList '{event_list_name}' in place with deadtime={deadtime}s."
)
else:
new_event_list = event_list.apply_deadtime(deadtime, inplace=False)
new_name = f"{event_list_name}_{deadtime}"
context.state.add_event_data(new_name, new_event_list)
results.append(
f"Created new EventList '{new_name}' with deadtime={deadtime}s."
)
except Exception as e:
warning_handler.warn(str(e), category=RuntimeWarning)
# Update the output box with results
if results:
output_box_container[:] = [create_eventlist_output_box("\n".join(results))]
else:
output_box_container[:] = [
create_eventlist_output_box("No event lists processed.")
]
# Callback: Convert PI to Energy
def convert_pi_callback(event):
selected_indices = multi_event_list_select.value
if selected_indices is None:
output_box_container[:] = [
create_eventlist_output_box("No event list selected.")
]
return
if len(selected_indices) > 1:
output_box_container[:] = [
create_eventlist_output_box(
"Please select only one event list for PI to Energy conversion."
)
]
return
try:
rmf_file = rmf_file_input.value
if not rmf_file:
warning_box_container[:] = [
create_eventlist_warning_box(
"Warning: No RMF file provided. Conversion cannot proceed."
)
]
return
if not os.path.isfile(rmf_file):
warning_box_container[:] = [
create_eventlist_warning_box(
f"Warning: RMF file '{rmf_file}' does not exist. Please provide a valid file path."
)
]
return
# Perform PI to Energy conversion
selected_index = selected_indices[0]
event_list_name, event_list = context.state.get_event_data()[selected_index]
# Check if PI data is available
if not hasattr(event_list, "pi") or event_list.pi is None:
warning_box_container[:] = [
create_eventlist_warning_box(
f"Warning: EventList '{event_list_name}' has no valid PI data. Cannot convert to Energy."
)
]
return
if rmf_newEventList_checkbox.value:
new_event_list = copy.deepcopy(
event_list
) # Deepcopy to ensure independence
new_event_list.convert_pi_to_energy(rmf_file)
new_event_list_name = f"{event_list_name}_converted_energy"
context.state.add_event_data(
(new_event_list_name, new_event_list)
) # Add new event list
output_box_container[:] = [
create_eventlist_output_box(
f"New EventList '{new_event_list_name}' created with converted energy values."
)
]
else: # Modify the existing event list in place
event_list.convert_pi_to_energy(rmf_file)
output_box_container[:] = [
create_eventlist_output_box(
f"Energy values converted in place for EventList '{event_list_name}'."
)
]
except Exception as e:
warning_handler.warn(str(e), category=RuntimeWarning)
# Callback: Filter by Energy Range
def filter_energy_callback(event):
selected_indices = multi_event_list_select.value
if selected_indices is None:
output_box_container[:] = [
create_eventlist_output_box("No event list selected.")
]
return
try:
energy_range_input_value = energy_range_input.value
if not energy_range_input_value:
raise ValueError(
"Energy range input cannot be empty. Please provide two comma-separated values."
)
try:
energy_range = [
float(val.strip()) for val in energy_range_input_value.split(",")
]
except ValueError:
raise ValueError(
"Invalid energy range input. Please provide two valid numbers separated by a comma."
)
if len(energy_range) != 2:
raise ValueError(
"Energy range must contain exactly two values (min, max)."
)
if energy_range[0] is None or energy_range[1] is None:
raise ValueError("Energy range values cannot be None.")
if energy_range[0] >= energy_range[1]:
raise ValueError(
"Invalid energy range: Minimum value must be less than maximum value."
)
# Get the options for inplace and use_pi
inplace = filterEnergy_inplace_checkbox.value
use_pi = filterEnergy_use_pi_checkbox.value
results = []
for selected_index in selected_indices:
event_list_name, event_list = context.state.get_event_data()[selected_index]
# Validate energy or PI data
if use_pi:
if not hasattr(event_list, "pi") or event_list.pi is None:
message = f"EventList '{event_list_name}' has no valid PI data."
warning_box_container[:] = [
create_eventlist_warning_box(message)
]
return
else:
if not hasattr(event_list, "energy") or event_list.energy is None:
message = (
f"EventList '{event_list_name}' has no valid energy data. "
f"Please ensure the energy data is initialized (e.g., by converting PI to energy)."
)
warning_box_container[:] = [
create_eventlist_warning_box(message)
]
return
if inplace:
# Modify the event list in place
event_list.filter_energy_range(
energy_range, inplace=True, use_pi=use_pi
)
results.append(
f"Filtered EventList '{event_list_name}' in place using energy range {energy_range} (use_pi={use_pi})."
)
else:
# Create a new event list
filtered_event_list = event_list.filter_energy_range(
energy_range, inplace=False, use_pi=use_pi
)
if use_pi:
new_event_list_name = f"{event_list_name}_filtered_pi_{energy_range[0]}_{energy_range[1]}"
else:
new_event_list_name = f"{event_list_name}_filtered_energy_{energy_range[0]}_{energy_range[1]}"
context.state.add_event_data((new_event_list_name, filtered_event_list))
results.append(
f"Created new EventList '{new_event_list_name}' filtered using energy range {energy_range} (use_pi={use_pi})."
)
# Update the output with the results
if results:
output_box_container[:] = [
create_eventlist_output_box("\n".join(results))
]
else:
output_box_container[:] = [
create_eventlist_output_box("No event lists were processed.")
]
except Exception as e:
warning_handler.warn(str(e), category=RuntimeWarning)
# Callback: Compute Color Evolution
def compute_color_callback(event):
selected_indices = multi_event_list_select.value
if not selected_indices:
output_box_container[:] = [
create_eventlist_output_box("No event list selected.")
]
return
try:
energy_ranges_input_value = energy_ranges_input.value
if not energy_ranges_input_value:
warning_box_container[:] = [
create_eventlist_warning_box(
"Warning: Energy ranges input cannot be empty. Provide two energy ranges as [[min1, max1], [min2, max2]]."
)
]
return
try:
energy_ranges = eval(energy_ranges_input_value)
if (
not isinstance(energy_ranges, list)
or len(energy_ranges) != 2
or not all(len(er) == 2 for er in energy_ranges)
):
warning_box_container[:] = [
create_eventlist_warning_box(
"Warning: Invalid energy ranges format. Provide two energy ranges as [[min1, max1], [min2, max2]]."
)
]
return
energy_ranges = [[float(x) for x in er] for er in energy_ranges]
except Exception:
warning_box_container[:] = [
create_eventlist_warning_box(
"Warning: Invalid energy ranges format. Provide two energy ranges as [[min1, max1], [min2, max2]]."
)
]
return
segment_size = segment_size_input.value
use_pi = color_use_pi_checkbox.value
results = []
for selected_index in selected_indices:
event_list_name, event_list = context.state.get_event_data()[selected_index]
# Validate energy or PI data
if use_pi:
if not hasattr(event_list, "pi") or event_list.pi is None:
message = f"EventList '{event_list_name}' has no valid PI data."
warning_box_container[:] = [
create_eventlist_warning_box(message)
]
return
else:
if not hasattr(event_list, "energy") or event_list.energy is None:
message = (
f"EventList '{event_list_name}' has no valid energy data. "
f"Please ensure the energy data is initialized (e.g., by converting PI to energy)."
)
warning_box_container[:] = [
create_eventlist_warning_box(message)
]
return
# Compute color evolution
color_evolution = event_list.get_color_evolution(
energy_ranges, segment_size=segment_size, use_pi=use_pi
)
results.append(
f"Computed color evolution for EventList '{event_list_name}' with energy ranges {energy_ranges} and segment size {segment_size}."
)
results.append(f"Color Evolution: {color_evolution}")
# Update the output with the results
if results:
output_box_container[:] = [
create_eventlist_output_box("\n".join(results))
]
else:
output_box_container[:] = [
create_eventlist_output_box("No event lists processed.")
]
except Exception as e:
error_message = (
f"An error occurred:\n{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
)
print(error_message)
warning_handler.warn(str(e), category=RuntimeWarning)
# Callback for Get Energy Mask
def get_energy_mask_callback(event):
selected_indices = multi_event_list_select.value
if not selected_indices:
output_box_container[:] = [
create_eventlist_output_box("No event list selected.")
]
return
try:
# Parse and validate energy range
energy_range_input_value = energy_mask_input.value
if not energy_range_input_value:
raise ValueError(
"Energy range input cannot be empty. Please provide two comma-separated values."
)
try:
energy_range = [
float(val.strip()) for val in energy_range_input_value.split(",")
]
except ValueError:
raise ValueError(
"Invalid energy range input. Please provide two valid numbers separated by a comma."
)
if len(energy_range) != 2:
raise ValueError(
"Energy range must contain exactly two values (min, max)."
)
if energy_range[0] is None or energy_range[1] is None:
raise ValueError("Energy range values cannot be None.")
if energy_range[0] >= energy_range[1]:
raise ValueError(
"Invalid energy range: Minimum value must be less than maximum value."
)
use_pi = energy_mask_use_pi_checkbox.value
results = []
for selected_index in selected_indices:
event_list_name, event_list = context.state.get_event_data()[selected_index]
# Validate energy or PI data
if use_pi:
if not hasattr(event_list, "pi") or event_list.pi is None:
message = f"EventList '{event_list_name}' has no valid PI data."
warning_box_container[:] = [
create_eventlist_warning_box(message)
]
return
else:
if not hasattr(event_list, "energy") or event_list.energy is None:
message = (
f"EventList '{event_list_name}' has no valid energy data. "
f"Please ensure the energy data is initialized (e.g., by converting PI to energy)."
)
warning_box_container[:] = [
create_eventlist_warning_box(message)
]
return
# Get energy mask
energy_mask = event_list.get_energy_mask(energy_range, use_pi=use_pi)
results.append(
f"Computed energy mask for EventList '{event_list_name}' with energy range {energy_range} (use_pi={use_pi})."
)
results.append(f"Energy Mask: {energy_mask}")
# Update the output with results
if results:
output_box_container[:] = [
create_eventlist_output_box("\n".join(results))
]
else:
output_box_container[:] = [
create_eventlist_output_box("No event lists processed.")
]
except Exception as e:
error_message = (
f"An error occurred:\n{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
)
print(error_message)
warning_handler.warn(error_message, category=RuntimeWarning)
# Callback for Intensity Evolution
def compute_intensity_callback(event):
selected_indices = multi_event_list_select.value
if not selected_indices:
output_box_container[:] = [
create_eventlist_output_box("No event list selected.")
]
return
try:
# Parse and validate energy range
energy_range_input_value = intensity_energy_range_input.value
if not energy_range_input_value:
raise ValueError(
"Energy range input cannot be empty. Please provide two comma-separated values."
)
try:
energy_range = [
float(val.strip()) for val in energy_range_input_value.split(",")
]
except ValueError:
raise ValueError(
"Invalid energy range input. Please provide two valid numbers separated by a comma."
)
if len(energy_range) != 2:
raise ValueError(
"Energy range must contain exactly two values (min, max)."
)
if energy_range[0] is None or energy_range[1] is None:
raise ValueError("Energy range values cannot be None.")
if energy_range[0] >= energy_range[1]:
raise ValueError(
"Invalid energy range: Minimum value must be less than maximum value."
)
segment_size = intensity_segment_size_input.value
use_pi = intensity_use_pi_checkbox.value
results = []
for selected_index in selected_indices:
event_list_name, event_list = context.state.get_event_data()[selected_index]
# Validate energy or PI data
if use_pi:
if not hasattr(event_list, "pi") or event_list.pi is None:
message = f"EventList '{event_list_name}' has no valid PI data."
warning_box_container[:] = [
create_eventlist_warning_box(message)
]
return
else:
if not hasattr(event_list, "energy") or event_list.energy is None:
message = (
f"EventList '{event_list_name}' has no valid energy data. "
f"Please ensure the energy data is initialized (e.g., by converting PI to energy)."
)
warning_box_container[:] = [
create_eventlist_warning_box(message)
]
return
# Compute intensity evolution
intensity_evolution = event_list.get_intensity_evolution(
energy_range, segment_size=segment_size, use_pi=use_pi
)
results.append(
f"Computed intensity evolution for EventList '{event_list_name}' with energy range {energy_range} and segment size {segment_size}."
)
results.append(f"Intensity Evolution: {intensity_evolution}")
# Update the output with results
if results:
output_box_container[:] = [
create_eventlist_output_box("\n".join(results))
]
else:
output_box_container[:] = [
create_eventlist_output_box("No event lists processed.")
]
except Exception as e:
error_message = (
f"An error occurred:\n{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
)
print(error_message)
warning_handler.warn(error_message, category=RuntimeWarning)
# Callback for Joining EventLists
def join_eventlists_callback(event):
selected_indices = multi_event_list_select.value
if len(selected_indices) < 2:
warning_box_container[:] = [
create_eventlist_warning_box(
"Please select at least two EventLists to join."
)
]
return
try:
strategy = join_strategy_select.value
# Retrieve the selected event lists
all_event_data = context.state.get_event_data()
selected_event_lists = [all_event_data[i][1] for i in selected_indices]
selected_names = [all_event_data[i][0] for i in selected_indices]
# Perform the join operation
result_event_list = selected_event_lists[0]
for other_event_list in selected_event_lists[1:]:
result_event_list = result_event_list.join(
other_event_list, strategy=strategy
)
# Generate a new name for the joined EventList
new_event_list_name = f"joined_{'_'.join(selected_names)}_{strategy}"
context.state.add_event_data(new_event_list_name, result_event_list)
# Update the output container with success message
output_box_container[:] = [
create_eventlist_output_box(
f"Joined EventLists: {', '.join(selected_names)} using strategy '{strategy}'.\n"
f"New EventList saved as '{new_event_list_name}'."
)
]
except Exception as e:
error_message = (
f"An error occurred:\n{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
)
print(error_message)
warning_handler.warn(error_message, category=RuntimeWarning)
# Callback for Sorting EventLists
def sort_eventlists_callback(event):
selected_indices = multi_event_list_select.value
if not selected_indices:
warning_box_container[:] = [
create_eventlist_warning_box(
"Please select at least one EventList to sort."
)
]
return
inplace = sort_inplace_checkbox.value
results = []
try:
for selected_index in selected_indices:
event_list_name, event_list = context.state.get_event_data()[selected_index]
if inplace:
# Sort in place
event_list.sort(inplace=True)
results.append(f"Sorted EventList '{event_list_name}' in place.")
else:
# Sort and create a new EventList
sorted_event_list = event_list.sort(inplace=False)
new_event_list_name = f"{event_list_name}_sorted"
context.state.add_event_data((new_event_list_name, sorted_event_list))
results.append(
f"Created a new sorted EventList '{new_event_list_name}' from '{event_list_name}'."
)
# Update output container with results
output_box_container[:] = [create_eventlist_output_box("\n".join(results))]
except Exception as e:
error_message = (
f"An error occurred:\n{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
)
print(error_message)
warning_handler.warn(error_message, category=RuntimeWarning)
# Callback for Exporting to Astropy Table
def export_astropy_callback(event):
selected_indices = multi_event_list_select.value
if not selected_indices:
warning_box_container[:] = [
create_eventlist_warning_box(
"Please select at least one EventList to export."
)
]
return
if len(selected_indices) > 1:
warning_box_container[:] = [
create_eventlist_warning_box(
"Please select only one EventList for export."
)
]
return
output_path = astropy_export_path_input.value.strip()
if not output_path:
warning_box_container[:] = [
create_eventlist_warning_box(
"Please provide an output file path."
)
]
return
try:
selected_index = selected_indices[0]
event_list_name, event_list = context.state.get_event_data()[selected_index]
export_format = astropy_export_format_select.value
# Call the service method
result = context.services.data.export_event_list_to_astropy_table(
event_list_name=event_list_name,
output_path=output_path,
fmt=export_format
)
if result["success"]:
output_box_container[:] = [
create_eventlist_output_box(
f"Successfully exported EventList '{event_list_name}' to:\n"
f"{output_path}\n"
f"Format: {export_format}\n"
f"Rows: {result['metadata']['n_rows']}"
)
]
else:
warning_box_container[:] = [
create_eventlist_warning_box(
f"Export failed: {result['message']}"
)
]
except Exception as e:
error_message = (
f"An error occurred during export:\n{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
)
print(error_message)
warning_handler.warn(error_message, category=RuntimeWarning)
# Callback for Importing from Astropy Table
def import_astropy_callback(event):
input_path = astropy_import_path_input.value.strip()
if not input_path:
warning_box_container[:] = [
create_eventlist_warning_box(
"Please provide an input file path."
)
]
return
import_name = astropy_import_name_input.value.strip()
if not import_name:
warning_box_container[:] = [
create_eventlist_warning_box(
"Please provide a name for the imported EventList."
)
]
return
if not os.path.isfile(input_path):
warning_box_container[:] = [
create_eventlist_warning_box(
f"File not found: {input_path}"
)
]
return
try:
import_format = astropy_import_format_select.value
# Call the service method
result = context.services.data.import_event_list_from_astropy_table(
file_path=input_path,
name=import_name,
fmt=import_format
)
if result["success"]:
output_box_container[:] = [
create_eventlist_output_box(
f"Successfully imported EventList '{import_name}' from:\n"
f"{input_path}\n"
f"Format: {import_format}\n"
f"Events: {result['metadata']['n_events']}"
)
]
else:
warning_box_container[:] = [
create_eventlist_warning_box(
f"Import failed: {result['message']}"
)
]
except Exception as e:
error_message = (
f"An error occurred during import:\n{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
)
print(error_message)
warning_handler.warn(error_message, category=RuntimeWarning)
# Assign callbacks to buttons
multi_event_list_select.param.watch(update_event_list_properties, "value")
multi_light_curve_select.param.watch(update_light_curve_properties, "value")
apply_deadtime_button.on_click(apply_deadtime_callback)
convert_pi_button.on_click(convert_pi_callback)
filter_energy_button.on_click(filter_energy_callback)
compute_color_button.on_click(compute_color_callback)
get_energy_mask_button.on_click(get_energy_mask_callback)
compute_intensity_button.on_click(compute_intensity_callback)
join_button.on_click(join_eventlists_callback)
sort_button.on_click(sort_eventlists_callback)
export_astropy_button.on_click(export_astropy_callback)
import_astropy_button.on_click(import_astropy_callback)
# Layout for the tab
tab_content = pn.Column(
pn.pane.Markdown("# EventList Operations"),
pn.Row(
pn.Column(
multi_event_list_select,
event_list_properties_box,
),
pn.Column(
multi_light_curve_select,
light_curve_properties_box,
),
),
pn.Column(
pn.FlexBox(
pn.Column(
pn.pane.Markdown("## Apply Deadtime"),
deadtime_input,
deadtime_inplace_checkbox,
apply_deadtime_button,
width=400,
height=300,
),
pn.Column(
pn.pane.Markdown("## Convert PI to Energy"),
rmf_file_input,
rmf_newEventList_checkbox,
convert_pi_button,
width=400,
height=300,
),
pn.Column(
pn.pane.Markdown("## Filter by Energy Range"),
energy_range_input,
filterEnergy_inplace_checkbox,
filterEnergy_use_pi_checkbox,
filter_energy_button,
width=400,
height=300,
),
pn.Column(
pn.pane.Markdown("## Compute Color Evolution"),
energy_ranges_input,
segment_size_input,
color_use_pi_checkbox,
compute_color_button,
width=400,
height=300,
),
pn.Column(
pn.pane.Markdown("## Get Energy Mask"),
energy_mask_input,
energy_mask_use_pi_checkbox,
get_energy_mask_button,
width=400,
height=300,
),
pn.Column(
pn.pane.Markdown("## Compute Intensity Evolution"),
intensity_energy_range_input,
intensity_segment_size_input,
intensity_use_pi_checkbox,
compute_intensity_button,
width=400,
height=300,
),
pn.Column(
pn.pane.Markdown("## Join EventLists"),
join_strategy_select,
join_button,
width=400,
height=300,
),
pn.Column(
pn.pane.Markdown("## Sort EventLists"),
sort_inplace_checkbox,
sort_button,
width=400,
height=300,
),
pn.Column(
pn.pane.Markdown("## Export to Astropy Table"),
astropy_export_path_input,
astropy_export_format_select,
export_astropy_button,
width=400,
height=300,
),
pn.Column(
pn.pane.Markdown("## Import from Astropy Table"),
astropy_import_path_input,
astropy_import_format_select,
astropy_import_name_input,
import_astropy_button,
width=400,
height=300,
),
flex_direction="row",
flex_wrap="wrap",
align_items="center",
justify_content="center",
)
),
pn.pane.Markdown("<br/>"),
)
return tab_content
def create_eventlist_main_area(context: AppContext):
"""
Create the main area for the EventList tab, including all sub-tabs.
Args:
context (AppContext): The application context containing containers and state.
Returns:
MainArea: An instance of MainArea with all the necessary tabs.
Example:
>>> main_area = create_eventlist_main_area(context)
>>> isinstance(main_area, MainArea)
True
"""
warning_handler = create_warning_handler()
tabs_content = {
"Create Event List": create_event_list_tab(
context=context,
warning_handler=warning_handler,
),
"Simulate Event List": create_simulate_event_list_tab(
context=context,
warning_handler=warning_handler,
),
"EventList Operations": create_eventlist_operations_tab(
context=context,
warning_handler=warning_handler,
),
}
return MainArea(tabs_content=tabs_content)
def create_eventlist_help_area():
"""
Create the help area for the data loading tab.
Returns:
HelpBox: An instance of HelpBox with the help content.
"""
# Content for "Introduction to Event Lists"
intro_content = """
## Introduction to Event Lists
### What are Event Lists?
In X-ray astronomy, an **Event List** represents a record of individual photon detection events as observed by a telescope. Each event corresponds to the detection of a photon and includes attributes like:
- **Time of Arrival (TOA)**: The exact time when the photon was detected.
- **Photon Energy**: Derived from the pulse height or energy channel recorded.
- **Good Time Intervals (GTIs)**: Periods during which the instrument was actively recording valid data.
- **Pulse Invariant (PI) Channel**: A standardized representation of photon energy.
Event Lists are typically the starting point for data analysis in high-energy astrophysics. They provide unbinned, high-precision information about individual photon arrivals, enabling various scientific analyses such as timing, spectral, and correlation studies.
### Scientific Significance of Event Lists
Event Lists allow astronomers to study the variability of astrophysical sources across a wide range of timescales:
- **Fast Transients**: Sources like X-ray bursts, magnetar flares, or fast radio bursts, which brighten and dim on millisecond-to-minute scales.
- **Quasi-Periodic Oscillations (QPOs)**: Oscillations in black hole and neutron star systems that vary unpredictably around a central frequency.
- **Stochastic Variability**: Random fluctuations in brightness, often associated with accretion processes.
Additionally, Event Lists are fundamental for studying:
- **Time Lags**: Delays between high- and low-energy photon emissions due to processes like reflection or turbulent flows in accretion disks.
- **Spectral Timing**: Techniques that combine time and energy data to probe the physical processes near compact objects.
### Anatomy of an Event List
An Event List is often stored as a FITS (Flexible Image Transport System) file, with each row in the table corresponding to a single detected photon. The table contains columns for various attributes:
- **Time**: Precise timestamp of the event (e.g., in seconds or Modified Julian Date).
- **Energy or PI Channel**: Photon energy or pulse invariant channel.
- **GTIs**: Intervals of valid observation time.
- **Spatial Information** (optional): Detector coordinates or celestial coordinates.
### How Event Lists are Used
Event Lists are typically processed and filtered to remove invalid events or background noise. They can then be converted into:
- **Light Curves**: Binned time series of photon counts.
- **Spectra**: Energy distributions of detected photons.
- **Power Spectra**: Frequency-domain representations of variability.
### Key Terms in Event Lists
- **Photon Time of Arrival (TOA)**: The recorded time when a photon hits the detector.
- **Good Time Intervals (GTIs)**: Periods when the instrument was actively recording valid data.
- **Pulse Invariant (PI) Channel**: A detector-specific channel number that maps to the photon’s energy.
- **RMF File**: Response Matrix File, used to calibrate PI channels into physical energy values (e.g., keV).
- **FITS Format**: The standard file format for Event Lists in high-energy astrophysics.
### Example: Event List Data Structure
A typical Event List in FITS format contains columns like:
```
TIME PI ENERGY GTI
---------------------------------
0.0012 12 2.3 keV [0, 100]
0.0034 15 3.1 keV [0, 100]
0.0048 10 1.8 keV [0, 100]
```
### Advantages of Event Lists
- **High Precision**: Tracks individual photon events without binning, preserving maximum information.
- **Flexibility**: Can be transformed into various forms (e.g., light curves, spectra) for different analyses.
- **Time-Energy Data**: Enables advanced spectral-timing techniques.
### Challenges and Considerations
- **Dead Time**: Time intervals when the detector cannot record new events, affecting variability measurements.
- **Instrumental Noise**: False events caused by electronics or background radiation.
- **Time Resolution**: Limited by the instrument's precision in recording photon arrival times.
By understanding Event Lists, astronomers gain insight into the underlying physical processes driving variability in high-energy astrophysical sources.
### References
- van der Klis, M. (2006). "Rapid X-ray Variability."
- Miniutti, G., et al. (2019). "Quasi-Periodic Eruptions in AGN."
- Galloway, D., & Keek, L. (2021). "X-ray Bursts: Physics and Observations."
- HEASARC Guidelines for FITS Event List Formats.
<br><br>
"""
eventlist_read_content = """
## Reading EventList
The `EventList.read` method is used to read event data files and load them as `EventList` objects in Stingray.
This process involves parsing photon event data, such as arrival times, PI (Pulse Invariant) channels, and energy values.
### Supported File Formats
- **`pickle`**: Serialized Python objects (not recommended for long-term storage).
- **`hea`** / **`ogip`**: FITS event files (commonly used in X-ray astronomy).
- **Other Table-supported formats**: e.g., `hdf5`, `ascii.ecsv`, etc.
### Parameters
- **`filename` (str)**: Path to the file containing the event data.
- **`fmt` (str)**: File format. Supported formats include:
- `'pickle'`
- `'hea'` or `'ogip'`
- Table-compatible formats like `'hdf5'`, `'ascii.ecsv'`.
- If `fmt` is not specified, the method attempts to infer the format based on the file extension.
- **`rmf_file` (str, default=None)**:
- Path to the RMF (Response Matrix File) for energy calibration.
- Behavior:
1. **If `fmt="hea"` or `fmt="ogip"`**:
- `rmf_file` is ignored during the `read` process.
- You must apply it manually after loading using `convert_pi_to_energy`.
2. **If `fmt` is not `hea` or `ogip`**:
- `rmf_file` can be directly specified in the `read` method for automatic energy calibration.
- **`kwargs` (dict)**:
- Additional parameters passed to the FITS reader (`load_events_and_gtis`) for reading OGIP/HEASOFT-compatible event lists.
- Example: `additional_columns` for specifying extra data columns to read.
### Attributes in the Loaded EventList
- **`time`**: Array of photon arrival times in seconds relative to `mjdref`.
- **`energy`**: Array of photon energy values (if calibrated using `rmf_file`).
- **`pi`**: Array of Pulse Invariant (PI) channels.
- **`mjdref`**: Reference time (Modified Julian Date).
- **`gtis`**: Good Time Intervals, defining valid observation periods.
### Stingray Classes and Functions in Use
Below are the key classes and methods from Stingray that are used during this process:
#### Class: `EventList`
```python
from stingray.events import EventList
class EventList:
def __init__(self, time=None, energy=None, pi=None, gti=None, mjdref=0, rmf_file=None):
# Initializes the event list with time, energy, PI channels, and other parameters
```
#### Method: `EventList.read`
```python
@classmethod
def read(cls, filename, fmt=None, rmf_file=None, **kwargs):
if fmt in ("hea", "ogip"):
evt = FITSTimeseriesReader(filename, output_class=EventList, **kwargs)[:]
if rmf_file:
evt.convert_pi_to_energy(rmf_file) # Must be applied manually for hea/ogip
return evt
return super().read(filename, fmt=fmt)
```
#### Function: `convert_pi_to_energy`
```python
def convert_pi_to_energy(self, rmf_file):
self.energy = pi_to_energy(self.pi, rmf_file)
```
### Example Usage
```python
from stingray.events import EventList
# Reading an OGIP-compatible FITS file
event_list = EventList.read("example.evt", fmt="ogip")
# Applying RMF manually after reading
event_list.convert_pi_to_energy("example.rmf")
# Reading an HDF5 file with direct RMF calibration
event_list = EventList.read("example.hdf5", fmt="hdf5", rmf_file="example.rmf")
# Accessing attributes
print(event_list.time) # Photon arrival times
print(event_list.energy) # Calibrated energy values (if rmf_file used)
print(event_list.pi) # PI channels
print(event_list.gtis) # Good Time Intervals
```
### Important Notes
1. **FITS Event Files (`hea` or `ogip`)**:
- `rmf_file` must be applied manually after loading:
```python
event_list.convert_pi_to_energy("example.rmf")
```
2. **Energy Calibration**:
- Ensure the file contains PI channel data for energy calibration.
- Without PI channels, RMF calibration will not work, and energy values will remain `None`.
3. **Good Time Intervals (GTIs)**:
- GTIs define valid observation periods and are automatically extracted from compatible files.
### Common Issues
- **Unsupported File Format**:
Ensure the file extension and format (`fmt`) match.
- **Energy Not Calibrated**:
Check for PI channels and provide an RMF file if needed.
- **Missing Columns**:
For OGIP/HEASOFT-compatible files, ensure required columns (e.g., `time`, `PI`) are available.
### Additional Parameters for Advanced Use
- **`additional_columns`**:
Specify extra columns to read from the file.
Example:
```python
event_list = EventList.read("example.fits", fmt="hea", additional_columns=["detector_id"])
```
<br><br>
"""
# Create the help box
return HelpBox(
title="Help Section",
tabs_content={
"Event Lists": pn.pane.Markdown(intro_content),
"Reading EventList": pn.pane.Markdown(eventlist_read_content),
},
)
def create_eventlist_plots_area():
"""
Create the plots area for the data loading tab.
Returns:
PlotsContainer: An instance of PlotsContainer with the plots for the data loading tab.
Example:
>>> plots_area = create_loadingdata_plots_area()
>>> isinstance(plots_area, PlotsContainer)
True
"""
return PlotsContainer()