import sqlite3
import numpy as np
from scipy.interpolate import UnivariateSpline
from ska_ost_senscalc.utilities import STATIC_DATA_PATH
[docs]
class SEFDTable:
"""
Encapsulation of the SEFD lookup table to be used for SKA LOW calculations
"""
def __init__(self, db_path=None):
if db_path is None:
db_path = STATIC_DATA_PATH / "lookups/ska_station_sensitivity_AAVS2.db"
self._db_path = db_path
[docs]
def lookup_stokes_i_sefd(
self,
az: float,
el: float,
start_lst: float,
end_lst: float,
freq_fine_mhz: np.ndarray,
) -> np.ndarray:
"""
Look up the SEFD values from the database corresponding to the local coordinates
(az, el) between LST start_lst and end_lst.
:return: an array of Stokes I SEFD values at frequencies in fine_freq_mhz.
"""
# Now that we all the relevant boundary conditions for this time cell,
# query the DB and get the A-over-T values (labelled as sensitivity) corresponding
# to a coarse grid of frequency values from 50 to 350 MHz (35 elements on the coarse grid).
freq_elements_per_query = 35
with sqlite3.connect(self._db_path) as sql_connection:
# Since end_lst time can wrap around 24h, create an appropriate SQL string
if end_lst < start_lst:
lst_clause = (
"((lst>=:start_lst AND lst<24.0) OR (lst>=0 AND lst<=:end_lst))"
)
else:
lst_clause = "(lst>=:start_lst AND lst<=:end_lst)"
# Build the params along with the query. These will be passed separately to the psycopg cursor, which avoids the risk of SQL injection.
params = {"start_lst": start_lst, "end_lst": end_lst}
# The azim_deg and za_deg values in the table increase in 5 degree steps, so in the
# query we match to the closest step by checking if the difference is less
# than 2.5 degrees rather than using an exact equals.
azim_and_za_deg_clause = "ABS(za_deg-(90-:el))<=2.5"
params["el"] = el
# The zenith is represented in the database as az=za=0 whilst normally there are multiple azimuth values for each zenith angle.
# To ensure a result is retured for queries near to the zenith, we only query on azimuth if the source is not close to the zenith.
if 90 - el >= 2.5:
if az > 357.5:
# azim_deg has values in the table for range 0 <= azim_deg < 360, so for az > 357.5 we want
# to match azim_deg=0 rather than azim_deg=360
azim_and_za_deg_clause += " AND ABS(azim_deg+(360-:az))<=2.5"
else:
azim_and_za_deg_clause += " AND ABS(azim_deg-:az)<=2.5"
params["az"] = az
sql_query = (
"SELECT frequency_mhz,sensitivity "
"FROM Sensitivity "
"WHERE polarisation=:polarisation AND "
f"{azim_and_za_deg_clause} AND {lst_clause}"
)
# Fetch X polarization A-over-T
fetched_rows_x = sql_connection.execute(
sql_query, {**params, "polarisation": "X"}
).fetchall()
freq_x, aot_x = list(zip(*fetched_rows_x))
# Fetch Y polarization A-over-T
fetched_rows_y = sql_connection.execute(
sql_query, {**params, "polarisation": "Y"}
).fetchall()
freq_y, aot_y = list(zip(*fetched_rows_y))
# Due to a bug in how the LOW SEFD database was generated, sometimes the
# db query returns multiple rows. This is a temporary fix by reading just the first
# row. We can get rid of this fix once a new SEFD table is generated during
# AAVS3 commissioning.
if len(freq_x) > freq_elements_per_query:
freq_x = freq_x[:freq_elements_per_query]
freq_y = freq_y[:freq_elements_per_query]
aot_x = aot_x[:freq_elements_per_query]
aot_y = aot_y[:freq_elements_per_query]
# Convert A-over-T to SEFD
# Note that the Boltzmann constant here has units [Jy m^2 / K]
sefd_x_coarse = 2 * 1380.0 / np.asarray(aot_x)
sefd_y_coarse = 2 * 1380.0 / np.asarray(aot_y)
# Fit a cubic spline to coarse SEFD and interpolate to get
# SEFD corresponding to the fine frequency grid.
spline_x = UnivariateSpline(freq_x, sefd_x_coarse, s=0)
spline_y = UnivariateSpline(freq_y, sefd_y_coarse, s=0)
sefd_x_fine = spline_x(freq_fine_mhz)
sefd_y_fine = spline_y(freq_fine_mhz)
sefd_i_fine = 0.5 * np.sqrt(sefd_x_fine**2 + sefd_y_fine**2)
return sefd_i_fine