"""
Web application module for AstroQ.
"""
# Standard library imports
import base64
import os
import pickle
import threading
from configparser import ConfigParser
from io import BytesIO
# Third-party imports
import imageio.v3 as iio
import numpy as np
import pandas as pd
import plotly.io as pio
from flask import Flask, render_template, request, abort
from socket import gethostname
# Local imports
import astroq.nplan as nplan
import astroq.plot as pl
import astroq.splan as splan
from astroq.splan import SemesterPlanner
from astroq.nplan import NightPlanner
from astroq.nplan import get_nightly_times_from_allocation
running_on_keck_machines = False
app = Flask(__name__, template_folder="../templates")
# Global variables to store loaded data
data_astroq = None
data_ttp = None
semester_planner = None
night_planner = None
uptree_path = None
semester_planner_timestamp = None
[docs]
def load_data_for_path(semester_code, date, band, uptree_path):
"""
Load data for a specific semester_code/date/band combination
Args:
semester_code (str): the semester code
date (str): the date in YYYY-MM-DD format
band (str): the band
uptree_path (str): the path to the uptree directory
Returns:
success (bool): True if data loaded successfully, False otherwise
"""
global data_astroq, data_ttp, semester_planner, night_planner, request_frame_path, night_start_time
# Construct the workdir path based on URL parameters
workdir = os.path.join(uptree_path, semester_code, date, band, "outputs")
request_frame_path = os.path.join(workdir, 'request_selected.csv')
# Check if the directory exists
if not os.path.exists(workdir):
return False, f"Directory not found: {workdir}"
semester_planner_h5 = os.path.join(workdir, 'semester_planner.h5')
night_planner_h5 = os.path.join(workdir, 'night_planner.h5')
# Load semester planner
global semester_planner_timestamp
try:
semester_planner = SemesterPlanner.from_hdf5(semester_planner_h5)
data_astroq = pl.process_stars(semester_planner)
# Get file modification time
if os.path.exists(semester_planner_h5):
from datetime import datetime
mtime = os.path.getmtime(semester_planner_h5)
semester_planner_timestamp = datetime.fromtimestamp(mtime).strftime('%Y-%m-%d %H:%M:%S')
else:
semester_planner_timestamp = None
except Exception as e:
semester_planner = None
data_astroq = None
semester_planner_timestamp = None
return False, f"Error loading semester planner: {str(e)}"
# Load night planner (optional)
try:
print(night_planner_h5)
night_planner = NightPlanner.from_hdf5(night_planner_h5)
data_ttp = night_planner.solution
# Get the night start time from allocation file (this is "Minute 0")
night_start_time, _ = nplan.get_nightly_times_from_allocation(
night_planner.allocation_file,
night_planner.current_day
)
except Exception as e:
print(f"No night planner found")
# import traceback
# traceback.print_exc()
night_planner = None
data_ttp = None
return True, "Data loaded successfully"
# New homepage with navigation instructions
[docs]
@app.route("/", methods=["GET"])
def index():
navigation_text = """
To navigate, append to the URL in the following way:
url/{semester_code}/{date}/{band}/{page}
where:
- semester_code is the four digit year and one letter semester
- date is in format YYYY-MM-DD
- band is either band1, band2, or band3 (or full-band1, full-band2, or full-band3)
- page is one of: {program_code}, {program_code}/{starname}, nightplan, or admin
Examples:
- /2025B/2025-01-15/band1/admin
- /2025B/2025-01-15/band3/nightplan
- /2025B/2025-01-15/band1/2025B_N001 (program overview)
- /2025B/2025-01-15/band1/2025B_N001/HD4614 (star under program)
Note: program_code contains the semester information. Correct: 2025B_N001, Incorrect: N001
Note: You only have access to the programs and stars for which you are a PI or named Co-I on the proposal coversheet.
Note: Access to nightplan pages is for observers.
Note: Access to admin pages is for the queue manager and observatory staff.
"""
return render_template("homepage.html", navigation_text=navigation_text)
# Star page: /semester/date/band/program_code/starname (star under program)
[docs]
@app.route("/<semester_code>/<date>/<band>/<program_code>/<starname>")
def star_page(semester_code, date, band, program_code, starname):
"""Handle star page route: star is under program in URL."""
global uptree_path
if band not in ['band1', 'band2', 'band3', 'full-band1', 'full-band2', 'full-band3']:
abort(400, description="Band must be 'band1', 'band2', 'band3', 'full-band1', 'full-band2', or 'full-band3'")
success, message = load_data_for_path(semester_code, date, band, uptree_path)
if not success:
return f"Error: {message}", 404
return render_star_page(starname, program_code)
# Dynamic route for program, admin, nightplan
[docs]
@app.route("/<semester_code>/<date>/<band>/<page>")
def dynamic_page(semester_code, date, band, page):
"""Handle program, admin, and nightplan routes."""
global uptree_path
if band not in ['band1', 'band2', 'band3', 'full-band1', 'full-band2', 'full-band3']:
abort(400, description="Band must be 'band1', 'band2', 'band3', 'full-band1', 'full-band2', or 'full-band3'")
success, message = load_data_for_path(semester_code, date, band, uptree_path)
if not success:
return f"Error: {message}", 404
if page == "admin":
return render_admin_page(semester_code, date, band)
elif page == "nightplan":
return render_nightplan_page(band)
elif page in data_astroq[0]:
return render_program_page(semester_code, date, band, page)
else:
abort(404, description=f"Page '{page}' not found")
[docs]
def render_admin_page(semester_code, date, band):
"""Render the admin page"""
if data_astroq is None:
return "Error: No data available", 404
all_stars_from_all_programs = np.concatenate(list(data_astroq[0].values()))
# Get request frame table for all stars, with starname as links under program
request_df = pl.get_request_frame(semester_planner, all_stars_from_all_programs)
request_table_html = pl.request_frame_to_html(request_df, semester_code, date, band)
fig_cof1 = pl.get_cof(semester_planner, list(data_astroq[1].values()))
fig_cof2 = pl.get_cof(semester_planner, list(data_astroq[1].values()), use_time=True)
fig_birdseye = pl.get_birdseye(semester_planner, data_astroq[2], list(data_astroq[1].values()))
fig_football = pl.get_football(semester_planner, all_stars_from_all_programs, use_program_colors=True)
fig_tau_inter_line = pl.get_tau_inter_line(semester_planner, all_stars_from_all_programs, use_program_colors=True)
fig_timebar = pl.get_timebar(semester_planner, all_stars_from_all_programs, use_program_colors=True)
fig_timebar_by_program = pl.get_timebar_by_program(semester_planner, data_astroq[0])
fig_rawobs = pl.get_rawobs(semester_planner, all_stars_from_all_programs, use_program_colors=True)
fig_cof_html1 = pio.to_html(fig_cof1, full_html=True, include_plotlyjs='cdn')
fig_cof_html2 = pio.to_html(fig_cof2, full_html=True, include_plotlyjs='cdn')
fig_birdseye_html = pio.to_html(fig_birdseye, full_html=True, include_plotlyjs='cdn')
fig_football_html = pio.to_html(fig_football, full_html=True, include_plotlyjs='cdn')
fig_tau_inter_line_html = pio.to_html(fig_tau_inter_line, full_html=True, include_plotlyjs='cdn')
fig_timebar_html = pio.to_html(fig_timebar, full_html=True, include_plotlyjs='cdn')
fig_timebar_by_program_html = pio.to_html(fig_timebar_by_program, full_html=True, include_plotlyjs='cdn')
fig_rawobs_html = pio.to_html(fig_rawobs, full_html=True, include_plotlyjs='cdn')
figures_html = [fig_timebar_html, fig_timebar_by_program_html, fig_cof_html1, fig_cof_html2, fig_birdseye_html, fig_rawobs_html, fig_tau_inter_line_html, fig_football_html]
past_history_table_html = pl.past_history_table(semester_planner, all_stars_from_all_programs)
return render_template("admin.html", tables_html=[request_table_html, past_history_table_html], figures_html=figures_html, timestamp=semester_planner_timestamp)
[docs]
def render_program_page(semester_code, date, band, program_code):
"""Render the program overview page for a specific program"""
if data_astroq is None:
return "Error: No data available", 404
# Get all stars in the specified program
if program_code not in data_astroq[0]:
return f"Error: Program {program_code} not found", 404
program_stars = data_astroq[0][program_code]
# Get request frame table for this program's stars, with starname as links
request_df = pl.get_request_frame(semester_planner, program_stars)
request_table_html = pl.request_frame_to_html(request_df, semester_code, date, band)
# Create overview figures for this program
fig_cof = pl.get_cof(semester_planner, program_stars)
fig_birdseye = pl.get_birdseye(semester_planner, data_astroq[2], program_stars)
fig_tau_inter_line = pl.get_tau_inter_line(semester_planner, program_stars)
fig_football = pl.get_football(semester_planner, program_stars)
fig_timebar = pl.get_timebar(semester_planner, program_stars, use_program_colors=True)
fig_rawobs = pl.get_rawobs(semester_planner, program_stars)
fig_cof_html = pio.to_html(fig_cof, full_html=True, include_plotlyjs='cdn')
fig_birdseye_html = pio.to_html(fig_birdseye, full_html=True, include_plotlyjs='cdn')
fig_tau_inter_line_html = pio.to_html(fig_tau_inter_line, full_html=True, include_plotlyjs='cdn')
fig_football_html = pio.to_html(fig_football, full_html=True, include_plotlyjs='cdn')
fig_timebar_html = pio.to_html(fig_timebar, full_html=True, include_plotlyjs='cdn')
fig_rawobs_html = pio.to_html(fig_rawobs, full_html=True, include_plotlyjs='cdn')
figures_html = [fig_timebar_html, fig_cof_html, fig_birdseye_html, fig_rawobs_html, fig_tau_inter_line_html, fig_football_html]
past_history_table_html = pl.past_history_table(semester_planner, program_stars)
return render_template("semesterplan.html",
programname=program_code,
tables_html=[request_table_html, past_history_table_html],
figures_html=figures_html,
programs=[program_code],
timestamp=semester_planner_timestamp)
[docs]
def render_star_page(starname, program_code=None):
"""Render a specific star page. If program_code is given, only look in that program."""
if data_astroq is None:
return "Error: No data available", 404
compare_starname = starname.lower().replace(' ', '') # Lower case and remove all spaces
programs_to_search = [program_code] if program_code and program_code in data_astroq[0] else data_astroq[0].keys()
for program in programs_to_search:
for star_ind in range(len(data_astroq[0][program])):
star_obj = data_astroq[0][program][star_ind]
true_starname = star_obj.starname
object_compare_starname = true_starname.lower().replace(' ', '')
if object_compare_starname == compare_starname:
# Get request frame table for this specific star (no star links needed)
request_df = pl.get_request_frame(semester_planner, [star_obj])
request_table_html = pl.request_frame_to_html(request_df)
fig_cof = pl.get_cof(semester_planner, [data_astroq[0][program][star_ind]])
fig_birdseye = pl.get_birdseye(semester_planner, data_astroq[2], [star_obj])
fig_tau_inter_line = pl.get_tau_inter_line(semester_planner, [star_obj])
fig_football = pl.get_football(semester_planner, [star_obj])
fig_rawobs = pl.get_rawobs(semester_planner, [star_obj])
fig_cof_html = pio.to_html(fig_cof, full_html=True, include_plotlyjs='cdn')
fig_birdseye_html = pio.to_html(fig_birdseye, full_html=True, include_plotlyjs='cdn')
fig_tau_inter_line_html = pio.to_html(fig_tau_inter_line, full_html=True, include_plotlyjs='cdn')
fig_football_html = pio.to_html(fig_football, full_html=True, include_plotlyjs='cdn')
fig_rawobs_html = pio.to_html(fig_rawobs, full_html=True, include_plotlyjs='cdn')
past_history_table_html = pl.past_history_table(semester_planner, [star_obj])
tables_html = [request_table_html, past_history_table_html]
figures_html = [fig_cof_html, fig_birdseye_html, fig_rawobs_html, fig_tau_inter_line_html, fig_football_html]
return render_template("star.html", starname=true_starname, tables_html=tables_html, figures_html=figures_html, timestamp=semester_planner_timestamp)
return f"Error, star {starname} not found in programs {list(program_names)}"
[docs]
def render_nightplan_page(band):
"""Render the night plan page"""
if data_ttp is None:
return "Error: No night planner data available", 404
plots = ['script_table', 'slewgif', 'ladder', 'slewpath']
script_table_df = pl.get_script_plan(night_planner)
ladder_fig = pl.get_ladder(data_ttp, night_start_time)
slew_animation_fig = pl.get_slew_animation_plotly(data_ttp, request_frame_path, animationStep=120)
slew_path_fig = pl.plot_path_2D_interactive(data_ttp, night_start_time=night_start_time)
script_table_html = pl.nightplan_table_to_html(script_table_df, table_id='script-table', page_size=100)
# Convert figures to HTML
ladder_html = pio.to_html(ladder_fig, full_html=True, include_plotlyjs='cdn')
slew_animation_html = pio.to_html(slew_animation_fig, full_html=True, include_plotlyjs='cdn')
slew_path_html = pio.to_html(slew_path_fig, full_html=True, include_plotlyjs='cdn')
figure_html_list = [script_table_html, ladder_html, slew_animation_html, slew_path_html]
return render_template("nightplan.html", starname=None, figure_html_list=figure_html_list,
semester_planner=semester_planner, night_planner=night_planner, band=band)
[docs]
@app.route("/<semester_code>/<date>/<band>/download_nightplan")
def download_nightplan(semester_code, date, band):
"""Download the Magiq formatted night plan file"""
global uptree_path, semester_planner, night_planner
# Validate parameters
if band not in ['band1', 'band3']:
abort(400, description="Band must be 'band1' or 'band3'")
# Load data for this path
success, message = load_data_for_path(semester_code, date, band, uptree_path)
if not success:
return f"Error: {message}", 404
if semester_planner is None or night_planner is None:
return "Error: No planner data available", 404
try:
# Construct the path to the script file
script_file_path = os.path.join(semester_planner.output_directory,
f'script_{night_planner.current_day}_nominal.txt')
if not os.path.exists(script_file_path):
return "Error: Night plan file not found", 404
# Return the file for download
from flask import send_file
return send_file(script_file_path,
as_attachment=True,
download_name=f'script_{night_planner.current_day}_nominal.txt',
mimetype='text/plain')
except Exception as e:
return f"Error downloading file: {str(e)}", 500
[docs]
def launch_app(uptree_path_param):
"""Launch the Flask app"""
global uptree_path
uptree_path = uptree_path_param
if running_on_keck_machines:
app.run(host=gethostname(), debug=False, use_reloader=False, port=50001)
else:
app.run(debug=True, use_reloader=True, port=50001)
if __name__ == "__main__":
launch_app(".")