diff --git a/Project.toml b/Project.toml index 92e0228..efc9cf0 100644 --- a/Project.toml +++ b/Project.toml @@ -16,8 +16,10 @@ JuliaFormatter = "98e50ef6-434e-11e9-1051-2b60c6c9e899" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" NaNMath = "77ba4419-2d1f-58cd-9bb1-8ffee604a2e3" +Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" ProgressBars = "49802e3a-d2f1-5c88-81d8-b72133a6f568" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" ResumableFunctions = "c5292f4c-5179-55e1-98c5-05642aab7184" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" @@ -35,8 +37,10 @@ JuliaFormatter = "1.0.62" LinearAlgebra = "1.11.0" Logging = "1.11.0" NaNMath = "0.3, 1" +Plots = "1.40.17" ProgressBars = "1.4" Random = "1.11.0" +RecipesBase = "1.3.4" ResumableFunctions = "0.6" StatsBase = "0.33" julia = "1.11" diff --git a/src/Stingray.jl b/src/Stingray.jl index 8f42a49..6b6ecca 100644 --- a/src/Stingray.jl +++ b/src/Stingray.jl @@ -6,6 +6,7 @@ using ProgressBars: tqdm as show_progress using DocStringExtensions using LinearAlgebra using Random +using RecipesBase include("fourier.jl") export positive_fft_bins @@ -76,4 +77,8 @@ export create_filtered_lightcurve export check_gtis export split_by_gtis +include("plotting/plots_recipes_lightcurve.jl") +export create_segments +include("plotting/plots_recipes_gti.jl") +export BTIAnalysisPlot end diff --git a/src/events.jl b/src/events.jl index 7b76660..235286c 100644 --- a/src/events.jl +++ b/src/events.jl @@ -290,7 +290,7 @@ gti_info(ev) # Notes - Issues a warning if no GTI information is available -- Information is logged using the `@info` macro for structured output +- Information is logged using the `@debug` macro for structured output """ function gti_info(ev::EventList) if !has_gti(ev) @@ -806,10 +806,9 @@ function readevents( gti_hdu_candidates, gti_hdu_indices, combine_gtis) if !isnothing(gti_data) - println("Found GTI data: $(size(gti_data, 1)) intervals") - println("GTI time range: $(minimum(gti_data)) to $(maximum(gti_data))") + @debug "Found GTI data" n_intervals=size(gti_data, 1) time_range=(minimum(gti_data), maximum(gti_data)) else - println("No GTI data found") + @debug "No GTI data found" end end diff --git a/src/lightcurve.jl b/src/lightcurve.jl index ab76161..0b5c6d7 100644 --- a/src/lightcurve.jl +++ b/src/lightcurve.jl @@ -495,6 +495,77 @@ function apply_filters( return filtered_times, filtered_energies, start_t, stop_t end +""" + apply_filters(times, energies, tstart, tstop, energy_filter) + +Basic event filtering without GTI consideration. + +# Arguments +- `times::AbstractVector{T}`: Event arrival times +- `energies::Union{Nothing,AbstractVector{T}}`: Event energies (optional) +- `tstart::Union{Nothing,Real}`: Start time filter (inclusive) +- `tstop::Union{Nothing,Real}`: Stop time filter (inclusive) +- `energy_filter::Union{Nothing,Tuple{Real,Real}}`: Energy range (emin, emax) + +# Returns +`Tuple{Vector, Union{Nothing,Vector}, Real, Real}`: +- Filtered times +- Filtered energies (if provided) +- Actual start time +- Actual stop time + +# Examples +```julia +# Time filtering only +filtered_times, _, start_t, stop_t = apply_filters(times, nothing, 1000.0, 2000.0, nothing) + +# Energy and time filtering +filtered_times, filtered_energies, start_t, stop_t = apply_filters( + times, energies, 1000.0, 2000.0, (0.5, 10.0) +) +Notes +- Energy filter is applied as: emin ≤ energy < emax +- Time filter is applied as: tstart ≤ time ≤ tstop +""" +function apply_filters( + times::AbstractVector{T}, + energies::Union{Nothing,AbstractVector{T}}, + eventlist::EventList, + tstart::Union{Nothing,Real}, + tstop::Union{Nothing,Real}, + energy_filter::Union{Nothing,Tuple{Real,Real}}, + binsize::Real +) where T + mask = trues(length(times)) + + # Apply energy filter + if !isnothing(energy_filter) && !isnothing(energies) + emin, emax = energy_filter + mask = mask .& (energies .>= emin) .& (energies .< emax) + end + + # Apply time filters + if !isnothing(tstart) + mask = mask .& (times .>= tstart) + end + if !isnothing(tstop) + mask = mask .& (times .<= tstop) + end + # If GTI is present, apply GTI mask + if has_gti(eventlist) + gti_mask, _ = create_gti_mask(times, eventlist.meta.gti, dt=binsize) + mask = mask .& gti_mask + end + + !any(mask) && throw(ArgumentError("No events remain after applying filters")) + + filtered_times = times[mask] + filtered_energies = isnothing(energies) ? nothing : energies[mask] + start_t = isnothing(tstart) ? minimum(filtered_times) : tstart + stop_t = isnothing(tstop) ? maximum(filtered_times) : tstop + + return filtered_times, filtered_energies, start_t, stop_t +end """ calculate_event_properties(times, energies, dt, bin_centers) -> Vector{EventProperty} @@ -605,17 +676,24 @@ function extract_metadata(eventlist::EventList, start_time, stop_time, binsize, n_filtered_events # Assume it's already a count end + # Create extra metadata with GTI information[storing purpose] extra_metadata = Dict{String,Any}( "filtered_nevents" => n_events, "total_nevents" => length(eventlist.times), "energy_filter" => energy_filter, - "binning_method" => "histogram" + "binning_method" => "histogram", + "gti" => eventlist.meta.gti #GTI information[this 'eventlist.meta.gti' need to bw rembered since it is the one where u will all gti information:)] ) if hasfield(typeof(eventlist.meta), :extra) merge!(extra_metadata, eventlist.meta.extra) end + # Add GTI source information if available + if !isnothing(eventlist.meta.gti_source) + extra_metadata["gti_source"] = eventlist.meta.gti_source + end + return LightCurveMetadata( telescope, instrument, object, Float64(mjdref), (Float64(start_time), Float64(stop_time)), Float64(binsize), @@ -691,22 +769,20 @@ function create_lightcurve( !(err_method in [:poisson, :gaussian]) && throw(ArgumentError( "Unsupported error method: $err_method. Use :poisson or :gaussian" )) - binsize_t = convert(T, binsize) - # Apply filters to get filtered times and energies + filtered_times, filtered_energies, start_t, stop_t = apply_filters( eventlist.times, eventlist.energies, + eventlist, tstart, tstop, - energy_filter + energy_filter, + binsize_t ) - # Check if we have any events left after filtering - if isempty(filtered_times) - throw(ArgumentError("No events remain after filtering")) - end + isempty(filtered_times) && throw(ArgumentError("No events remain after filtering")) # Determine time range start_time = minimum(filtered_times) @@ -716,17 +792,17 @@ function create_lightcurve( dt, bin_centers = create_time_bins(start_time, stop_time, binsize_t) counts = bin_events(filtered_times, dt) - @info "Created light curve: $(length(bin_centers)) bins, bin size = $(binsize_t) s" + @debug "Created light curve: $(length(bin_centers)) bins, bin size = $(binsize_t) s" # Calculate exposure and properties exposure = fill(binsize_t, length(bin_centers)) properties = calculate_event_properties(filtered_times, filtered_energies, dt, bin_centers) - # Extract metadata - use filtered time range if time filtering was applied + # Extract metadata with GTI information actual_start = !isnothing(tstart) ? T(tstart) : start_time actual_stop = !isnothing(tstop) ? T(tstop) : stop_time metadata = extract_metadata(eventlist, actual_start, actual_stop, binsize_t, - length(filtered_times), energy_filter) + length(filtered_times), energy_filter) # Create light curve (errors will be calculated when needed) lc = LightCurve{T}( @@ -737,6 +813,11 @@ function create_lightcurve( # Calculate initial errors calculate_errors!(lc) + # Add debug info about GTI + if has_gti(eventlist) + @debug "GTI information preserved" n_intervals=size(eventlist.meta.gti, 1) time_range=extrema(eventlist.meta.gti) + end + return lc end """ diff --git a/src/plotting/plots_recipes_gti.jl b/src/plotting/plots_recipes_gti.jl new file mode 100644 index 0000000..1ff4127 --- /dev/null +++ b/src/plotting/plots_recipes_gti.jl @@ -0,0 +1,256 @@ +#wrapper to avoid recpies conflicts +struct BTIAnalysisPlot{T} + eventlist::EventList{T} +end + +""" + btianalysis(events::EventList{T}) -> BTIAnalysisPlot{T} + +Create a BTIAnalysisPlot object that can be plotted to show Bad Time Interval (BTI) length distribution. + +This function creates a plottable object that analyzes the distribution of bad time intervals by extracting +Good Time Intervals (GTIs) from the event metadata and computing the complementary BTIs. When plotted, it +generates a logarithmic histogram showing the frequency distribution of BTI durations. + +# Arguments +- `events::EventList{T}`: The input event list containing timing data and GTI metadata + +# Returns +- `BTIAnalysisPlot{T}`: A plottable object containing the event list data + +# Usage +The returned object can be plotted using the standard `plot()` function with various customization options: + +```julia +# Basic BTI analysis plot +bti_plot = btianalysis(events) +plot(bti_plot, bti_analysis=true) + +# Custom binning and axis limits +plot(bti_plot, bti_analysis=true, nbins=50, xlims_range=(1e-4, 1e4)) + +# Analysis with custom y-axis range +plot(bti_plot, bti_analysis=true, ylims_range=(1, 1000)) +``` + +--- + + plot(bti_plot::BTIAnalysisPlot{T}; bti_analysis=false, nbins=30, min_length=1e-3, max_length=10000.0, xlims_range=nothing, ylims_range=nothing) where T + +Plot a histogram of Bad Time Interval (BTI) lengths from a BTIAnalysisPlot object. + +# Plot Arguments +- `bti_plot::BTIAnalysisPlot{T}`: The BTI analysis object created by `btianalysis()` +- `bti_analysis::Bool=false`: Enable BTI analysis (plot returns `nothing` if `false`) +- `nbins::Int=30`: Number of histogram bins for the distribution plot +- `min_length::Float64=1e-3`: Minimum BTI length threshold (currently unused in filtering) +- `max_length::Float64=10000.0`: Maximum BTI length threshold (currently unused in filtering) +- `xlims_range=nothing`: Custom x-axis limits as a tuple `(min, max)`, or `nothing` for auto-scaling +- `ylims_range=nothing`: Custom y-axis limits as a tuple `(min, max)`, or `nothing` for auto-scaling + +# Returns +- `Vector{Float64}`: Array of BTI lengths in seconds when BTIs are found +- `Vector{Float64}`: Dummy data `[0.5]` when no BTIs exist (creates informational plot) +- `nothing`: When `bti_analysis=false` + +# Behavior +- Returns `nothing` immediately if `bti_analysis=false` +- Creates informational plots with status messages when no GTIs or BTIs are found +- Generates logarithmic histogram of BTI length distribution when BTIs exist +- Prints diagnostic statistics including total exposure and BTI lengths +- Handles various error conditions gracefully with fallback plots + +# Plot Properties +- Uses logarithmic scaling on both axes for better visualization of wide dynamic ranges +- Automatic bin range calculation based on data extent +- Steel blue fill color with transparency +- Grid enabled for easier reading + +# Complete Examples +```julia +using Plots + +# Read event data +events = readevents("your_file.fits") + +# Create BTI analysis object and plot +bti_plot = btianalysis(events) +plot(bti_plot, bti_analysis=true) + +# Customized analysis with specific parameters +plot(bti_plot, + bti_analysis=true, + nbins=50, + xlims_range=(1e-4, 1e4), + ylims_range=(1, 1000)) + +# Save the plot +savefig("bti_analysis.png") +``` + +# Workflow +1. Create EventList with GTI metadata: `events = readevents("file")` +2. Create BTI analysis object: `bti_plot = btianalysis(events)` +3. Generate plot: `plot(bti_plot, bti_analysis=true)` + +# Notes +- Requires GTI information in `events.meta.gti` to function properly +- Short BTIs (< 1.0 second) are tracked separately in diagnostic output +- Function includes extensive error handling for missing or invalid GTI data +- The `bti_analysis=true` parameter must be set to generate the actual analysis plot +""" + +# Constructor function (typically defined elsewhere) +btianalysis(events::EventList{T}) where T = BTIAnalysisPlot(events) + +@recipe function f(events::BTIAnalysisPlot{T}; + bti_analysis=false, + nbins=30, + min_length=1e-3, + max_length=10000.0, + xlims_range=nothing, + ylims_range=nothing) where T + + !bti_analysis && return nothing + + # Extract EventList from wrapper + gti_event = events.eventlist + + # Helper function to create "No BTI found" plot + function create_no_bti_plot() + title --> "Bad Time Interval Analysis: No BTIs Found" + xlabel --> "Observation Quality" + ylabel --> "Coverage Status" + grid --> false + legend --> false + size --> (600, 200) + seriestype --> :scatter + markersize --> 0 + xlims --> (0, 1) + ylims --> (0, 1) + annotations --> [(0.5, 0.5, "No Bad Time Intervals Found\nAll observation time covered by GTIs")] + return [0.5], [0.5] + end + + gtis = gti_event.meta.gti + if isnothing(gtis) || isempty(gtis) + error("No GTI information available in EventList") + end + + total_exposure = gti_exposure(gti_event) + + if isempty(gtis) || size(gtis, 1) == 0 + println("No BTI found - all GTI") + + title --> "Bad Time Interval Analysis: No GTIs Available" + xlabel --> "Observation Quality" + ylabel --> "Coverage Status" + grid --> false + legend --> false + size --> (600, 200) + seriestype --> :scatter + markersize --> 0 + xlims --> (0, 1) + ylims --> (0, 1) + annotations --> [(0.5, 0.5, "No GTI Information Available")] + + return [0.5], [0.5] + end + + start_time = isempty(gti_event.times) ? 0.0 : minimum(gti_event.times) + stop_time = isempty(gti_event.times) ? 0.0 : maximum(gti_event.times) + + btis = nothing + try + btis = get_btis(gtis, start_time, stop_time) + catch + println("No BTI found - all GTI") + return create_no_bti_plot() + end + + # Check if BTIs exist and are valid + if isnothing(btis) || isempty(btis) || size(btis, 1) == 0 + println("No BTI found - all GTI") + return create_no_bti_plot() + end + + bti_lengths = nothing + try + bti_lengths = get_gti_lengths(btis) + catch + println("No BTI found - all GTI") + return create_no_bti_plot() + end + + if isnothing(bti_lengths) || isempty(bti_lengths) || length(bti_lengths) == 0 + println("No BTI found - all GTI") + return create_no_bti_plot() + end + + total_bti_length = 0.0 + try + total_bti_length = get_total_gti_length(btis) + catch + total_bti_length = 0.0 + end + + # Calculate short BTI length (< 1.0 second) + total_short_bti_length = 0.0 + try + short_bti_mask = bti_lengths .< 1.0 + if any(short_bti_mask) + short_btis = btis[short_bti_mask, :] + total_short_bti_length = get_total_gti_length(short_btis) + end + catch + total_short_bti_length = 0.0 + end + + # Print diagnostic statistics + println("Total exposure: $(total_exposure)") + println("Total BTI length: $(total_bti_length)") + println("Total BTI length (short BTIs): $(total_short_bti_length)") + data_min, data_max = 0.0, 1.0 + try + data_min = minimum(bti_lengths) + data_max = maximum(bti_lengths) + catch + println("Error calculating data range - no BTI found") + return create_no_bti_plot() + end + + # Calculate bin range for display + bin_min = min(1e-3, data_min * 0.1) + bin_max = max(10000, data_max * 2.0) + num_bins = Int(nbins) + + title --> "Distribution of Bad Time Interval Lengths" + xlabel --> "Length of bad time interval" + ylabel --> "Number of intervals" + xscale --> :log10 + yscale --> :log10 + + if !isnothing(xlims_range) + xlims --> xlims_range + else + xlims --> (bin_min, bin_max) + end + + if !isnothing(ylims_range) + ylims --> ylims_range + else + ylims --> (0.5, max(100, length(bti_lengths))) + end + + grid --> true + legend --> false + size --> (600, 400) + seriestype --> :histogram + nbins --> num_bins + fillcolor --> :steelblue + fillalpha --> 0.7 + linecolor --> :steelblue + linewidth --> 1 + + return bti_lengths +end \ No newline at end of file diff --git a/src/plotting/plots_recipes_lightcurve.jl b/src/plotting/plots_recipes_lightcurve.jl new file mode 100644 index 0000000..85d538e --- /dev/null +++ b/src/plotting/plots_recipes_lightcurve.jl @@ -0,0 +1,1010 @@ +function create_segments(events::EventList, segment_duration::Real; bin_size::Real = 1.0) + # Get actual time range from the data + t_start = minimum(events.times) + t_stop = maximum(events.times) + total_time = t_stop - t_start + println("Data time range: $(t_start) to $(t_stop) ($(total_time) seconds total)") + + # Calculate number of segments + n_segments = ceil(Int, total_time / segment_duration) + println("Creating $(n_segments) segments of $(segment_duration) seconds each") + + segments = Vector{LightCurve}() + + for i in 1:n_segments + start_time = t_start + (i-1) * segment_duration + stop_time = min(t_start + i * segment_duration, t_stop) + + # Filter events in this segment using matrix operations + event_times = events.times + mask = (event_times .>= start_time) .& (event_times .<= stop_time) + segment_events = event_times[mask] + + events_in_segment = sum(mask) + println("Segment $i: $(events_in_segment) events from $(start_time) to $(stop_time)") + + # Create time bins for this segment + time_edges = collect(start_time:bin_size:stop_time) + if length(time_edges) < 2 + time_edges = [start_time, stop_time] + end + + # Bin centers + time_centers = (time_edges[1:end-1] + time_edges[2:end]) / 2 + n_bins = length(time_centers) + + # Initialize counts + counts = zeros(Int, n_bins) + + # Histogram events into bins using matrix operations + if events_in_segment > 0 + # Use searchsortedlast for efficient binning + for event_time in segment_events + bin_idx = searchsortedlast(time_edges, event_time) + if bin_idx > 0 && bin_idx <= n_bins + counts[bin_idx] += 1 + end + end + end + + # Create light curve data matrix: [time, counts, errors] + # Calculate Poisson errors + count_errors = sqrt.(max.(counts, 1)) # Avoid sqrt(0) + + # Create the data matrix + lc_matrix = hcat(time_centers, counts, count_errors) + + # Create dummy metadata + dummy_metadata = LightCurveMetadata( + "Unknown", "Unknown", "Unknown", 0.0, + (start_time, stop_time), bin_size, + Vector{Dict{String,Any}}(), Dict{String,Any}() + ) + + # Create LightCurve object + lc = LightCurve( + lc_matrix[:, 1], # time + bin_size, # dt + Int.(lc_matrix[:, 2]), # counts + lc_matrix[:, 3], # count_error + nothing, # exposure + Vector{EventProperty{Float64}}(), # properties + dummy_metadata, # metadata + :poisson # err_method + ) + + push!(segments, lc) + end + + return segments +end +""" + plot(el::EventList{T}, bin_size::Real=1.0; kwargs...) + +Plot a light curve from an EventList with optional Good Time Intervals (GTIs) and Bad Time Intervals (BTIs). + +# Arguments +- `el::EventList{T}`: Event list containing photon arrival times +- `bin_size::Real=1.0`: Time bin size in seconds + +# Keywords +- `tstart=nothing`: Start time for the light curve (defaults to first event) +- `tstop=nothing`: Stop time for the light curve (defaults to last event) +- `energy_filter=nothing`: Energy range filter as (min, max) tuple +- `show_errors=false`: Display error bars using specified error method +- `show_btis=false`: Show Bad Time Intervals as red shaded regions +- `show_bti=false`: Alias for `show_btis` +- `show_gtis=false`: Show Good Time Intervals as green shaded regions +- `show_gti=false`: Alias for `show_gtis` +- `gtis=nothing`: GTI matrix to use (overrides file/metadata GTIs) +- `gti_file=nothing`: FITS file containing GTI extension +- `gti_hdu="GTI"`: HDU name for GTI data +- `bti_alpha=0.3`: Transparency for BTI shading +- `gti_alpha=0.2`: Transparency for GTI shading +- `gap_threshold=10.0`: Minimum gap size to consider as BTI +- `axis_limits=nothing`: Plot limits as `[xmin, xmax]` or `[xmin, xmax, ymin, ymax]` +- `err_method=:poisson`: Error calculation method (`:poisson`, `:gaussian`) + +# Returns +- `Tuple{Vector, Vector}`: Time bins and corresponding count rates + +# Examples +```julia +# Basic light curve +plot(events, 1.0) + +# With error bars and GTIs +plot(events, 0.5, show_errors=true, show_gtis=true) + +# Custom time range with energy filter +plot(events, 2.0, tstart=100.0, tstop=500.0, energy_filter=(2.0, 10.0)) + +# With custom axis limits +plot(events, 1.0, axis_limits=[0, 1000, 0, 100]) +``` + +# Notes +- GTI priority: explicit `gtis` > `gti_file` > `el.meta.gti` +- BTIs are calculated as gaps between GTIs exceeding `gap_threshold` +- Error bars use Poisson statistics by default +""" +@recipe function f(el::EventList{T}, bin_size::Real=1.0; + tstart=nothing, + tstop=nothing, + energy_filter=nothing, + show_errors=false, + show_btis=false, + show_bti=false, + show_gtis=false, + show_gti=false, + gtis=nothing, + gti_file=nothing, + gti_hdu="GTI", + bti_alpha=0.3, + gti_alpha=0.2, + gap_threshold=10.0, + axis_limits=nothing, + err_method=:poisson) where T + + isempty(el.times) && error("EventList is empty") + + show_gtis = show_gtis || show_gti + show_btis = show_btis || show_bti + + # Create light curve + lc = create_lightcurve(el, bin_size; + tstart=tstart, + tstop=tstop, + energy_filter=energy_filter, + err_method=err_method) + + calculate_errors!(lc) + + # Convert to matrix format + lc_matrix = hcat(lc.time, lc.counts, lc.count_error) + + # Basic plot settings + title --> "Light Curve" + xlabel --> "Time (s)" + ylabel --> "Counts" + grid --> true + minorgrid --> true + legend --> :bottomright + + # Axis limits handling + if !isnothing(axis_limits) + if length(axis_limits) == 4 + xmin, xmax, ymin, ymax = axis_limits + + if !isnothing(xmin) || !isnothing(xmax) + xlims --> ( + isnothing(xmin) ? minimum(lc_matrix[:,1]) : xmin, + isnothing(xmax) ? maximum(lc_matrix[:,1]) : xmax + ) + end + + if !isnothing(ymin) || !isnothing(ymax) + ylims --> ( + isnothing(ymin) ? minimum(lc_matrix[:,2]) : ymin, + isnothing(ymax) ? maximum(lc_matrix[:,2]) : ymax + ) + end + elseif length(axis_limits) == 2 + xmin, xmax = axis_limits + xlims --> ( + isnothing(xmin) ? minimum(lc_matrix[:,1]) : xmin, + isnothing(xmax) ? maximum(lc_matrix[:,1]) : xmax + ) + else + @warn "axis_limits should be a vector of length 2 or 4: [xmin, xmax] or [xmin, xmax, ymin, ymax]" + end + end + + # Determine time range + plot_tstart = isnothing(tstart) ? lc.time[1] : tstart + plot_tstop = isnothing(tstop) ? lc.time[end] : tstop + + # Handle GTI/BTI visualization + if show_btis || show_gtis + effective_gtis = nothing + + # Priority: explicit gtis > gti_file > eventlist.meta.gti + if !isnothing(gtis) + effective_gtis = gtis + elseif !isnothing(gti_file) + try + effective_gtis = load_gtis(gti_file, gti_hdu) + catch e + @warn "Could not load GTIs from file: $e" + end + elseif !isnothing(el.meta.gti) + effective_gtis = el.meta.gti + end + + if !isnothing(effective_gtis) + y_min, y_max = extrema(lc_matrix[:,2]) + + # Pre-allocate arrays for shapes + if show_gtis + n_gtis = size(effective_gtis, 1) + gti_x = Vector{Float64}(undef, n_gtis * 6) + gti_y = Vector{Float64}(undef, n_gtis * 6) + gti_idx = 0 + + @inbounds for i in 1:n_gtis + gti_start, gti_stop = effective_gtis[i, 1], effective_gtis[i, 2] + + if gti_stop >= plot_tstart && gti_start <= plot_tstop + gti_start = max(gti_start, plot_tstart) + gti_stop = min(gti_stop, plot_tstop) + + # Rectangle vertices + base_idx = gti_idx * 6 + gti_x[base_idx + 1] = gti_start + gti_x[base_idx + 2] = gti_stop + gti_x[base_idx + 3] = gti_stop + gti_x[base_idx + 4] = gti_start + gti_x[base_idx + 5] = gti_start + gti_x[base_idx + 6] = NaN + + gti_y[base_idx + 1] = y_min + gti_y[base_idx + 2] = y_min + gti_y[base_idx + 3] = y_max + gti_y[base_idx + 4] = y_max + gti_y[base_idx + 5] = y_min + gti_y[base_idx + 6] = NaN + + gti_idx += 1 + end + end + + if gti_idx > 0 + resize!(gti_x, gti_idx * 6) + resize!(gti_y, gti_idx * 6) + + @series begin + seriestype := :shape + fillcolor := :green + fillalpha := gti_alpha + linecolor := :green + linewidth := 0.5 + label := "Good Time Intervals" + gti_x, gti_y + end + end + end + + if show_btis + btis = get_btis(effective_gtis, plot_tstart, plot_tstop) + + if !isempty(btis) + n_btis = size(btis, 1) + bti_x = Vector{Float64}(undef, n_btis * 6) + bti_y = Vector{Float64}(undef, n_btis * 6) + + @inbounds for i in 1:n_btis + bti_start, bti_stop = btis[i, 1], btis[i, 2] + + base_idx = (i - 1) * 6 + bti_x[base_idx + 1] = bti_start + bti_x[base_idx + 2] = bti_stop + bti_x[base_idx + 3] = bti_stop + bti_x[base_idx + 4] = bti_start + bti_x[base_idx + 5] = bti_start + bti_x[base_idx + 6] = NaN + + bti_y[base_idx + 1] = y_min + bti_y[base_idx + 2] = y_min + bti_y[base_idx + 3] = y_max + bti_y[base_idx + 4] = y_max + bti_y[base_idx + 5] = y_min + bti_y[base_idx + 6] = NaN + end + + @series begin + seriestype := :shape + fillcolor := :red + fillalpha := bti_alpha + linecolor := :red + linewidth := 0.5 + label := "Bad Time Intervals" + bti_x, bti_y + end + end + end + end + end + + # Main light curve plot + if show_errors + seriestype --> :scatter + marker --> :circle + markersize --> 3 + markercolor --> :blue + markerstrokewidth --> 0.5 + yerror --> lc_matrix[:,3] + errorbar_color --> :black + color --> :blue + label --> "Light Curve with $(err_method) errors" + else + seriestype --> :steppost + linewidth --> 1.5 + color --> :blue + label --> "Light Curve (bin size: $bin_size s)" + end + + return lc_matrix[:,1], lc_matrix[:,2] +end + +""" + plot(lc::LightCurve{T}; kwargs...) + +Plot a pre-computed light curve with optional properties, GTIs, and BTIs. + +# Arguments +- `lc::LightCurve{T}`: Light curve object containing binned time series data + +# Keywords +- `show_errors=false`: Display error bars if available +- `show_properties=false`: Show additional properties on secondary y-axis +- `property_name=:mean_energy`: Which property to display (if `show_properties=true`) +- `show_btis=false`: Show Bad Time Intervals as red shaded regions +- `show_bti=false`: Alias for `show_btis` +- `show_gtis=false`: Show Good Time Intervals as green shaded regions +- `show_gti=false`: Alias for `show_gtis` +- `gtis=nothing`: GTI matrix to use (overrides metadata GTIs) +- `gti_file=nothing`: FITS file containing GTI extension +- `gti_hdu="GTI"`: HDU name for GTI data +- `bti_alpha=0.3`: Transparency for BTI shading +- `gti_alpha=0.2`: Transparency for GTI shading +- `axis_limits=nothing`: Plot limits as `[xmin, xmax]` or `[xmin, xmax, ymin, ymax]` + +# Returns +- `Tuple{Vector, Vector}`: Time bins and corresponding count rates + +# Examples +```julia +# Basic light curve plot +plot(lightcurve) + +# With error bars and mean energy overlay +plot(lightcurve, show_errors=true, show_properties=true, property_name=:mean_energy) + +# Show GTIs with custom transparency +plot(lightcurve, show_gtis=true, gti_alpha=0.4) +``` + +# Notes +- Errors are calculated on-demand if not already present +- Properties must exist in `lc.properties` to be displayed +- GTI priority: explicit `gtis` > `gti_file` > `lc.metadata.extra["gti_bounds"]` +""" +@recipe function f(lc::LightCurve{T}; + show_errors=false, + show_properties=false, + property_name=:mean_energy, + show_btis=false, + show_bti=false, + show_gtis=false, + show_gti=false, + gtis=nothing, + gti_file=nothing, + gti_hdu="GTI", + bti_alpha=0.3, + gti_alpha=0.2, + axis_limits=nothing) where T + + show_gtis = show_gtis || show_gti + show_btis = show_btis || show_bti + + if show_errors && isnothing(lc.count_error) + calculate_errors!(lc) + end + + # Convert to matrix format + lc_matrix = hcat(lc.time, lc.counts, lc.count_error) + + title --> "Light Curve" + xlabel --> "Time (s)" + ylabel --> "Counts" + grid --> true + minorgrid --> true + legend --> :bottomright + + # Axis limits handling + if !isnothing(axis_limits) + if length(axis_limits) == 4 + xmin, xmax, ymin, ymax = axis_limits + + if !isnothing(xmin) || !isnothing(xmax) + xlims --> ( + isnothing(xmin) ? minimum(lc_matrix[:,1]) : xmin, + isnothing(xmax) ? maximum(lc_matrix[:,1]) : xmax + ) + end + + if !isnothing(ymin) || !isnothing(ymax) + ylims --> ( + isnothing(ymin) ? minimum(lc_matrix[:,2]) : ymin, + isnothing(ymax) ? maximum(lc_matrix[:,2]) : ymax + ) + end + elseif length(axis_limits) == 2 + xmin, xmax = axis_limits + xlims --> ( + isnothing(xmin) ? minimum(lc_matrix[:,1]) : xmin, + isnothing(xmax) ? maximum(lc_matrix[:,1]) : xmax + ) + else + @warn "axis_limits should be a vector of length 2 or 4: [xmin, xmax] or [xmin, xmax, ymin, ymax]" + end + end + + plot_tstart, plot_tstop = lc.metadata.time_range + + # Handle GTI/BTI visualization + if show_btis || show_gtis + effective_gtis = nothing + + if !isnothing(gtis) + effective_gtis = gtis + elseif !isnothing(gti_file) + try + effective_gtis = load_gtis(gti_file, gti_hdu) + catch e + @warn "Could not load GTIs from file: $e" + end + elseif haskey(lc.metadata.extra, "gti_applied") && haskey(lc.metadata.extra, "gti_bounds") + gti_bounds = lc.metadata.extra["gti_bounds"] + effective_gtis = reshape(gti_bounds, 1, 2) + end + + if !isnothing(effective_gtis) + y_min, y_max = extrema(lc_matrix[:,2]) + + if show_gtis + n_gtis = size(effective_gtis, 1) + gti_x = Vector{Float64}(undef, n_gtis * 6) + gti_y = Vector{Float64}(undef, n_gtis * 6) + gti_idx = 0 + + @inbounds for i in 1:n_gtis + gti_start, gti_stop = effective_gtis[i, 1], effective_gtis[i, 2] + + if gti_stop >= plot_tstart && gti_start <= plot_tstop + gti_start = max(gti_start, plot_tstart) + gti_stop = min(gti_stop, plot_tstop) + + base_idx = gti_idx * 6 + gti_x[base_idx + 1] = gti_start + gti_x[base_idx + 2] = gti_stop + gti_x[base_idx + 3] = gti_stop + gti_x[base_idx + 4] = gti_start + gti_x[base_idx + 5] = gti_start + gti_x[base_idx + 6] = NaN + + gti_y[base_idx + 1] = y_min + gti_y[base_idx + 2] = y_min + gti_y[base_idx + 3] = y_max + gti_y[base_idx + 4] = y_max + gti_y[base_idx + 5] = y_min + gti_y[base_idx + 6] = NaN + + gti_idx += 1 + end + end + + if gti_idx > 0 + resize!(gti_x, gti_idx * 6) + resize!(gti_y, gti_idx * 6) + + @series begin + seriestype := :shape + fillcolor := :green + fillalpha := gti_alpha + linecolor := :green + linewidth := 0.5 + label := "Good Time Intervals" + gti_x, gti_y + end + end + end + + if show_btis + btis = get_btis(effective_gtis, plot_tstart, plot_tstop) + + if !isempty(btis) + n_btis = size(btis, 1) + bti_x = Vector{Float64}(undef, n_btis * 6) + bti_y = Vector{Float64}(undef, n_btis * 6) + + @inbounds for i in 1:n_btis + bti_start, bti_stop = btis[i, 1], btis[i, 2] + + base_idx = (i - 1) * 6 + bti_x[base_idx + 1] = bti_start + bti_x[base_idx + 2] = bti_stop + bti_x[base_idx + 3] = bti_stop + bti_x[base_idx + 4] = bti_start + bti_x[base_idx + 5] = bti_start + bti_x[base_idx + 6] = NaN + + bti_y[base_idx + 1] = y_min + bti_y[base_idx + 2] = y_min + bti_y[base_idx + 3] = y_max + bti_y[base_idx + 4] = y_max + bti_y[base_idx + 5] = y_min + bti_y[base_idx + 6] = NaN + end + + @series begin + seriestype := :shape + fillcolor := :red + fillalpha := bti_alpha + linecolor := :red + linewidth := 0.5 + label := "Bad Time Intervals" + bti_x, bti_y + end + end + end + end + end + + # Handle properties display + if show_properties + prop_idx = findfirst(p -> p.name == property_name, lc.properties) + if !isnothing(prop_idx) + prop = lc.properties[prop_idx] + + prop_matrix = hcat(lc.time, prop.values) + + @series begin + yaxis := :right + ylabel := "$(prop.name) ($(prop.unit))" + seriestype := :line + color := :red + linewidth := 1.5 + label := String(prop.name) + prop_matrix[:,1], prop_matrix[:,2] + end + end + end + + # Main light curve plot + if show_errors + seriestype --> :scatter + marker --> :circle + markersize --> 3 + markercolor --> :blue + markerstrokewidth --> 0.5 + yerror --> lc_matrix[:,3] + errorbar_color --> :black + color --> :blue + label --> "Light Curve with $(lc.err_method) errors" + else + seriestype --> :steppost + linewidth --> 1.5 + color --> :blue + label --> "Light Curve (bin size: $(lc.metadata.bin_size)s)" + end + + return lc_matrix[:,1], lc_matrix[:,2] +end + +""" + plot(lc_segments::Vector{<:LightCurve}; kwargs...) + +Plot multiple light curve segments with optional segment boundaries and individual coloring. + +# Arguments +- `lc_segments::Vector{<:LightCurve}`: Vector of light curve segments to plot + +# Keywords +- `show_errors=false`: Display error bars for each segment +- `show_segment_boundaries=true`: Show vertical dashed lines at segment boundaries +- `segment_colors=nothing`: Custom colors for each segment (defaults to standard palette) +- `axis_limits=nothing`: Plot limits as `[xmin, xmax]` or `[xmin, xmax, ymin, ymax]` + +# Returns +- Multiple plot series, one per segment + +# Examples +```julia +# Basic segmented light curve +plot(segments) + +# With custom colors and no boundaries +plot(segments, segment_colors=[:red, :blue, :green], show_segment_boundaries=false) + +# With error bars and custom limits +plot(segments, show_errors=true, axis_limits=[0, 1000, 0, 50]) +``` + +# Notes +- Default color palette cycles through 8 colors for segments +- Segment boundaries are drawn at the start of each segment after the first +- Each segment can have different bin sizes and error methods +""" +@recipe function f(lc_segments::Vector{<:LightCurve}; + show_errors=false, + show_segment_boundaries=true, + segment_colors=nothing, + axis_limits=nothing) + + title --> "Segmented Light Curve" + xlabel --> "Time (s)" + ylabel --> "Counts" + grid --> true + minorgrid --> true + legend --> :bottomright + + # Axis limits handling + if !isnothing(axis_limits) + if length(axis_limits) == 4 + xmin, xmax, ymin, ymax = axis_limits + + # Get overall data bounds + all_times = vcat([lc.time for lc in lc_segments]...) + all_counts = vcat([lc.counts for lc in lc_segments]...) + + if !isnothing(xmin) || !isnothing(xmax) + xlims --> ( + isnothing(xmin) ? minimum(all_times) : xmin, + isnothing(xmax) ? maximum(all_times) : xmax + ) + end + + if !isnothing(ymin) || !isnothing(ymax) + ylims --> ( + isnothing(ymin) ? minimum(all_counts) : ymin, + isnothing(ymax) ? maximum(all_counts) : ymax + ) + end + elseif length(axis_limits) == 2 + xmin, xmax = axis_limits + all_times = vcat([lc.time for lc in lc_segments]...) + xlims --> ( + isnothing(xmin) ? minimum(all_times) : xmin, + isnothing(xmax) ? maximum(all_times) : xmax + ) + else + @warn "axis_limits should be a vector of length 2 or 4: [xmin, xmax] or [xmin, xmax, ymin, ymax]" + end + end + + default_colors = [:blue, :red, :green, :orange, :purple, :brown, :pink, :gray] + colors = isnothing(segment_colors) ? default_colors : segment_colors + n_colors = length(colors) + + boundaries = Vector{Float64}() + + for (i, lc) in enumerate(lc_segments) + color = colors[((i-1) % n_colors) + 1] + + if show_errors && isnothing(lc.count_error) + calculate_errors!(lc) + end + + lc_matrix = hcat(lc.time, lc.counts, lc.count_error) + + @series begin + if show_errors + seriestype := :scatter + marker := :circle + markersize := 3 + markerstrokewidth := 0.5 + yerror := lc_matrix[:,3] + errorbar_color := color + else + seriestype := :steppost + linewidth := 1.5 + end + color := color + label := "Segment $i" + + lc_matrix[:,1], lc_matrix[:,2] + end + + if show_segment_boundaries && i > 1 + push!(boundaries, minimum(lc.time)) + end + end + + if show_segment_boundaries && !isempty(boundaries) + @series begin + seriestype := :vline + color := :black + linestyle := :dash + linewidth := 1 + alpha := 0.7 + label := "Segment boundaries" + boundaries + end + end +end +""" + plot(lc::LightCurve{T}, new_binsize::Real; kwargs...) + +Plot a light curve after rebinning it to a new bin size. + +# Arguments +- `lc::LightCurve{T}`: Original light curve to rebin and plot +- `new_binsize::Real`: New bin size in seconds (must be larger than current bin size) + +# Keywords +- `show_errors=false`: Display error bars using rebinned error estimates +- `show_properties=false`: Show additional properties on secondary y-axis +- `property_name=:mean_energy`: Which property to display (if `show_properties=true`) +- `show_btis=false`: Show Bad Time Intervals as red shaded regions +- `show_bti=false`: Alias for `show_btis` +- `show_gtis=false`: Show Good Time Intervals as green shaded regions +- `show_gti=false`: Alias for `show_gtis` +- `gtis=nothing`: GTI matrix to use (overrides metadata GTIs) +- `gti_file=nothing`: FITS file containing GTI extension +- `gti_hdu="GTI"`: HDU name for GTI data +- `bti_alpha=0.3`: Transparency for BTI shading +- `gti_alpha=0.2`: Transparency for GTI shading +- `axis_limits=nothing`: Plot limits as `[xmin, xmax]` or `[xmin, xmax, ymin, ymax]` +- `show_original=false`: Overlay original light curve for comparison +- `original_alpha=0.3`: Transparency for original light curve overlay + +# Returns +- `Tuple{Vector, Vector}`: Rebinned time bins and corresponding count rates + +# Examples +```julia +# Basic rebinned light curve +plot(lc, 100.0) # Rebin to 100s + +# With error bars and GTIs +plot(lc, 50.0, show_errors=true, show_gtis=true) + +# Compare with original +plot(lc, 200.0, show_original=true, original_alpha=0.4) + +# With properties overlay +plot(lc, 30.0, show_properties=true, property_name=:mean_energy) +``` + +# Notes +- The new bin size must be larger than the current bin size +- Original light curve data is preserved; only the plot shows rebinned data +- Error bars are recalculated for the rebinned light curve +- GTI/BTI regions are preserved from the original light curve metadata +""" +@recipe function f(lc::LightCurve{T}, new_binsize::Real; + show_errors=false, + show_properties=false, + property_name=:mean_energy, + show_btis=false, + show_bti=false, + show_gtis=false, + show_gti=false, + gtis=nothing, + gti_file=nothing, + gti_hdu="GTI", + bti_alpha=0.3, + gti_alpha=0.2, + axis_limits=nothing, + show_original=false, + original_alpha=0.3) where T + + # Validate new bin size + new_binsize <= lc.metadata.bin_size && throw(ArgumentError( + "New bin size ($new_binsize s) must be larger than current bin size ($(lc.metadata.bin_size) s)" + )) + + # Create rebinned light curve + rebinned_lc = rebin(lc, new_binsize) + + show_gtis = show_gtis || show_gti + show_btis = show_btis || show_bti + + if show_errors && isnothing(rebinned_lc.count_error) + calculate_errors!(rebinned_lc) + end + + # Convert to matrix format + lc_matrix = hcat(rebinned_lc.time, rebinned_lc.counts, rebinned_lc.count_error) + + title --> "Rebinned Light Curve ($(lc.metadata.bin_size)s → $(new_binsize)s)" + xlabel --> "Time (s)" + ylabel --> "Counts" + grid --> true + minorgrid --> true + legend --> :bottomright + + # Axis limits handling + if !isnothing(axis_limits) + if length(axis_limits) == 4 + xmin, xmax, ymin, ymax = axis_limits + + if !isnothing(xmin) || !isnothing(xmax) + xlims --> ( + isnothing(xmin) ? minimum(lc_matrix[:,1]) : xmin, + isnothing(xmax) ? maximum(lc_matrix[:,1]) : xmax + ) + end + + if !isnothing(ymin) || !isnothing(ymax) + ylims --> ( + isnothing(ymin) ? minimum(lc_matrix[:,2]) : ymin, + isnothing(ymax) ? maximum(lc_matrix[:,2]) : ymax + ) + end + elseif length(axis_limits) == 2 + xmin, xmax = axis_limits + xlims --> ( + isnothing(xmin) ? minimum(lc_matrix[:,1]) : xmin, + isnothing(xmax) ? maximum(lc_matrix[:,1]) : xmax + ) + else + @warn "axis_limits should be a vector of length 2 or 4: [xmin, xmax] or [xmin, xmax, ymin, ymax]" + end + end + + plot_tstart, plot_tstop = rebinned_lc.metadata.time_range + + # Show original light curve for comparison + if show_original + original_matrix = hcat(lc.time, lc.counts, lc.count_error) + + @series begin + seriestype := :steppost + linewidth := 1 + color := :gray + alpha := original_alpha + label := "Original ($(lc.metadata.bin_size)s bins)" + original_matrix[:,1], original_matrix[:,2] + end + end + + # Handle GTI/BTI visualization (use original metadata) + if show_btis || show_gtis + effective_gtis = nothing + + if !isnothing(gtis) + effective_gtis = gtis + elseif !isnothing(gti_file) + try + effective_gtis = load_gtis(gti_file, gti_hdu) + catch e + @warn "Could not load GTIs from file: $e" + end + elseif haskey(lc.metadata.extra, "gti_applied") && haskey(lc.metadata.extra, "gti_bounds") + gti_bounds = lc.metadata.extra["gti_bounds"] + effective_gtis = reshape(gti_bounds, 1, 2) + end + + if !isnothing(effective_gtis) + y_min, y_max = extrema(lc_matrix[:,2]) + + if show_gtis + n_gtis = size(effective_gtis, 1) + gti_x = Vector{Float64}(undef, n_gtis * 6) + gti_y = Vector{Float64}(undef, n_gtis * 6) + gti_idx = 0 + + @inbounds for i in 1:n_gtis + gti_start, gti_stop = effective_gtis[i, 1], effective_gtis[i, 2] + + if gti_stop >= plot_tstart && gti_start <= plot_tstop + gti_start = max(gti_start, plot_tstart) + gti_stop = min(gti_stop, plot_tstop) + + base_idx = gti_idx * 6 + gti_x[base_idx + 1] = gti_start + gti_x[base_idx + 2] = gti_stop + gti_x[base_idx + 3] = gti_stop + gti_x[base_idx + 4] = gti_start + gti_x[base_idx + 5] = gti_start + gti_x[base_idx + 6] = NaN + + gti_y[base_idx + 1] = y_min + gti_y[base_idx + 2] = y_min + gti_y[base_idx + 3] = y_max + gti_y[base_idx + 4] = y_max + gti_y[base_idx + 5] = y_min + gti_y[base_idx + 6] = NaN + + gti_idx += 1 + end + end + + if gti_idx > 0 + resize!(gti_x, gti_idx * 6) + resize!(gti_y, gti_idx * 6) + + @series begin + seriestype := :shape + fillcolor := :green + fillalpha := gti_alpha + linecolor := :green + linewidth := 0.5 + label := "Good Time Intervals" + gti_x, gti_y + end + end + end + + if show_btis + btis = get_btis(effective_gtis, plot_tstart, plot_tstop) + + if !isempty(btis) + n_btis = size(btis, 1) + bti_x = Vector{Float64}(undef, n_btis * 6) + bti_y = Vector{Float64}(undef, n_btis * 6) + + @inbounds for i in 1:n_btis + bti_start, bti_stop = btis[i, 1], btis[i, 2] + + base_idx = (i - 1) * 6 + bti_x[base_idx + 1] = bti_start + bti_x[base_idx + 2] = bti_stop + bti_x[base_idx + 3] = bti_stop + bti_x[base_idx + 4] = bti_start + bti_x[base_idx + 5] = bti_start + bti_x[base_idx + 6] = NaN + + bti_y[base_idx + 1] = y_min + bti_y[base_idx + 2] = y_min + bti_y[base_idx + 3] = y_max + bti_y[base_idx + 4] = y_max + bti_y[base_idx + 5] = y_min + bti_y[base_idx + 6] = NaN + end + + @series begin + seriestype := :shape + fillcolor := :red + fillalpha := bti_alpha + linecolor := :red + linewidth := 0.5 + label := "Bad Time Intervals" + bti_x, bti_y + end + end + end + end + end + + # Handle properties display + if show_properties + prop_idx = findfirst(p -> p.name == property_name, rebinned_lc.properties) + if !isnothing(prop_idx) + prop = rebinned_lc.properties[prop_idx] + + prop_matrix = hcat(rebinned_lc.time, prop.values) + + @series begin + yaxis := :right + ylabel := "$(prop.name) ($(prop.unit))" + seriestype := :line + color := :red + linewidth := 1.5 + label := String(prop.name) + prop_matrix[:,1], prop_matrix[:,2] + end + end + end + + if show_errors + seriestype --> :scatter + marker --> :circle + markersize --> 3 + markercolor --> :blue + markerstrokewidth --> 0.5 + yerror --> lc_matrix[:,3] + errorbar_color --> :black + color --> :blue + label --> "Rebinned LC ($(new_binsize)s) with $(rebinned_lc.err_method) errors" + else + seriestype --> :steppost + linewidth --> 1.5 + color --> :blue + label --> "Rebinned LC ($(new_binsize)s bins)" + end + + return lc_matrix[:,1], lc_matrix[:,2] +end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index 12ca46a..2c58ac4 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -4,6 +4,7 @@ using FFTW, Distributions, Statistics, StatsBase, HDF5, FITSIO using Logging ,LinearAlgebra using CFITSIO using Random +using Plots include("test_fourier.jl") @testset "GTI" begin @@ -15,4 +16,9 @@ end @testset "lightcurve" begin include("test_lightcurve.jl") -end \ No newline at end of file +end + +@testset "recipes" begin + include("test_plotting/test_plots_recipes_lightcurve.jl") + include("test_plotting/test_plots_recipes_gti.jl") +end diff --git a/test/test_plotting/test_plots_recipes_gti.jl b/test/test_plotting/test_plots_recipes_gti.jl new file mode 100644 index 0000000..6d9f811 --- /dev/null +++ b/test/test_plotting/test_plots_recipes_gti.jl @@ -0,0 +1,83 @@ +# BTI Histogram Recipe Tests +let + # Suppress prints output during tests(prints to avoid length limit) + original_stdout = stdout + redirect_stdout(devnull) + + try + # Test 1: No BTIs + times = collect(0.0:1.0:100.0) + energies = rand(length(times)) .* 100 + gti_full = reshape([-1.0, 101.0], 1, 2) + metadata = FITSMetadata("test_file.fits", 1, "keV", Dict{String,Vector}(), + Dict{String,Any}(), gti_full, "GTI") + el_no_btis = EventList(times, energies, metadata) + p1 = plot(BTIAnalysisPlot(el_no_btis), bti_analysis=true) + @test p1 isa Plots.Plot + + # Test 2: With BTIs + times2 = collect(0.0:0.5:50.0) + energies2 = rand(length(times2)) .* 100 + gti_gaps = [0.0 10.0; 20.0 30.0; 40.0 50.0] # Creates BTIs at [10-20], [30-40] + metadata2 = FITSMetadata("test_file2.fits", 1, "keV", Dict{String,Vector}(), + Dict{String,Any}(), gti_gaps, "GTI") + el_with_btis = EventList(times2, energies2, metadata2) + p2 = plot(BTIAnalysisPlot(el_with_btis), bti_analysis=true) + @test p2 isa Plots.Plot + + # Test 3: Parameter variations with BTIs + p3 = plot(BTIAnalysisPlot(el_with_btis), bti_analysis=true, nbins=50) + @test p3 isa Plots.Plot + p4 = plot(BTIAnalysisPlot(el_with_btis), bti_analysis=true, nbins=20, xlims_range=(5, 15)) + @test p4 isa Plots.Plot + p5 = plot(BTIAnalysisPlot(el_with_btis), bti_analysis=true, ylims_range=(0.5, 5)) + @test p5 isa Plots.Plot + + # Test 4: Complex GTI pattern with multiple BTIs + times3 = collect(0.0:0.1:30.0) + energies3 = rand(length(times3)) .* 100 + # Creates multiple BTIs: [5-6], [10-12], [15-18], [22-25] + gti_complex = [0.0 5.0; 6.0 10.0; 12.0 15.0; 18.0 22.0; 25.0 30.0] + metadata3 = FITSMetadata("test_file3.fits", 1, "keV", Dict{String,Vector}(), + Dict{String,Any}(), gti_complex, "GTI") + el_complex = EventList(times3, energies3, metadata3) + p6 = plot(BTIAnalysisPlot(el_complex), bti_analysis=true) + @test p6 isa Plots.Plot + + # Test 5: Single very large BTI + times4 = [0.0, 1.0, 100.0, 101.0] + energies4 = [10.0, 20.0, 30.0, 40.0] + gti_large_gap = [0.0 1.0; 100.0 101.0] # Creates 99s BTI gap + metadata4 = FITSMetadata("test_file4.fits", 1, "keV", Dict{String,Vector}(), + Dict{String,Any}(), gti_large_gap, "GTI") + el_large_gap = EventList(times4, energies4, metadata4) + p8 = plot(BTIAnalysisPlot(el_large_gap), bti_analysis=true) + @test p8 isa Plots.Plot + + # Test 6: Edge case - very short BTIs only + times5 = collect(0.0:0.01:10.0) + energies5 = rand(length(times5)) .* 100 + # Creates very short BTIs: [1-1.1], [2-2.05], [3-3.02] + gti_short = [0.0 1.0; 1.1 2.0; 2.05 3.0; 3.02 10.0] + metadata5 = FITSMetadata("test_file5.fits", 1, "keV", Dict{String,Vector}(), + Dict{String,Any}(), gti_short, "GTI") + el_short_btis = EventList(times5, energies5, metadata5) + p9 = plot(BTIAnalysisPlot(el_short_btis), bti_analysis=true) + @test p9 isa Plots.Plot + + # Test 7: Mixed BTI lengths (short and long) + times6 = collect(0.0:0.1:100.0) + energies6 = rand(length(times6)) .* 100 + # Creates mixed BTIs: [5-5.1] (0.1s), [10-20] (10s), [30-30.5] (0.5s), [50-80] (30s) + gti_mixed = [0.0 5.0; 5.1 10.0; 20.0 30.0; 30.5 50.0; 80.0 100.0] + metadata6 = FITSMetadata("test_file6.fits", 1, "keV", Dict{String,Vector}(), + Dict{String,Any}(), gti_mixed, "GTI") + el_mixed_btis = EventList(times6, energies6, metadata6) + p10 = plot(BTIAnalysisPlot(el_mixed_btis), bti_analysis=true) + @test p10 isa Plots.Plot + + finally + # Always restore stdout + redirect_stdout(original_stdout) + end +end \ No newline at end of file diff --git a/test/test_plotting/test_plots_recipes_lightcurve.jl b/test/test_plotting/test_plots_recipes_lightcurve.jl new file mode 100644 index 0000000..089cc27 --- /dev/null +++ b/test/test_plotting/test_plots_recipes_lightcurve.jl @@ -0,0 +1,345 @@ +# EventList plotting tests +let + times = [1.0, 2.0, 3.0, 4.0, 5.0] + energies = [10.0, 20.0, 30.0, 40.0, 50.0] + el = EventList(times, energies) + + @test plot(el, 1.0) isa Plots.Plot + @test plot(el, 2.0) isa Plots.Plot +end + +# Empty EventList error handling +let + empty_el = EventList(Float64[], Float64[]) + @test_throws ErrorException plot(empty_el, 1.0) +end + +# Time range filtering +let + times = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0] + energies = [10.0, 20.0, 30.0, 40.0, 50.0, 60.0] + el = EventList(times, energies) + + @test plot(el, 1.0, tstart=2.0, tstop=5.0) isa Plots.Plot + @test plot(el, 1.0, tstart=2.0) isa Plots.Plot + @test plot(el, 1.0, tstop=4.0) isa Plots.Plot +end + +# Energy filtering +let + times = [1.0, 2.0, 3.0, 4.0, 5.0] + energies = [10.0, 20.0, 30.0, 40.0, 50.0] + el = EventList(times, energies) + + @test plot(el, 1.0, energy_filter=(15.0, 45.0)) isa Plots.Plot + @test plot(el, 1.0, energy_filter=(10.0, 30.0)) isa Plots.Plot +end + +# Error display options +let + times = [1.0, 2.0, 3.0, 4.0, 5.0] + energies = [10.0, 20.0, 30.0, 40.0, 50.0] + el = EventList(times, energies) + + @test plot(el, 1.0, show_errors=true) isa Plots.Plot + @test plot(el, 1.0, show_errors=false) isa Plots.Plot + @test plot(el, 1.0, show_errors=true, err_method=:poisson) isa Plots.Plot +end + +# GTI visualization +let + times = [1.0, 2.0, 3.0, 4.0, 5.0] + energies = [10.0, 20.0, 30.0, 40.0, 50.0] + gti_matrix = [1.0 3.0; 4.0 6.0] + meta = FITSMetadata{Dict{String,Any}}( + "test.fits", 2, "ENERGY", Dict{String,Vector}(), Dict{String,Any}(), + gti_matrix, "GTI" + ) + el = EventList(times, energies, meta) + + @test plot(el, 1.0, show_gtis=true) isa Plots.Plot + @test plot(el, 1.0, show_gti=true) isa Plots.Plot + @test plot(el, 1.0, show_gtis=true, gti_alpha=0.5) isa Plots.Plot +end + +# BTI visualization +let + times = [1.0, 2.0, 3.0, 4.0, 5.0] + energies = [10.0, 20.0, 30.0, 40.0, 50.0] + gti_matrix = [1.0 3.0; 4.0 6.0] + meta = FITSMetadata{Dict{String,Any}}( + "test.fits", 2, "ENERGY", Dict{String,Vector}(), Dict{String,Any}(), + gti_matrix, "GTI" + ) + el = EventList(times, energies, meta) + + @test plot(el, 1.0, show_btis=true) isa Plots.Plot + @test plot(el, 1.0, show_bti=true) isa Plots.Plot + @test plot(el, 1.0, show_btis=true, bti_alpha=0.4) isa Plots.Plot +end + +# External GTI support +let + times = [1.0, 2.0, 3.0, 4.0, 5.0] + energies = [10.0, 20.0, 30.0, 40.0, 50.0] + el = EventList(times, energies) + external_gtis = [1.0 2.5; 3.5 5.0] + + @test plot(el, 1.0, show_gtis=true, gtis=external_gtis) isa Plots.Plot + @test plot(el, 1.0, show_btis=true, gtis=external_gtis) isa Plots.Plot +end + +# Axis limits configuration +let + times = [1.0, 2.0, 3.0, 4.0, 5.0] + energies = [10.0, 20.0, 30.0, 40.0, 50.0] + el = EventList(times, energies) + + @test plot(el, 1.0, axis_limits=[1.5, 4.5, 0, 10]) isa Plots.Plot + @test plot(el, 1.0, axis_limits=[1.5, 4.5]) isa Plots.Plot + @test plot(el, 1.0, axis_limits=[nothing, 4.5, 0, nothing]) isa Plots.Plot +end + +# Invalid axis limits warning +let + times = [1.0, 2.0, 3.0, 4.0, 5.0] + energies = [10.0, 20.0, 30.0, 40.0, 50.0] + el = EventList(times, energies) + + @test plot(el, 1.0, axis_limits=[1.5, 4.5, 0]) isa Plots.Plot +end + +# LightCurve plotting +let + times = [1.0, 2.0, 3.0, 4.0, 5.0] + counts = [10, 15, 20, 12, 8] + errors = [3.0, 4.0, 5.0, 3.5, 2.8] + metadata = LightCurveMetadata("TEST", "TEST_INST", "TEST_OBJ", 58000.0, (1.0, 5.0), 1.0, Dict{String,Any}[], Dict{String,Any}()) + lc = LightCurve(times, 1.0, counts, errors, nothing, EventProperty{Float64}[], metadata, :poisson) + + @test plot(lc) isa Plots.Plot + @test plot(lc, show_errors=true) isa Plots.Plot + @test plot(lc, show_errors=false) isa Plots.Plot +end + +# LightCurve without errors +let + times = [1.0, 2.0, 3.0, 4.0, 5.0] + counts = [10, 15, 20, 12, 8] + metadata = LightCurveMetadata("TEST", "TEST_INST", "TEST_OBJ", 58000.0, (1.0, 5.0), 1.0, Dict{String,Any}[], Dict{String,Any}()) + lc = LightCurve(times, 1.0, counts, nothing, nothing, EventProperty{Float64}[], metadata, :poisson) + + @test plot(lc, show_errors=true) isa Plots.Plot +end + +# LightCurve with event properties +let + times = [1.0, 2.0, 3.0, 4.0, 5.0] + counts = [10, 15, 20, 12, 8] + errors = [3.0, 4.0, 5.0, 3.5, 2.8] + metadata = LightCurveMetadata("TEST", "TEST_INST", "TEST_OBJ", 58000.0, (1.0, 5.0), 1.0, Dict{String,Any}[], Dict{String,Any}()) + properties = [EventProperty(:mean_energy, [25.0, 30.0, 35.0, 28.0, 22.0], "keV")] + lc = LightCurve(times, 1.0, counts, errors, nothing, properties, metadata, :poisson) + + @test plot(lc, show_properties=true) isa Plots.Plot + @test plot(lc, show_properties=true, property_name=:mean_energy) isa Plots.Plot +end + +# Non-existent property handling +let + times = [1.0, 2.0, 3.0, 4.0, 5.0] + counts = [10, 15, 20, 12, 8] + errors = [3.0, 4.0, 5.0, 3.5, 2.8] + metadata = LightCurveMetadata("TEST", "TEST_INST", "TEST_OBJ", 58000.0, (1.0, 5.0), 1.0, Dict{String,Any}[], Dict{String,Any}()) + lc = LightCurve(times, 1.0, counts, errors, nothing, EventProperty{Float64}[], metadata, :poisson) + + @test plot(lc, show_properties=true, property_name=:nonexistent) isa Plots.Plot +end + +# LightCurve with GTI metadata +let + times = [1.0, 2.0, 3.0, 4.0, 5.0] + counts = [10, 15, 20, 12, 8] + errors = [3.0, 4.0, 5.0, 3.5, 2.8] + extra_data = Dict{String,Any}("gti_applied" => true, "gti_bounds" => [1.0, 5.0]) + metadata = LightCurveMetadata("TEST", "TEST_INST", "TEST_OBJ", 58000.0, (1.0, 5.0), 1.0, Dict{String,Any}[], extra_data) + lc = LightCurve(times, 1.0, counts, errors, nothing, EventProperty{Float64}[], metadata, :poisson) + + @test plot(lc, show_gtis=true) isa Plots.Plot + @test plot(lc, show_btis=true) isa Plots.Plot +end + +# LightCurve axis limits +let + times = [1.0, 2.0, 3.0, 4.0, 5.0] + counts = [10, 15, 20, 12, 8] + errors = [3.0, 4.0, 5.0, 3.5, 2.8] + metadata = LightCurveMetadata("TEST", "TEST_INST", "TEST_OBJ", 58000.0, (1.0, 5.0), 1.0, Dict{String,Any}[], Dict{String,Any}()) + lc = LightCurve(times, 1.0, counts, errors, nothing, EventProperty{Float64}[], metadata, :poisson) + + @test plot(lc, axis_limits=[1.5, 4.5, 5, 25]) isa Plots.Plot + @test plot(lc, axis_limits=[1.5, 4.5]) isa Plots.Plot +end + +# Segmented LightCurve plotting +let + times1 = [1.0, 2.0, 3.0] + counts1 = [10, 15, 20] + errors1 = [3.0, 4.0, 5.0] + metadata1 = LightCurveMetadata("TEST", "TEST_INST", "TEST_OBJ", 58000.0, (1.0, 3.0), 1.0, Dict{String,Any}[], Dict{String,Any}()) + lc1 = LightCurve(times1, 1.0, counts1, errors1, nothing, EventProperty{Float64}[], metadata1, :poisson) + + times2 = [4.0, 5.0, 6.0] + counts2 = [12, 8, 18] + errors2 = [3.5, 2.8, 4.2] + metadata2 = LightCurveMetadata("TEST", "TEST_INST", "TEST_OBJ", 58000.0, (4.0, 6.0), 1.0, Dict{String,Any}[], Dict{String,Any}()) + lc2 = LightCurve(times2, 1.0, counts2, errors2, nothing, EventProperty{Float64}[], metadata2, :poisson) + + segments = [lc1, lc2] + + @test plot(segments) isa Plots.Plot + @test plot(segments, show_errors=true) isa Plots.Plot + @test plot(segments, show_segment_boundaries=true) isa Plots.Plot + @test plot(segments, show_segment_boundaries=false) isa Plots.Plot +end + +# Segmented LightCurve with custom colors +let + times1 = [1.0, 2.0, 3.0] + counts1 = [10, 15, 20] + errors1 = [3.0, 4.0, 5.0] + metadata1 = LightCurveMetadata("TEST", "TEST_INST", "TEST_OBJ", 58000.0, (1.0, 3.0), 1.0, Dict{String,Any}[], Dict{String,Any}()) + lc1 = LightCurve(times1, 1.0, counts1, errors1, nothing, EventProperty{Float64}[], metadata1, :poisson) + + times2 = [4.0, 5.0, 6.0] + counts2 = [12, 8, 18] + errors2 = [3.5, 2.8, 4.2] + metadata2 = LightCurveMetadata("TEST", "TEST_INST", "TEST_OBJ", 58000.0, (4.0, 6.0), 1.0, Dict{String,Any}[], Dict{String,Any}()) + lc2 = LightCurve(times2, 1.0, counts2, errors2, nothing, EventProperty{Float64}[], metadata2, :poisson) + + segments = [lc1, lc2] + custom_colors = [:red, :green] + + @test plot(segments, segment_colors=custom_colors) isa Plots.Plot +end + +# Segmented LightCurve axis limits +let + times1 = [1.0, 2.0, 3.0] + counts1 = [10, 15, 20] + errors1 = [3.0, 4.0, 5.0] + metadata1 = LightCurveMetadata("TEST", "TEST_INST", "TEST_OBJ", 58000.0, (1.0, 3.0), 1.0, Dict{String,Any}[], Dict{String,Any}()) + lc1 = LightCurve(times1, 1.0, counts1, errors1, nothing, EventProperty{Float64}[], metadata1, :poisson) + + times2 = [4.0, 5.0, 6.0] + counts2 = [12, 8, 18] + errors2 = [3.5, 2.8, 4.2] + metadata2 = LightCurveMetadata("TEST", "TEST_INST", "TEST_OBJ", 58000.0, (4.0, 6.0), 1.0, Dict{String,Any}[], Dict{String,Any}()) + lc2 = LightCurve(times2, 1.0, counts2, errors2, nothing, EventProperty{Float64}[], metadata2, :poisson) + + segments = [lc1, lc2] + + @test plot(segments, axis_limits=[1.5, 5.5, 5, 25]) isa Plots.Plot + @test plot(segments, axis_limits=[1.5, 5.5]) isa Plots.Plot +end + +# Single segment handling +let + times1 = [1.0, 2.0, 3.0] + counts1 = [10, 15, 20] + errors1 = [3.0, 4.0, 5.0] + metadata1 = LightCurveMetadata("TEST", "TEST_INST", "TEST_OBJ", 58000.0, (1.0, 3.0), 1.0, Dict{String,Any}[], Dict{String,Any}()) + lc1 = LightCurve(times1, 1.0, counts1, errors1, nothing, EventProperty{Float64}[], metadata1, :poisson) + + segments = [lc1] + + @test plot(segments) isa Plots.Plot + @test plot(segments, show_segment_boundaries=true) isa Plots.Plot +end + +# Segmented LightCurve without errors +let + times1 = [1.0, 2.0, 3.0] + counts1 = [10, 15, 20] + metadata1 = LightCurveMetadata("TEST", "TEST_INST", "TEST_OBJ", 58000.0, (1.0, 3.0), 1.0, Dict{String,Any}[], Dict{String,Any}()) + lc1 = LightCurve(times1, 1.0, counts1, nothing, nothing, EventProperty{Float64}[], metadata1, :poisson) + + times2 = [4.0, 5.0, 6.0] + counts2 = [12, 8, 18] + metadata2 = LightCurveMetadata("TEST", "TEST_INST", "TEST_OBJ", 58000.0, (4.0, 6.0), 1.0, Dict{String,Any}[], Dict{String,Any}()) + lc2 = LightCurve(times2, 1.0, counts2, nothing, nothing, EventProperty{Float64}[], metadata2, :poisson) + + segments = [lc1, lc2] + + @test plot(segments, show_errors=true) isa Plots.Plot +end + +# Color cycling for multiple segments +let + segments = LightCurve[] + for i in 1:10 + times = [Float64(i), Float64(i+1)] + counts = [10, 15] + errors = [3.0, 4.0] + metadata = LightCurveMetadata("TEST", "TEST_INST", "TEST_OBJ", 58000.0, (Float64(i), Float64(i+1)), 1.0, Dict{String,Any}[], Dict{String,Any}()) + lc = LightCurve(times, 1.0, counts, errors, nothing, EventProperty{Float64}[], metadata, :poisson) + push!(segments, lc) + end + + @test plot(segments) isa Plots.Plot +end + +# GTI file loading error handling +let + times = [1.0, 2.0, 3.0, 4.0, 5.0] + energies = [10.0, 20.0, 30.0, 40.0, 50.0] + el = EventList(times, energies) + + @test plot(el, 1.0, show_gtis=true, gti_file="nonexistent.fits") isa Plots.Plot +end + +# Complex GTI/BTI visualization +let + times = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0] + energies = [10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0] + gti_matrix = [1.0 2.5; 3.5 5.0; 6.5 8.0] + meta = FITSMetadata{Dict{String,Any}}( + "test.fits", 2, "ENERGY", Dict{String,Vector}(), Dict{String,Any}(), + gti_matrix, "GTI" + ) + el = EventList(times, energies, meta) + + @test plot(el, 1.0, show_gtis=true, show_btis=true, tstart=0.5, tstop=8.5) isa Plots.Plot +end + +# GTI boundary edge cases +let + times = [2.0, 3.0, 4.0] + energies = [10.0, 20.0, 30.0] + gti_matrix = [1.0 2.5; 3.5 5.0] + meta = FITSMetadata{Dict{String,Any}}( + "test.fits", 2, "ENERGY", Dict{String,Vector}(), Dict{String,Any}(), + gti_matrix, "GTI" + ) + el = EventList(times, energies, meta) + + @test plot(el, 1.0, show_gtis=true, tstart=2.0, tstop=4.0) isa Plots.Plot + @test plot(el, 1.0, show_btis=true, tstart=2.0, tstop=4.0) isa Plots.Plot +end +# Basic rebinning functionality test +let + times = collect(1.0:0.1:10.0) + counts = rand(1:10, length(times)) + + # Create mock light curve + metadata = LightCurveMetadata( + "TEST", "TEST", "TEST", 0.0, (1.0, 10.0), 0.1, + [Dict{String,Any}()], Dict{String,Any}() + ) + lc = LightCurve(times, 0.1, counts, nothing, nothing, + EventProperty{Float64}[], metadata, :poisson) + + @test plot(lc, 1.0) isa Plots.Plot + @test plot(lc, 0.5) isa Plots.Plot + @test plot(lc, 2.0) isa Plots.Plot +end