From 3f4202c15ee7e63bc445423089381d575bdaac43 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 20 Jan 2026 11:19:12 -0700 Subject: [PATCH 001/295] Created files for starting migration work for extract_pointing. --- src/astrohack/core/extract_pointing_2.py | 380 +++++++++++++++++++++++ src/astrohack/extract_pointing_2.py | 106 +++++++ src/astrohack/io/point_mds.py | 15 + 3 files changed, 501 insertions(+) create mode 100644 src/astrohack/core/extract_pointing_2.py create mode 100644 src/astrohack/extract_pointing_2.py create mode 100644 src/astrohack/io/point_mds.py diff --git a/src/astrohack/core/extract_pointing_2.py b/src/astrohack/core/extract_pointing_2.py new file mode 100644 index 00000000..c503f88f --- /dev/null +++ b/src/astrohack/core/extract_pointing_2.py @@ -0,0 +1,380 @@ +import os + +import dask +import numpy as np +import toolviper.utils.logger as logger +import xarray as xr +import copy + +from astrohack.utils.conversion import convert_dict_from_numba +from astrohack.utils.file import load_point_file +from astrohack.utils.tools import get_valid_state_ids +from casacore import tables as ctables +from numba import njit +from numba.core import types +from numba.typed import Dict +from scipy import spatial + + +def process_extract_pointing(ms_name, pnt_name, exclude, parallel=True): + """Top level function to extract subset of pointing table data into a dictionary of xarray data arrays. + + Args: + exclude (): + ms_name (str): Measurement file name. + pnt_name (str): Output pointing dictionary file name. + parallel (bool, optional): Process in parallel. Defaults to True. + + Returns: + dict: pointing dictionary of xarray data arrays + """ + + # Get antenna names and ids + ctb = ctables.table( + os.path.join(ms_name, "ANTENNA"), + readonly=True, + lockoptions={"option": "usernoread"}, + ack=False, + ) + + antenna_name = ctb.getcol("NAME") + ctb.close() + + antenna_id = list(range(len(antenna_name))) + + # Exclude antennas according to user direction + if exclude: + if not isinstance(exclude, list): + exclude = list(exclude) + for i_ant, antenna in enumerate(exclude): + if antenna in antenna_name: + antenna_name.remove(antenna) + antenna_id.remove(i_ant) + + antenna_id = np.array(antenna_id) + + # Get Holography scans with start and end times. + ctb = ctables.table( + ms_name, + readonly=True, + lockoptions={"option": "usernoread"}, + ack=False, + ) + scan_ids = ctb.getcol("SCAN_NUMBER") + time = ctb.getcol("TIME") + ddi = ctb.getcol("DATA_DESC_ID") + state_ids = ctb.getcol("STATE_ID") + ctb.close() + + # Get state ids where holography is done + ctb = ctables.table( + os.path.join(ms_name, "STATE"), + readonly=True, + lockoptions={"option": "usernoread"}, + ack=False, + ) + # scan intent (with subscan intent) is stored in the OBS_MODE column of the STATE sub-table. + obs_modes = ctb.getcol("OBS_MODE") + ctb.close() + mapping_state_ids = get_valid_state_ids(obs_modes) + + mapping_state_ids = np.array(mapping_state_ids) + + # For each ddi get holography scan start and end times: + scan_time_dict = _extract_scan_time_dict( + time, scan_ids, state_ids, ddi, mapping_state_ids + ) + + point_meta_ds = xr.Dataset() + point_meta_ds.attrs["mapping_state_ids"] = mapping_state_ids + point_meta_ds.to_zarr(pnt_name, mode="w", compute=True, consolidated=True) + + ########################################################################################### + pnt_params = {"pnt_name": pnt_name, "scan_time_dict": scan_time_dict} + + if parallel: + delayed_pnt_list = [] + for i_ant in range(len(antenna_id)): + this_pars = copy.deepcopy(pnt_params) + this_pars["ant_id"] = antenna_id[i_ant] + this_pars["ant_name"] = antenna_name[i_ant] + + delayed_pnt_list.append( + dask.delayed(_make_ant_pnt_chunk)(ms_name, this_pars) + ) + + dask.compute(delayed_pnt_list) + + else: + for i_ant in range(len(antenna_id)): + pnt_params["ant_id"] = antenna_id[i_ant] + pnt_params["ant_name"] = antenna_name[i_ant] + + _make_ant_pnt_chunk(ms_name, pnt_params) + + return load_point_file(pnt_name, diagnostic=True) + + +def _make_ant_pnt_chunk(ms_name, pnt_params): + """Extract subset of pointing table data into a dictionary of xarray data arrays. This is written to disk as a + zarr file. This function processes a chunk the overall data and is managed by Dask. + + Args: + ms_name (str): Measurement file name. + ant_id (int): Antenna id + pnt_name (str): Name of output pointing dictionary file name. + """ + + ant_id = pnt_params["ant_id"] + ant_name = pnt_params["ant_name"] + pnt_name = pnt_params["pnt_name"] + scan_time_dict = pnt_params["scan_time_dict"] + + table_obj = ctables.table( + os.path.join(ms_name, "POINTING"), + readonly=True, + lockoptions={"option": "usernoread"}, + ack=False, + ) + + tb = ctables.taql( + "select DIRECTION, TIME, TARGET, ENCODER, ANTENNA_ID, POINTING_OFFSET from $table_obj WHERE ANTENNA_ID == %s" + % ant_id + ) + + # NB: Add check if directions reference frame is Azemuth Elevation (AZELGEO) + try: + direction = tb.getcol("DIRECTION")[:, 0, :] + target = tb.getcol("TARGET")[:, 0, :] + encoder = tb.getcol("ENCODER") + direction_time = tb.getcol("TIME") + pointing_offset = tb.getcol("POINTING_OFFSET")[:, 0, :] + + except Exception: + tb.close() + logger.warning("Skipping antenna " + str(ant_id) + " no pointing info") + + return 0 + + tb.close() + table_obj.close() + + evaluate_time_samping(direction_time, ant_name) + + pnt_xds = xr.Dataset() + coords = {"time": direction_time} + pnt_xds = pnt_xds.assign_coords(coords) + + # Measurement set v2 definition: https://drive.google.com/file/d/1IapBTsFYnUT1qPu_UK09DIFGM81EIZQr/view?usp=sharing + # DIRECTION: Antenna pointing direction + pnt_xds["DIRECTION"] = xr.DataArray(direction, dims=("time", "az_el")) + + # ENCODER: The current encoder values on the primary axes of the mount type for the antenna, expressed as a + # Direction Measure. + pnt_xds["ENCODER"] = xr.DataArray(encoder, dims=("time", "az_el")) + + # TARGET: This is the true expected position of the source, including all coordinate corrections such as precession, + # nutation etc. + pnt_xds["TARGET"] = xr.DataArray(target, dims=("time", "az_el")) + + # POINTING_OFFSET: The a priori pointing corrections applied by the telescope in pointing to the DIRECTION position, + # optionally expressed as polynomial coefficients. + pnt_xds["POINTING_OFFSET"] = xr.DataArray(pointing_offset, dims=("time", "az_el")) + + # Calculate directional cosines (l,m) which are used as the gridding locations. + # See equations 8,9 in https://library.nrao.edu/public/memos/evla/EVLAM_212.pdf. + # TARGET: A_s, E_s (target source position) + # DIRECTION: A_a, E_a (Antenna's pointing direction) + + # ## NB: Is VLA's definition of Azimuth the same for ALMA, MeerKAT, etc.? (positive for a clockwise rotation from + # north, viewed from above) ## NB: Compare with calculation using WCS in astropy. + l = np.cos(target[:, 1]) * np.sin(target[:, 0] - direction[:, 0]) + m = np.sin(target[:, 1]) * np.cos(direction[:, 1]) - np.cos(target[:, 1]) * np.sin( + direction[:, 1] + ) * np.cos(target[:, 0] - direction[:, 0]) + + pnt_xds["DIRECTIONAL_COSINES"] = xr.DataArray( + np.array([l, m]).T, dims=("time", "lm") + ) + + """ + Notes from ASDM (https://drive.google.com/file/d/16a3g0GQxgcO7N_ZabfdtexQ8r2jRbYIS/view) + Science Data Model Binary Data Format: https://drive.google.com/file/d/1PMrZFbkrMVfe57K6AAh1dR1FalS35jP2/view + + A - ASDM, MS - MS + + A_encoder = The values measured from the antenna. They may be however affected by metrology, if applied. Note + that for ALMA this column will contain positions obtained using the AZ POSN RSP and EL POSN RSP monitor points of + the ACU and not the GET AZ ENC and GET EL ENC monitor points (as these do not include the metrology corrections). + It is agreed that the the vendor pointing model will never be applied. AZELNOWAntenna.position + + A_pointing_direction : This is the commanded direction of the antenna. It is obtained by adding the target and + offset columns, and then applying the pointing model referenced by PointingModelId. The pointing model can be the + composition of the absolute pointing model and of a local pointing model. In that case their coefficients will + both be in the PointingModel table. A_target : This is the field center direction (as given in the Field Table), + possibly affected by the optional antenna-based sourceOffset. This column is in horizontal coordinates. + + AZELNOWAntenna.position A_offset : Additional offsets in horizontal coordinates (usually meant for measuring the + pointing corrections, mapping the antenna beam, ...). AZELNOWAntenna.positiontarget A_sourceOffset : Optionally, + the antenna-based mapping offsets in the field. These are in the equatorial system, and used, for instance, + in on-the-fly mapping when the antennas are driven independently across the field. + + + M_direction = rotate(A_target,A_offset) #A_target is rotated to by A_offset + if withPointingCorrection: + M_direction = rotate(A_target,A_offset) + (A_encoder - A_pointing_direction) + + M_target = A_target + M_pointing_offset = A_offset + M_encoder = A_encoder + + From the above description I suspect encoder should be used instead of direction, however for the VLA mapping + antenna data no grid pattern appears (ALMA data does not have this problem).""" + + # Detect during which scans an antenna is mapping by averaging the POINTING_OFFSET radius. + time_tree = spatial.KDTree(direction_time[:, None]) # Use for nearest interpolation + + mapping_scans_obs_dict = {} + + for ddi_id, ddi in scan_time_dict.items(): + map_scans_dict = {} + map_id = 0 + + for scan_id, scan_time in ddi.items(): + _, time_index = time_tree.query(scan_time[:, None]) + + pointing_offset_scan_slice = pnt_xds["POINTING_OFFSET"].isel( + time=slice(time_index[0], time_index[1]) + ) + + r = ( + np.sqrt( + pointing_offset_scan_slice.isel(az_el=0) ** 2 + + pointing_offset_scan_slice.isel(az_el=1) ** 2 + ) + ).mean() + + if r > 10**-12: # Antenna is mapping since lm is non-zero + if ("map_" + str(map_id)) in map_scans_dict: + map_scans_dict["map_" + str(map_id)].append(scan_id) + + else: + map_scans_dict["map_" + str(map_id)] = [scan_id] + + else: + map_id = map_id + 1 + + mapping_scans_obs_dict["ddi_" + str(ddi_id)] = map_scans_dict + + pnt_xds.attrs["mapping_scans_obs_dict"] = [mapping_scans_obs_dict] + ############### + + pnt_xds.attrs["ant_name"] = pnt_params["ant_name"] + + logger.debug( + "Writing pointing xds to {file}".format( + file=os.path.join(pnt_name, "ant_" + str(ant_name)) + ) + ) + + pnt_xds.to_zarr( + os.path.join(pnt_name, "ant_{}".format(str(ant_name))), + mode="w", + compute=True, + consolidated=True, + ) + + +def _extract_scan_time_dict(time, scan_ids, state_ids, ddi_ids, mapping_state_ids): + """ + [ddi][scan][start, stop] + """ + scan_time_dict = _extract_scan_time_dict_jit( + time, scan_ids, state_ids, ddi_ids, mapping_state_ids + ) + + # This section cleans up the case of when there was an issue with the scan time data. If the scan start and end + # times are the same the mapping(reference) state identification does not work. For this reason, scans containing + # instance of this are removed and any empty ddi(s) are dropped as well. + drops = {} + + for ddi in scan_time_dict.keys(): + drops[ddi] = [] + + for scan in scan_time_dict[ddi].keys(): + if scan_time_dict[ddi][scan][0] == scan_time_dict[ddi][scan][1]: + drops[ddi].append(scan) + + for ddi, scans in drops.items(): + for scan in scans: + del scan_time_dict[ddi][scan] + + if len(scan_time_dict[ddi]) == 0: + del scan_time_dict[ddi] + + return scan_time_dict + + +@convert_dict_from_numba +@njit(cache=False, nogil=True) +def _extract_scan_time_dict_jit(time, scan_ids, state_ids, ddi_ids, mapping_state_ids): + """For each ddi get holography scan start and end times. A holography scan is detected when a scan_ids appears in + mapping_state_ids. + + """ + d1 = Dict.empty( + key_type=types.int64, + value_type=np.zeros(2, dtype=types.float64), + ) + + scan_time_dict = Dict.empty( + key_type=types.int64, + value_type=d1, + ) + + mapping_scans = set() + + for i, s in enumerate(scan_ids): + s = types.int64(s) + t = time[i] + ddi = ddi_ids[i] + + state_id = state_ids[i] + + if state_id in mapping_state_ids: + mapping_scans.add(s) + if ddi in scan_time_dict: + if s in scan_time_dict[ddi]: + if scan_time_dict[ddi][s][0] > t: + scan_time_dict[ddi][s][0] = t + + if scan_time_dict[ddi][s][1] < t: + scan_time_dict[ddi][s][1] = t + + else: + scan_time_dict[ddi][s] = np.array([t, t]) + + else: + scan_time_dict[ddi] = {s: np.array([t, t])} + + return scan_time_dict + + +def evaluate_time_samping( + time_sampling, data_label, threshold=0.01, expected_interval=0.1 +): + bin_sz = expected_interval / 4 + time_bin_edge = np.arange(-bin_sz / 2, 2.5 * expected_interval, bin_sz) + time_bin_axis = time_bin_edge[:-1] + bin_sz / 2 + i_mid = int(np.argmin(np.abs(time_bin_axis - expected_interval))) + + intervals = np.diff(time_sampling) + hist, edges = np.histogram(intervals, bins=time_bin_edge) + n_total = np.sum(hist) + outlier_fraction = 1 - hist[i_mid] / n_total + + if outlier_fraction > threshold: + logger.warning( + f"{data_label} pointing table has {100*outlier_fraction:.2}% of data with irregular " + f"time sampling" + ) diff --git a/src/astrohack/extract_pointing_2.py b/src/astrohack/extract_pointing_2.py new file mode 100644 index 00000000..48779832 --- /dev/null +++ b/src/astrohack/extract_pointing_2.py @@ -0,0 +1,106 @@ +import pathlib +import toolviper.utils.parameter +import toolviper.utils.logger as logger + +from astrohack.utils.text import get_default_file_name +from astrohack.utils.file import overwrite_file +from astrohack.utils.file import load_point_file +from astrohack.utils.data import write_meta_data +from astrohack.core.extract_pointing import process_extract_pointing +from astrohack.io.mds import AstrohackPointFile + +from typing import List, Union + + +@toolviper.utils.parameter.validate() +def extract_pointing( + ms_name: str, + point_name: str = None, + exclude: Union[str, List[str]] = None, + parallel: bool = False, + overwrite: bool = False, +) -> AstrohackPointFile: + """ Extract pointing data from measurement set. Creates holography output file. + + :param ms_name: Name of input measurement file name. + :type ms_name: str + + :param point_name: Name of *.point.zarr* file to create. Defaults to measurement set name with \ + *point.zarr* extension. + :type point_name: str, optional + + :param exclude: Name of antenna to exclude from extraction. + :type exclude: list, optional + + :param parallel: Boolean for whether to process in parallel. Defaults to False + :type parallel: bool, optional + + :param overwrite: Overwrite pointing file on disk, defaults to False + :type overwrite: bool, optional + + :return: Holography point object. + :rtype: AstrohackPointFile + + .. _Description: + + **Example Usage** + In this case, the pointing_name is the file name to be created after extraction. + + .. parsed-literal:: + from astrohack.extract_pointing import extract_pointing + + extract_pointing( + ms_name="astrohack_observation.ms", + point_name="astrohack_observation.point.zarr" + ) + + **AstrohackPointFile** + + Point object allows the user to access point data via dictionary keys with values `ant`. The point object also + provides a `summary()` helper function to list available keys for each file. + + + """ + # Doing this here allows it to get captured by locals() + if point_name is None: + point_name = get_default_file_name( + input_file=ms_name, output_type=".point.zarr" + ) + + # Returns the current local variables in dictionary form + extract_pointing_params = locals() + + input_params = extract_pointing_params.copy() + + assert ( + pathlib.Path(extract_pointing_params["ms_name"]).exists() is True + ), logger.error(f'File {extract_pointing_params["ms_name"]} does not exists.') + + overwrite_file( + extract_pointing_params["point_name"], extract_pointing_params["overwrite"] + ) + + pnt_dict = process_extract_pointing( + ms_name=extract_pointing_params["ms_name"], + pnt_name=extract_pointing_params["point_name"], + exclude=extract_pointing_params["exclude"], + parallel=extract_pointing_params["parallel"], + ) + + # Calling this directly since it is so simple it doesn't need a "_create_{}" function. + write_meta_data( + file_name="{name}/{ext}".format( + name=extract_pointing_params["point_name"], ext=".point_input" + ), + input_dict=input_params, + ) + + logger.info(f"Finished processing") + point_dict = load_point_file( + file=extract_pointing_params["point_name"], dask_load=True + ) + + pointing_mds = AstrohackPointFile(extract_pointing_params["point_name"]) + pointing_mds.open() + + return pointing_mds diff --git a/src/astrohack/io/point_mds.py b/src/astrohack/io/point_mds.py new file mode 100644 index 00000000..ff0455c2 --- /dev/null +++ b/src/astrohack/io/point_mds.py @@ -0,0 +1,15 @@ +from astrohack.io.base_mds import AstrohackBaseFile + + +class AstrohackPointFile(AstrohackBaseFile): + + def __init__(self, file: str): + """Initialize an AstrohackPointFile object. + + :param file: File to be linked to this object + :type file: str + + :return: AstrohackPointFile object + :rtype: AstrohackPointFile + """ + super().__init__(file=file) From e40c3a592dfd00249bec3f19e625c9351a89acb2 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 20 Jan 2026 12:38:36 -0700 Subject: [PATCH 002/295] Ported extract_pointing to use the new datatree mdses. --- src/astrohack/core/extract_pointing_2.py | 156 +++++++++++------------ src/astrohack/extract_pointing_2.py | 33 +---- 2 files changed, 78 insertions(+), 111 deletions(-) diff --git a/src/astrohack/core/extract_pointing_2.py b/src/astrohack/core/extract_pointing_2.py index c503f88f..97b78008 100644 --- a/src/astrohack/core/extract_pointing_2.py +++ b/src/astrohack/core/extract_pointing_2.py @@ -1,34 +1,36 @@ import os -import dask import numpy as np -import toolviper.utils.logger as logger import xarray as xr -import copy -from astrohack.utils.conversion import convert_dict_from_numba -from astrohack.utils.file import load_point_file -from astrohack.utils.tools import get_valid_state_ids from casacore import tables as ctables from numba import njit from numba.core import types from numba.typed import Dict from scipy import spatial +import toolviper.utils.logger as logger + +from astrohack.utils.conversion import convert_dict_from_numba +from astrohack.utils.graph import compute_graph_to_mds_tree +from astrohack.utils.tools import get_valid_state_ids +from astrohack.io.point_mds import AstrohackPointFile + -def process_extract_pointing(ms_name, pnt_name, exclude, parallel=True): +def process_extract_pointing(input_params): """Top level function to extract subset of pointing table data into a dictionary of xarray data arrays. Args: - exclude (): - ms_name (str): Measurement file name. - pnt_name (str): Output pointing dictionary file name. - parallel (bool, optional): Process in parallel. Defaults to True. + input_params(dict): extract_pointing parameters Returns: - dict: pointing dictionary of xarray data arrays + AstrohackPointFile: point mds file """ + ms_name = input_params["ms_name"] + pnt_name = input_params["point_name"] + exclude = input_params["exclude"] + # Get antenna names and ids ctb = ctables.table( os.path.join(ms_name, "ANTENNA"), @@ -85,51 +87,52 @@ def process_extract_pointing(ms_name, pnt_name, exclude, parallel=True): time, scan_ids, state_ids, ddi, mapping_state_ids ) - point_meta_ds = xr.Dataset() - point_meta_ds.attrs["mapping_state_ids"] = mapping_state_ids - point_meta_ds.to_zarr(pnt_name, mode="w", compute=True, consolidated=True) + # Create mds file here + point_mds = AstrohackPointFile.create_from_input_parameters(pnt_name, input_params) + point_mds.root.attrs["mapping_state_ids"] = mapping_state_ids ########################################################################################### - pnt_params = {"pnt_name": pnt_name, "scan_time_dict": scan_time_dict} - - if parallel: - delayed_pnt_list = [] - for i_ant in range(len(antenna_id)): - this_pars = copy.deepcopy(pnt_params) - this_pars["ant_id"] = antenna_id[i_ant] - this_pars["ant_name"] = antenna_name[i_ant] - - delayed_pnt_list.append( - dask.delayed(_make_ant_pnt_chunk)(ms_name, this_pars) - ) - - dask.compute(delayed_pnt_list) - + pnt_params = { + "ms_name": ms_name, + "pnt_name": pnt_name, + "scan_time_dict": scan_time_dict, + "ant": "all", + } + + looping_dict = {} + for i_ant, ant_name in enumerate(antenna_name): + looping_dict[f"ant_{ant_name}"] = {"id": antenna_id[i_ant], "name": ant_name} + + executed_graph = compute_graph_to_mds_tree( + looping_dict, + _make_ant_pnt_chunk, + pnt_params, + ["ant"], + point_mds, + ) + if executed_graph: + point_mds.write() + return point_mds else: - for i_ant in range(len(antenna_id)): - pnt_params["ant_id"] = antenna_id[i_ant] - pnt_params["ant_name"] = antenna_name[i_ant] + logger.warning("No data to process") + return None - _make_ant_pnt_chunk(ms_name, pnt_params) - return load_point_file(pnt_name, diagnostic=True) - - -def _make_ant_pnt_chunk(ms_name, pnt_params): +def _make_ant_pnt_chunk(pnt_params): """Extract subset of pointing table data into a dictionary of xarray data arrays. This is written to disk as a zarr file. This function processes a chunk the overall data and is managed by Dask. Args: - ms_name (str): Measurement file name. - ant_id (int): Antenna id - pnt_name (str): Name of output pointing dictionary file name. + pnt_params(dict): extract_pointing parameters """ - - ant_id = pnt_params["ant_id"] - ant_name = pnt_params["ant_name"] - pnt_name = pnt_params["pnt_name"] + data_dict = pnt_params["data_dict"] + ms_name = pnt_params["ms_name"] scan_time_dict = pnt_params["scan_time_dict"] + ant_id = data_dict["id"] + ant_name = data_dict["name"] + ant_key = pnt_params["this_ant"] + table_obj = ctables.table( os.path.join(ms_name, "POINTING"), readonly=True, @@ -142,7 +145,7 @@ def _make_ant_pnt_chunk(ms_name, pnt_params): % ant_id ) - # NB: Add check if directions reference frame is Azemuth Elevation (AZELGEO) + # NB: Add check if directions reference frame is Azimuth Elevation (AZELGEO) try: direction = tb.getcol("DIRECTION")[:, 0, :] target = tb.getcol("TARGET")[:, 0, :] @@ -150,7 +153,7 @@ def _make_ant_pnt_chunk(ms_name, pnt_params): direction_time = tb.getcol("TIME") pointing_offset = tb.getcol("POINTING_OFFSET")[:, 0, :] - except Exception: + except RuntimeError: tb.close() logger.warning("Skipping antenna " + str(ant_id) + " no pointing info") @@ -188,13 +191,13 @@ def _make_ant_pnt_chunk(ms_name, pnt_params): # ## NB: Is VLA's definition of Azimuth the same for ALMA, MeerKAT, etc.? (positive for a clockwise rotation from # north, viewed from above) ## NB: Compare with calculation using WCS in astropy. - l = np.cos(target[:, 1]) * np.sin(target[:, 0] - direction[:, 0]) - m = np.sin(target[:, 1]) * np.cos(direction[:, 1]) - np.cos(target[:, 1]) * np.sin( - direction[:, 1] - ) * np.cos(target[:, 0] - direction[:, 0]) + l_points = np.cos(target[:, 1]) * np.sin(target[:, 0] - direction[:, 0]) + m_points = np.sin(target[:, 1]) * np.cos(direction[:, 1]) - np.cos( + target[:, 1] + ) * np.sin(direction[:, 1]) * np.cos(target[:, 0] - direction[:, 0]) pnt_xds["DIRECTIONAL_COSINES"] = xr.DataArray( - np.array([l, m]).T, dims=("time", "lm") + np.array([l_points, m_points]).T, dims=("time", "lm") ) """ @@ -247,14 +250,14 @@ def _make_ant_pnt_chunk(ms_name, pnt_params): time=slice(time_index[0], time_index[1]) ) - r = ( + avg_lm_dist = ( np.sqrt( pointing_offset_scan_slice.isel(az_el=0) ** 2 + pointing_offset_scan_slice.isel(az_el=1) ** 2 ) ).mean() - if r > 10**-12: # Antenna is mapping since lm is non-zero + if avg_lm_dist > 10**-12: # Antenna is mapping since lm is non-zero if ("map_" + str(map_id)) in map_scans_dict: map_scans_dict["map_" + str(map_id)].append(scan_id) @@ -269,20 +272,9 @@ def _make_ant_pnt_chunk(ms_name, pnt_params): pnt_xds.attrs["mapping_scans_obs_dict"] = [mapping_scans_obs_dict] ############### - pnt_xds.attrs["ant_name"] = pnt_params["ant_name"] + pnt_xds.attrs["ant_name"] = ant_name - logger.debug( - "Writing pointing xds to {file}".format( - file=os.path.join(pnt_name, "ant_" + str(ant_name)) - ) - ) - - pnt_xds.to_zarr( - os.path.join(pnt_name, "ant_{}".format(str(ant_name))), - mode="w", - compute=True, - consolidated=True, - ) + return xr.DataTree(dataset=pnt_xds, name=ant_key) def _extract_scan_time_dict(time, scan_ids, state_ids, ddi_ids, mapping_state_ids): @@ -322,40 +314,40 @@ def _extract_scan_time_dict_jit(time, scan_ids, state_ids, ddi_ids, mapping_stat mapping_state_ids. """ - d1 = Dict.empty( + dict_template = Dict.empty( key_type=types.int64, value_type=np.zeros(2, dtype=types.float64), ) scan_time_dict = Dict.empty( key_type=types.int64, - value_type=d1, + value_type=dict_template, ) mapping_scans = set() - for i, s in enumerate(scan_ids): - s = types.int64(s) - t = time[i] - ddi = ddi_ids[i] + for i_scan, scan_num in enumerate(scan_ids): + scan_num = types.int64(scan_num) + time_val = time[i_scan] + ddi = ddi_ids[i_scan] - state_id = state_ids[i] + state_id = state_ids[i_scan] if state_id in mapping_state_ids: - mapping_scans.add(s) + mapping_scans.add(scan_num) if ddi in scan_time_dict: - if s in scan_time_dict[ddi]: - if scan_time_dict[ddi][s][0] > t: - scan_time_dict[ddi][s][0] = t + if scan_num in scan_time_dict[ddi]: + if scan_time_dict[ddi][scan_num][0] > time_val: + scan_time_dict[ddi][scan_num][0] = time_val - if scan_time_dict[ddi][s][1] < t: - scan_time_dict[ddi][s][1] = t + if scan_time_dict[ddi][scan_num][1] < time_val: + scan_time_dict[ddi][scan_num][1] = time_val else: - scan_time_dict[ddi][s] = np.array([t, t]) + scan_time_dict[ddi][scan_num] = np.array([time_val, time_val]) else: - scan_time_dict[ddi] = {s: np.array([t, t])} + scan_time_dict[ddi] = {scan_num: np.array([time_val, time_val])} return scan_time_dict diff --git a/src/astrohack/extract_pointing_2.py b/src/astrohack/extract_pointing_2.py index 48779832..30042615 100644 --- a/src/astrohack/extract_pointing_2.py +++ b/src/astrohack/extract_pointing_2.py @@ -4,15 +4,13 @@ from astrohack.utils.text import get_default_file_name from astrohack.utils.file import overwrite_file -from astrohack.utils.file import load_point_file -from astrohack.utils.data import write_meta_data -from astrohack.core.extract_pointing import process_extract_pointing -from astrohack.io.mds import AstrohackPointFile +from astrohack.core.extract_pointing_2 import process_extract_pointing +from astrohack.io.point_mds import AstrohackPointFile from typing import List, Union -@toolviper.utils.parameter.validate() +# @toolviper.utils.parameter.validate() def extract_pointing( ms_name: str, point_name: str = None, @@ -80,27 +78,4 @@ def extract_pointing( extract_pointing_params["point_name"], extract_pointing_params["overwrite"] ) - pnt_dict = process_extract_pointing( - ms_name=extract_pointing_params["ms_name"], - pnt_name=extract_pointing_params["point_name"], - exclude=extract_pointing_params["exclude"], - parallel=extract_pointing_params["parallel"], - ) - - # Calling this directly since it is so simple it doesn't need a "_create_{}" function. - write_meta_data( - file_name="{name}/{ext}".format( - name=extract_pointing_params["point_name"], ext=".point_input" - ), - input_dict=input_params, - ) - - logger.info(f"Finished processing") - point_dict = load_point_file( - file=extract_pointing_params["point_name"], dask_load=True - ) - - pointing_mds = AstrohackPointFile(extract_pointing_params["point_name"]) - pointing_mds.open() - - return pointing_mds + return process_extract_pointing(extract_pointing_params) From e0c20d21d4d494019c983a49bb7bd6bfde312335 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 20 Jan 2026 16:55:14 -0700 Subject: [PATCH 003/295] Prototype of a add to tree and dump method for AstrohackBaseFile. --- src/astrohack/io/base_mds.py | 43 ++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/src/astrohack/io/base_mds.py b/src/astrohack/io/base_mds.py index f26f6dd3..81fefa83 100644 --- a/src/astrohack/io/base_mds.py +++ b/src/astrohack/io/base_mds.py @@ -141,11 +141,11 @@ def open(self, file: str = None) -> bool: return self._file_is_open - def write(self): + def write(self, mode="w"): """ Write mds to disk by saving the data tree to a file """ - self.root.to_zarr(self.file, mode="w", consolidated=True) + self.root.to_zarr(self.file, mode=mode, consolidated=True) def summary(self) -> None: """ @@ -179,3 +179,42 @@ def create_from_input_parameters(cls, file_name: str, input_parameters: dict): add_caller_and_version_to_dict_2(data_obj.root.attrs, direct_call=False) data_obj.root.attrs["input_parameters"] = input_parameters return data_obj + + def add_node_to_tree(self, new_node, dump_to_disk=True): + assert isinstance(new_node, xr.DataTree) + lvls = new_node.name.split("-") + n_lvls = len(lvls) + if n_lvls == 1: + lvl_0 = lvls[0] + self.root.update({lvl_0: new_node}) + elif n_lvls == 2: + lvl_0, lvl_1 = lvls + if lvl_0 in self.keys(): + self[lvl_0].update({lvl_1: new_node}) + else: + self[lvl_0] = xr.DataTree(name=lvl_0, children={lvl_1: new_node}) + elif n_lvls == 3: + lvl_0, lvl_1, lvl_2 = lvls + if lvl_0 in self.keys(): + if lvl_1 in self[lvl_0].keys(): + self[lvl_0][lvl_1].update({lvl_2: new_node}) + else: + self[lvl_0][lvl_1] = xr.DataTree( + name=lvl_1, children={lvl_2: new_node} + ) + else: + self[lvl_0] = xr.DataTree( + name=lvl_0, + children={ + lvl_1: xr.DataTree(name=lvl_1, children={lvl_2: new_node}) + }, + ) + else: + raise NotImplementedError("Cannot handle a case of more than three levels") + + if dump_to_disk: + self.write(mode="a") + del self.root + self.open() + + return From dfb857c89890d5659056fc248e2f9d738b59cb15 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Wed, 21 Jan 2026 11:15:25 -0700 Subject: [PATCH 004/295] Removed dropbox from astrohack dependencies, as it is no longer used. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 839df1b0..e7cc431a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ version = "0.10.1" description = "Holography Antenna Commissioning Kit" readme = "README.md" requires-python = ">= 3.11, < 3.14" -dependencies = [ "astropy", "dask", "shapely", "distributed", "dropbox", "toolviper", "ipywidgets", "matplotlib", "numba>=0.57.0", "numpy<=2.2", "prettytable", "pycryptodome", "pytest", "pytest-cov", "pytest-html", "scikit_image", "scikit-learn", "scipy", "rich", "xarray", "zarr<3.0.0", "bokeh", "pillow", "jupyterlab", "python_casacore>=3.6.1; sys_platform != \"darwin\" ",] +dependencies = [ "astropy", "dask", "shapely", "distributed", "toolviper", "ipywidgets", "matplotlib", "numba>=0.57.0", "numpy<=2.2", "prettytable", "pycryptodome", "pytest", "pytest-cov", "pytest-html", "scikit_image", "scikit-learn", "scipy", "rich", "xarray", "zarr<3.0.0", "bokeh", "pillow", "jupyterlab", "python_casacore>=3.6.1; sys_platform != \"darwin\" ",] [[project.authors]] name = "Joshua Hoskins" email = "jhoskins@nrao.edu" From 272819501c08b33a1948c50b1482aea2c26c691c Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Thu, 22 Jan 2026 09:04:13 -0700 Subject: [PATCH 005/295] Added documentation to base_mds.py add node code. --- src/astrohack/io/base_mds.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/astrohack/io/base_mds.py b/src/astrohack/io/base_mds.py index 81fefa83..3f672349 100644 --- a/src/astrohack/io/base_mds.py +++ b/src/astrohack/io/base_mds.py @@ -144,6 +144,9 @@ def open(self, file: str = None) -> bool: def write(self, mode="w"): """ Write mds to disk by saving the data tree to a file + + :param mode: File mode + :type mode: str """ self.root.to_zarr(self.file, mode=mode, consolidated=True) @@ -181,6 +184,18 @@ def create_from_input_parameters(cls, file_name: str, input_parameters: dict): return data_obj def add_node_to_tree(self, new_node, dump_to_disk=True): + """ + Add a node to root at a position determined by new_node's name + + :param new_node: Node to be included in root + :type new_node: xarray.DataTree + + :param dump_to_disk: Dump root to disk to freeup RAM + :type dump_to_disk: bool + + :return: None + :rtype: NoneType + """ assert isinstance(new_node, xr.DataTree) lvls = new_node.name.split("-") n_lvls = len(lvls) From ba796f30014c79a5184ea5e374efbd41a8708eea Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Thu, 22 Jan 2026 09:21:43 -0700 Subject: [PATCH 006/295] Added dask lock when dumping tree. --- src/astrohack/io/base_mds.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/astrohack/io/base_mds.py b/src/astrohack/io/base_mds.py index 3f672349..80665f6b 100644 --- a/src/astrohack/io/base_mds.py +++ b/src/astrohack/io/base_mds.py @@ -1,5 +1,7 @@ import xarray as xr +from dask.distributed import Lock + import toolviper.utils.logger as logger from astrohack.utils import ( @@ -228,8 +230,10 @@ def add_node_to_tree(self, new_node, dump_to_disk=True): raise NotImplementedError("Cannot handle a case of more than three levels") if dump_to_disk: + lock = Lock("Root dump lock") + lock.acquire(timeout=1) self.write(mode="a") del self.root self.open() - + lock.release() return From 3d498732a515b0c4df31496abe0dc87131cb9afd Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Thu, 22 Jan 2026 10:19:53 -0700 Subject: [PATCH 007/295] skipping data equality test as it is not robust yet. --- tests/unit/user_facing_functions/test_beamcut.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/unit/user_facing_functions/test_beamcut.py b/tests/unit/user_facing_functions/test_beamcut.py index 9e09d769..abd16cb3 100644 --- a/tests/unit/user_facing_functions/test_beamcut.py +++ b/tests/unit/user_facing_functions/test_beamcut.py @@ -3,6 +3,7 @@ import shutil import glob +import pytest from toolviper.utils import data from astrohack import beamcut, extract_holog, extract_pointing, open_beamcut @@ -79,6 +80,7 @@ def teardown_class(cls): shutil.rmtree(cls.data_folder, ignore_errors=True) shutil.rmtree(cls.destination_folder, ignore_errors=True) + @pytest.mark.skip(reason="mds equality test is not yet robust") def test_results(self): # Has to be run first local_beamcut_mds = beamcut( From 170a1c3731bccc8d3a7de2587765b24d5f977c76 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Thu, 22 Jan 2026 10:37:08 -0700 Subject: [PATCH 008/295] skipping summary test as it is not robust yet. --- tests/unit/mdses/test_locit_mds.py | 2 ++ tests/unit/mdses/test_position_mds.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/tests/unit/mdses/test_locit_mds.py b/tests/unit/mdses/test_locit_mds.py index 0a3550d6..6f42e1eb 100644 --- a/tests/unit/mdses/test_locit_mds.py +++ b/tests/unit/mdses/test_locit_mds.py @@ -1,4 +1,5 @@ import shutil +import pytest from toolviper.utils import data @@ -45,6 +46,7 @@ def test_locit_mds_init(self): locit_mds = AstrohackLocitFile(self.locit_name) assert isinstance(locit_mds, AstrohackLocitFile) + @pytest.mark.skip(reason="Summary test is fickle") def test_locit_mds_summary(self): locit_mds = open_locit(self.locit_name) summary_reference_name = f"{self.ref_products_folder}/summary_reference.txt" diff --git a/tests/unit/mdses/test_position_mds.py b/tests/unit/mdses/test_position_mds.py index 63c21b68..ea32dcf3 100644 --- a/tests/unit/mdses/test_position_mds.py +++ b/tests/unit/mdses/test_position_mds.py @@ -1,4 +1,5 @@ import shutil +import pytest from toolviper.utils import data @@ -63,6 +64,7 @@ def test_position_mds_init(self): position_mds = AstrohackPositionFile(self.position_no_comb_name) assert isinstance(position_mds, AstrohackPositionFile) + @pytest.mark.skip(reason="Summary test is fickle") def test_position_mds_summary(self): for label, filename in self.position_files.items(): position_mds = open_position(filename) From da003ad7e41f98968a3f3f3b5792e6cb016d15fd Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Thu, 22 Jan 2026 10:53:14 -0700 Subject: [PATCH 009/295] skipping summary test as it is not robust yet. --- tests/unit/mdses/test_beamcut_mds.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/mdses/test_beamcut_mds.py b/tests/unit/mdses/test_beamcut_mds.py index c44f65df..c55e8f62 100644 --- a/tests/unit/mdses/test_beamcut_mds.py +++ b/tests/unit/mdses/test_beamcut_mds.py @@ -47,6 +47,7 @@ def test_beamcut_mds_init(self): assert isinstance(beamcut_mds, AstrohackBeamcutFile) + @pytest.mark.skip(reason="Summary test is fickle") def test_beamcut_mds_summary(self): beamcut_mds = open_beamcut(self.remote_beamcut_name) summary_reference_name = f"{self.ref_products_folder}/summary_reference.txt" From 743eac66fd6833f73064d38c19360fd0b8aaace0 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Thu, 22 Jan 2026 10:58:33 -0700 Subject: [PATCH 010/295] Added distinction between parallel and non parallel execution to control lock behaviour. --- src/astrohack/io/base_mds.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/astrohack/io/base_mds.py b/src/astrohack/io/base_mds.py index 80665f6b..15425195 100644 --- a/src/astrohack/io/base_mds.py +++ b/src/astrohack/io/base_mds.py @@ -185,7 +185,12 @@ def create_from_input_parameters(cls, file_name: str, input_parameters: dict): data_obj.root.attrs["input_parameters"] = input_parameters return data_obj - def add_node_to_tree(self, new_node, dump_to_disk=True): + def _dump_to_disk(self): + self.write(mode="a") + del self.root + self.open() + + def add_node_to_tree(self, new_node, dump_to_disk=True, running_in_parallel=False): """ Add a node to root at a position determined by new_node's name @@ -230,10 +235,11 @@ def add_node_to_tree(self, new_node, dump_to_disk=True): raise NotImplementedError("Cannot handle a case of more than three levels") if dump_to_disk: - lock = Lock("Root dump lock") - lock.acquire(timeout=1) - self.write(mode="a") - del self.root - self.open() - lock.release() + if running_in_parallel: + lock = Lock("Root dump lock") + lock.acquire(timeout=1) + self._dump_to_disk() + lock.release() + else: + self._dump_to_disk() return From 127bc9009eae294813a0d32503203161981ff159 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Thu, 22 Jan 2026 11:00:19 -0700 Subject: [PATCH 011/295] Adapted graph machinery to the new API where the output_mds is passed so that each chunk can dump its data to disk. --- src/astrohack/utils/graph.py | 58 ++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/src/astrohack/utils/graph.py b/src/astrohack/utils/graph.py index 2ac23818..fd6379ff 100644 --- a/src/astrohack/utils/graph.py +++ b/src/astrohack/utils/graph.py @@ -13,15 +13,20 @@ def _construct_xdtree_graph_recursively( param_dict, delayed_list, key_order, + output_mds=None, parallel=False, oneup=None, ): if len(key_order) == 0: param_dict["xdt_data"] = xr_datatree + if output_mds is None: + args = [param_dict] + else: + args = [param_dict, output_mds] if parallel: - delayed_list.append(dask.delayed(chunk_function)(dask.delayed(param_dict))) + delayed_list.append(dask.delayed(chunk_function)(dask.delayed(args))) else: - delayed_list.append((chunk_function, param_dict)) + delayed_list.append((chunk_function, args)) else: key_base = key_order[0] exec_list = param_to_list(param_dict[key_base], xr_datatree, key_base) @@ -40,6 +45,7 @@ def _construct_xdtree_graph_recursively( delayed_list=delayed_list, key_order=key_order[1:], parallel=parallel, + output_mds=output_mds, oneup=item, ) @@ -56,6 +62,7 @@ def _construct_general_graph_recursively( param_dict, delayed_list, key_order, + output_mds=None, parallel=False, oneup=None, ): @@ -66,11 +73,14 @@ def _construct_general_graph_recursively( elif isinstance(looping_dict, dict): param_dict["data_dict"] = looping_dict + if output_mds is None: + args = [param_dict] + else: + args = [param_dict, output_mds] if parallel: - delayed_list.append(dask.delayed(chunk_function)(dask.delayed(param_dict))) - + delayed_list.append(dask.delayed(chunk_function)(dask.delayed(args))) else: - delayed_list.append((chunk_function, param_dict)) + delayed_list.append((chunk_function, args)) else: key = key_order[0] @@ -89,6 +99,7 @@ def _construct_general_graph_recursively( param_dict=this_param_dict, delayed_list=delayed_list, key_order=key_order[1:], + output_mds=output_mds, parallel=parallel, oneup=item, ) @@ -107,6 +118,7 @@ def compute_graph_to_mds_tree( key_order, output_mds, parallel=False, + fetch_returns=False, ): delayed_list = [] if hasattr(looping_dict, "root"): @@ -116,6 +128,7 @@ def compute_graph_to_mds_tree( param_dict=param_dict, delayed_list=delayed_list, key_order=key_order, + output_mds=output_mds, parallel=parallel, ) else: @@ -125,40 +138,27 @@ def compute_graph_to_mds_tree( param_dict=param_dict, delayed_list=delayed_list, key_order=key_order, + output_mds=output_mds, parallel=parallel, ) if len(delayed_list) == 0: logger.warning(f"List of delayed processing jobs is empty: No data to process") - return False + return False, None else: if parallel: return_list = dask.compute(delayed_list)[0] else: return_list = [] - for pair in delayed_list: - return_list.append(pair[0](pair[1])) - - for xdtree in return_list: - if xdtree is None: - # This if deals with the case that the calling routine did not produce output (e.g. locit) - continue - lvls = xdtree.name.split("-") - n_lvls = len(lvls) - if n_lvls == 1: - lvl_0 = lvls[0] - output_mds.root.update({lvl_0: xdtree}) - elif n_lvls == 2: - lvl_0, lvl_1 = lvls - if lvl_0 in output_mds.keys(): - output_mds[lvl_0].update({lvl_1: xdtree}) - else: - output_mds[lvl_0] = xr.DataTree( - name=lvl_0, children={lvl_1: xdtree} - ) - return True + for function, args in delayed_list: + return_list.append(function(*args)) + + if fetch_returns: + return True, return_list + else: + return True def compute_graph( @@ -213,8 +213,8 @@ def compute_graph( return_list = dask.compute(delayed_list)[0] else: return_list = [] - for pair in delayed_list: - return_list.append(pair[0](pair[1])) + for function, args in delayed_list: + return_list.append(function(*args)) if fetch_returns: return True, return_list From 8122a1daeff57d729b3502d2eec47941da219ad3 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Thu, 22 Jan 2026 11:00:40 -0700 Subject: [PATCH 012/295] Beamcut now uses chunk dumping on disk. --- src/astrohack/beamcut.py | 2 +- src/astrohack/core/beamcut.py | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/astrohack/beamcut.py b/src/astrohack/beamcut.py index 244bcdce..af98a991 100644 --- a/src/astrohack/beamcut.py +++ b/src/astrohack/beamcut.py @@ -150,7 +150,7 @@ def beamcut( ) if executed_graph: - beamcut_mds.write() + beamcut_mds.write(mode="a") return beamcut_mds else: logger.warning("No data to process") diff --git a/src/astrohack/core/beamcut.py b/src/astrohack/core/beamcut.py index 765f196f..77363417 100644 --- a/src/astrohack/core/beamcut.py +++ b/src/astrohack/core/beamcut.py @@ -29,13 +29,16 @@ ########################################################### ### Working Chunks ########################################################### -def process_beamcut_chunk(beamcut_chunk_params): +def process_beamcut_chunk(beamcut_chunk_params, output_mds): """ Ingests a holog_xds containing beamcuts and produces a beamcut_xdtree containing the cuts separated in xdses. :param beamcut_chunk_params: Parameter dictionary with inputs :type beamcut_chunk_params: dict + :param output_mds: Output mds file + :type output_mds: AstrohackBeamcutFile + :return: Beamcut_xdtree containing the different cuts for this antenna and DDI. :rtype: xr.DataTree """ @@ -67,7 +70,11 @@ def process_beamcut_chunk(beamcut_chunk_params): create_report_chunk(beamcut_chunk_params, cut_xdtree) logger.info(f"Completed plots for {datalabel}") - return cut_xdtree + output_mds.add_node_to_tree( + cut_xdtree, + dump_to_disk=True, + running_in_parallel=beamcut_chunk_params["parallel"], + ) def plot_beamcut_in_amplitude_chunk(par_dict, cut_xdtree=None): From 4df228bcf4118b23a0ee092f25d050e59b40c746 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Thu, 22 Jan 2026 11:00:50 -0700 Subject: [PATCH 013/295] extract_locit now uses chunk dumping on disk. --- src/astrohack/core/extract_locit.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/astrohack/core/extract_locit.py b/src/astrohack/core/extract_locit.py index 211b1e8a..77dd199d 100644 --- a/src/astrohack/core/extract_locit.py +++ b/src/astrohack/core/extract_locit.py @@ -88,7 +88,10 @@ def extract_antenna_data(extract_locit_parms, locit_mds): "offset": ant_off[i_ant].tolist(), } ant_xdtree.attrs["antenna_info"] = ant_info - locit_mds[ant_key] = ant_xdtree + locit_mds.add_node_to_tree( + ant_xdtree, dump_to_disk=True, running_in_parallel=False + ) + locit_mds.root.attrs["full_antenna_list"] = ant_nam if error: msg = f"Unsupported antenna characteristics" From 3e8dc715b573531b5d85bf041ef5b82ae8647e22 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Thu, 22 Jan 2026 11:00:59 -0700 Subject: [PATCH 014/295] locit now uses chunk dumping on disk. --- src/astrohack/core/locit.py | 54 ++++++++++++++++++++----------------- src/astrohack/locit.py | 2 +- 2 files changed, 30 insertions(+), 26 deletions(-) diff --git a/src/astrohack/core/locit.py b/src/astrohack/core/locit.py index af5c1a4f..5201a358 100644 --- a/src/astrohack/core/locit.py +++ b/src/astrohack/core/locit.py @@ -1,4 +1,3 @@ -import numpy as np from astropy.coordinates import EarthLocation from astropy.time import Time from scipy import optimize as opt @@ -28,11 +27,12 @@ ) -def locit_separated_chunk(locit_parms): +def locit_separated_chunk(locit_parms, output_mds): """ This is the chunk function for locit when treating each DDI separately Args: locit_parms: the locit parameter dictionary + output_mds: Output mds file onto which to add results Returns: xds save to disk in the .zarr format @@ -60,7 +60,7 @@ def locit_separated_chunk(locit_parms): locit_parms["fit_kterm"], locit_parms["fit_delay_rate"], ) - return _create_output_xds( + new_node = _create_output_xds( coordinates, lst, delays, @@ -75,19 +75,19 @@ def locit_separated_chunk(locit_parms): ant_key, ddi_key, ) - else: - return None - else: - return None - else: - return None + output_mds.add_node_to_tree( + new_node, + dump_to_disk=False, + running_in_parallel=locit_parms["parallel"], + ) -def locit_combined_chunk(locit_parms): +def locit_combined_chunk(locit_parms, output_mds): """ This is the chunk function for locit when we are combining the DDIs for an antenna for a single solution Args: locit_parms: the locit parameter dictionary + output_mds: Output mds file onto which to add results Returns: xds save to disk in the .zarr format @@ -129,7 +129,7 @@ def locit_combined_chunk(locit_parms): locit_parms["fit_kterm"], locit_parms["fit_delay_rate"], ) - return _create_output_xds( + new_node = _create_output_xds( coordinates, lst, delays, @@ -143,20 +143,20 @@ def locit_combined_chunk(locit_parms): antenna_info, ant_key, ) - else: - return None - else: - return None - else: - return None + output_mds.add_node_to_tree( + new_node, + dump_to_disk=True, + running_in_parallel=locit_parms["parallel"], + ) -def locit_difference_chunk(locit_parms): +def locit_difference_chunk(locit_parms, output_mds): """ This is the chunk function for locit when we are combining two DDIs for an antenna for a single solution by using the difference in phase between the two DDIs of different frequencies Args: locit_parms: the locit parameter dictionary + output_mds: Output mds file onto which to add results Returns: xds save to disk in the .zarr format @@ -202,7 +202,7 @@ def locit_difference_chunk(locit_parms): locit_parms["fit_kterm"], locit_parms["fit_delay_rate"], ) - return _create_output_xds( + new_node = _create_output_xds( coordinates, lst, delays, @@ -216,12 +216,11 @@ def locit_difference_chunk(locit_parms): antenna_info, ant_key, ) - else: - return None - else: - return None - else: - return None + output_mds.add_node_to_tree( + new_node, + dump_to_disk=True, + running_in_parallel=locit_parms["parallel"], + ) def plot_sky_coverage_chunk(parm_dict): @@ -691,6 +690,8 @@ def _create_output_xds( fit_rate = locit_parms["fit_delay_rate"] error = np.sqrt(variance) + # print(delays) + output_xds = xr.Dataset() output_xds.attrs["polarization"] = locit_parms["polarization"] output_xds.attrs["frequency"] = frequency @@ -724,11 +725,14 @@ def _create_output_xds( output_xds["ELEVATION"] = xr.DataArray(coordinates[2, :], dims=["time"]) output_xds["LST"] = xr.DataArray(lst, dims=["time"]) + # print(output_xds["DELAYS"].values) + if ddi_key is None: xdt_name = f"{ant_key}" else: xdt_name = f"{ant_key}-{ddi_key}" output_xdt = xr.DataTree(dataset=output_xds.assign_coords(coords), name=xdt_name) + print(output_xds["DELAYS"].values) return output_xdt diff --git a/src/astrohack/locit.py b/src/astrohack/locit.py index d8a357b6..3b17da11 100644 --- a/src/astrohack/locit.py +++ b/src/astrohack/locit.py @@ -209,7 +209,7 @@ def locit( "reference_antenna": locit_mds.root.attrs["reference_antenna"], } ) - position_mds.write() + position_mds.write(mode="a") return position_mds else: logger.warning("No data to process") From 2bdad3c19fed42d78d0544f2d7661e334a4fff1b Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Thu, 22 Jan 2026 11:11:22 -0700 Subject: [PATCH 015/295] extract_pointing_2.py now uses the new node adding API. --- src/astrohack/core/extract_pointing_2.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/astrohack/core/extract_pointing_2.py b/src/astrohack/core/extract_pointing_2.py index 97b78008..c63c11ef 100644 --- a/src/astrohack/core/extract_pointing_2.py +++ b/src/astrohack/core/extract_pointing_2.py @@ -97,6 +97,7 @@ def process_extract_pointing(input_params): "pnt_name": pnt_name, "scan_time_dict": scan_time_dict, "ant": "all", + "parallel": input_params["parallel"], } looping_dict = {} @@ -111,19 +112,20 @@ def process_extract_pointing(input_params): point_mds, ) if executed_graph: - point_mds.write() + point_mds.write(mode="a") return point_mds else: logger.warning("No data to process") return None -def _make_ant_pnt_chunk(pnt_params): +def _make_ant_pnt_chunk(pnt_params, output_mds): """Extract subset of pointing table data into a dictionary of xarray data arrays. This is written to disk as a zarr file. This function processes a chunk the overall data and is managed by Dask. Args: pnt_params(dict): extract_pointing parameters + output_mds: Output AstrohackPointFile """ data_dict = pnt_params["data_dict"] ms_name = pnt_params["ms_name"] @@ -154,10 +156,8 @@ def _make_ant_pnt_chunk(pnt_params): pointing_offset = tb.getcol("POINTING_OFFSET")[:, 0, :] except RuntimeError: - tb.close() logger.warning("Skipping antenna " + str(ant_id) + " no pointing info") - - return 0 + return tb.close() table_obj.close() @@ -274,7 +274,11 @@ def _make_ant_pnt_chunk(pnt_params): pnt_xds.attrs["ant_name"] = ant_name - return xr.DataTree(dataset=pnt_xds, name=ant_key) + output_mds.add_node_to_tree( + xr.DataTree(dataset=pnt_xds, name=ant_key), + dump_to_disk=True, + running_in_parallel=pnt_params["parallel"], + ) def _extract_scan_time_dict(time, scan_ids, state_ids, ddi_ids, mapping_state_ids): From 87c3be6f048421de28204604c44bfeebd15ea5c9 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Thu, 22 Jan 2026 11:17:05 -0700 Subject: [PATCH 016/295] Added printing method to AstrohackBaseFile. --- src/astrohack/io/base_mds.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/astrohack/io/base_mds.py b/src/astrohack/io/base_mds.py index 15425195..7d01d29a 100644 --- a/src/astrohack/io/base_mds.py +++ b/src/astrohack/io/base_mds.py @@ -10,6 +10,7 @@ get_property_string, get_data_content_string, get_method_list_string, + lnbr, ) @@ -243,3 +244,13 @@ def add_node_to_tree(self, new_node, dump_to_disk=True, running_in_parallel=Fals else: self._dump_to_disk() return + + def __repr__(self): + """ + Simple printing function to glance at the datatree inside + :return: Print contents + """ + outstr = f"<{type(self).__name__}>{lnbr}" + outstr += f"File on disk: {self.file}{lnbr}" + outstr += f"Data tree: {lnbr}{self.root.__repr__()}" + return outstr From 3d671af3bab26912652bd31b600e563053f71e92 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Thu, 22 Jan 2026 11:39:43 -0700 Subject: [PATCH 017/295] Created files to start porting extract_holog to the new data tree based format. --- src/astrohack/core/extract_holog_2.py | 901 ++++++++++++++++++++++ src/astrohack/extract_holog_2.py | 1028 +++++++++++++++++++++++++ src/astrohack/io/holog_mds.py | 15 + 3 files changed, 1944 insertions(+) create mode 100644 src/astrohack/core/extract_holog_2.py create mode 100644 src/astrohack/extract_holog_2.py create mode 100644 src/astrohack/io/holog_mds.py diff --git a/src/astrohack/core/extract_holog_2.py b/src/astrohack/core/extract_holog_2.py new file mode 100644 index 00000000..610f5e9f --- /dev/null +++ b/src/astrohack/core/extract_holog_2.py @@ -0,0 +1,901 @@ +import os +import json + +import numpy as np +import xarray as xr +import astropy +import toolviper.utils.logger as logger + +from numba import njit +from numba.core import types + +from casacore import tables as ctables + +from astrohack.antenna import get_proper_telescope +from astrohack.utils import create_dataset_label +from astrohack.utils.imaging import calculate_parallactic_angle_chunk +from astrohack.utils.algorithms import calculate_optimal_grid_parameters +from astrohack.utils.conversion import casa_time_to_mjd +from astrohack.utils.constants import twopi, clight +from astrohack.utils.gridding import grid_1d_data + +from astrohack.utils.file import load_point_file + + +def process_extract_holog_chunk(extract_holog_params): + """Perform data query on holography data chunk and get unique time and state_ids/ + + Args: + extract_holog_params: parameters controlling work to be done on this chunk + """ + + ms_name = extract_holog_params["ms_name"] + pnt_name = extract_holog_params["point_name"] + data_column = extract_holog_params["data_column"] + ddi = extract_holog_params["ddi"] + scans = extract_holog_params["scans"] + ant_names = extract_holog_params["ant_names"] + ant_station = extract_holog_params["ant_station"] + ref_ant_per_map_ant_tuple = extract_holog_params["ref_ant_per_map_ant_tuple"] + map_ant_tuple = extract_holog_params["map_ant_tuple"] + map_ant_name_tuple = extract_holog_params["map_ant_name_tuple"] + holog_map_key = extract_holog_params["holog_map_key"] + time_interval = extract_holog_params["time_smoothing_interval"] + pointing_interpolation_method = extract_holog_params[ + "pointing_interpolation_method" + ] + + # This piece of information is no longer used leaving them here commented out for completeness + # ref_ant_per_map_ant_name_tuple = extract_holog_params["ref_ant_per_map_ant_name_tuple"] + + if len(ref_ant_per_map_ant_tuple) != len(map_ant_tuple): + logger.error( + "Reference antenna per mapping antenna list and mapping antenna list should have same length." + ) + raise Exception( + "Inconsistancy between antenna list length, see error above for more info." + ) + + sel_state_ids = extract_holog_params["sel_state_ids"] + holog_name = extract_holog_params["holog_name"] + + chan_freq = extract_holog_params["chan_setup"]["chan_freq"] + pol = extract_holog_params["pol_setup"]["pol"] + + table_obj = ctables.table( + ms_name, readonly=True, lockoptions={"option": "usernoread"}, ack=False + ) + scans = [int(scan) for scan in scans] + if sel_state_ids: + ctb = ctables.taql( + "select %s, SCAN_NUMBER, ANTENNA1, ANTENNA2, TIME, TIME_CENTROID, WEIGHT, FLAG_ROW, FLAG, FIELD_ID from " + "$table_obj WHERE DATA_DESC_ID == %s AND SCAN_NUMBER in %s AND STATE_ID in %s" + % (data_column, ddi, scans, list(sel_state_ids)) + ) + else: + ctb = ctables.taql( + "select %s, SCAN_NUMBER, ANTENNA1, ANTENNA2, TIME, TIME_CENTROID, WEIGHT, FLAG_ROW, FLAG, FIELD_ID from " + "$table_obj WHERE DATA_DESC_ID == %s AND SCAN_NUMBER in %s" + % (data_column, ddi, scans) + ) + vis_data = ctb.getcol(data_column) + weight = ctb.getcol("WEIGHT") + ant1 = ctb.getcol("ANTENNA1") + ant2 = ctb.getcol("ANTENNA2") + time_vis_row = ctb.getcol("TIME") + # Centroid is never used, hence it is commented out to improve efficiency + # time_vis_row_centroid = ctb.getcol("TIME_CENTROID") + flag = ctb.getcol("FLAG") + flag_row = ctb.getcol("FLAG_ROW") + scan_list = ctb.getcol("SCAN_NUMBER") + field_ids = ctb.getcol("FIELD_ID") + + gen_info = _get_general_summary(ms_name, field_ids) + + # Here we use the median of the differences between dumps as this is a good proxy for the integration time + if time_interval is None: + time_interval = np.median(np.diff(np.unique(time_vis_row))) + + ctb.close() + table_obj.close() + + map_ref_dict = _get_map_ref_dict( + map_ant_tuple, ref_ant_per_map_ant_tuple, ant_names, ant_station + ) + + ( + time_vis, + vis_map_dict, + weight_map_dict, + flagged_mapping_antennas, + used_samples_dict, + scan_time_ranges, + unq_scans, + ) = _extract_holog_chunk_jit( + vis_data, + weight, + ant1, + ant2, + time_vis_row, + flag, + flag_row, + ref_ant_per_map_ant_tuple, + map_ant_tuple, + time_interval, + scan_list, + ) + + del vis_data, weight, ant1, ant2, time_vis_row, flag, flag_row, field_ids, scan_list + + map_ant_name_list = list(map(str, map_ant_name_tuple)) + + map_ant_name_list = ["_".join(("ant", i)) for i in map_ant_name_list] + + pnt_ant_dict = load_point_file(pnt_name, map_ant_name_list, dask_load=False) + pnt_map_dict = _extract_pointing_chunk( + map_ant_name_list, time_vis, pnt_ant_dict, pointing_interpolation_method + ) + + grid_params = {} + + # The loop has been moved out of the function here making the gridding parameter auto-calculation + # function more general use (hopefully). I honestly couldn't see a reason to keep it inside. + for ant_index in vis_map_dict.keys(): + antenna_name = "_".join(("ant", ant_names[ant_index])) + telescope = get_proper_telescope(gen_info["telescope name"], antenna_name) + n_pix, cell_size = calculate_optimal_grid_parameters( + pnt_map_dict, antenna_name, telescope.diameter, chan_freq, ddi + ) + + grid_params[antenna_name] = {"n_pix": n_pix, "cell_size": cell_size} + + # ## To DO: ################## Average multiple repeated samples over_flow_protector_constant = float("%.5g" % + # time_vis[0]) # For example 5076846059.4 -> 5076800000.0 time_vis = time_vis - over_flow_protector_constant + # from astrohack.utils._algorithms import _average_repeated_pointings time_vis = _average_repeated_pointings( + # vis_map_dict, weight_map_dict, flagged_mapping_antennas,time_vis,pnt_map_dict,ant_names) time_vis = time_vis + + # over_flow_protector_constant + + _create_holog_file( + holog_name, + vis_map_dict, + weight_map_dict, + pnt_map_dict, + time_vis, + used_samples_dict, + chan_freq, + pol, + flagged_mapping_antennas, + holog_map_key, + ddi, + ms_name, + ant_names, + ant_station, + grid_params, + time_interval, + gen_info, + map_ref_dict, + scan_time_ranges, + unq_scans, + ) + + logger.info( + "Finished extracting holography chunk for ddi: {ddi} holog_map_key: {holog_map_key}".format( + ddi=ddi, holog_map_key=holog_map_key + ) + ) + + +def _get_map_ref_dict(map_ant_tuple, ref_ant_per_map_ant_tuple, ant_names, ant_station): + map_dict = {} + for ii, map_id in enumerate(map_ant_tuple): + map_name = ant_names[map_id] + ref_list = [] + for ref_id in ref_ant_per_map_ant_tuple[ii]: + ref_list.append(f"{ant_names[ref_id]} @ {ant_station[ref_id]}") + map_dict[map_name] = ref_list + return map_dict + + +@njit(cache=False, nogil=True) +def _get_time_intervals(time_vis_row, scan_list, time_interval): + unq_scans = np.unique(scan_list) + scan_time_ranges = [] + for scan in unq_scans: + selected_times = time_vis_row[scan_list == scan] + min_time, max_time = np.min(selected_times), np.max(selected_times) + scan_time_ranges.append([min_time, max_time]) + + half_int = time_interval / 2 + start = np.min(time_vis_row) + half_int + total_time = np.max(time_vis_row) - start + n_time = int(np.ceil(total_time / time_interval)) + 1 + stop = start + n_time * time_interval + raw_time_samples = np.linspace(start, stop, n_time + 1) + + filtered_time_samples = [] + for time_sample in raw_time_samples: + for time_range in scan_time_ranges: + if time_range[0] <= time_sample <= time_range[1]: + filtered_time_samples.append(time_sample) + break + time_samples = np.array(filtered_time_samples) + return time_samples, scan_time_ranges, unq_scans + + +@njit(cache=False, nogil=True) +def _extract_holog_chunk_jit( + vis_data, + weight, + ant1, + ant2, + time_vis_row, + flag, + flag_row, + ref_ant_per_map_ant_tuple, + map_ant_tuple, + time_interval, + scan_list, +): + """JIT compiled function to extract relevant visibilty data from chunk after flagging and applying weights. + + Args: + vis_data (numpy.ndarray): Visibility data (row, channel, polarization) + weight (numpy.ndarray): Data weight values (row, polarization) + ant1 (numpy.ndarray): List of antenna_ids for antenna1 + ant2 (numpy.ndarray): List of antenna_ids for antenna2 + time_vis_row (numpy.ndarray): Array of full time talues by row + flag (numpy.ndarray): Array of data quality flags to apply to data + flag_row (numpy.ndarray): Array indicating when a full row of data should be flagged + ref_ant_per_map_ant_tuple(tuple): reference antenna per mapping antenna + map_ant_tuple(tuple): mapping antennas? + time_interval(float): time smoothing interval + scan_list(list): list of valid holography scans + + Returns: + dict: Antenna_id referenced (key) dictionary containing the visibility data selected by (time, channel, + polarization) + """ + + time_samples, scan_time_ranges, unq_scans = _get_time_intervals( + time_vis_row, scan_list, time_interval + ) + n_time = len(time_samples) + + n_row, n_chan, n_pol = vis_data.shape + + half_int = time_interval / 2 + + vis_map_dict = {} + sum_weight_map_dict = {} + used_samples_dict = {} + + for antenna_id in map_ant_tuple: + vis_map_dict[antenna_id] = np.zeros( + (n_time, n_chan, n_pol), + dtype=types.complex128, + ) + sum_weight_map_dict[antenna_id] = np.zeros( + (n_time, n_chan, n_pol), + dtype=types.float64, + ) + used_samples_dict[antenna_id] = np.full(n_time, False, dtype=bool) + + time_index = 0 + for row in range(n_row): + if flag_row is False: + continue + + # Find index of time_vis_row[row] in time_samples, assumes time_vis_row is ordered in time + + if time_vis_row[row] < time_samples[time_index] - half_int: + continue + else: + time_index = _get_time_index( + time_vis_row[row], time_index, time_samples, half_int + ) + if time_index < 0: + break + + ant1_id = ant1[row] + ant2_id = ant2[row] + + if ant1_id in map_ant_tuple: + indx = map_ant_tuple.index(ant1_id) + conjugate = False + ref_ant_id = ant2_id + map_ant_id = ant1_id # mapping antenna index + elif ant2_id in map_ant_tuple: + indx = map_ant_tuple.index(ant2_id) + conjugate = True + ref_ant_id = ant1_id + map_ant_id = ant2_id # mapping antenna index + else: + continue + + if ref_ant_id in ref_ant_per_map_ant_tuple[indx]: + if conjugate: + vis_baseline = np.conjugate(vis_data[row, :, :]) + else: + vis_baseline = vis_data[row, :, :] # n_chan x n_pol + else: + continue + + for chan in range(n_chan): + for pol in range(n_pol): + if ~(flag[row, chan, pol]): + # Calculate running weighted sum of visibilities + used_samples_dict[map_ant_id][time_index] = True + vis_map_dict[map_ant_id][time_index, chan, pol] = ( + vis_map_dict[map_ant_id][time_index, chan, pol] + + vis_baseline[chan, pol] * weight[row, pol] + ) + + # Calculate running sum of weights + sum_weight_map_dict[map_ant_id][time_index, chan, pol] = ( + sum_weight_map_dict[map_ant_id][time_index, chan, pol] + + weight[row, pol] + ) + + flagged_mapping_antennas = [] + + for map_ant_id in vis_map_dict.keys(): + sum_of_sum_weight = 0 + + for time_index in range(n_time): + for chan in range(n_chan): + for pol in range(n_pol): + sum_weight = sum_weight_map_dict[map_ant_id][time_index, chan, pol] + sum_of_sum_weight = sum_of_sum_weight + sum_weight + if sum_weight == 0: + vis_map_dict[map_ant_id][time_index, chan, pol] = 0.0 + else: + vis_map_dict[map_ant_id][time_index, chan, pol] = ( + vis_map_dict[map_ant_id][time_index, chan, pol] / sum_weight + ) + + if sum_of_sum_weight == 0: + flagged_mapping_antennas.append(map_ant_id) + + return ( + time_samples, + vis_map_dict, + sum_weight_map_dict, + flagged_mapping_antennas, + used_samples_dict, + scan_time_ranges, + unq_scans, + ) + + +def _get_time_samples(time_vis): + """Sample three values for time vis and cooresponding indices. Values are sammpled as (first, middle, last) + + Args: + time_vis (numpy.ndarray): a list of visibility times + + Returns: + numpy.ndarray, list: a select subset of visibility times (first, middle, last) + """ + + n_time_vis = time_vis.shape[0] + + middle = int(n_time_vis // 2) + indices = [0, middle, n_time_vis - 1] + + return np.take(time_vis, indices), indices + + +def _create_holog_file( + holog_name, + vis_map_dict, + weight_map_dict, + pnt_map_dict, + time_vis, + used_samples_dict, + chan, + pol, + flagged_mapping_antennas, + holog_map_key, + ddi, + ms_name, + ant_names, + ant_station, + grid_params, + time_interval, + gen_info, + map_ref_dict, + scan_time_ranges, + unq_scans, +): + """Create holog-structured, formatted output file and save to zarr. + + Args: + holog_name (str): holog file name. + vis_map_dict (dict): a nested dictionary/map of weighted visibilities indexed as [antenna][time, chan, pol]; \ + mainains time ordering. + weight_map_dict (dict): weights dictionary/map for visibilites in vis_map_dict + pnt_map_dict (dict): pointing table map dictionary + time_vis (numpy.ndarray): time_vis values + chan (numpy.ndarray): channel values + pol (numpy.ndarray): polarization values + flagged_mapping_antennas (numpy.ndarray): list of mapping antennas that have been flagged. + holog_map_key(string): holog map id string + ddi (numpy.ndarray): data description id; a combination of polarization and spectral window + """ + + ctb = ctables.table("/".join((ms_name, "ANTENNA")), ack=False) + observing_location = ctb.getcol("POSITION") + ctb.close() + + for map_ant_index in vis_map_dict.keys(): + if map_ant_index not in flagged_mapping_antennas: + valid_data = used_samples_dict[map_ant_index] == 1.0 + + ant_time_vis = time_vis[valid_data] + + time_vis_days = ant_time_vis / (3600 * 24) + astro_time_vis = astropy.time.Time(time_vis_days, format="mjd") + time_samples, indicies = _get_time_samples(astro_time_vis) + coords = {"time": ant_time_vis, "chan": chan, "pol": pol} + map_ant_tag = ( + "ant_" + ant_names[map_ant_index] + ) # 'ant_' + str(map_ant_index) + + direction = np.take( + pnt_map_dict[map_ant_tag]["DIRECTIONAL_COSINES"].values, + indicies, + axis=0, + ) + + parallactic_samples = calculate_parallactic_angle_chunk( + time_samples=time_samples, + observing_location=observing_location[map_ant_index], + direction=direction, + ) + + xds = xr.Dataset() + xds = xds.assign_coords(coords) + xds["VIS"] = xr.DataArray( + vis_map_dict[map_ant_index][valid_data, ...], + dims=["time", "chan", "pol"], + ) + + xds["WEIGHT"] = xr.DataArray( + weight_map_dict[map_ant_index][valid_data, ...], + dims=["time", "chan", "pol"], + ) + + xds["DIRECTIONAL_COSINES"] = xr.DataArray( + pnt_map_dict[map_ant_tag]["DIRECTIONAL_COSINES"].values[ + valid_data, ... + ], + dims=["time", "lm"], + ) + + xds["IDEAL_DIRECTIONAL_COSINES"] = xr.DataArray( + pnt_map_dict[map_ant_tag]["POINTING_OFFSET"].values[valid_data, ...], + dims=["time", "lm"], + ) + + xds.attrs["holog_map_key"] = holog_map_key + xds.attrs["ddi"] = ddi + xds.attrs["parallactic_samples"] = parallactic_samples + xds.attrs["time_smoothing_interval"] = time_interval + xds.attrs["scan_time_ranges"] = scan_time_ranges + xds.attrs["scan_list"] = unq_scans + + xds.attrs["summary"] = _crate_observation_summary( + ant_names[map_ant_index], + ant_station[map_ant_index], + gen_info, + grid_params, + xds["DIRECTIONAL_COSINES"].values, + chan, + pnt_map_dict[map_ant_tag], + valid_data, + map_ref_dict, + ) + + holog_file = holog_name + + logger.debug( + f"Writing {create_dataset_label(ant_names[map_ant_index], ddi)} holog file to {holog_file}" + ) + xds.to_zarr( + os.path.join( + holog_file, + "ddi_" + + str(ddi) + + "/" + + str(holog_map_key) + + "/" + + "ant_" + + str(ant_names[map_ant_index]), + ), + mode="w", + compute=True, + consolidated=True, + ) + + else: + logger.warning( + "Mapping antenna {index} has no data".format( + index=ant_names[map_ant_index] + ) + ) + + +def create_holog_obs_dict( + pnt_dict, + baseline_average_distance, + baseline_average_nearest, + ant_names, + ant_pos, + ant_names_main, + exclude_antennas=None, + write_distance_matrix=False, +): + """ + Generate holog_obs_dict. + """ + + import pandas as pd + from scipy.spatial import distance_matrix + + mapping_scans_dict = {} + holog_obs_dict = {} + map_id = 0 + ant_names_set = set() + + if exclude_antennas is None: + exclude_antennas = [] + elif isinstance(exclude_antennas, str): + exclude_antennas = [exclude_antennas] + else: + pass + + for ant_name in exclude_antennas: + prefixed = "ant_" + ant_name + if prefixed not in pnt_dict.keys(): + logger.warning( + f"Bad reference antenna {ant_name} is not present in the data." + ) + + # Generate {ddi: {map: {scan:[i ...], ant:{ant_map_0:[], ...}}}} structure. No reference antennas are added + # because we first need to populate all mapping antennas. + for ant_name, ant_ds in pnt_dict.items(): + if "ant" in ant_name: + ant_name = ant_name.replace("ant_", "") + if ant_name in exclude_antennas: + pass + else: + if ant_name in ant_names_main: # Check if antenna in main table. + ant_names_set.add(ant_name) + for ddi, map_dict in ant_ds.attrs["mapping_scans_obs_dict"][ + 0 + ].items(): + if ddi not in holog_obs_dict: + holog_obs_dict[ddi] = {} + for ant_map_id, scan_list in map_dict.items(): + if scan_list: + map_key = _check_if_array_in_dict( + mapping_scans_dict, scan_list + ) + if not map_key: + map_key = "map_" + str(map_id) + mapping_scans_dict[map_key] = scan_list + map_id = map_id + 1 + + if map_key not in holog_obs_dict[ddi]: + holog_obs_dict[ddi][map_key] = { + "scans": np.array(scan_list), + "ant": {}, + } + + holog_obs_dict[ddi][map_key]["ant"][ant_name] = [] + + df = pd.DataFrame(ant_pos, columns=["x", "y", "z"], index=ant_names) + df_mat = pd.DataFrame( + distance_matrix(df.values, df.values), index=df.index, columns=df.index + ) + logger.debug("".join(("\n", str(df_mat)))) + + if write_distance_matrix: + df_mat.to_csv( + path_or_buf="{base}/.baseline_distance_matrix.csv".format(base=os.getcwd()), + sep="\t", + ) + logger.info( + "Writing distance matrix to {base}/.baseline_distance_matrix.csv ...".format( + base=os.getcwd() + ) + ) + + if (baseline_average_distance != "all") and (baseline_average_nearest != "all"): + logger.error( + "baseline_average_distance and baseline_average_nearest can not both be specified." + ) + + raise Exception("Too many baseline parameters specified.") + + # The reference antennas are then given by ref_ant_set = ant_names_set - map_ant_set. + for ddi, ddi_dict in holog_obs_dict.items(): + for map_id, map_dict in ddi_dict.items(): + map_ant_set = set(map_dict["ant"].keys()) + + # Need a copy because of del holog_obs_dict[ddi][map_id]['ant'][map_ant_key] below. + map_ant_keys = list(map_dict["ant"].keys()) + + for map_ant_key in map_ant_keys: + ref_ant_set = ant_names_set - map_ant_set + + # Select reference antennas by distance from mapping antenna + if baseline_average_distance != "all": + sub_ref_ant_set = [] + + for ref_ant in ref_ant_set: + if df_mat.loc[map_ant_key, ref_ant] < baseline_average_distance: + sub_ref_ant_set.append(ref_ant) + + if (not sub_ref_ant_set) and ref_ant_set: + logger.warning( + "DDI " + + str(ddi) + + " and mapping antenna " + + str(map_ant_key) + + " has no reference antennas. If baseline_average_distance was specified " + "increase this distance. See antenna distance matrix in log by setting " + "debug level to DEBUG in client function." + ) + + ref_ant_set = sub_ref_ant_set + + # Select reference antennas by the n-closest antennas + if baseline_average_nearest != "all": + sub_ref_ant_set = [] + nearest_ant_list = ( + df_mat.loc[map_ant_key, :] + .loc[list(ref_ant_set)] + .sort_values() + .index.tolist()[0:baseline_average_nearest] + ) + + logger.debug(nearest_ant_list) + for ref_ant in ref_ant_set: + if ref_ant in nearest_ant_list: + sub_ref_ant_set.append(ref_ant) + + ref_ant_set = sub_ref_ant_set + ################################################## + + if ref_ant_set: + holog_obs_dict[ddi][map_id]["ant"][map_ant_key] = np.array( + list(ref_ant_set) + ) + else: + del holog_obs_dict[ddi][map_id]["ant"][ + map_ant_key + ] # Don't want mapping antennas with no reference antennas. + logger.warning( + "DDI " + + str(ddi) + + " and mapping antenna " + + str(map_ant_key) + + " has no reference antennas." + ) + + return holog_obs_dict + + +def _check_if_array_in_dict(array_dict, array): + for key, val in array_dict.items(): + if np.array_equiv(val, array): + return key + return False + + +def _extract_pointing_chunk( + map_ant_ids, time_vis, pnt_ant_dict, pointing_interpolation_method +): + """Averages pointing within the time sampling of the visibilities + + Args: + map_ant_ids (list): list of antenna ids + time_vis (numpy.ndarray): sorted, unique list of visibility times + pnt_ant_dict (dict): map of pointing directional cosines with a map key based on the antenna id and indexed by + the MAIN table visibility time. + + Returns: + dict: Dictionary of directional cosine data mapped to nearest MAIN table sample times. + """ + keys = ["DIRECTION", "DIRECTIONAL_COSINES", "ENCODER", "POINTING_OFFSET", "TARGET"] + pnt_map_dict = {} + coords = {"time": time_vis} + for antenna in map_ant_ids: + pnt_xds = pnt_ant_dict[antenna] + y_data = [] + for key in keys: + y_data.append(pnt_xds[key].values) + pnt_time = pnt_xds.time.values + + resample_pnt = grid_1d_data( + time_vis, + pnt_time, + y_data, + pointing_interpolation_method, + f'{antenna.split("_")[1]} pointing data', + "visibility times", + ) + + new_pnt_xds = xr.Dataset() + new_pnt_xds.assign_coords(coords) + + for i_key, key in enumerate(keys): + new_pnt_xds[key] = xr.DataArray(resample_pnt[i_key], dims=("time", "az_el")) + + new_pnt_xds.attrs = pnt_xds.attrs + pnt_map_dict[antenna] = new_pnt_xds + return pnt_map_dict + + +@njit(cache=False, nogil=True) +def _get_time_index(data_time, i_time, time_axis, half_int): + if i_time == time_axis.shape[0]: + return -1 + while data_time > time_axis[i_time] + half_int: + i_time += 1 + if i_time == time_axis.shape[0]: + return -1 + return i_time + + +def _get_general_summary(ms_name, field_ids): + unq_ids = np.unique(field_ids) + field_tbl = ctables.table( + ms_name + "::FIELD", + readonly=True, + lockoptions={"option": "usernoread"}, + ack=False, + ) + i_src = int(unq_ids[0]) + src_name = field_tbl.getcol("NAME") + phase_center_fk5 = field_tbl.getcol("PHASE_DIR")[:, 0, :] + field_tbl.close() + + obs_table = ctables.table( + ms_name + "::OBSERVATION", + readonly=True, + lockoptions={"option": "usernoread"}, + ack=False, + ) + time_range = casa_time_to_mjd(obs_table.getcol("TIME_RANGE")[0]) + telescope_name = obs_table.getcol("TELESCOPE_NAME")[0] + obs_table.close() + + phase_center_fk5[:, 0] = np.where( + phase_center_fk5[:, 0] < 0, + phase_center_fk5[:, 0] + twopi, + phase_center_fk5[:, 0], + ) + + gen_info = { + "source": src_name[i_src], + "phase center": phase_center_fk5[i_src].tolist(), + "telescope name": telescope_name, + "start time": time_range[0], # start time is in MJD in days + "stop time": time_range[-1], # stop time is in MJD in days + "duration": (time_range[-1] - time_range[0]) + * 86400, # Store it in seconds rather than days + } + return gen_info + + +def _get_az_el_characteristics(pnt_map_xds, valid_data): + az_el = pnt_map_xds["ENCODER"].values[valid_data, ...] + lm = pnt_map_xds["DIRECTIONAL_COSINES"].values[valid_data, ...] + mean_az_el = np.mean(az_el, axis=0) + median_az_el = np.median(az_el, axis=0) + lmmid = lm.shape[0] // 2 + lmquart = lmmid // 2 + ilow = lmmid - lmquart + iupper = lmmid + lmquart + 1 + ic = np.argmin((lm[ilow:iupper, 0] ** 2 + lm[ilow:iupper, 1]) ** 2) + ilow + center_az_el = az_el[ic] + az_el_info = { + "center": center_az_el.tolist(), + "mean": mean_az_el.tolist(), + "median": median_az_el.tolist(), + } + return az_el_info + + +def _get_freq_summary(chan_axis): + chan_width = np.abs(chan_axis[1] - chan_axis[0]) + rep_freq = chan_axis[chan_axis.shape[0] // 2] + freq_info = { + "channel width": chan_width, + "number of channels": chan_axis.shape[0], + "frequency range": [ + chan_axis[0] - chan_width / 2, + chan_axis[-1] + chan_width / 2, + ], + "rep. frequency": rep_freq, + "rep. wavelength": clight / rep_freq, + } + + return freq_info + + +def _crate_observation_summary( + antenna_name, + station, + obs_info, + grid_params, + lm, + chan_axis, + pnt_map_xds, + valid_data, + map_ref_dict, +): + spw_info = _get_freq_summary(chan_axis) + obs_info["az el info"] = _get_az_el_characteristics(pnt_map_xds, valid_data) + obs_info["reference antennas"] = map_ref_dict[antenna_name] + obs_info["antenna name"] = antenna_name + obs_info["station"] = station + + l_max = np.max(lm[:, 0]) + l_min = np.min(lm[:, 0]) + m_max = np.max(lm[:, 1]) + m_min = np.min(lm[:, 1]) + + beam_info = { + "grid size": grid_params[f"ant_{antenna_name}"]["n_pix"], + "cell size": grid_params[f"ant_{antenna_name}"]["cell_size"], + "l extent": [l_min, l_max], + "m extent": [m_min, m_max], + } + + summary = { + "spectral": spw_info, + "beam": beam_info, + "general": obs_info, + "aperture": None, + } + return summary + + +def create_holog_json(holog_file, holog_dict): + """Save holog file meta information to json file with the transformation + of the ordering (ddi, holog_map, ant) --> (ant, ddi, holog_map). + + Args: + input_params (): + holog_file (str): holog file name. + holog_dict (dict): Dictionary containing msdx data. + """ + + ant_holog_dict = {} + + for ddi, map_dict in holog_dict.items(): + if "ddi_" in ddi: + for mapping, ant_dict in map_dict.items(): + if "map_" in mapping: + for ant, xds in ant_dict.items(): + if "ant_" in ant: + if ant not in ant_holog_dict: + ant_holog_dict[ant] = {ddi: {mapping: {}}} + elif ddi not in ant_holog_dict[ant]: + ant_holog_dict[ant][ddi] = {mapping: {}} + + ant_holog_dict[ant][ddi][mapping] = xds.to_dict(data=False) + + output_meta_file = "{name}/{ext}".format(name=holog_file, ext=".holog_json") + + try: + with open(output_meta_file, "w") as json_file: + json.dump(ant_holog_dict, json_file) + + except Exception as error: + logger.error(f"{error}") + + raise Exception(error) diff --git a/src/astrohack/extract_holog_2.py b/src/astrohack/extract_holog_2.py new file mode 100644 index 00000000..8b403ef8 --- /dev/null +++ b/src/astrohack/extract_holog_2.py @@ -0,0 +1,1028 @@ +import copy +import json +import os +import pathlib +import pickle +import shutil +import math +import multiprocessing + +import toolviper.utils.parameter +import dask + +import astrohack +import psutil + +import numpy as np +import toolviper.utils.logger as logger + +from casacore import tables as ctables +from rich.console import Console +from rich.table import Table + +from astrohack.utils.constants import pol_str + +from astrohack.utils.file import overwrite_file, check_if_file_can_be_opened +from astrohack.utils.file import load_holog_file +from astrohack.utils.file import load_point_file +from astrohack.utils.data import write_meta_data +from astrohack.core.extract_holog import create_holog_obs_dict, create_holog_json +from astrohack.core.extract_holog import process_extract_holog_chunk +from astrohack.utils.tools import get_valid_state_ids +from astrohack.utils.text import get_default_file_name +from astrohack.utils.text import NumpyEncoder +from astrohack.io.mds import AstrohackHologFile +from astrohack.io.mds import AstrohackPointFile +from astrohack.extract_pointing import extract_pointing + +from typing import Union, List, NewType, Dict, Any, Tuple + +JSON = NewType("JSON", Dict[str, Any]) +KWARGS = NewType("KWARGS", Union[Dict[str, str], Dict[str, int]]) + + +class HologObsDict(dict): + """ + ddi --> map --> ant, scan + | + o--> map: [reference, ...] + """ + + def __init__(self, obj: JSON = None): + if obj is None: + super().__init__() + else: + super().__init__(obj) + + def __getitem__(self, key: str): + return super().__getitem__(key) + + def __setitem__(self, key: str, value: Any): + return super().__setitem__(key, value) + + @classmethod + def from_file(cls, filepath): + if filepath.endswith(".holog.zarr"): + filepath = str( + pathlib.Path(filepath).resolve().joinpath("holog_obs_dict.json") + ) + + try: + with open(filepath, "r") as file: + obj = json.load(file) + + return HologObsDict(obj) + + except FileNotFoundError: + logger.error(f"File {filepath} not found") + + def print(self, style: str = "static"): + if style == "dynamic": + return astrohack.io.dio.inspect_holog_obs_dict(self, style="dynamic") + + else: + return astrohack.io.dio.inspect_holog_obs_dict(self, style="static") + + def select( + self, key: str, value: any, inplace: bool = False, **kwargs: KWARGS + ) -> object: + + if inplace: + obs_dict = self + + else: + obs_dict = HologObsDict(copy.deepcopy(self)) + + if key == "ddi": + return self._select_ddi(value, obs_dict=obs_dict) + + elif key == "map": + return self._select_map(value, obs_dict=obs_dict) + + elif key == "antenna": + return self._select_antenna(value, obs_dict=obs_dict) + + elif key == "scan": + return self._select_scan(value, obs_dict=obs_dict) + + elif key == "baseline": + if "reference" in kwargs.keys(): + return self._select_baseline( + value, + n_baselines=None, + reference=kwargs["reference"], + obs_dict=obs_dict, + ) + + elif "n_baselines" in kwargs.keys(): + return self._select_baseline( + value, + n_baselines=kwargs["n_baselines"], + reference=None, + obs_dict=obs_dict, + ) + + else: + logger.error( + "Must specify a list of reference antennas for this option." + ) + else: + logger.error("Valid key not found: {key}".format(key=key)) + return {} + + @staticmethod + def get_nearest_baselines( + antenna: str, n_baselines: int = None, path_to_matrix: str = None + ) -> object: + import pandas as pd + + if path_to_matrix is None: + path_to_matrix = str( + pathlib.Path.cwd().joinpath(".baseline_distance_matrix.csv") + ) + + if not pathlib.Path(path_to_matrix).exists(): + logger.error( + "Unable to find baseline distance matrix in: {path}".format( + path=path_to_matrix + ) + ) + + df_matrix = pd.read_csv(path_to_matrix, sep="\t", index_col=0) + + # Skip the first index because it is a self distance + if n_baselines is None: + return ( + df_matrix[antenna].sort_values(ascending=True).index[1:].values.tolist() + ) + + return ( + df_matrix[antenna] + .sort_values(ascending=True) + .index[1:n_baselines] + .values.tolist() + ) + + @staticmethod + def _select_ddi(value: Union[int, List[int]], obs_dict: object) -> object: + convert = lambda x: "ddi_" + str(x) + + if not isinstance(value, list): + value = [value] + + value = list(map(convert, value)) + ddi_list = list(obs_dict.keys()) + + for ddi in ddi_list: + if ddi not in value: + obs_dict.pop(ddi) + + return obs_dict + + @staticmethod + def _select_map(value: Union[int, List[int]], obs_dict: object) -> object: + convert = lambda x: "map_" + str(x) + + if not isinstance(value, list): + value = [value] + + value = list(map(convert, value)) + ddi_list = list(obs_dict.keys()) + + for ddi in ddi_list: + map_list = list(obs_dict[ddi].keys()) + for mp in map_list: + if mp not in value: + obs_dict[ddi].pop(mp) + + return obs_dict + + @staticmethod + def _select_antenna(value: Union[str, List[str]], obs_dict: object) -> object: + if not isinstance(value, list): + value = [value] + + ddi_list = list(obs_dict.keys()) + + for ddi in ddi_list: + map_list = list(obs_dict[ddi].keys()) + for mp in map_list: + ant_list = list(obs_dict[ddi][mp]["ant"].keys()) + for ant in ant_list: + if ant not in value: + obs_dict[ddi][mp]["ant"].pop(ant) + + return obs_dict + + @staticmethod + def _select_scan(value: Union[int, List[int]], obs_dict: object) -> object: + if not isinstance(value, list): + value = [value] + + ddi_list = list(obs_dict.keys()) + + for ddi in ddi_list: + map_list = list(obs_dict[ddi].keys()) + for mp in map_list: + obs_dict[ddi][mp]["scans"] = value + + return obs_dict + + @staticmethod + def _select_baseline( + value: str, + n_baselines: int, + obs_dict: object, + reference: Union[str, List[int]] = None, + ) -> object: + if reference is not None: + if not isinstance(reference, list): + reference = [reference] + + ddi_list = list(obs_dict.keys()) + + for ddi in ddi_list: + map_list = list(obs_dict[ddi].keys()) + for mp in map_list: + ant_list = list(obs_dict[ddi][mp]["ant"].keys()) + for ant in ant_list: + if ant not in value: + obs_dict[ddi][mp]["ant"].pop(ant) + continue + + if reference is None and n_baselines is not None: + reference_antennas = obs_dict[ddi][mp]["ant"][ant] + + if n_baselines > len(reference_antennas): + n_baselines = len(reference_antennas) + + sorted_antennas = np.array( + obs_dict.get_nearest_baselines(antenna=ant) + ) + + values, i, j = np.intersect1d( + reference_antennas, sorted_antennas, return_indices=True + ) + index = np.sort(j) + + obs_dict[ddi][mp]["ant"][ant] = sorted_antennas[index][ + :n_baselines + ] + + else: + obs_dict[ddi][mp]["ant"][ant] = reference + + return obs_dict + + +# @toolviper.utils.parameter.validate(add_data_type=HologObsDict) +def extract_holog( + ms_name: str, + point_name: str, + holog_name: str = None, + holog_obs_dict: HologObsDict = None, + ddi: Union[int, List[int], str] = "all", + baseline_average_distance: Union[float, str] = "all", + baseline_average_nearest: Union[float, str] = 1, + exclude_antennas: Union[list[str], str] = None, + data_column: str = "CORRECTED_DATA", + time_smoothing_interval: float = None, + pointing_interpolation_method: str = "linear", + parallel: bool = False, + overwrite: bool = False, +) -> Union[AstrohackHologFile, None]: + """ + Extract holography and optionally pointing data, from measurement set. Creates holography output file. + + :param ms_name: Name of input measurement file name. + :type ms_name: str + + :param point_name: Name of *.point.zarr* file to use. This is must be provided. + :type holog_name: str + + :param holog_name: Name of *.holog.zarr* file to create. Defaults to measurement set name with \ + *holog.zarr* extension. + :type holog_name: str, optional + + :param holog_obs_dict: The *holog_obs_dict* describes which scan and antenna data to extract from the measurement \ + set. As detailed below, this compound dictionary also includes important metadata needed for preprocessing and \ + extraction of the holography data from the measurement set. If not specified holog_obs_dict will be generated. \ + For auto generation of the holog_obs_dict the assumption is made that the same antenna beam is not mapped twice in \ + a row (alternating sets of antennas is fine). If the holog_obs_dict is specified, the ddi input is ignored. The \ + user can self generate this dictionary using `generate_holog_obs_dict`. + :type holog_obs_dict: dict, optional + + :param ddi: DDI(s) that should be extracted from the measurement set. Defaults to all DDI's in the ms. + :type ddi: int numpy.ndarray | int list, optional + + :param baseline_average_distance: To increase the signal-to-noise for a mapping antenna multiple reference \ + antennas can be used. The baseline_average_distance is the acceptable distance (in meters) between a mapping \ + antenna and a reference antenna. The baseline_average_distance is only used if the holog_obs_dict is not \ + specified. If no distance is specified all reference antennas will be used. baseline_average_distance and \ + baseline_average_nearest can not be used together. + :type baseline_average_distance: float, optional + + :param baseline_average_nearest: To increase the signal-to-noise for a mapping antenna multiple reference antennas \ + can be used. The baseline_average_nearest is the number of nearest reference antennas to use. The \ + baseline_average_nearest is only used if the holog_obs_dict is not specified. baseline_average_distance and \ + baseline_average_nearest can not be used together. + :type baseline_average_nearest: int, optional + + :param exclude_antennas: If an antenna is given for exclusion it will not be processed as a reference or a \ + mapping antenna. This can be used to exclude antennas that have bad data for whatever reason. Default is None, \ + meaning no antenna is excluded. + :type exclude_antennas: str | list, optional + + :param data_column: Determines the data column to pull from the measurement set. Defaults to "CORRECTED_DATA". + :type data_column: str, optional, ex. DATA, CORRECTED_DATA + + :param time_smoothing_interval: Determines the time smoothing interval, set to the integration time when None. + :type time_smoothing_interval: float, optional + + :param pointing_interpolation_method: Determines which algorithm to use to interpolate pointing in time, two \ + options are available: 'linear' (Faster but brittle) and gaussian (slower but robust). + :type pointing_interpolation_method: str, optional + + :param parallel: Boolean for whether to process in parallel, defaults to False. + :type parallel: bool, optional + + :param overwrite: Boolean for whether to overwrite current holog.zarr and point.zarr files, defaults to False. + :type overwrite: bool, optional + + :return: Holography holog object. + :rtype: AstrohackHologFile + + .. _Description: + + **AstrohackHologFile** + + Holog object allows the user to access holog data via compound dictionary keys with values, in order of depth, + `ddi` -> `map` -> `ant`. The holog object also provides a `summary()` helper function to list available keys for + each file. An outline of the holog object structure is show below: + + .. parsed-literal:: + holog_mds = + { + ddi_0:{ + map_0:{ + ant_0: holog_ds, + ⋮ + ant_n: holog_ds + }, + ⋮ + map_p: … + }, + ⋮ + ddi_m: … + } + + **Example Usage** In this case the pointing file has already been created. In addition, the appropriate + data_column value nees to be set for the type of measurement set data you are extracting. + + .. parsed-literal:: + from astrohack.extract_holog import extract_holog + + holog_mds = extract_holog( + ms_name="astrohack_observation.ms", + point_name="astrohack_observation.point.ms", + holog_name="astrohack_observation.holog.ms", + data_column='CORRECTED_DATA', + parallel=True, + overwrite=True + ) + + **Additional Information** + + This function extracts the holography related information from the given measurement file. The data is + restructured into an astrohack file format and saved into a file in the form of *.holog.zarr*. + The extension *.holog.zarr* is used for all holography files. In addition, the pointing information is + recorded into a holography file of format *.point.zarr*. The extension *.point.zarr* is used + for all holography pointing files. + + **holog_obs_dict[holog_mapping_id] (dict):** *holog_mapping_id* is a unique, arbitrary, user-defined integer + assigned to the data that describes a single complete mapping of the beam. + + .. rubric:: This is needed for two reasons: + * A complete mapping of the beam can be done over more than one scan (for example the VLA data). + * A measurement set can contain more than one mapping of the beam (for example the ALMA data). + + **holog_obs_dict[holog_mapping_id][scans] (int | numpy.ndarray | list):** + All the scans in the measurement set the *holog_mapping_id*. + + **holog_obs_dict[holog_mapping_id][ant] (dict):** The dictionary keys are the mapping antenna names and the + values a list of the reference antennas. See example below. + + The below example shows how the *holog_obs_description* dictionary should be laid out. For each + *holog_mapping_id* the relevant scans and antennas must be provided. For the `ant` key, an entry is required + for each mapping antenna and the accompanying reference antenna(s). + + .. parsed-literal:: + holog_obs_description = { + 'map_0' :{ + 'scans':[2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22], + 'ant':{ + 'DA44':[ + 'DV02', 'DV03', 'DV04', + 'DV11', 'DV12', 'DV13', + 'DV14', 'DV15', 'DV16', + 'DV17', 'DV18', 'DV19', + 'DV20', 'DV21', 'DV22', + 'DV23', 'DV24', 'DV25' + ] + } + } + } + + """ + + check_if_file_can_be_opened(point_name, "0.7.2") + + # Doing this here allows it to get captured by locals() + if holog_name is None: + holog_name = get_default_file_name( + input_file=ms_name, output_type=".holog.zarr" + ) + + extract_holog_params = locals() + + input_pars = extract_holog_params.copy() + + assert pathlib.Path(extract_holog_params["ms_name"]).exists() is True, logger.error( + f'File {extract_holog_params["ms_name"]} does not exists.' + ) + + overwrite_file( + extract_holog_params["holog_name"], extract_holog_params["overwrite"] + ) + + try: + pnt_dict = load_point_file(extract_holog_params["point_name"]) + + except Exception as error: + logger.error( + "Error loading {name}. - {error}".format( + name=extract_holog_params["point_name"], error=error + ) + ) + + return None + + # Get spectral windows + ctb = ctables.table( + os.path.join(extract_holog_params["ms_name"], "DATA_DESCRIPTION"), + readonly=True, + lockoptions={"option": "usernoread"}, + ack=False, + ) + + ddi_spw = ctb.getcol("SPECTRAL_WINDOW_ID") + ddpol_indexol = ctb.getcol("POLARIZATION_ID") + ms_ddi = np.arange(len(ddi_spw)) + ctb.close() + + # Get antenna IDs and names + ctb = ctables.table( + os.path.join(extract_holog_params["ms_name"], "ANTENNA"), + readonly=True, + lockoptions={"option": "usernoread"}, + ack=False, + ) + + ant_names = np.array(ctb.getcol("NAME")) + ant_id = np.arange(len(ant_names)) + ant_pos = ctb.getcol("POSITION") + ant_station = ctb.getcol("STATION") + + ctb.close() + + # Get antenna IDs in the main table + ctb = ctables.table( + extract_holog_params["ms_name"], + readonly=True, + lockoptions={"option": "usernoread"}, + ack=False, + ) + + ant1 = np.unique(ctb.getcol("ANTENNA1")) + ant2 = np.unique(ctb.getcol("ANTENNA2")) + ant_id_main = np.unique(np.append(ant1, ant2)) + + ant_names_main = ant_names[ant_id_main] + ctb.close() + + # Create holog_obs_dict or modify user supplied holog_obs_dict. + ddi = extract_holog_params["ddi"] + if isinstance(ddi, int): + ddi = [ddi] + + # Create holog_obs_dict if not specified + if holog_obs_dict is None: + holog_obs_dict = create_holog_obs_dict( + pnt_dict, + extract_holog_params["baseline_average_distance"], + extract_holog_params["baseline_average_nearest"], + ant_names, + ant_pos, + ant_names_main, + exclude_antennas=extract_holog_params["exclude_antennas"], + ) + + # From the generated holog_obs_dict subselect user supplied ddis. + if ddi != "all": + holog_obs_dict_keys = list(holog_obs_dict.keys()) + for ddi_key in holog_obs_dict_keys: + if "ddi" in ddi_key: + ddi_id = int(ddi_key.replace("ddi_", "")) + if ddi_id not in ddi: + del holog_obs_dict[ddi_key] + + ctb = ctables.table( + os.path.join(extract_holog_params["ms_name"], "STATE"), + readonly=True, + lockoptions={"option": "usernoread"}, + ack=False, + ) + + # Scan intent (with subscan intent) is stored in the OBS_MODE column of the STATE sub-table. + obs_modes = ctb.getcol("OBS_MODE") + ctb.close() + + state_ids = get_valid_state_ids(obs_modes) + + spw_ctb = ctables.table( + os.path.join(extract_holog_params["ms_name"], "SPECTRAL_WINDOW"), + readonly=True, + lockoptions={"option": "usernoread"}, + ack=False, + ) + pol_ctb = ctables.table( + os.path.join(extract_holog_params["ms_name"], "POLARIZATION"), + readonly=True, + lockoptions={"option": "usernoread"}, + ack=False, + ) + + obs_ctb = ctables.table( + os.path.join(extract_holog_params["ms_name"], "OBSERVATION"), + readonly=True, + lockoptions={"option": "usernoread"}, + ack=False, + ) + + telescope_name = obs_ctb.getcol("TELESCOPE_NAME")[0] + # start_time_unix = obs_ctb.getcol('TIME_RANGE')[0][0] - 3506716800.0 + # time = Time(start_time_unix, format='unix').jyear + + # If we have an EVLA run from before 2023 the pointing table needs to be fixed. + if telescope_name == "EVLA": # and time < 2023: + n_mapping = 0 + for ddi_key, ddi_dict in holog_obs_dict.items(): + n_map_ddi = 0 + for map_dict in ddi_dict.values(): + n_map_ddi += len(map_dict["ant"]) + if n_map_ddi == 0: + logger.warning(f"DDI {ddi_key} has 0 mapping antennas") + n_mapping += n_map_ddi + + if n_mapping == 0: + msg = "No mapping antennas to process, maybe you need to fix the pointing table?" + logger.error(msg) + raise Exception(msg) + + count = 0 + delayed_list = [] + + for ddi_name in holog_obs_dict.keys(): + ddi = int(ddi_name.replace("ddi_", "")) + spw_setup_id = ddi_spw[ddi] + pol_setup_id = ddpol_indexol[ddi] + + extract_holog_params["ddi"] = ddi + extract_holog_params["chan_setup"] = {} + extract_holog_params["pol_setup"] = {} + extract_holog_params["chan_setup"]["chan_freq"] = spw_ctb.getcol( + "CHAN_FREQ", startrow=spw_setup_id, nrow=1 + )[0, :] + + extract_holog_params["chan_setup"]["chan_width"] = spw_ctb.getcol( + "CHAN_WIDTH", startrow=spw_setup_id, nrow=1 + )[0, :] + + extract_holog_params["chan_setup"]["eff_bw"] = spw_ctb.getcol( + "EFFECTIVE_BW", startrow=spw_setup_id, nrow=1 + )[0, :] + + extract_holog_params["chan_setup"]["ref_freq"] = spw_ctb.getcol( + "REF_FREQUENCY", startrow=spw_setup_id, nrow=1 + )[0] + + extract_holog_params["chan_setup"]["total_bw"] = spw_ctb.getcol( + "TOTAL_BANDWIDTH", startrow=spw_setup_id, nrow=1 + )[0] + + extract_holog_params["pol_setup"]["pol"] = pol_str[ + pol_ctb.getcol("CORR_TYPE", startrow=pol_setup_id, nrow=1)[0, :] + ] + + # Loop over all beam_scan_ids, a beam_scan_id can consist of more than one scan in a measurement set (this is + # the case for the VLA pointed mosaics). + for holog_map_key in holog_obs_dict[ddi_name].keys(): + + if "map" in holog_map_key: + scans = holog_obs_dict[ddi_name][holog_map_key]["scans"] + if len(scans) > 1: + logger.info( + "Processing ddi: {ddi}, scans: [{min} ... {max}]".format( + ddi=ddi, min=scans[0], max=scans[-1] + ) + ) + else: + logger.info( + "Processing ddi: {ddi}, scan: {scan}".format( + ddi=ddi, scan=scans + ) + ) + + if ( + len(list(holog_obs_dict[ddi_name][holog_map_key]["ant"].keys())) + != 0 + ): + map_ant_list = [] + ref_ant_per_map_ant_list = [] + + map_ant_name_list = [] + ref_ant_per_map_ant_name_list = [] + for map_ant_str in holog_obs_dict[ddi_name][holog_map_key][ + "ant" + ].keys(): + + ref_ant_ids = np.array( + _convert_ant_name_to_id( + ant_names, + list( + holog_obs_dict[ddi_name][holog_map_key]["ant"][ + map_ant_str + ] + ), + ) + ) + + map_ant_id = _convert_ant_name_to_id(ant_names, map_ant_str)[0] + + ref_ant_per_map_ant_list.append(ref_ant_ids) + map_ant_list.append(map_ant_id) + + ref_ant_per_map_ant_name_list.append( + list( + holog_obs_dict[ddi_name][holog_map_key]["ant"][ + map_ant_str + ] + ) + ) + map_ant_name_list.append(map_ant_str) + + extract_holog_params["ref_ant_per_map_ant_tuple"] = tuple( + ref_ant_per_map_ant_list + ) + extract_holog_params["map_ant_tuple"] = tuple(map_ant_list) + + extract_holog_params["ref_ant_per_map_ant_name_tuple"] = tuple( + ref_ant_per_map_ant_name_list + ) + extract_holog_params["map_ant_name_tuple"] = tuple( + map_ant_name_list + ) + + extract_holog_params["scans"] = scans + extract_holog_params["sel_state_ids"] = state_ids + extract_holog_params["holog_map_key"] = holog_map_key + extract_holog_params["ant_names"] = ant_names + extract_holog_params["ant_station"] = ant_station + + if parallel: + delayed_list.append( + dask.delayed(process_extract_holog_chunk)( + dask.delayed(copy.deepcopy(extract_holog_params)) + ) + ) + else: + process_extract_holog_chunk(extract_holog_params) + + count += 1 + + else: + logger.warning( + "DDI " + str(ddi) + " has no holography data to extract." + ) + + spw_ctb.close() + pol_ctb.close() + obs_ctb.close() + + if parallel: + dask.compute(delayed_list) + + if count > 0: + logger.info("Finished processing") + + holog_dict = load_holog_file( + file=extract_holog_params["holog_name"], dask_load=True, load_pnt_dict=False + ) + + create_holog_json(extract_holog_params["holog_name"], holog_dict) + + holog_attr_file = "{name}/{ext}".format( + name=extract_holog_params["holog_name"], ext=".holog_input" + ) + write_meta_data(holog_attr_file, input_pars) + + with open( + f"{extract_holog_params['holog_name']}/holog_obs_dict.json", "w" + ) as outfile: + json.dump(holog_obs_dict, outfile, cls=NumpyEncoder) + + holog_mds = AstrohackHologFile(extract_holog_params["holog_name"]) + holog_mds.open() + + return holog_mds + + else: + logger.warning("No data to process") + return None + + +def generate_holog_obs_dict( + ms_name: str, + point_name: str, + baseline_average_distance: str = "all", + baseline_average_nearest: str = "all", + write=True, + parallel: bool = False, +) -> HologObsDict: + """ + Generate holography observation dictionary, from measurement set.. + + :param ms_name: Name of input measurement file name. + :type ms_name: str + + :param baseline_average_distance: To increase the signal-to-noise for a mapping antenna multiple reference + antennas can be used. The baseline_average_distance is the acceptable distance between a mapping antenna and a + reference antenna. The baseline_average_distance is only used if the holog_obs_dict is not specified. If no + distance is specified all reference antennas will be used. baseline_average_distance and baseline_average_nearest + can not be used together. + :type baseline_average_distance: float, optional + + :param baseline_average_nearest: To increase the signal-to-noise for a mapping antenna multiple reference antennas + can be used. The baseline_average_nearest is the number of nearest reference antennas to use. The + baseline_average_nearest is only used if the holog_obs_dict is not specified. baseline_average_distance and + baseline_average_nearest can not be used together. + :type baseline_average_nearest: int, optional + + :param write: Write file flag. + :type point_name: bool, optional + + :param point_name: Name of *.point.zarr* file to use. + :type point_name: str, optional + + :param parallel: Boolean for whether to process in parallel. Defaults to False + :type parallel: bool, optional + + :return: holog observation dictionary + :rtype: json + + .. _Description: + + **AstrohackHologFile** + + Holog object allows the user to access holog data via compound dictionary keys with values, in order of depth, + `ddi` -> `map` -> `ant`. The holog object also provides a `summary()` helper function to list available keys for + each file. An outline of the holog object structure is show below: + + .. parsed-literal:: + holog_mds = + { + ddi_0:{ + map_0:{ + ant_0: holog_ds, + ⋮ + ant_n: holog_ds + }, + ⋮ + map_p: … + }, + ⋮ + ddi_m: … + } + + **Example Usage** + In this case the pointing file has already been created. + + .. parsed-literal:: + from astrohack.extract_holog import generate_holog_obs_dict + + holog_obs_obj = generate_holog_obs_dict( + ms_name="astrohack_observation.ms", + point_name="astrohack_observation.point.zarr" + ) + + **Additional Information** + + **holog_obs_dict[holog_mapping_id] (dict):** *holog_mapping_id* is a unique, arbitrary, user-defined integer + assigned to the data that describes a single complete mapping of the beam. + + .. rubric:: This is needed for two reasons: + * A complete mapping of the beam can be done over more than one scan (for example the VLA data). + * A measurement set can contain more than one mapping of the beam (for example the ALMA data). + + **holog_obs_dict[holog_mapping_id][scans] (int | numpy.ndarray | list):** + All the scans in the measurement set the *holog_mapping_id*. + + **holog_obs_dict[holog_mapping_id][ant] (dict):** The dictionary keys are the mapping antenna names and the + values a list of the reference antennas. See example below. + + The below example shows how the *holog_obs_description* dictionary should be laid out. For each + *holog_mapping_id* the relevant scans and antennas must be provided. For the `ant` key, an entry is required + for each mapping antenna and the accompanying reference antenna(s). + + .. parsed-literal:: + holog_obs_description = { + 'map_0' :{ + 'scans':[2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22], + 'ant':{ + 'DA44':[ + 'DV02', 'DV03', 'DV04', + 'DV11', 'DV12', 'DV13', + 'DV14', 'DV15', 'DV16', + 'DV17', 'DV18', 'DV19', + 'DV20', 'DV21', 'DV22', + 'DV23', 'DV24', 'DV25' + ] + } + } + } + + """ + extract_holog_params = locals() + + assert pathlib.Path(ms_name).exists() is True, logger.error( + f"File {ms_name} does not exists." + ) + assert pathlib.Path(point_name).exists() is True, logger.error( + f"File {point_name} does not exists." + ) + + # Get antenna IDs and names + ctb = ctables.table( + os.path.join(extract_holog_params["ms_name"], "ANTENNA"), + readonly=True, + lockoptions={"option": "usernoread"}, + ack=False, + ) + + ant_names = np.array(ctb.getcol("NAME")) + ant_id = np.arange(len(ant_names)) + ant_pos = ctb.getcol("POSITION") + + ctb.close() + + # Get antenna IDs that are in the main table + ctb = ctables.table( + extract_holog_params["ms_name"], + readonly=True, + lockoptions={"option": "usernoread"}, + ack=False, + ) + + ant1 = np.unique(ctb.getcol("ANTENNA1")) + ant2 = np.unique(ctb.getcol("ANTENNA2")) + ant_id_main = np.unique(np.append(ant1, ant2)) + + ant_names_main = ant_names[ant_id_main] + ctb.close() + + pnt_mds = AstrohackPointFile(extract_holog_params["point_name"]) + pnt_mds.open() + + holog_obs_dict = create_holog_obs_dict( + pnt_mds, + extract_holog_params["baseline_average_distance"], + extract_holog_params["baseline_average_nearest"], + ant_names, + ant_pos, + ant_names_main, + write_distance_matrix=True, + ) + + encoded_obj = json.dumps(holog_obs_dict, cls=NumpyEncoder) + + if write: + with open("holog_obs_dict.json", "w") as outfile: + outfile.write(encoded_obj) + + return HologObsDict(json.loads(encoded_obj)) + + +def get_number_of_parameters(holog_obs_dict: HologObsDict) -> Tuple[int, int, int, int]: + scan_list = [] + ant_list = [] + baseline_list = [] + + for ddi in holog_obs_dict.keys(): + for mapping in holog_obs_dict[ddi].keys(): + scan_list.append(len(holog_obs_dict[ddi][mapping]["scans"])) + ant_list.append(len(holog_obs_dict[ddi][mapping]["ant"].keys())) + + for ant in holog_obs_dict[ddi][mapping]["ant"].keys(): + baseline_list.append(len(holog_obs_dict[ddi][mapping]["ant"][ant])) + + n_ddi = len(holog_obs_dict.keys()) + n_scans = max(scan_list) + n_ant = max(ant_list) + n_baseline = max(baseline_list) + + return n_ddi, n_scans, n_ant, n_baseline + + +def model_memory_usage(ms_name: str, holog_obs_dict: HologObsDict = None) -> int: + """Determine the approximate memory usage per core of a given measurement file. + + :param ms_name: Measurement file name + :type ms_name: str + + :param holog_obs_dict: Holography observations dictionary. + :type holog_obs_dict: HologObsDict, optional + + :return: Memory per core + :rtype: int + """ + + # Get holog observations dictionary + if holog_obs_dict is None: + extract_pointing( + ms_name=ms_name, + point_name="temporary.pointing.zarr", + parallel=False, + overwrite=True, + ) + + holog_obs_dict = generate_holog_obs_dict( + ms_name=ms_name, + point_name="temporary.pointing.zarr", + baseline_average_distance="all", + baseline_average_nearest="all", + parallel=False, + ) + + shutil.rmtree("temporary.pointing.zarr") + + # Get number of each parameter + n_ddi, n_scans, n_ant, n_baseline = get_number_of_parameters(holog_obs_dict) + + # Get model file + if not pathlib.Path("model").exists(): + os.mkdir("model") + + toolviper.utils.data.download("heuristic_model", folder="model") + + with open("model/elastic.model", "rb") as model_file: + model = pickle.load(model_file) + + memory_per_core = math.ceil(model.predict([[n_ddi, n_scans, n_ant, n_baseline]])[0]) + + cores = multiprocessing.cpu_count() + memory_available = round((psutil.virtual_memory().available / (1024**2))) + memory_limit = round((psutil.virtual_memory().total / (1024**2))) + + table = Table( + title="System Info", + caption="Available memory: represents the system memory available without going into swap", + ) + + table.add_column("N-cores", justify="right", style="blue", no_wrap=True) + table.add_column("Available memory (MB)", style="magenta") + table.add_column("Total memory (MB)", style="cyan") + table.add_column("Suggested memory per core (MB)", justify="right", style="green") + + table.add_row( + str(cores), str(memory_available), str(memory_limit), str(memory_per_core) + ) + + console = Console() + console.print(table) + + # Make prediction of memory per core in MB + return memory_per_core + + +def _convert_ant_name_to_id(ant_list, ant_names): + """_summary_ + + Args: + ant_list (_type_): _description_ + ant_names (_type_): _description_ + + Returns: + _type_: _description_ + """ + + return np.nonzero(np.isin(ant_list, ant_names))[0] diff --git a/src/astrohack/io/holog_mds.py b/src/astrohack/io/holog_mds.py new file mode 100644 index 00000000..daa6c830 --- /dev/null +++ b/src/astrohack/io/holog_mds.py @@ -0,0 +1,15 @@ +from astrohack.io.base_mds import AstrohackBaseFile + + +class AstrohackHologFile(AstrohackBaseFile): + + def __init__(self, file: str): + """Initialize an AstrohackHologFile object. + + :param file: File to be linked to this object + :type file: str + + :return: AstrohackHologFile object + :rtype: AstrohackHologFile + """ + super().__init__(file=file) From bced6c46c0d2cff1797def8cdb51907601dcefa8 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Thu, 22 Jan 2026 11:55:42 -0700 Subject: [PATCH 018/295] Moved holog_obs_dict class to a separate file. --- src/astrohack/core/holog_obs_dict.py | 248 ++++++++++++++++++++++++++ src/astrohack/extract_holog_2.py | 249 +-------------------------- 2 files changed, 254 insertions(+), 243 deletions(-) create mode 100644 src/astrohack/core/holog_obs_dict.py diff --git a/src/astrohack/core/holog_obs_dict.py b/src/astrohack/core/holog_obs_dict.py new file mode 100644 index 00000000..f1ca5d8a --- /dev/null +++ b/src/astrohack/core/holog_obs_dict.py @@ -0,0 +1,248 @@ +import json +import pathlib +import copy +import numpy as np + +import toolviper.utils.logger as logger + +from typing import Union, List, NewType, Dict, Any + +from astrohack.io.dio import inspect_holog_obs_dict + +JSON = NewType("JSON", Dict[str, Any]) +KWARGS = NewType("KWARGS", Union[Dict[str, str], Dict[str, int]]) + + +class HologObsDict(dict): + """ + ddi --> map --> ant, scan + | + o--> map: [reference, ...] + """ + + def __init__(self, obj: JSON = None): + if obj is None: + super().__init__() + else: + super().__init__(obj) + + def __getitem__(self, key: str): + return super().__getitem__(key) + + def __setitem__(self, key: str, value: Any): + return super().__setitem__(key, value) + + @classmethod + def from_file(cls, filepath): + if filepath.endswith(".holog.zarr"): + filepath = str( + pathlib.Path(filepath).resolve().joinpath("holog_obs_dict.json") + ) + + try: + with open(filepath, "r") as file: + obj = json.load(file) + + return HologObsDict(obj) + + except FileNotFoundError: + logger.error(f"File {filepath} not found") + + def print(self, style: str = "static"): + if style == "dynamic": + return inspect_holog_obs_dict(self, style="dynamic") + + else: + return inspect_holog_obs_dict(self, style="static") + + def select( + self, key: str, value: any, inplace: bool = False, **kwargs: KWARGS + ) -> object: + + if inplace: + obs_dict = self + + else: + obs_dict = HologObsDict(copy.deepcopy(self)) + + if key == "ddi": + return self._select_ddi(value, obs_dict=obs_dict) + + elif key == "map": + return self._select_map(value, obs_dict=obs_dict) + + elif key == "antenna": + return self._select_antenna(value, obs_dict=obs_dict) + + elif key == "scan": + return self._select_scan(value, obs_dict=obs_dict) + + elif key == "baseline": + if "reference" in kwargs.keys(): + return self._select_baseline( + value, + n_baselines=None, + reference=kwargs["reference"], + obs_dict=obs_dict, + ) + + elif "n_baselines" in kwargs.keys(): + return self._select_baseline( + value, + n_baselines=kwargs["n_baselines"], + reference=None, + obs_dict=obs_dict, + ) + + else: + logger.error( + "Must specify a list of reference antennas for this option." + ) + return {} + else: + logger.error("Valid key not found: {key}".format(key=key)) + return {} + + @staticmethod + def get_nearest_baselines( + antenna: str, n_baselines: int = None, path_to_matrix: str = None + ) -> object: + import pandas as pd + + if path_to_matrix is None: + path_to_matrix = str( + pathlib.Path.cwd().joinpath(".baseline_distance_matrix.csv") + ) + + if not pathlib.Path(path_to_matrix).exists(): + logger.error( + "Unable to find baseline distance matrix in: {path}".format( + path=path_to_matrix + ) + ) + + df_matrix = pd.read_csv(path_to_matrix, sep="\t", index_col=0) + + # Skip the first index because it is a self distance + if n_baselines is None: + return ( + df_matrix[antenna].sort_values(ascending=True).index[1:].values.tolist() + ) + + return ( + df_matrix[antenna] + .sort_values(ascending=True) + .index[1:n_baselines] + .values.tolist() + ) + + @staticmethod + def _select_ddi(value: Union[int, List[int]], obs_dict: object) -> object: + convert = lambda x: "ddi_" + str(x) + + if not isinstance(value, list): + value = [value] + + value = list(map(convert, value)) + ddi_list = list(obs_dict.keys()) + + for ddi in ddi_list: + if ddi not in value: + obs_dict.pop(ddi) + + return obs_dict + + @staticmethod + def _select_map(value: Union[int, List[int]], obs_dict: object) -> object: + convert = lambda x: "map_" + str(x) + + if not isinstance(value, list): + value = [value] + + value = list(map(convert, value)) + ddi_list = list(obs_dict.keys()) + + for ddi in ddi_list: + map_list = list(obs_dict[ddi].keys()) + for mp in map_list: + if mp not in value: + obs_dict[ddi].pop(mp) + + return obs_dict + + @staticmethod + def _select_antenna(value: Union[str, List[str]], obs_dict: object) -> object: + if not isinstance(value, list): + value = [value] + + ddi_list = list(obs_dict.keys()) + + for ddi in ddi_list: + map_list = list(obs_dict[ddi].keys()) + for mp in map_list: + ant_list = list(obs_dict[ddi][mp]["ant"].keys()) + for ant in ant_list: + if ant not in value: + obs_dict[ddi][mp]["ant"].pop(ant) + + return obs_dict + + @staticmethod + def _select_scan(value: Union[int, List[int]], obs_dict: object) -> object: + if not isinstance(value, list): + value = [value] + + ddi_list = list(obs_dict.keys()) + + for ddi in ddi_list: + map_list = list(obs_dict[ddi].keys()) + for mp in map_list: + obs_dict[ddi][mp]["scans"] = value + + return obs_dict + + @staticmethod + def _select_baseline( + value: str, + n_baselines: int, + obs_dict: object, + reference: Union[str, List[int]] = None, + ) -> object: + if reference is not None: + if not isinstance(reference, list): + reference = [reference] + + ddi_list = list(obs_dict.keys()) + + for ddi in ddi_list: + map_list = list(obs_dict[ddi].keys()) + for mp in map_list: + ant_list = list(obs_dict[ddi][mp]["ant"].keys()) + for ant in ant_list: + if ant not in value: + obs_dict[ddi][mp]["ant"].pop(ant) + continue + + if reference is None and n_baselines is not None: + reference_antennas = obs_dict[ddi][mp]["ant"][ant] + + if n_baselines > len(reference_antennas): + n_baselines = len(reference_antennas) + + sorted_antennas = np.array( + obs_dict.get_nearest_baselines(antenna=ant) + ) + + values, i, j = np.intersect1d( + reference_antennas, sorted_antennas, return_indices=True + ) + index = np.sort(j) + + obs_dict[ddi][mp]["ant"][ant] = sorted_antennas[index][ + :n_baselines + ] + + else: + obs_dict[ddi][mp]["ant"][ant] = reference + + return obs_dict diff --git a/src/astrohack/extract_holog_2.py b/src/astrohack/extract_holog_2.py index 8b403ef8..f107f478 100644 --- a/src/astrohack/extract_holog_2.py +++ b/src/astrohack/extract_holog_2.py @@ -10,7 +10,6 @@ import toolviper.utils.parameter import dask -import astrohack import psutil import numpy as np @@ -26,253 +25,17 @@ from astrohack.utils.file import load_holog_file from astrohack.utils.file import load_point_file from astrohack.utils.data import write_meta_data -from astrohack.core.extract_holog import create_holog_obs_dict, create_holog_json -from astrohack.core.extract_holog import process_extract_holog_chunk +from astrohack.core.extract_holog_2 import create_holog_obs_dict, create_holog_json +from astrohack.core.extract_holog_2 import process_extract_holog_chunk from astrohack.utils.tools import get_valid_state_ids from astrohack.utils.text import get_default_file_name from astrohack.utils.text import NumpyEncoder -from astrohack.io.mds import AstrohackHologFile -from astrohack.io.mds import AstrohackPointFile +from astrohack.io.holog_mds import AstrohackHologFile +from astrohack.io.point_mds import AstrohackPointFile from astrohack.extract_pointing import extract_pointing +from astrohack.core.holog_obs_dict import HologObsDict -from typing import Union, List, NewType, Dict, Any, Tuple - -JSON = NewType("JSON", Dict[str, Any]) -KWARGS = NewType("KWARGS", Union[Dict[str, str], Dict[str, int]]) - - -class HologObsDict(dict): - """ - ddi --> map --> ant, scan - | - o--> map: [reference, ...] - """ - - def __init__(self, obj: JSON = None): - if obj is None: - super().__init__() - else: - super().__init__(obj) - - def __getitem__(self, key: str): - return super().__getitem__(key) - - def __setitem__(self, key: str, value: Any): - return super().__setitem__(key, value) - - @classmethod - def from_file(cls, filepath): - if filepath.endswith(".holog.zarr"): - filepath = str( - pathlib.Path(filepath).resolve().joinpath("holog_obs_dict.json") - ) - - try: - with open(filepath, "r") as file: - obj = json.load(file) - - return HologObsDict(obj) - - except FileNotFoundError: - logger.error(f"File {filepath} not found") - - def print(self, style: str = "static"): - if style == "dynamic": - return astrohack.io.dio.inspect_holog_obs_dict(self, style="dynamic") - - else: - return astrohack.io.dio.inspect_holog_obs_dict(self, style="static") - - def select( - self, key: str, value: any, inplace: bool = False, **kwargs: KWARGS - ) -> object: - - if inplace: - obs_dict = self - - else: - obs_dict = HologObsDict(copy.deepcopy(self)) - - if key == "ddi": - return self._select_ddi(value, obs_dict=obs_dict) - - elif key == "map": - return self._select_map(value, obs_dict=obs_dict) - - elif key == "antenna": - return self._select_antenna(value, obs_dict=obs_dict) - - elif key == "scan": - return self._select_scan(value, obs_dict=obs_dict) - - elif key == "baseline": - if "reference" in kwargs.keys(): - return self._select_baseline( - value, - n_baselines=None, - reference=kwargs["reference"], - obs_dict=obs_dict, - ) - - elif "n_baselines" in kwargs.keys(): - return self._select_baseline( - value, - n_baselines=kwargs["n_baselines"], - reference=None, - obs_dict=obs_dict, - ) - - else: - logger.error( - "Must specify a list of reference antennas for this option." - ) - else: - logger.error("Valid key not found: {key}".format(key=key)) - return {} - - @staticmethod - def get_nearest_baselines( - antenna: str, n_baselines: int = None, path_to_matrix: str = None - ) -> object: - import pandas as pd - - if path_to_matrix is None: - path_to_matrix = str( - pathlib.Path.cwd().joinpath(".baseline_distance_matrix.csv") - ) - - if not pathlib.Path(path_to_matrix).exists(): - logger.error( - "Unable to find baseline distance matrix in: {path}".format( - path=path_to_matrix - ) - ) - - df_matrix = pd.read_csv(path_to_matrix, sep="\t", index_col=0) - - # Skip the first index because it is a self distance - if n_baselines is None: - return ( - df_matrix[antenna].sort_values(ascending=True).index[1:].values.tolist() - ) - - return ( - df_matrix[antenna] - .sort_values(ascending=True) - .index[1:n_baselines] - .values.tolist() - ) - - @staticmethod - def _select_ddi(value: Union[int, List[int]], obs_dict: object) -> object: - convert = lambda x: "ddi_" + str(x) - - if not isinstance(value, list): - value = [value] - - value = list(map(convert, value)) - ddi_list = list(obs_dict.keys()) - - for ddi in ddi_list: - if ddi not in value: - obs_dict.pop(ddi) - - return obs_dict - - @staticmethod - def _select_map(value: Union[int, List[int]], obs_dict: object) -> object: - convert = lambda x: "map_" + str(x) - - if not isinstance(value, list): - value = [value] - - value = list(map(convert, value)) - ddi_list = list(obs_dict.keys()) - - for ddi in ddi_list: - map_list = list(obs_dict[ddi].keys()) - for mp in map_list: - if mp not in value: - obs_dict[ddi].pop(mp) - - return obs_dict - - @staticmethod - def _select_antenna(value: Union[str, List[str]], obs_dict: object) -> object: - if not isinstance(value, list): - value = [value] - - ddi_list = list(obs_dict.keys()) - - for ddi in ddi_list: - map_list = list(obs_dict[ddi].keys()) - for mp in map_list: - ant_list = list(obs_dict[ddi][mp]["ant"].keys()) - for ant in ant_list: - if ant not in value: - obs_dict[ddi][mp]["ant"].pop(ant) - - return obs_dict - - @staticmethod - def _select_scan(value: Union[int, List[int]], obs_dict: object) -> object: - if not isinstance(value, list): - value = [value] - - ddi_list = list(obs_dict.keys()) - - for ddi in ddi_list: - map_list = list(obs_dict[ddi].keys()) - for mp in map_list: - obs_dict[ddi][mp]["scans"] = value - - return obs_dict - - @staticmethod - def _select_baseline( - value: str, - n_baselines: int, - obs_dict: object, - reference: Union[str, List[int]] = None, - ) -> object: - if reference is not None: - if not isinstance(reference, list): - reference = [reference] - - ddi_list = list(obs_dict.keys()) - - for ddi in ddi_list: - map_list = list(obs_dict[ddi].keys()) - for mp in map_list: - ant_list = list(obs_dict[ddi][mp]["ant"].keys()) - for ant in ant_list: - if ant not in value: - obs_dict[ddi][mp]["ant"].pop(ant) - continue - - if reference is None and n_baselines is not None: - reference_antennas = obs_dict[ddi][mp]["ant"][ant] - - if n_baselines > len(reference_antennas): - n_baselines = len(reference_antennas) - - sorted_antennas = np.array( - obs_dict.get_nearest_baselines(antenna=ant) - ) - - values, i, j = np.intersect1d( - reference_antennas, sorted_antennas, return_indices=True - ) - index = np.sort(j) - - obs_dict[ddi][mp]["ant"][ant] = sorted_antennas[index][ - :n_baselines - ] - - else: - obs_dict[ddi][mp]["ant"][ant] = reference - - return obs_dict +from typing import Union, List, Tuple # @toolviper.utils.parameter.validate(add_data_type=HologObsDict) From 2c665b14719f905bc629ce3fdbb3bd3d63d13f17 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Thu, 22 Jan 2026 11:58:41 -0700 Subject: [PATCH 019/295] Added proper check on input .locit.zarr file to locit --- src/astrohack/locit.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/astrohack/locit.py b/src/astrohack/locit.py index 3b17da11..cedcee75 100644 --- a/src/astrohack/locit.py +++ b/src/astrohack/locit.py @@ -4,7 +4,7 @@ import toolviper.utils.logger as logger from astrohack.utils.graph import compute_graph_to_mds_tree -from astrohack.utils.file import overwrite_file +from astrohack.utils.file import overwrite_file, check_if_file_can_be_opened_2 from astrohack.core.locit import ( locit_separated_chunk, locit_combined_chunk, @@ -157,9 +157,8 @@ def locit( input_params = locit_params.copy() attributes = locit_params.copy() - assert pathlib.Path(locit_params["locit_name"]).exists() is True, logger.error( - f'File {locit_params["locit_name"]} does not exists.' - ) + check_if_file_can_be_opened_2(locit_params["locit_name"], "extract_locit", "0.10.1") + overwrite_file(locit_params["position_name"], locit_params["overwrite"]) locit_mds = AstrohackLocitFile(locit_params["locit_name"]) From e6a0166c38a52e67decbd4b7a8e0e61789f09663 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Thu, 22 Jan 2026 12:05:13 -0700 Subject: [PATCH 020/295] Made open_pointing to point to the new AstrohackPointFile type. --- src/astrohack/io/dio.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/astrohack/io/dio.py b/src/astrohack/io/dio.py index 573ef96e..22afbff4 100644 --- a/src/astrohack/io/dio.py +++ b/src/astrohack/io/dio.py @@ -16,7 +16,7 @@ from astrohack.io.mds import AstrohackImageFile from astrohack.io.mds import AstrohackHologFile from astrohack.io.mds import AstrohackPanelFile -from astrohack.io.mds import AstrohackPointFile +from astrohack.io.point_mds import AstrohackPointFile from astrohack.io.position_mds import AstrohackPositionFile from astrohack.utils.text import print_array @@ -290,7 +290,7 @@ def open_pointing(file: str) -> Union[AstrohackPointFile, None]: } """ - check_if_file_can_be_opened(file, "0.7.2") + check_if_file_can_be_opened_2(file, "locit", "0.10.1") _data_file = AstrohackPointFile(file=file) if _data_file.open(): From a8a56388e6be4b8dcb28bfd9847dfc5941775a7a Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Thu, 22 Jan 2026 12:13:14 -0700 Subject: [PATCH 021/295] Moved preprocessing in extract_holog to a function in core/extract_holog. --- src/astrohack/core/extract_holog_2.py | 279 ++++++++++++++++++++ src/astrohack/extract_holog_2.py | 358 +++----------------------- 2 files changed, 321 insertions(+), 316 deletions(-) diff --git a/src/astrohack/core/extract_holog_2.py b/src/astrohack/core/extract_holog_2.py index 610f5e9f..194e5f5a 100644 --- a/src/astrohack/core/extract_holog_2.py +++ b/src/astrohack/core/extract_holog_2.py @@ -1,5 +1,7 @@ import os import json +import dask +import copy import numpy as np import xarray as xr @@ -11,6 +13,7 @@ from casacore import tables as ctables +from astrohack.utils.tools import get_valid_state_ids from astrohack.antenna import get_proper_telescope from astrohack.utils import create_dataset_label from astrohack.utils.imaging import calculate_parallactic_angle_chunk @@ -18,10 +21,272 @@ from astrohack.utils.conversion import casa_time_to_mjd from astrohack.utils.constants import twopi, clight from astrohack.utils.gridding import grid_1d_data +from astrohack.utils.constants import pol_str + from astrohack.utils.file import load_point_file +def extract_holog_processing(extract_holog_params, pnt_mds): + holog_obs_dict = extract_holog_params["holog_obs_dict"] + parallel = extract_holog_params["parallel"] + + # Get spectral windows + ctb = ctables.table( + os.path.join(extract_holog_params["ms_name"], "DATA_DESCRIPTION"), + readonly=True, + lockoptions={"option": "usernoread"}, + ack=False, + ) + + ddi_spw = ctb.getcol("SPECTRAL_WINDOW_ID") + ddpol_indexol = ctb.getcol("POLARIZATION_ID") + ms_ddi = np.arange(len(ddi_spw)) + ctb.close() + + # Get antenna IDs and names + ctb = ctables.table( + os.path.join(extract_holog_params["ms_name"], "ANTENNA"), + readonly=True, + lockoptions={"option": "usernoread"}, + ack=False, + ) + + ant_names = np.array(ctb.getcol("NAME")) + ant_id = np.arange(len(ant_names)) + ant_pos = ctb.getcol("POSITION") + ant_station = ctb.getcol("STATION") + + ctb.close() + + # Get antenna IDs in the main table + ctb = ctables.table( + extract_holog_params["ms_name"], + readonly=True, + lockoptions={"option": "usernoread"}, + ack=False, + ) + + ant1 = np.unique(ctb.getcol("ANTENNA1")) + ant2 = np.unique(ctb.getcol("ANTENNA2")) + ant_id_main = np.unique(np.append(ant1, ant2)) + + ant_names_main = ant_names[ant_id_main] + ctb.close() + + # Create holog_obs_dict or modify user supplied holog_obs_dict. + ddi = extract_holog_params["ddi"] + if isinstance(ddi, int): + ddi = [ddi] + + # Create holog_obs_dict if not specified + if holog_obs_dict is None: + holog_obs_dict = create_holog_obs_dict( + pnt_mds, + extract_holog_params["baseline_average_distance"], + extract_holog_params["baseline_average_nearest"], + ant_names, + ant_pos, + ant_names_main, + exclude_antennas=extract_holog_params["exclude_antennas"], + ) + + # From the generated holog_obs_dict subselect user supplied ddis. + if ddi != "all": + holog_obs_dict_keys = list(holog_obs_dict.keys()) + for ddi_key in holog_obs_dict_keys: + if "ddi" in ddi_key: + ddi_id = int(ddi_key.replace("ddi_", "")) + if ddi_id not in ddi: + del holog_obs_dict[ddi_key] + + ctb = ctables.table( + os.path.join(extract_holog_params["ms_name"], "STATE"), + readonly=True, + lockoptions={"option": "usernoread"}, + ack=False, + ) + + # Scan intent (with subscan intent) is stored in the OBS_MODE column of the STATE sub-table. + obs_modes = ctb.getcol("OBS_MODE") + ctb.close() + + state_ids = get_valid_state_ids(obs_modes) + + spw_ctb = ctables.table( + os.path.join(extract_holog_params["ms_name"], "SPECTRAL_WINDOW"), + readonly=True, + lockoptions={"option": "usernoread"}, + ack=False, + ) + pol_ctb = ctables.table( + os.path.join(extract_holog_params["ms_name"], "POLARIZATION"), + readonly=True, + lockoptions={"option": "usernoread"}, + ack=False, + ) + + obs_ctb = ctables.table( + os.path.join(extract_holog_params["ms_name"], "OBSERVATION"), + readonly=True, + lockoptions={"option": "usernoread"}, + ack=False, + ) + + telescope_name = obs_ctb.getcol("TELESCOPE_NAME")[0] + # start_time_unix = obs_ctb.getcol('TIME_RANGE')[0][0] - 3506716800.0 + # time = Time(start_time_unix, format='unix').jyear + + # If we have an EVLA run from before 2023 the pointing table needs to be fixed. + if telescope_name == "EVLA": # and time < 2023: + n_mapping = 0 + for ddi_key, ddi_dict in holog_obs_dict.items(): + n_map_ddi = 0 + for map_dict in ddi_dict.values(): + n_map_ddi += len(map_dict["ant"]) + if n_map_ddi == 0: + logger.warning(f"DDI {ddi_key} has 0 mapping antennas") + n_mapping += n_map_ddi + + if n_mapping == 0: + msg = "No mapping antennas to process, maybe you need to fix the pointing table?" + logger.error(msg) + raise Exception(msg) + + count = 0 + delayed_list = [] + + for ddi_name in holog_obs_dict.keys(): + ddi = int(ddi_name.replace("ddi_", "")) + spw_setup_id = ddi_spw[ddi] + pol_setup_id = ddpol_indexol[ddi] + + extract_holog_params["ddi"] = ddi + extract_holog_params["chan_setup"] = {} + extract_holog_params["pol_setup"] = {} + extract_holog_params["chan_setup"]["chan_freq"] = spw_ctb.getcol( + "CHAN_FREQ", startrow=spw_setup_id, nrow=1 + )[0, :] + + extract_holog_params["chan_setup"]["chan_width"] = spw_ctb.getcol( + "CHAN_WIDTH", startrow=spw_setup_id, nrow=1 + )[0, :] + + extract_holog_params["chan_setup"]["eff_bw"] = spw_ctb.getcol( + "EFFECTIVE_BW", startrow=spw_setup_id, nrow=1 + )[0, :] + + extract_holog_params["chan_setup"]["ref_freq"] = spw_ctb.getcol( + "REF_FREQUENCY", startrow=spw_setup_id, nrow=1 + )[0] + + extract_holog_params["chan_setup"]["total_bw"] = spw_ctb.getcol( + "TOTAL_BANDWIDTH", startrow=spw_setup_id, nrow=1 + )[0] + + extract_holog_params["pol_setup"]["pol"] = pol_str[ + pol_ctb.getcol("CORR_TYPE", startrow=pol_setup_id, nrow=1)[0, :] + ] + + # Loop over all beam_scan_ids, a beam_scan_id can consist of more than one scan in a measurement set (this is + # the case for the VLA pointed mosaics). + for holog_map_key in holog_obs_dict[ddi_name].keys(): + + if "map" in holog_map_key: + scans = holog_obs_dict[ddi_name][holog_map_key]["scans"] + if len(scans) > 1: + logger.info( + "Processing ddi: {ddi}, scans: [{min} ... {max}]".format( + ddi=ddi, min=scans[0], max=scans[-1] + ) + ) + else: + logger.info( + "Processing ddi: {ddi}, scan: {scan}".format( + ddi=ddi, scan=scans + ) + ) + + if ( + len(list(holog_obs_dict[ddi_name][holog_map_key]["ant"].keys())) + != 0 + ): + map_ant_list = [] + ref_ant_per_map_ant_list = [] + + map_ant_name_list = [] + ref_ant_per_map_ant_name_list = [] + for map_ant_str in holog_obs_dict[ddi_name][holog_map_key][ + "ant" + ].keys(): + + ref_ant_ids = np.array( + _convert_ant_name_to_id( + ant_names, + list( + holog_obs_dict[ddi_name][holog_map_key]["ant"][ + map_ant_str + ] + ), + ) + ) + + map_ant_id = _convert_ant_name_to_id(ant_names, map_ant_str)[0] + + ref_ant_per_map_ant_list.append(ref_ant_ids) + map_ant_list.append(map_ant_id) + + ref_ant_per_map_ant_name_list.append( + list( + holog_obs_dict[ddi_name][holog_map_key]["ant"][ + map_ant_str + ] + ) + ) + map_ant_name_list.append(map_ant_str) + + extract_holog_params["ref_ant_per_map_ant_tuple"] = tuple( + ref_ant_per_map_ant_list + ) + extract_holog_params["map_ant_tuple"] = tuple(map_ant_list) + + extract_holog_params["ref_ant_per_map_ant_name_tuple"] = tuple( + ref_ant_per_map_ant_name_list + ) + extract_holog_params["map_ant_name_tuple"] = tuple( + map_ant_name_list + ) + + extract_holog_params["scans"] = scans + extract_holog_params["sel_state_ids"] = state_ids + extract_holog_params["holog_map_key"] = holog_map_key + extract_holog_params["ant_names"] = ant_names + extract_holog_params["ant_station"] = ant_station + + if parallel: + delayed_list.append( + dask.delayed(process_extract_holog_chunk)( + dask.delayed(copy.deepcopy(extract_holog_params)) + ) + ) + else: + process_extract_holog_chunk(extract_holog_params) + + count += 1 + + else: + logger.warning( + "DDI " + str(ddi) + " has no holography data to extract." + ) + + spw_ctb.close() + pol_ctb.close() + obs_ctb.close() + + if parallel: + dask.compute(delayed_list) + + def process_extract_holog_chunk(extract_holog_params): """Perform data query on holography data chunk and get unique time and state_ids/ @@ -899,3 +1164,17 @@ def create_holog_json(holog_file, holog_dict): logger.error(f"{error}") raise Exception(error) + + +def _convert_ant_name_to_id(ant_list, ant_names): + """_summary_ + + Args: + ant_list (_type_): _description_ + ant_names (_type_): _description_ + + Returns: + _type_: _description_ + """ + + return np.nonzero(np.isin(ant_list, ant_names))[0] diff --git a/src/astrohack/extract_holog_2.py b/src/astrohack/extract_holog_2.py index f107f478..4a579cca 100644 --- a/src/astrohack/extract_holog_2.py +++ b/src/astrohack/extract_holog_2.py @@ -19,15 +19,22 @@ from rich.console import Console from rich.table import Table -from astrohack.utils.constants import pol_str +from astrohack import open_pointing -from astrohack.utils.file import overwrite_file, check_if_file_can_be_opened +from astrohack.utils.file import ( + overwrite_file, + check_if_file_can_be_opened, + check_if_file_can_be_opened_2, +) from astrohack.utils.file import load_holog_file from astrohack.utils.file import load_point_file from astrohack.utils.data import write_meta_data -from astrohack.core.extract_holog_2 import create_holog_obs_dict, create_holog_json +from astrohack.core.extract_holog_2 import ( + create_holog_obs_dict, + create_holog_json, + extract_holog_processing, +) from astrohack.core.extract_holog_2 import process_extract_holog_chunk -from astrohack.utils.tools import get_valid_state_ids from astrohack.utils.text import get_default_file_name from astrohack.utils.text import NumpyEncoder from astrohack.io.holog_mds import AstrohackHologFile @@ -198,8 +205,6 @@ def extract_holog( """ - check_if_file_can_be_opened(point_name, "0.7.2") - # Doing this here allows it to get captured by locals() if holog_name is None: holog_name = get_default_file_name( @@ -208,8 +213,6 @@ def extract_holog( extract_holog_params = locals() - input_pars = extract_holog_params.copy() - assert pathlib.Path(extract_holog_params["ms_name"]).exists() is True, logger.error( f'File {extract_holog_params["ms_name"]} does not exists.' ) @@ -218,300 +221,37 @@ def extract_holog( extract_holog_params["holog_name"], extract_holog_params["overwrite"] ) - try: - pnt_dict = load_point_file(extract_holog_params["point_name"]) - - except Exception as error: - logger.error( - "Error loading {name}. - {error}".format( - name=extract_holog_params["point_name"], error=error - ) - ) - - return None - - # Get spectral windows - ctb = ctables.table( - os.path.join(extract_holog_params["ms_name"], "DATA_DESCRIPTION"), - readonly=True, - lockoptions={"option": "usernoread"}, - ack=False, - ) - - ddi_spw = ctb.getcol("SPECTRAL_WINDOW_ID") - ddpol_indexol = ctb.getcol("POLARIZATION_ID") - ms_ddi = np.arange(len(ddi_spw)) - ctb.close() - - # Get antenna IDs and names - ctb = ctables.table( - os.path.join(extract_holog_params["ms_name"], "ANTENNA"), - readonly=True, - lockoptions={"option": "usernoread"}, - ack=False, - ) - - ant_names = np.array(ctb.getcol("NAME")) - ant_id = np.arange(len(ant_names)) - ant_pos = ctb.getcol("POSITION") - ant_station = ctb.getcol("STATION") - - ctb.close() - - # Get antenna IDs in the main table - ctb = ctables.table( - extract_holog_params["ms_name"], - readonly=True, - lockoptions={"option": "usernoread"}, - ack=False, - ) - - ant1 = np.unique(ctb.getcol("ANTENNA1")) - ant2 = np.unique(ctb.getcol("ANTENNA2")) - ant_id_main = np.unique(np.append(ant1, ant2)) - - ant_names_main = ant_names[ant_id_main] - ctb.close() - - # Create holog_obs_dict or modify user supplied holog_obs_dict. - ddi = extract_holog_params["ddi"] - if isinstance(ddi, int): - ddi = [ddi] - - # Create holog_obs_dict if not specified - if holog_obs_dict is None: - holog_obs_dict = create_holog_obs_dict( - pnt_dict, - extract_holog_params["baseline_average_distance"], - extract_holog_params["baseline_average_nearest"], - ant_names, - ant_pos, - ant_names_main, - exclude_antennas=extract_holog_params["exclude_antennas"], - ) - - # From the generated holog_obs_dict subselect user supplied ddis. - if ddi != "all": - holog_obs_dict_keys = list(holog_obs_dict.keys()) - for ddi_key in holog_obs_dict_keys: - if "ddi" in ddi_key: - ddi_id = int(ddi_key.replace("ddi_", "")) - if ddi_id not in ddi: - del holog_obs_dict[ddi_key] - - ctb = ctables.table( - os.path.join(extract_holog_params["ms_name"], "STATE"), - readonly=True, - lockoptions={"option": "usernoread"}, - ack=False, - ) - - # Scan intent (with subscan intent) is stored in the OBS_MODE column of the STATE sub-table. - obs_modes = ctb.getcol("OBS_MODE") - ctb.close() - - state_ids = get_valid_state_ids(obs_modes) - - spw_ctb = ctables.table( - os.path.join(extract_holog_params["ms_name"], "SPECTRAL_WINDOW"), - readonly=True, - lockoptions={"option": "usernoread"}, - ack=False, - ) - pol_ctb = ctables.table( - os.path.join(extract_holog_params["ms_name"], "POLARIZATION"), - readonly=True, - lockoptions={"option": "usernoread"}, - ack=False, - ) - - obs_ctb = ctables.table( - os.path.join(extract_holog_params["ms_name"], "OBSERVATION"), - readonly=True, - lockoptions={"option": "usernoread"}, - ack=False, - ) - - telescope_name = obs_ctb.getcol("TELESCOPE_NAME")[0] - # start_time_unix = obs_ctb.getcol('TIME_RANGE')[0][0] - 3506716800.0 - # time = Time(start_time_unix, format='unix').jyear - - # If we have an EVLA run from before 2023 the pointing table needs to be fixed. - if telescope_name == "EVLA": # and time < 2023: - n_mapping = 0 - for ddi_key, ddi_dict in holog_obs_dict.items(): - n_map_ddi = 0 - for map_dict in ddi_dict.values(): - n_map_ddi += len(map_dict["ant"]) - if n_map_ddi == 0: - logger.warning(f"DDI {ddi_key} has 0 mapping antennas") - n_mapping += n_map_ddi - - if n_mapping == 0: - msg = "No mapping antennas to process, maybe you need to fix the pointing table?" - logger.error(msg) - raise Exception(msg) - - count = 0 - delayed_list = [] - - for ddi_name in holog_obs_dict.keys(): - ddi = int(ddi_name.replace("ddi_", "")) - spw_setup_id = ddi_spw[ddi] - pol_setup_id = ddpol_indexol[ddi] - - extract_holog_params["ddi"] = ddi - extract_holog_params["chan_setup"] = {} - extract_holog_params["pol_setup"] = {} - extract_holog_params["chan_setup"]["chan_freq"] = spw_ctb.getcol( - "CHAN_FREQ", startrow=spw_setup_id, nrow=1 - )[0, :] - - extract_holog_params["chan_setup"]["chan_width"] = spw_ctb.getcol( - "CHAN_WIDTH", startrow=spw_setup_id, nrow=1 - )[0, :] - - extract_holog_params["chan_setup"]["eff_bw"] = spw_ctb.getcol( - "EFFECTIVE_BW", startrow=spw_setup_id, nrow=1 - )[0, :] - - extract_holog_params["chan_setup"]["ref_freq"] = spw_ctb.getcol( - "REF_FREQUENCY", startrow=spw_setup_id, nrow=1 - )[0] - - extract_holog_params["chan_setup"]["total_bw"] = spw_ctb.getcol( - "TOTAL_BANDWIDTH", startrow=spw_setup_id, nrow=1 - )[0] - - extract_holog_params["pol_setup"]["pol"] = pol_str[ - pol_ctb.getcol("CORR_TYPE", startrow=pol_setup_id, nrow=1)[0, :] - ] - - # Loop over all beam_scan_ids, a beam_scan_id can consist of more than one scan in a measurement set (this is - # the case for the VLA pointed mosaics). - for holog_map_key in holog_obs_dict[ddi_name].keys(): - - if "map" in holog_map_key: - scans = holog_obs_dict[ddi_name][holog_map_key]["scans"] - if len(scans) > 1: - logger.info( - "Processing ddi: {ddi}, scans: [{min} ... {max}]".format( - ddi=ddi, min=scans[0], max=scans[-1] - ) - ) - else: - logger.info( - "Processing ddi: {ddi}, scan: {scan}".format( - ddi=ddi, scan=scans - ) - ) - - if ( - len(list(holog_obs_dict[ddi_name][holog_map_key]["ant"].keys())) - != 0 - ): - map_ant_list = [] - ref_ant_per_map_ant_list = [] - - map_ant_name_list = [] - ref_ant_per_map_ant_name_list = [] - for map_ant_str in holog_obs_dict[ddi_name][holog_map_key][ - "ant" - ].keys(): - - ref_ant_ids = np.array( - _convert_ant_name_to_id( - ant_names, - list( - holog_obs_dict[ddi_name][holog_map_key]["ant"][ - map_ant_str - ] - ), - ) - ) - - map_ant_id = _convert_ant_name_to_id(ant_names, map_ant_str)[0] - - ref_ant_per_map_ant_list.append(ref_ant_ids) - map_ant_list.append(map_ant_id) - - ref_ant_per_map_ant_name_list.append( - list( - holog_obs_dict[ddi_name][holog_map_key]["ant"][ - map_ant_str - ] - ) - ) - map_ant_name_list.append(map_ant_str) - - extract_holog_params["ref_ant_per_map_ant_tuple"] = tuple( - ref_ant_per_map_ant_list - ) - extract_holog_params["map_ant_tuple"] = tuple(map_ant_list) - - extract_holog_params["ref_ant_per_map_ant_name_tuple"] = tuple( - ref_ant_per_map_ant_name_list - ) - extract_holog_params["map_ant_name_tuple"] = tuple( - map_ant_name_list - ) - - extract_holog_params["scans"] = scans - extract_holog_params["sel_state_ids"] = state_ids - extract_holog_params["holog_map_key"] = holog_map_key - extract_holog_params["ant_names"] = ant_names - extract_holog_params["ant_station"] = ant_station - - if parallel: - delayed_list.append( - dask.delayed(process_extract_holog_chunk)( - dask.delayed(copy.deepcopy(extract_holog_params)) - ) - ) - else: - process_extract_holog_chunk(extract_holog_params) - - count += 1 - - else: - logger.warning( - "DDI " + str(ddi) + " has no holography data to extract." - ) - - spw_ctb.close() - pol_ctb.close() - obs_ctb.close() - - if parallel: - dask.compute(delayed_list) - - if count > 0: - logger.info("Finished processing") - - holog_dict = load_holog_file( - file=extract_holog_params["holog_name"], dask_load=True, load_pnt_dict=False - ) - - create_holog_json(extract_holog_params["holog_name"], holog_dict) - - holog_attr_file = "{name}/{ext}".format( - name=extract_holog_params["holog_name"], ext=".holog_input" - ) - write_meta_data(holog_attr_file, input_pars) - - with open( - f"{extract_holog_params['holog_name']}/holog_obs_dict.json", "w" - ) as outfile: - json.dump(holog_obs_dict, outfile, cls=NumpyEncoder) - - holog_mds = AstrohackHologFile(extract_holog_params["holog_name"]) - holog_mds.open() - - return holog_mds - - else: - logger.warning("No data to process") - return None + pnt_mds = open_pointing(point_name) + + extract_holog_processing(extract_holog_params, pnt_mds) + + # if count > 0: + # logger.info("Finished processing") + # + # holog_dict = load_holog_file( + # file=extract_holog_params["holog_name"], dask_load=True, load_pnt_dict=False + # ) + # + # create_holog_json(extract_holog_params["holog_name"], holog_dict) + # + # holog_attr_file = "{name}/{ext}".format( + # name=extract_holog_params["holog_name"], ext=".holog_input" + # ) + # write_meta_data(holog_attr_file, input_pars) + # + # with open( + # f"{extract_holog_params['holog_name']}/holog_obs_dict.json", "w" + # ) as outfile: + # json.dump(holog_obs_dict, outfile, cls=NumpyEncoder) + # + # holog_mds = AstrohackHologFile(extract_holog_params["holog_name"]) + # holog_mds.open() + # + # return holog_mds + # + # else: + # logger.warning("No data to process") + # return None def generate_holog_obs_dict( @@ -775,17 +515,3 @@ def model_memory_usage(ms_name: str, holog_obs_dict: HologObsDict = None) -> int # Make prediction of memory per core in MB return memory_per_core - - -def _convert_ant_name_to_id(ant_list, ant_names): - """_summary_ - - Args: - ant_list (_type_): _description_ - ant_names (_type_): _description_ - - Returns: - _type_: _description_ - """ - - return np.nonzero(np.isin(ant_list, ant_names))[0] From 2d108f5c12fe6e8bf6ba3e0610cd443b7fcccb8d Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Thu, 22 Jan 2026 12:17:46 -0700 Subject: [PATCH 022/295] Fixed typo. --- src/astrohack/io/dio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/astrohack/io/dio.py b/src/astrohack/io/dio.py index 22afbff4..5a327644 100644 --- a/src/astrohack/io/dio.py +++ b/src/astrohack/io/dio.py @@ -290,7 +290,7 @@ def open_pointing(file: str) -> Union[AstrohackPointFile, None]: } """ - check_if_file_can_be_opened_2(file, "locit", "0.10.1") + check_if_file_can_be_opened_2(file, "process_extract_pointing", "0.10.1") _data_file = AstrohackPointFile(file=file) if _data_file.open(): From da34e3a9b076f5a9b7b75991b82d6057a6cbac09 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Thu, 22 Jan 2026 12:18:17 -0700 Subject: [PATCH 023/295] Removed unused variables, added return to point of verification. --- src/astrohack/core/extract_holog_2.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/astrohack/core/extract_holog_2.py b/src/astrohack/core/extract_holog_2.py index 194e5f5a..62c83704 100644 --- a/src/astrohack/core/extract_holog_2.py +++ b/src/astrohack/core/extract_holog_2.py @@ -41,7 +41,6 @@ def extract_holog_processing(extract_holog_params, pnt_mds): ddi_spw = ctb.getcol("SPECTRAL_WINDOW_ID") ddpol_indexol = ctb.getcol("POLARIZATION_ID") - ms_ddi = np.arange(len(ddi_spw)) ctb.close() # Get antenna IDs and names @@ -53,7 +52,6 @@ def extract_holog_processing(extract_holog_params, pnt_mds): ) ant_names = np.array(ctb.getcol("NAME")) - ant_id = np.arange(len(ant_names)) ant_pos = ctb.getcol("POSITION") ant_station = ctb.getcol("STATION") @@ -79,6 +77,7 @@ def extract_holog_processing(extract_holog_params, pnt_mds): if isinstance(ddi, int): ddi = [ddi] + return # Create holog_obs_dict if not specified if holog_obs_dict is None: holog_obs_dict = create_holog_obs_dict( From d161121934ebdfe49ab7ea88d15672743654f5ba Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Thu, 22 Jan 2026 16:57:23 -0700 Subject: [PATCH 024/295] Factorized computation of baseline distance matrix dictionary. --- src/astrohack/utils/algorithms.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/astrohack/utils/algorithms.py b/src/astrohack/utils/algorithms.py index 44d00478..781dcc7c 100644 --- a/src/astrohack/utils/algorithms.py +++ b/src/astrohack/utils/algorithms.py @@ -5,6 +5,9 @@ import xarray as xr from numba import njit +import pandas as pd +from scipy.spatial import distance_matrix + import toolviper.utils.logger as logger from astrohack.utils.text import format_angular_distance, create_dataset_label @@ -611,3 +614,19 @@ def regrid_data_onto_2d_grid(x_axis, y_axis, linear_array, grid_idx): gridded = np.full(grid_shape, np.nan) gridded[grid_idx[:, 0], grid_idx[:, 1]] = linear_array[:] return gridded + + +def compute_antenna_baseline_distance_matrix_dict(ant_pos, ant_names): + """ + Compute a matrix of antenna position distances from antenna positions + :param ant_pos: antenna position array + :param ant_names: antenna names array + :return: dict with antenna distance matrix + """ + pos_df = pd.DataFrame(ant_pos, columns=["x", "y", "z"], index=ant_names) + dist_mat_df = pd.DataFrame( + distance_matrix(pos_df.values, pos_df.values), + index=pos_df.index, + columns=pos_df.index, + ) + return dist_mat_df.to_dict(orient="index") From de2f3dc4b3ced77ea6cbfb1ee4975f44232bfa8e Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Thu, 22 Jan 2026 16:57:50 -0700 Subject: [PATCH 025/295] Rewrote HologObsDict class in a simpler and more complete manner. --- src/astrohack/core/holog_obs_dict.py | 394 +++++++++++++++------------ 1 file changed, 223 insertions(+), 171 deletions(-) diff --git a/src/astrohack/core/holog_obs_dict.py b/src/astrohack/core/holog_obs_dict.py index f1ca5d8a..daa08c85 100644 --- a/src/astrohack/core/holog_obs_dict.py +++ b/src/astrohack/core/holog_obs_dict.py @@ -1,30 +1,32 @@ import json -import pathlib -import copy import numpy as np +import pandas as pd import toolviper.utils.logger as logger -from typing import Union, List, NewType, Dict, Any +from typing import Union, List, Any +from rich.console import Console -from astrohack.io.dio import inspect_holog_obs_dict -JSON = NewType("JSON", Dict[str, Any]) -KWARGS = NewType("KWARGS", Union[Dict[str, str], Dict[str, int]]) +def _add_prefix_to_keys(prefix, key_list): + add_prefix_lambda = lambda list_item: f"{prefix}_{list_item}" + return list(map(add_prefix_lambda, key_list)) + + +def _check_if_array_in_dict(array_dict, array): + for key, val in array_dict.items(): + if np.array_equiv(val, array): + return key + return False class HologObsDict(dict): - """ - ddi --> map --> ant, scan - | - o--> map: [reference, ...] - """ - - def __init__(self, obj: JSON = None): - if obj is None: + + def __init__(self, dict_obj: dict = None): + if dict_obj is None: super().__init__() else: - super().__init__(obj) + super().__init__(dict_obj) def __getitem__(self, key: str): return super().__getitem__(key) @@ -32,96 +34,203 @@ def __getitem__(self, key: str): def __setitem__(self, key: str, value: Any): return super().__setitem__(key, value) - @classmethod - def from_file(cls, filepath): - if filepath.endswith(".holog.zarr"): - filepath = str( - pathlib.Path(filepath).resolve().joinpath("holog_obs_dict.json") - ) - - try: - with open(filepath, "r") as file: - obj = json.load(file) - - return HologObsDict(obj) - - except FileNotFoundError: - logger.error(f"File {filepath} not found") - def print(self, style: str = "static"): if style == "dynamic": - return inspect_holog_obs_dict(self, style="dynamic") + from IPython.display import JSON - else: - return inspect_holog_obs_dict(self, style="static") + return JSON(self) - def select( - self, key: str, value: any, inplace: bool = False, **kwargs: KWARGS - ) -> object: + else: + console = Console() + console.log(self, log_locals=False) + return None - if inplace: - obs_dict = self + @classmethod + def from_json_file(cls, filepath): + with open(filepath, "r") as file: + json_dict = json.load(file) + return cls(json_dict) + @classmethod + def create_from_ms_info( + cls, + pnt_mds, + exclude_antennas, + baseline_average_distance, + baseline_average_nearest, + dist_matrix_dict, + ant_names_main, + ): + + mapping_scans_dict = {} + holog_obs_dict = cls() + map_id = 0 + ant_names_set = set() + + if exclude_antennas is None: + exclude_antennas = [] + elif isinstance(exclude_antennas, str): + exclude_antennas = [exclude_antennas] else: - obs_dict = HologObsDict(copy.deepcopy(self)) + pass - if key == "ddi": - return self._select_ddi(value, obs_dict=obs_dict) + for ant_name in exclude_antennas: + prefixed = "ant_" + ant_name + if prefixed not in pnt_mds.keys(): + logger.warning( + f"Bad reference antenna {ant_name} is not present in the data." + ) - elif key == "map": - return self._select_map(value, obs_dict=obs_dict) + # Generate {ddi: {map: {scan:[i ...], ant:{ant_map_0:[], ...}}}} structure. No reference antennas are added + # because we first need to populate all mapping antennas. + for ant_name, ant_ds in pnt_mds.items(): + if "ant" in ant_name: + ant_name = ant_name.replace("ant_", "") + if ant_name in exclude_antennas: + pass + else: + if ant_name in ant_names_main: # Check if antenna in main table. + ant_names_set.add(ant_name) + for ddi, map_dict in ant_ds.attrs["mapping_scans_obs_dict"][ + 0 + ].items(): + if ddi not in holog_obs_dict: + holog_obs_dict[ddi] = {} + for ant_map_id, scan_list in map_dict.items(): + if scan_list: + map_key = _check_if_array_in_dict( + mapping_scans_dict, scan_list + ) + if not map_key: + map_key = "map_" + str(map_id) + mapping_scans_dict[map_key] = scan_list + map_id = map_id + 1 + + if map_key not in holog_obs_dict[ddi]: + holog_obs_dict[ddi][map_key] = { + "scans": np.array(scan_list), + "ant": {}, + } + + holog_obs_dict[ddi][map_key]["ant"][ant_name] = [] + + df_mat = pd.DataFrame.from_dict(dist_matrix_dict, orient="index") + + if (baseline_average_distance != "all") and (baseline_average_nearest != "all"): + logger.error( + "baseline_average_distance and baseline_average_nearest can not both be specified." + ) - elif key == "antenna": - return self._select_antenna(value, obs_dict=obs_dict) + raise RuntimeError("Too many baseline parameters specified.") + + # The reference antennas are then given by ref_ant_set = ant_names_set - map_ant_set. + for ddi, ddi_dict in holog_obs_dict.items(): + for map_id, map_dict in ddi_dict.items(): + map_ant_set = set(map_dict["ant"].keys()) + + # Need a copy because of del holog_obs_dict[ddi][map_id]['ant'][map_ant_key] below. + map_ant_keys = list(map_dict["ant"].keys()) + + for map_ant_key in map_ant_keys: + ref_ant_set = ant_names_set - map_ant_set + + # Select reference antennas by distance from mapping antenna + if baseline_average_distance != "all": + sub_ref_ant_set = [] + + for ref_ant in ref_ant_set: + if ( + df_mat.loc[map_ant_key, ref_ant] + < baseline_average_distance + ): + sub_ref_ant_set.append(ref_ant) + + if (not sub_ref_ant_set) and ref_ant_set: + logger.warning( + "DDI " + + str(ddi) + + " and mapping antenna " + + str(map_ant_key) + + " has no reference antennas. If baseline_average_distance was specified " + "increase this distance. See antenna distance matrix in log by setting " + "debug level to DEBUG in client function." + ) + + ref_ant_set = sub_ref_ant_set + + # Select reference antennas by the n-closest antennas + if baseline_average_nearest != "all": + sub_ref_ant_set = [] + nearest_ant_list = ( + df_mat.loc[map_ant_key, :] + .loc[list(ref_ant_set)] + .sort_values() + .index.tolist()[0:baseline_average_nearest] + ) - elif key == "scan": - return self._select_scan(value, obs_dict=obs_dict) + logger.debug(nearest_ant_list) + for ref_ant in ref_ant_set: + if ref_ant in nearest_ant_list: + sub_ref_ant_set.append(ref_ant) - elif key == "baseline": - if "reference" in kwargs.keys(): - return self._select_baseline( - value, - n_baselines=None, - reference=kwargs["reference"], - obs_dict=obs_dict, - ) + ref_ant_set = sub_ref_ant_set + ################################################## - elif "n_baselines" in kwargs.keys(): - return self._select_baseline( - value, - n_baselines=kwargs["n_baselines"], - reference=None, - obs_dict=obs_dict, - ) + if ref_ant_set: + holog_obs_dict[ddi][map_id]["ant"][map_ant_key] = np.array( + list(ref_ant_set) + ) + else: + del holog_obs_dict[ddi][map_id]["ant"][ + map_ant_key + ] # Don't want mapping antennas with no reference antennas. + logger.warning( + "DDI " + + str(ddi) + + " and mapping antenna " + + str(map_ant_key) + + " has no reference antennas." + ) - else: - logger.error( - "Must specify a list of reference antennas for this option." - ) - return {} - else: - logger.error("Valid key not found: {key}".format(key=key)) - return {} + return holog_obs_dict + + def _select_ddi(self, selected_values: Union[int, List[int]]): + prefixed_selected_values = _add_prefix_to_keys("ddi", selected_values) + ddi_list = list(self.keys()) + for ddi_key in ddi_list: + if ddi_key not in prefixed_selected_values: + self.pop(ddi_key) + return + + def _select_antenna(self, selected_values: Union[str, List[str]]): + for ddi_key in self.keys(): + for map_key in self[ddi_key].keys(): + ant_list = list(self[ddi_key][map_key]["ant"].keys()) + for ant_key in ant_list: + if ant_key not in selected_values: + self[ddi_key][map_key]["ant"].pop(ant_key) + return + + def _select_map(self, selected_values: Union[int, List[int]]): + prefixed_selected_values = _add_prefix_to_keys("map", selected_values) + for ddi_key in self.keys(): + map_list = list(self[ddi_key].keys()) + for map_key in map_list: + if map_key not in prefixed_selected_values: + self[ddi_key].pop(map_key) + + def _select_scan(self, selected_values: Union[int, List[int]]): + for ddi_key in self.keys(): + for map_key in self[ddi_key].keys(): + self[ddi_key][map_key]["scan"] = selected_values + return @staticmethod def get_nearest_baselines( - antenna: str, n_baselines: int = None, path_to_matrix: str = None + antenna: str, dist_matrix_dict, n_baselines: int = None ) -> object: - import pandas as pd - - if path_to_matrix is None: - path_to_matrix = str( - pathlib.Path.cwd().joinpath(".baseline_distance_matrix.csv") - ) - - if not pathlib.Path(path_to_matrix).exists(): - logger.error( - "Unable to find baseline distance matrix in: {path}".format( - path=path_to_matrix - ) - ) - df_matrix = pd.read_csv(path_to_matrix, sep="\t", index_col=0) + df_matrix = pd.DataFrame.from_dict(dist_matrix_dict, orient="index") # Skip the first index because it is a self distance if n_baselines is None: @@ -136,101 +245,32 @@ def get_nearest_baselines( .values.tolist() ) - @staticmethod - def _select_ddi(value: Union[int, List[int]], obs_dict: object) -> object: - convert = lambda x: "ddi_" + str(x) - - if not isinstance(value, list): - value = [value] - - value = list(map(convert, value)) - ddi_list = list(obs_dict.keys()) - - for ddi in ddi_list: - if ddi not in value: - obs_dict.pop(ddi) - - return obs_dict - - @staticmethod - def _select_map(value: Union[int, List[int]], obs_dict: object) -> object: - convert = lambda x: "map_" + str(x) - - if not isinstance(value, list): - value = [value] - - value = list(map(convert, value)) - ddi_list = list(obs_dict.keys()) - - for ddi in ddi_list: - map_list = list(obs_dict[ddi].keys()) - for mp in map_list: - if mp not in value: - obs_dict[ddi].pop(mp) - - return obs_dict - - @staticmethod - def _select_antenna(value: Union[str, List[str]], obs_dict: object) -> object: - if not isinstance(value, list): - value = [value] - - ddi_list = list(obs_dict.keys()) - - for ddi in ddi_list: - map_list = list(obs_dict[ddi].keys()) - for mp in map_list: - ant_list = list(obs_dict[ddi][mp]["ant"].keys()) - for ant in ant_list: - if ant not in value: - obs_dict[ddi][mp]["ant"].pop(ant) - - return obs_dict - - @staticmethod - def _select_scan(value: Union[int, List[int]], obs_dict: object) -> object: - if not isinstance(value, list): - value = [value] - - ddi_list = list(obs_dict.keys()) - - for ddi in ddi_list: - map_list = list(obs_dict[ddi].keys()) - for mp in map_list: - obs_dict[ddi][mp]["scans"] = value - - return obs_dict - - @staticmethod - def _select_baseline( - value: str, - n_baselines: int, - obs_dict: object, - reference: Union[str, List[int]] = None, - ) -> object: + def select_baseline( + self, selected_values, n_baselines, dist_matrix_dict, reference=None + ): if reference is not None: if not isinstance(reference, list): reference = [reference] - ddi_list = list(obs_dict.keys()) + ddi_list = list(self.keys()) for ddi in ddi_list: - map_list = list(obs_dict[ddi].keys()) + map_list = list(self[ddi].keys()) for mp in map_list: - ant_list = list(obs_dict[ddi][mp]["ant"].keys()) + ant_list = list(self[ddi][mp]["ant"].keys()) for ant in ant_list: - if ant not in value: - obs_dict[ddi][mp]["ant"].pop(ant) + if ant not in selected_values: + self[ddi][mp]["ant"].pop(ant) continue if reference is None and n_baselines is not None: - reference_antennas = obs_dict[ddi][mp]["ant"][ant] + reference_antennas = self[ddi][mp]["ant"][ant] if n_baselines > len(reference_antennas): n_baselines = len(reference_antennas) sorted_antennas = np.array( - obs_dict.get_nearest_baselines(antenna=ant) + self.get_nearest_baselines(ant, dist_matrix_dict) ) values, i, j = np.intersect1d( @@ -238,11 +278,23 @@ def _select_baseline( ) index = np.sort(j) - obs_dict[ddi][mp]["ant"][ant] = sorted_antennas[index][ - :n_baselines - ] + self[ddi][mp]["ant"][ant] = sorted_antennas[index][:n_baselines] else: - obs_dict[ddi][mp]["ant"][ant] = reference - - return obs_dict + self[ddi][mp]["ant"][ant] = reference + return + + def select(self, key, selected_values): + if not isinstance(selected_values, (list, tuple)): + selected_values = [selected_values] + + match key: + case "ddi": + self._select_ddi(selected_values) + case "antenna": + self._select_antenna(selected_values) + case "map": + self._select_map(selected_values) + case "scan": + self._select_scan(selected_values) + return From ddcc3d50ec3a95ea0f9e631de40511a562dd884d Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Thu, 22 Jan 2026 16:58:57 -0700 Subject: [PATCH 026/295] extract_holog_2 now uses the new HologObsDict. --- src/astrohack/core/extract_holog_2.py | 207 +++----------------------- src/astrohack/extract_holog_2.py | 37 ++--- 2 files changed, 32 insertions(+), 212 deletions(-) diff --git a/src/astrohack/core/extract_holog_2.py b/src/astrohack/core/extract_holog_2.py index 62c83704..ecf5e84e 100644 --- a/src/astrohack/core/extract_holog_2.py +++ b/src/astrohack/core/extract_holog_2.py @@ -15,13 +15,17 @@ from astrohack.utils.tools import get_valid_state_ids from astrohack.antenna import get_proper_telescope -from astrohack.utils import create_dataset_label +from astrohack.utils import ( + create_dataset_label, + compute_antenna_baseline_distance_matrix_dict, +) from astrohack.utils.imaging import calculate_parallactic_angle_chunk from astrohack.utils.algorithms import calculate_optimal_grid_parameters from astrohack.utils.conversion import casa_time_to_mjd from astrohack.utils.constants import twopi, clight from astrohack.utils.gridding import grid_1d_data from astrohack.utils.constants import pol_str +from astrohack.core.holog_obs_dict import HologObsDict from astrohack.utils.file import load_point_file @@ -67,38 +71,32 @@ def extract_holog_processing(extract_holog_params, pnt_mds): ant1 = np.unique(ctb.getcol("ANTENNA1")) ant2 = np.unique(ctb.getcol("ANTENNA2")) - ant_id_main = np.unique(np.append(ant1, ant2)) + ctb.close() + ant_id_main = np.unique(np.append(ant1, ant2)) ant_names_main = ant_names[ant_id_main] - ctb.close() # Create holog_obs_dict or modify user supplied holog_obs_dict. ddi = extract_holog_params["ddi"] if isinstance(ddi, int): ddi = [ddi] - return - # Create holog_obs_dict if not specified + dist_matrix_dict = compute_antenna_baseline_distance_matrix_dict(ant_pos, ant_names) + if holog_obs_dict is None: - holog_obs_dict = create_holog_obs_dict( - pnt_mds, - extract_holog_params["baseline_average_distance"], - extract_holog_params["baseline_average_nearest"], - ant_names, - ant_pos, - ant_names_main, + holog_obs_dict = HologObsDict.create_from_ms_info( + pnt_mds=pnt_mds, exclude_antennas=extract_holog_params["exclude_antennas"], + baseline_average_distance=extract_holog_params["baseline_average_distance"], + baseline_average_nearest=extract_holog_params["baseline_average_nearest"], + dist_matrix_dict=dist_matrix_dict, + ant_names_main=ant_names_main, ) - # From the generated holog_obs_dict subselect user supplied ddis. if ddi != "all": - holog_obs_dict_keys = list(holog_obs_dict.keys()) - for ddi_key in holog_obs_dict_keys: - if "ddi" in ddi_key: - ddi_id = int(ddi_key.replace("ddi_", "")) - if ddi_id not in ddi: - del holog_obs_dict[ddi_key] - + holog_obs_dict.select("ddi", ddi) + holog_obs_dict.print() + return ctb = ctables.table( os.path.join(extract_holog_params["ms_name"], "STATE"), readonly=True, @@ -789,175 +787,6 @@ def _create_holog_file( ) -def create_holog_obs_dict( - pnt_dict, - baseline_average_distance, - baseline_average_nearest, - ant_names, - ant_pos, - ant_names_main, - exclude_antennas=None, - write_distance_matrix=False, -): - """ - Generate holog_obs_dict. - """ - - import pandas as pd - from scipy.spatial import distance_matrix - - mapping_scans_dict = {} - holog_obs_dict = {} - map_id = 0 - ant_names_set = set() - - if exclude_antennas is None: - exclude_antennas = [] - elif isinstance(exclude_antennas, str): - exclude_antennas = [exclude_antennas] - else: - pass - - for ant_name in exclude_antennas: - prefixed = "ant_" + ant_name - if prefixed not in pnt_dict.keys(): - logger.warning( - f"Bad reference antenna {ant_name} is not present in the data." - ) - - # Generate {ddi: {map: {scan:[i ...], ant:{ant_map_0:[], ...}}}} structure. No reference antennas are added - # because we first need to populate all mapping antennas. - for ant_name, ant_ds in pnt_dict.items(): - if "ant" in ant_name: - ant_name = ant_name.replace("ant_", "") - if ant_name in exclude_antennas: - pass - else: - if ant_name in ant_names_main: # Check if antenna in main table. - ant_names_set.add(ant_name) - for ddi, map_dict in ant_ds.attrs["mapping_scans_obs_dict"][ - 0 - ].items(): - if ddi not in holog_obs_dict: - holog_obs_dict[ddi] = {} - for ant_map_id, scan_list in map_dict.items(): - if scan_list: - map_key = _check_if_array_in_dict( - mapping_scans_dict, scan_list - ) - if not map_key: - map_key = "map_" + str(map_id) - mapping_scans_dict[map_key] = scan_list - map_id = map_id + 1 - - if map_key not in holog_obs_dict[ddi]: - holog_obs_dict[ddi][map_key] = { - "scans": np.array(scan_list), - "ant": {}, - } - - holog_obs_dict[ddi][map_key]["ant"][ant_name] = [] - - df = pd.DataFrame(ant_pos, columns=["x", "y", "z"], index=ant_names) - df_mat = pd.DataFrame( - distance_matrix(df.values, df.values), index=df.index, columns=df.index - ) - logger.debug("".join(("\n", str(df_mat)))) - - if write_distance_matrix: - df_mat.to_csv( - path_or_buf="{base}/.baseline_distance_matrix.csv".format(base=os.getcwd()), - sep="\t", - ) - logger.info( - "Writing distance matrix to {base}/.baseline_distance_matrix.csv ...".format( - base=os.getcwd() - ) - ) - - if (baseline_average_distance != "all") and (baseline_average_nearest != "all"): - logger.error( - "baseline_average_distance and baseline_average_nearest can not both be specified." - ) - - raise Exception("Too many baseline parameters specified.") - - # The reference antennas are then given by ref_ant_set = ant_names_set - map_ant_set. - for ddi, ddi_dict in holog_obs_dict.items(): - for map_id, map_dict in ddi_dict.items(): - map_ant_set = set(map_dict["ant"].keys()) - - # Need a copy because of del holog_obs_dict[ddi][map_id]['ant'][map_ant_key] below. - map_ant_keys = list(map_dict["ant"].keys()) - - for map_ant_key in map_ant_keys: - ref_ant_set = ant_names_set - map_ant_set - - # Select reference antennas by distance from mapping antenna - if baseline_average_distance != "all": - sub_ref_ant_set = [] - - for ref_ant in ref_ant_set: - if df_mat.loc[map_ant_key, ref_ant] < baseline_average_distance: - sub_ref_ant_set.append(ref_ant) - - if (not sub_ref_ant_set) and ref_ant_set: - logger.warning( - "DDI " - + str(ddi) - + " and mapping antenna " - + str(map_ant_key) - + " has no reference antennas. If baseline_average_distance was specified " - "increase this distance. See antenna distance matrix in log by setting " - "debug level to DEBUG in client function." - ) - - ref_ant_set = sub_ref_ant_set - - # Select reference antennas by the n-closest antennas - if baseline_average_nearest != "all": - sub_ref_ant_set = [] - nearest_ant_list = ( - df_mat.loc[map_ant_key, :] - .loc[list(ref_ant_set)] - .sort_values() - .index.tolist()[0:baseline_average_nearest] - ) - - logger.debug(nearest_ant_list) - for ref_ant in ref_ant_set: - if ref_ant in nearest_ant_list: - sub_ref_ant_set.append(ref_ant) - - ref_ant_set = sub_ref_ant_set - ################################################## - - if ref_ant_set: - holog_obs_dict[ddi][map_id]["ant"][map_ant_key] = np.array( - list(ref_ant_set) - ) - else: - del holog_obs_dict[ddi][map_id]["ant"][ - map_ant_key - ] # Don't want mapping antennas with no reference antennas. - logger.warning( - "DDI " - + str(ddi) - + " and mapping antenna " - + str(map_ant_key) - + " has no reference antennas." - ) - - return holog_obs_dict - - -def _check_if_array_in_dict(array_dict, array): - for key, val in array_dict.items(): - if np.array_equiv(val, array): - return key - return False - - def _extract_pointing_chunk( map_ant_ids, time_vis, pnt_ant_dict, pointing_interpolation_method ): diff --git a/src/astrohack/extract_holog_2.py b/src/astrohack/extract_holog_2.py index 4a579cca..8c441f93 100644 --- a/src/astrohack/extract_holog_2.py +++ b/src/astrohack/extract_holog_2.py @@ -20,18 +20,12 @@ from rich.table import Table from astrohack import open_pointing +from astrohack.utils import compute_antenna_baseline_distance_matrix_dict from astrohack.utils.file import ( overwrite_file, - check_if_file_can_be_opened, - check_if_file_can_be_opened_2, ) -from astrohack.utils.file import load_holog_file -from astrohack.utils.file import load_point_file -from astrohack.utils.data import write_meta_data from astrohack.core.extract_holog_2 import ( - create_holog_obs_dict, - create_holog_json, extract_holog_processing, ) from astrohack.core.extract_holog_2 import process_extract_holog_chunk @@ -406,26 +400,23 @@ def generate_holog_obs_dict( pnt_mds = AstrohackPointFile(extract_holog_params["point_name"]) pnt_mds.open() - holog_obs_dict = create_holog_obs_dict( - pnt_mds, - extract_holog_params["baseline_average_distance"], - extract_holog_params["baseline_average_nearest"], - ant_names, - ant_pos, - ant_names_main, - write_distance_matrix=True, - ) - - encoded_obj = json.dumps(holog_obs_dict, cls=NumpyEncoder) + dist_matrix_dict = compute_antenna_baseline_distance_matrix_dict(ant_pos, ant_names) - if write: - with open("holog_obs_dict.json", "w") as outfile: - outfile.write(encoded_obj) + holog_obs_dict = HologObsDict.create_from_ms_info( + pnt_mds=pnt_mds, + exclude_antennas=extract_holog_params["exclude_antennas"], + baseline_average_distance=extract_holog_params["baseline_average_distance"], + baseline_average_nearest=extract_holog_params["baseline_average_nearest"], + dist_matrix_dict=dist_matrix_dict, + ant_names_main=ant_names_main, + ) - return HologObsDict(json.loads(encoded_obj)) + return holog_obs_dict -def get_number_of_parameters(holog_obs_dict: HologObsDict) -> Tuple[int, int, int, int]: +def get_number_of_parameters( + holog_obs_dict: HologObsDict, +) -> Tuple[int, int, int, int]: scan_list = [] ant_list = [] baseline_list = [] From 40031ae1e410d6865b64f0957aa4b39d6fc0ba28 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Thu, 22 Jan 2026 17:01:26 -0700 Subject: [PATCH 027/295] Simplified model_memory_usage. --- src/astrohack/extract_holog_2.py | 42 +++++++++++++------------------- 1 file changed, 17 insertions(+), 25 deletions(-) diff --git a/src/astrohack/extract_holog_2.py b/src/astrohack/extract_holog_2.py index 8c441f93..a2f19b3b 100644 --- a/src/astrohack/extract_holog_2.py +++ b/src/astrohack/extract_holog_2.py @@ -414,29 +414,6 @@ def generate_holog_obs_dict( return holog_obs_dict -def get_number_of_parameters( - holog_obs_dict: HologObsDict, -) -> Tuple[int, int, int, int]: - scan_list = [] - ant_list = [] - baseline_list = [] - - for ddi in holog_obs_dict.keys(): - for mapping in holog_obs_dict[ddi].keys(): - scan_list.append(len(holog_obs_dict[ddi][mapping]["scans"])) - ant_list.append(len(holog_obs_dict[ddi][mapping]["ant"].keys())) - - for ant in holog_obs_dict[ddi][mapping]["ant"].keys(): - baseline_list.append(len(holog_obs_dict[ddi][mapping]["ant"][ant])) - - n_ddi = len(holog_obs_dict.keys()) - n_scans = max(scan_list) - n_ant = max(ant_list) - n_baseline = max(baseline_list) - - return n_ddi, n_scans, n_ant, n_baseline - - def model_memory_usage(ms_name: str, holog_obs_dict: HologObsDict = None) -> int: """Determine the approximate memory usage per core of a given measurement file. @@ -449,7 +426,6 @@ def model_memory_usage(ms_name: str, holog_obs_dict: HologObsDict = None) -> int :return: Memory per core :rtype: int """ - # Get holog observations dictionary if holog_obs_dict is None: extract_pointing( @@ -470,7 +446,23 @@ def model_memory_usage(ms_name: str, holog_obs_dict: HologObsDict = None) -> int shutil.rmtree("temporary.pointing.zarr") # Get number of each parameter - n_ddi, n_scans, n_ant, n_baseline = get_number_of_parameters(holog_obs_dict) + + scan_list = [] + ant_list = [] + baseline_list = [] + + for ddi in holog_obs_dict.keys(): + for mapping in holog_obs_dict[ddi].keys(): + scan_list.append(len(holog_obs_dict[ddi][mapping]["scans"])) + ant_list.append(len(holog_obs_dict[ddi][mapping]["ant"].keys())) + + for ant in holog_obs_dict[ddi][mapping]["ant"].keys(): + baseline_list.append(len(holog_obs_dict[ddi][mapping]["ant"][ant])) + + n_ddi = len(holog_obs_dict.keys()) + n_scans = max(scan_list) + n_ant = max(ant_list) + n_baseline = max(baseline_list) # Get model file if not pathlib.Path("model").exists(): From 95e633ef9415ff2e1dcac91103422190c82e1ede Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Thu, 22 Jan 2026 17:09:11 -0700 Subject: [PATCH 028/295] Added a means to select antennas to be selected. --- src/astrohack/core/extract_holog_2.py | 22 +++++++++++++++------- src/astrohack/extract_holog_2.py | 4 ++++ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/astrohack/core/extract_holog_2.py b/src/astrohack/core/extract_holog_2.py index ecf5e84e..81e39d8f 100644 --- a/src/astrohack/core/extract_holog_2.py +++ b/src/astrohack/core/extract_holog_2.py @@ -76,13 +76,9 @@ def extract_holog_processing(extract_holog_params, pnt_mds): ant_id_main = np.unique(np.append(ant1, ant2)) ant_names_main = ant_names[ant_id_main] - # Create holog_obs_dict or modify user supplied holog_obs_dict. - ddi = extract_holog_params["ddi"] - if isinstance(ddi, int): - ddi = [ddi] - dist_matrix_dict = compute_antenna_baseline_distance_matrix_dict(ant_pos, ant_names) + # Create holog_obs_dict or modify user supplied holog_obs_dict. if holog_obs_dict is None: holog_obs_dict = HologObsDict.create_from_ms_info( pnt_mds=pnt_mds, @@ -93,8 +89,20 @@ def extract_holog_processing(extract_holog_params, pnt_mds): ant_names_main=ant_names_main, ) - if ddi != "all": - holog_obs_dict.select("ddi", ddi) + user_ddi_sel = extract_holog_params["ddi"] + if isinstance(user_ddi_sel, int): + user_ddi_sel = [user_ddi_sel] + + if user_ddi_sel != "all": + holog_obs_dict.select("ddi", user_ddi_sel) + + user_ant_sel = extract_holog_params["ant"] + if isinstance(user_ant_sel, int): + user_ant_sel = [user_ant_sel] + + if user_ant_sel != "all": + holog_obs_dict.select("antenna", user_ant_sel) + holog_obs_dict.print() return ctb = ctables.table( diff --git a/src/astrohack/extract_holog_2.py b/src/astrohack/extract_holog_2.py index a2f19b3b..2367b918 100644 --- a/src/astrohack/extract_holog_2.py +++ b/src/astrohack/extract_holog_2.py @@ -45,6 +45,7 @@ def extract_holog( point_name: str, holog_name: str = None, holog_obs_dict: HologObsDict = None, + ant: Union[str, List[str]] = "all", ddi: Union[int, List[int], str] = "all", baseline_average_distance: Union[float, str] = "all", baseline_average_nearest: Union[float, str] = 1, @@ -76,6 +77,9 @@ def extract_holog( user can self generate this dictionary using `generate_holog_obs_dict`. :type holog_obs_dict: dict, optional + :param ant: Antennas that should be extracted from the measurement set. Defaults to all mapping antennas in the ms. + :type ant: str | list[str], optional + :param ddi: DDI(s) that should be extracted from the measurement set. Defaults to all DDI's in the ms. :type ddi: int numpy.ndarray | int list, optional From 6288e1f93d0ac422b69375278e061f3a299bd758 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Thu, 22 Jan 2026 17:12:37 -0700 Subject: [PATCH 029/295] Made reference antennas and scans lists rather than arrays to simplify serialization. --- src/astrohack/core/holog_obs_dict.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/astrohack/core/holog_obs_dict.py b/src/astrohack/core/holog_obs_dict.py index daa08c85..4e128d36 100644 --- a/src/astrohack/core/holog_obs_dict.py +++ b/src/astrohack/core/holog_obs_dict.py @@ -108,7 +108,7 @@ def create_from_ms_info( if map_key not in holog_obs_dict[ddi]: holog_obs_dict[ddi][map_key] = { - "scans": np.array(scan_list), + "scans": scan_list, "ant": {}, } @@ -177,8 +177,8 @@ def create_from_ms_info( ################################################## if ref_ant_set: - holog_obs_dict[ddi][map_id]["ant"][map_ant_key] = np.array( - list(ref_ant_set) + holog_obs_dict[ddi][map_id]["ant"][map_ant_key] = list( + ref_ant_set ) else: del holog_obs_dict[ddi][map_id]["ant"][ From 558f5e97aaff3597894c6f28bc756fd65addaa79 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Thu, 22 Jan 2026 17:14:33 -0700 Subject: [PATCH 030/295] Removed inspect_holog_obs_dict as it is no longer useful. --- src/astrohack/io/dio.py | 79 ----------------------------------------- 1 file changed, 79 deletions(-) diff --git a/src/astrohack/io/dio.py b/src/astrohack/io/dio.py index 5a327644..92b5e4cc 100644 --- a/src/astrohack/io/dio.py +++ b/src/astrohack/io/dio.py @@ -1,11 +1,9 @@ -import json import pathlib import toolviper.utils.logger as logger import numpy as np from casacore import tables -from rich.console import Console from astrohack.io.beamcut_mds import AstrohackBeamcutFile from astrohack.io.locit_mds import AstrohackLocitFile @@ -397,80 +395,3 @@ def print_json(obj: JSON, indent: int = 6, columns: int = 7) -> None: print("{key}".format(key=key_str).rjust(indent, " ")) print_json(value, indent + 4, columns=columns) print("{close}".format(close="}").rjust(indent - 4, " ")) - - -def inspect_holog_obs_dict( - file: Union[str, JSON] = ".holog_obs_dict.json", style: str = "static" -) -> Union[NoReturn, JSON]: - """ Print formatted holography observation dictionary - - :param file: Input file, can be either JSON file or string., defaults to '.holog_obs_dict.json' - :type file: str | JSON, optional - - :param style: Print style of JSON dictionary. This can be static, formatted generalized print out or dynamic, \ - prints a collapsible formatted dictionary, defaults to static - :type style: str, optional - - .. _Description: - - **Example Usage** - The `inspect_holog_obs_dict` loads a holography observation dict either from disk or from memory (as an return \ - value from `generate_holog_obs_dict`) and displays it in a more readable way like JSON.stringify() in javascript. - - .. parsed-literal:: - import astrohack - - astrohack.dio.inspect_holog_obs_dict(file=holog_obs_obj) - - >> ddi_0:{ - map_0:{ - scans:{ - [ - 8, 9, 10, 12, 13, 14, 16 - 17, 18, 23, 24, 25, 27, 28 - 29, 31, 32, 33, 38, 39, 40 - 42, 43, 44, 46, 47, 48, 53 - 54, 55, 57 - ] - } - ant:{ - ea06:{ - [ - ea04, ea25 - ] - } - } - } - } - """ - - if not isinstance(file, dict): - try: - with open(file) as json_file: - json_object = json.load(json_file) - - except IsADirectoryError: - try: - with open(file + "/holog_obs_dict.json") as json_file: - json_object = json.load(json_file) - except FileNotFoundError: - logger.error( - "holog observations dictionary not found: {file}".format(file=file) - ) - except FileNotFoundError: - logger.error( - "holog observations dictionary not found: {file}".format(file=file) - ) - - else: - json_object = file - - if style == "dynamic": - from IPython.display import JSON - - return JSON(json_object) - - else: - console = Console() - console.log(json_object, log_locals=False) - return None From a9a4c910e854edf911fc32baddd3e51226f92e69 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Thu, 22 Jan 2026 17:16:22 -0700 Subject: [PATCH 031/295] Added method to save HologObsDict to a json file. --- src/astrohack/core/holog_obs_dict.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/astrohack/core/holog_obs_dict.py b/src/astrohack/core/holog_obs_dict.py index 4e128d36..50a94d9d 100644 --- a/src/astrohack/core/holog_obs_dict.py +++ b/src/astrohack/core/holog_obs_dict.py @@ -51,6 +51,10 @@ def from_json_file(cls, filepath): json_dict = json.load(file) return cls(json_dict) + def to_json_file(self, filepath): + with open(filepath, "w") as file: + json.dump(self, file, indent=4) + @classmethod def create_from_ms_info( cls, From d35d14c9f8e3b05ede6f988701c1e51c54b57c3f Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Mon, 26 Jan 2026 09:43:01 -0700 Subject: [PATCH 032/295] extract_pointing now add antenna position and station to individual xds attributes, and also adds baseline distance matrix to root attributes. --- src/astrohack/core/extract_pointing_2.py | 55 +++++++++++++----------- src/astrohack/extract_pointing_2.py | 33 ++++++++++++-- src/astrohack/io/dio.py | 2 +- 3 files changed, 60 insertions(+), 30 deletions(-) diff --git a/src/astrohack/core/extract_pointing_2.py b/src/astrohack/core/extract_pointing_2.py index c63c11ef..e3fea515 100644 --- a/src/astrohack/core/extract_pointing_2.py +++ b/src/astrohack/core/extract_pointing_2.py @@ -11,13 +11,17 @@ import toolviper.utils.logger as logger +from astrohack.utils import ( + compute_antenna_baseline_distance_matrix_dict, + print_dict_types, +) from astrohack.utils.conversion import convert_dict_from_numba from astrohack.utils.graph import compute_graph_to_mds_tree from astrohack.utils.tools import get_valid_state_ids from astrohack.io.point_mds import AstrohackPointFile -def process_extract_pointing(input_params): +def extract_pointing_preprocessing(input_params): """Top level function to extract subset of pointing table data into a dictionary of xarray data arrays. Args: @@ -39,18 +43,20 @@ def process_extract_pointing(input_params): ack=False, ) - antenna_name = ctb.getcol("NAME") + antenna_positions = ctb.getcol("POSITION") + antenna_stations = ctb.getcol("STATION") + antenna_names = ctb.getcol("NAME") ctb.close() - antenna_id = list(range(len(antenna_name))) + antenna_id = list(range(len(antenna_names))) # Exclude antennas according to user direction if exclude: if not isinstance(exclude, list): exclude = list(exclude) for i_ant, antenna in enumerate(exclude): - if antenna in antenna_name: - antenna_name.remove(antenna) + if antenna in antenna_names: + antenna_names.remove(antenna) antenna_id.remove(i_ant) antenna_id = np.array(antenna_id) @@ -87,10 +93,6 @@ def process_extract_pointing(input_params): time, scan_ids, state_ids, ddi, mapping_state_ids ) - # Create mds file here - point_mds = AstrohackPointFile.create_from_input_parameters(pnt_name, input_params) - point_mds.root.attrs["mapping_state_ids"] = mapping_state_ids - ########################################################################################### pnt_params = { "ms_name": ms_name, @@ -100,26 +102,23 @@ def process_extract_pointing(input_params): "parallel": input_params["parallel"], } - looping_dict = {} - for i_ant, ant_name in enumerate(antenna_name): - looping_dict[f"ant_{ant_name}"] = {"id": antenna_id[i_ant], "name": ant_name} - - executed_graph = compute_graph_to_mds_tree( - looping_dict, - _make_ant_pnt_chunk, - pnt_params, - ["ant"], - point_mds, + ant_dist_matrix = compute_antenna_baseline_distance_matrix_dict( + antenna_positions, antenna_names ) - if executed_graph: - point_mds.write(mode="a") - return point_mds - else: - logger.warning("No data to process") - return None + + looping_dict = {} + for i_ant, ant_name in enumerate(antenna_names): + looping_dict[f"ant_{ant_name}"] = { + "id": antenna_id[i_ant], + "name": ant_name, + "position": antenna_positions[i_ant].tolist(), + "station": antenna_stations[i_ant], + } + + return ant_dist_matrix, looping_dict, pnt_params, mapping_state_ids -def _make_ant_pnt_chunk(pnt_params, output_mds): +def make_ant_pnt_chunk(pnt_params, output_mds): """Extract subset of pointing table data into a dictionary of xarray data arrays. This is written to disk as a zarr file. This function processes a chunk the overall data and is managed by Dask. @@ -133,6 +132,8 @@ def _make_ant_pnt_chunk(pnt_params, output_mds): ant_id = data_dict["id"] ant_name = data_dict["name"] + ant_pos = data_dict["position"] + ant_station = data_dict["station"] ant_key = pnt_params["this_ant"] table_obj = ctables.table( @@ -273,6 +274,8 @@ def _make_ant_pnt_chunk(pnt_params, output_mds): ############### pnt_xds.attrs["ant_name"] = ant_name + pnt_xds.attrs["ant_pos"] = ant_pos + pnt_xds.attrs["ant_station"] = ant_station output_mds.add_node_to_tree( xr.DataTree(dataset=pnt_xds, name=ant_key), diff --git a/src/astrohack/extract_pointing_2.py b/src/astrohack/extract_pointing_2.py index 30042615..08d9b561 100644 --- a/src/astrohack/extract_pointing_2.py +++ b/src/astrohack/extract_pointing_2.py @@ -2,9 +2,14 @@ import toolviper.utils.parameter import toolviper.utils.logger as logger +from astrohack.utils import print_dict_types +from astrohack.utils.graph import compute_graph_to_mds_tree from astrohack.utils.text import get_default_file_name from astrohack.utils.file import overwrite_file -from astrohack.core.extract_pointing_2 import process_extract_pointing +from astrohack.core.extract_pointing_2 import ( + extract_pointing_preprocessing, + make_ant_pnt_chunk, +) from astrohack.io.point_mds import AstrohackPointFile from typing import List, Union @@ -17,7 +22,7 @@ def extract_pointing( exclude: Union[str, List[str]] = None, parallel: bool = False, overwrite: bool = False, -) -> AstrohackPointFile: +) -> Union[AstrohackPointFile, None]: """ Extract pointing data from measurement set. Creates holography output file. :param ms_name: Name of input measurement file name. @@ -77,5 +82,27 @@ def extract_pointing( overwrite_file( extract_pointing_params["point_name"], extract_pointing_params["overwrite"] ) + ant_dist_matrix, looping_dict, pnt_params, mapping_state_ids = ( + extract_pointing_preprocessing(extract_pointing_params) + ) + # Create mds file here + point_mds = AstrohackPointFile.create_from_input_parameters( + point_name, input_params + ) + point_mds.root.attrs["mapping_state_ids"] = mapping_state_ids + point_mds.root.attrs["baseline_dist_matrix"] = ant_dist_matrix + + executed_graph = compute_graph_to_mds_tree( + looping_dict, + make_ant_pnt_chunk, + pnt_params, + ["ant"], + point_mds, + ) - return process_extract_pointing(extract_pointing_params) + if executed_graph: + point_mds.write(mode="a") + return point_mds + else: + logger.warning("No data to process") + return None diff --git a/src/astrohack/io/dio.py b/src/astrohack/io/dio.py index 92b5e4cc..dc01d202 100644 --- a/src/astrohack/io/dio.py +++ b/src/astrohack/io/dio.py @@ -288,7 +288,7 @@ def open_pointing(file: str) -> Union[AstrohackPointFile, None]: } """ - check_if_file_can_be_opened_2(file, "process_extract_pointing", "0.10.1") + check_if_file_can_be_opened_2(file, "extract_pointing", "0.10.1") _data_file = AstrohackPointFile(file=file) if _data_file.open(): From 44144f812d79384b48eebd28af78e57912c64412 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Mon, 26 Jan 2026 09:55:29 -0700 Subject: [PATCH 033/295] Fixed a typo. --- src/astrohack/utils/text.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/astrohack/utils/text.py b/src/astrohack/utils/text.py index 99d94c90..a3a8b11c 100644 --- a/src/astrohack/utils/text.py +++ b/src/astrohack/utils/text.py @@ -85,7 +85,7 @@ def approve_prefix(key): if not key.endswith("_info"): logger.warning( - f"File meta data contains and unknown key ({key}), the file may not complete properly." + f"File meta data contains an unknown key ({key}), the file may not complete properly." ) return False From 59c8e3a96a43bbf2e968b0461349b058a5ec691a Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Mon, 26 Jan 2026 09:56:32 -0700 Subject: [PATCH 034/295] Added documentation. --- src/astrohack/io/base_mds.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/astrohack/io/base_mds.py b/src/astrohack/io/base_mds.py index 7d01d29a..610bff32 100644 --- a/src/astrohack/io/base_mds.py +++ b/src/astrohack/io/base_mds.py @@ -201,6 +201,9 @@ def add_node_to_tree(self, new_node, dump_to_disk=True, running_in_parallel=Fals :param dump_to_disk: Dump root to disk to freeup RAM :type dump_to_disk: bool + :param running_in_parallel: Get dask lock if running in parallel + :type running_in_parallel: bool + :return: None :rtype: NoneType """ From 68ff503eb4d5a4b2d3185f9dfe35d40a7123a95f Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Mon, 26 Jan 2026 10:03:20 -0700 Subject: [PATCH 035/295] Added antenna names and ids to root attributes in point_mds. --- src/astrohack/core/extract_pointing_2.py | 10 ++++++---- src/astrohack/extract_pointing_2.py | 2 ++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/astrohack/core/extract_pointing_2.py b/src/astrohack/core/extract_pointing_2.py index e3fea515..96c09b03 100644 --- a/src/astrohack/core/extract_pointing_2.py +++ b/src/astrohack/core/extract_pointing_2.py @@ -48,7 +48,7 @@ def extract_pointing_preprocessing(input_params): antenna_names = ctb.getcol("NAME") ctb.close() - antenna_id = list(range(len(antenna_names))) + antenna_ids = list(range(len(antenna_names))) # Exclude antennas according to user direction if exclude: @@ -57,9 +57,9 @@ def extract_pointing_preprocessing(input_params): for i_ant, antenna in enumerate(exclude): if antenna in antenna_names: antenna_names.remove(antenna) - antenna_id.remove(i_ant) + antenna_ids.remove(i_ant) - antenna_id = np.array(antenna_id) + antenna_ids = np.array(antenna_ids) # Get Holography scans with start and end times. ctb = ctables.table( @@ -100,6 +100,8 @@ def extract_pointing_preprocessing(input_params): "scan_time_dict": scan_time_dict, "ant": "all", "parallel": input_params["parallel"], + "antenna_names": antenna_names, + "antenna_ids": antenna_ids, } ant_dist_matrix = compute_antenna_baseline_distance_matrix_dict( @@ -109,7 +111,7 @@ def extract_pointing_preprocessing(input_params): looping_dict = {} for i_ant, ant_name in enumerate(antenna_names): looping_dict[f"ant_{ant_name}"] = { - "id": antenna_id[i_ant], + "id": antenna_ids[i_ant], "name": ant_name, "position": antenna_positions[i_ant].tolist(), "station": antenna_stations[i_ant], diff --git a/src/astrohack/extract_pointing_2.py b/src/astrohack/extract_pointing_2.py index 08d9b561..042960b5 100644 --- a/src/astrohack/extract_pointing_2.py +++ b/src/astrohack/extract_pointing_2.py @@ -91,6 +91,8 @@ def extract_pointing( ) point_mds.root.attrs["mapping_state_ids"] = mapping_state_ids point_mds.root.attrs["baseline_dist_matrix"] = ant_dist_matrix + point_mds.root.attrs["antenna_names"] = pnt_params.pop("antenna_names") + point_mds.root.attrs["antenna_ids"] = pnt_params.pop("antenna_ids") executed_graph = compute_graph_to_mds_tree( looping_dict, From 061949e1aafac48e768b75673b4594d68dddac8e Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Mon, 26 Jan 2026 10:18:00 -0700 Subject: [PATCH 036/295] HologObsDict creation no longer depends on a MS (all relevant info is already present in extract_pointing). --- src/astrohack/core/extract_holog_2.py | 31 +++++------------- src/astrohack/core/holog_obs_dict.py | 4 +-- src/astrohack/extract_holog_2.py | 47 ++------------------------- 3 files changed, 14 insertions(+), 68 deletions(-) diff --git a/src/astrohack/core/extract_holog_2.py b/src/astrohack/core/extract_holog_2.py index 81e39d8f..bd7e5a72 100644 --- a/src/astrohack/core/extract_holog_2.py +++ b/src/astrohack/core/extract_holog_2.py @@ -17,7 +17,6 @@ from astrohack.antenna import get_proper_telescope from astrohack.utils import ( create_dataset_label, - compute_antenna_baseline_distance_matrix_dict, ) from astrohack.utils.imaging import calculate_parallactic_angle_chunk from astrohack.utils.algorithms import calculate_optimal_grid_parameters @@ -47,21 +46,9 @@ def extract_holog_processing(extract_holog_params, pnt_mds): ddpol_indexol = ctb.getcol("POLARIZATION_ID") ctb.close() - # Get antenna IDs and names - ctb = ctables.table( - os.path.join(extract_holog_params["ms_name"], "ANTENNA"), - readonly=True, - lockoptions={"option": "usernoread"}, - ack=False, - ) - - ant_names = np.array(ctb.getcol("NAME")) - ant_pos = ctb.getcol("POSITION") - ant_station = ctb.getcol("STATION") - - ctb.close() - - # Get antenna IDs in the main table + ant_names = pnt_mds.root.attrs["antenna_names"] + ant_ids = pnt_mds.root.attrs["antenna_ids"] + # Get antenna IDs that are in the main table ctb = ctables.table( extract_holog_params["ms_name"], readonly=True, @@ -71,12 +58,14 @@ def extract_holog_processing(extract_holog_params, pnt_mds): ant1 = np.unique(ctb.getcol("ANTENNA1")) ant2 = np.unique(ctb.getcol("ANTENNA2")) - ctb.close() - ant_id_main = np.unique(np.append(ant1, ant2)) - ant_names_main = ant_names[ant_id_main] + ctb.close() - dist_matrix_dict = compute_antenna_baseline_distance_matrix_dict(ant_pos, ant_names) + ant_names_main = [] + for ant_id in ant_id_main: + i_name = ant_ids.index(ant_id) + ant_names_main.append(ant_names[i_name]) + ant_names_main = np.array(ant_names_main) # Create holog_obs_dict or modify user supplied holog_obs_dict. if holog_obs_dict is None: @@ -85,8 +74,6 @@ def extract_holog_processing(extract_holog_params, pnt_mds): exclude_antennas=extract_holog_params["exclude_antennas"], baseline_average_distance=extract_holog_params["baseline_average_distance"], baseline_average_nearest=extract_holog_params["baseline_average_nearest"], - dist_matrix_dict=dist_matrix_dict, - ant_names_main=ant_names_main, ) user_ddi_sel = extract_holog_params["ddi"] diff --git a/src/astrohack/core/holog_obs_dict.py b/src/astrohack/core/holog_obs_dict.py index 50a94d9d..b7fc7eba 100644 --- a/src/astrohack/core/holog_obs_dict.py +++ b/src/astrohack/core/holog_obs_dict.py @@ -62,10 +62,10 @@ def create_from_ms_info( exclude_antennas, baseline_average_distance, baseline_average_nearest, - dist_matrix_dict, - ant_names_main, ): + ant_names_main = pnt_mds.root.attrs["antenna_names"] + dist_matrix_dict = pnt_mds.root.attrs["baseline_dist_matrix"] mapping_scans_dict = {} holog_obs_dict = cls() map_id = 0 diff --git a/src/astrohack/extract_holog_2.py b/src/astrohack/extract_holog_2.py index 2367b918..cdbc282b 100644 --- a/src/astrohack/extract_holog_2.py +++ b/src/astrohack/extract_holog_2.py @@ -20,7 +20,6 @@ from rich.table import Table from astrohack import open_pointing -from astrohack.utils import compute_antenna_baseline_distance_matrix_dict from astrohack.utils.file import ( overwrite_file, @@ -253,7 +252,6 @@ def extract_holog( def generate_holog_obs_dict( - ms_name: str, point_name: str, baseline_average_distance: str = "all", baseline_average_nearest: str = "all", @@ -263,8 +261,8 @@ def generate_holog_obs_dict( """ Generate holography observation dictionary, from measurement set.. - :param ms_name: Name of input measurement file name. - :type ms_name: str + :param point_name: Name of *.point.zarr* file to use. + :type point_name: str, optional :param baseline_average_distance: To increase the signal-to-noise for a mapping antenna multiple reference antennas can be used. The baseline_average_distance is the acceptable distance between a mapping antenna and a @@ -282,14 +280,12 @@ def generate_holog_obs_dict( :param write: Write file flag. :type point_name: bool, optional - :param point_name: Name of *.point.zarr* file to use. - :type point_name: str, optional :param parallel: Boolean for whether to process in parallel. Defaults to False :type parallel: bool, optional :return: holog observation dictionary - :rtype: json + :rtype: HologObsDict .. _Description: @@ -365,54 +361,18 @@ def generate_holog_obs_dict( """ extract_holog_params = locals() - assert pathlib.Path(ms_name).exists() is True, logger.error( - f"File {ms_name} does not exists." - ) assert pathlib.Path(point_name).exists() is True, logger.error( f"File {point_name} does not exists." ) - # Get antenna IDs and names - ctb = ctables.table( - os.path.join(extract_holog_params["ms_name"], "ANTENNA"), - readonly=True, - lockoptions={"option": "usernoread"}, - ack=False, - ) - - ant_names = np.array(ctb.getcol("NAME")) - ant_id = np.arange(len(ant_names)) - ant_pos = ctb.getcol("POSITION") - - ctb.close() - - # Get antenna IDs that are in the main table - ctb = ctables.table( - extract_holog_params["ms_name"], - readonly=True, - lockoptions={"option": "usernoread"}, - ack=False, - ) - - ant1 = np.unique(ctb.getcol("ANTENNA1")) - ant2 = np.unique(ctb.getcol("ANTENNA2")) - ant_id_main = np.unique(np.append(ant1, ant2)) - - ant_names_main = ant_names[ant_id_main] - ctb.close() - pnt_mds = AstrohackPointFile(extract_holog_params["point_name"]) pnt_mds.open() - dist_matrix_dict = compute_antenna_baseline_distance_matrix_dict(ant_pos, ant_names) - holog_obs_dict = HologObsDict.create_from_ms_info( pnt_mds=pnt_mds, exclude_antennas=extract_holog_params["exclude_antennas"], baseline_average_distance=extract_holog_params["baseline_average_distance"], baseline_average_nearest=extract_holog_params["baseline_average_nearest"], - dist_matrix_dict=dist_matrix_dict, - ant_names_main=ant_names_main, ) return holog_obs_dict @@ -440,7 +400,6 @@ def model_memory_usage(ms_name: str, holog_obs_dict: HologObsDict = None) -> int ) holog_obs_dict = generate_holog_obs_dict( - ms_name=ms_name, point_name="temporary.pointing.zarr", baseline_average_distance="all", baseline_average_nearest="all", From 7279bbe22936ee0f537fcf55316a477ccd21bc3d Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Mon, 26 Jan 2026 11:13:50 -0700 Subject: [PATCH 037/295] Added option to add values to print in print_dict_types. --- src/astrohack/utils/text.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/astrohack/utils/text.py b/src/astrohack/utils/text.py index a3a8b11c..7de2c965 100644 --- a/src/astrohack/utils/text.py +++ b/src/astrohack/utils/text.py @@ -350,14 +350,17 @@ def print_dict_table( print(table) -def print_dict_types(le_dict, ident=0): +def print_dict_types(le_dict, ident=0, show_values=False): spc = " " for key, value in le_dict.items(): if isinstance(value, dict): - print(f"{key}:") - print_dict_types(value, ident=ident + 4) + print(f"{ident*spc}{key}:") + print_dict_types(value, ident=ident + 4, show_values=show_values) else: - print(f"{ident*spc}{key}: {type(value)}") + if show_values: + print(f"{ident * spc}{key}: {type(value)} => {value}") + else: + print(f"{ident * spc}{key}: {type(value)}") def get_property_string( From 3a3a7f234661e55008b376e4b108cf029adaff58 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Mon, 26 Jan 2026 11:15:23 -0700 Subject: [PATCH 038/295] core/extract_holog_2.py function extract_holog_preprocessing now produces a looping directory that is intended to be used with the general graph creation and execution. --- src/astrohack/core/extract_holog_2.py | 227 ++++++++++---------------- src/astrohack/extract_holog_2.py | 6 +- 2 files changed, 92 insertions(+), 141 deletions(-) diff --git a/src/astrohack/core/extract_holog_2.py b/src/astrohack/core/extract_holog_2.py index bd7e5a72..ef8cab1c 100644 --- a/src/astrohack/core/extract_holog_2.py +++ b/src/astrohack/core/extract_holog_2.py @@ -30,9 +30,8 @@ from astrohack.utils.file import load_point_file -def extract_holog_processing(extract_holog_params, pnt_mds): +def extract_holog_preprocessing(extract_holog_params, pnt_mds): holog_obs_dict = extract_holog_params["holog_obs_dict"] - parallel = extract_holog_params["parallel"] # Get spectral windows ctb = ctables.table( @@ -48,24 +47,6 @@ def extract_holog_processing(extract_holog_params, pnt_mds): ant_names = pnt_mds.root.attrs["antenna_names"] ant_ids = pnt_mds.root.attrs["antenna_ids"] - # Get antenna IDs that are in the main table - ctb = ctables.table( - extract_holog_params["ms_name"], - readonly=True, - lockoptions={"option": "usernoread"}, - ack=False, - ) - - ant1 = np.unique(ctb.getcol("ANTENNA1")) - ant2 = np.unique(ctb.getcol("ANTENNA2")) - ant_id_main = np.unique(np.append(ant1, ant2)) - ctb.close() - - ant_names_main = [] - for ant_id in ant_id_main: - i_name = ant_ids.index(ant_id) - ant_names_main.append(ant_names[i_name]) - ant_names_main = np.array(ant_names_main) # Create holog_obs_dict or modify user supplied holog_obs_dict. if holog_obs_dict is None: @@ -90,8 +71,6 @@ def extract_holog_processing(extract_holog_params, pnt_mds): if user_ant_sel != "all": holog_obs_dict.select("antenna", user_ant_sel) - holog_obs_dict.print() - return ctb = ctables.table( os.path.join(extract_holog_params["ms_name"], "STATE"), readonly=True, @@ -145,138 +124,96 @@ def extract_holog_processing(extract_holog_params, pnt_mds): logger.error(msg) raise Exception(msg) - count = 0 - delayed_list = [] - - for ddi_name in holog_obs_dict.keys(): - ddi = int(ddi_name.replace("ddi_", "")) + looping_dict = {} + for ddi_key in holog_obs_dict.keys(): + ddi = int(ddi_key.replace("ddi_", "")) spw_setup_id = ddi_spw[ddi] pol_setup_id = ddpol_indexol[ddi] - extract_holog_params["ddi"] = ddi - extract_holog_params["chan_setup"] = {} - extract_holog_params["pol_setup"] = {} - extract_holog_params["chan_setup"]["chan_freq"] = spw_ctb.getcol( - "CHAN_FREQ", startrow=spw_setup_id, nrow=1 - )[0, :] - - extract_holog_params["chan_setup"]["chan_width"] = spw_ctb.getcol( - "CHAN_WIDTH", startrow=spw_setup_id, nrow=1 - )[0, :] - - extract_holog_params["chan_setup"]["eff_bw"] = spw_ctb.getcol( - "EFFECTIVE_BW", startrow=spw_setup_id, nrow=1 - )[0, :] - - extract_holog_params["chan_setup"]["ref_freq"] = spw_ctb.getcol( - "REF_FREQUENCY", startrow=spw_setup_id, nrow=1 - )[0] - - extract_holog_params["chan_setup"]["total_bw"] = spw_ctb.getcol( - "TOTAL_BANDWIDTH", startrow=spw_setup_id, nrow=1 - )[0] - - extract_holog_params["pol_setup"]["pol"] = pol_str[ - pol_ctb.getcol("CORR_TYPE", startrow=pol_setup_id, nrow=1)[0, :] - ] - - # Loop over all beam_scan_ids, a beam_scan_id can consist of more than one scan in a measurement set (this is - # the case for the VLA pointed mosaics). - for holog_map_key in holog_obs_dict[ddi_name].keys(): - - if "map" in holog_map_key: - scans = holog_obs_dict[ddi_name][holog_map_key]["scans"] - if len(scans) > 1: - logger.info( - "Processing ddi: {ddi}, scans: [{min} ... {max}]".format( - ddi=ddi, min=scans[0], max=scans[-1] - ) - ) - else: - logger.info( - "Processing ddi: {ddi}, scan: {scan}".format( - ddi=ddi, scan=scans - ) + chan_freq = spw_ctb.getcol("CHAN_FREQ", startrow=spw_setup_id, nrow=1)[0, :] + chan_width = spw_ctb.getcol("CHAN_WIDTH", startrow=spw_setup_id, nrow=1)[0, :] + eff_bw = spw_ctb.getcol("EFFECTIVE_BW", startrow=spw_setup_id, nrow=1)[0, :] + ref_freq = spw_ctb.getcol("REF_FREQUENCY", startrow=spw_setup_id, nrow=1)[0] + total_bw = spw_ctb.getcol("TOTAL_BANDWIDTH", startrow=spw_setup_id, nrow=1)[0] + chan_setup = { + "chan_freq": chan_freq, + "chan_width": chan_width, + "eff_bw": eff_bw, + "ref_freq": ref_freq, + "total_bw": total_bw, + } + pol_setup = { + "pol": pol_str[ + pol_ctb.getcol("CORR_TYPE", startrow=pol_setup_id, nrow=1)[0, :] + ] + } + + ddi_dict = {} + for map_key, map_data in holog_obs_dict[ddi_key].items(): + scans = map_data["scans"] + if len(scans) > 1: + logger.info( + "Processing ddi: {ddi}, scans: [{min} ... {max}]".format( + ddi=ddi, min=scans[0], max=scans[-1] ) + ) + else: + logger.info( + "Processing ddi: {ddi}, scan: {scan}".format(ddi=ddi, scan=scans) + ) - if ( - len(list(holog_obs_dict[ddi_name][holog_map_key]["ant"].keys())) - != 0 - ): - map_ant_list = [] - ref_ant_per_map_ant_list = [] - - map_ant_name_list = [] - ref_ant_per_map_ant_name_list = [] - for map_ant_str in holog_obs_dict[ddi_name][holog_map_key][ - "ant" - ].keys(): - - ref_ant_ids = np.array( - _convert_ant_name_to_id( - ant_names, - list( - holog_obs_dict[ddi_name][holog_map_key]["ant"][ - map_ant_str - ] - ), - ) - ) + if len(list(map_data["ant"].keys())) != 0: + map_ant_list = [] + ref_ant_per_map_ant_list = [] - map_ant_id = _convert_ant_name_to_id(ant_names, map_ant_str)[0] + map_ant_name_list = [] + ref_ant_per_map_ant_name_list = [] + for map_ant_name, ref_ant_name_list in map_data["ant"].items(): - ref_ant_per_map_ant_list.append(ref_ant_ids) - map_ant_list.append(map_ant_id) + ref_ant_ids = _convert_ant_name_to_id( + ref_ant_name_list, + ant_names, + ant_ids, + ) - ref_ant_per_map_ant_name_list.append( - list( - holog_obs_dict[ddi_name][holog_map_key]["ant"][ - map_ant_str - ] - ) - ) - map_ant_name_list.append(map_ant_str) + map_ant_id = _convert_ant_name_to_id( + map_ant_name, ant_names, ant_ids + )[0] - extract_holog_params["ref_ant_per_map_ant_tuple"] = tuple( - ref_ant_per_map_ant_list - ) - extract_holog_params["map_ant_tuple"] = tuple(map_ant_list) + ref_ant_per_map_ant_list.append(ref_ant_ids) + map_ant_list.append(map_ant_id) - extract_holog_params["ref_ant_per_map_ant_name_tuple"] = tuple( - ref_ant_per_map_ant_name_list - ) - extract_holog_params["map_ant_name_tuple"] = tuple( - map_ant_name_list + ref_ant_per_map_ant_name_list.append( + list(map_data["ant"][map_ant_name]) ) + map_ant_name_list.append(map_ant_name) - extract_holog_params["scans"] = scans - extract_holog_params["sel_state_ids"] = state_ids - extract_holog_params["holog_map_key"] = holog_map_key - extract_holog_params["ant_names"] = ant_names - extract_holog_params["ant_station"] = ant_station - - if parallel: - delayed_list.append( - dask.delayed(process_extract_holog_chunk)( - dask.delayed(copy.deepcopy(extract_holog_params)) - ) - ) - else: - process_extract_holog_chunk(extract_holog_params) - - count += 1 + map_dict = { + "ref_ant_per_map_ant_tuple": tuple(ref_ant_per_map_ant_list), + "map_ant_tuple": tuple(map_ant_list), + "ref_ant_per_map_ant_name_tuple": tuple( + ref_ant_per_map_ant_name_list + ), + "map_ant_name_tuple": tuple(map_ant_name_list), + "scans": scans, + "sel_state_ids": state_ids, + "ant_names": ant_names, + "chan_setup": chan_setup, + "pol_setup": pol_setup, + } + ddi_dict[map_key] = map_dict - else: - logger.warning( - "DDI " + str(ddi) + " has no holography data to extract." - ) + else: + logger.warning( + f"{create_dataset_label(ddi_key, map_key)} has no holography data to extract." + ) + looping_dict[ddi_key] = ddi_dict spw_ctb.close() pol_ctb.close() obs_ctb.close() - if parallel: - dask.compute(delayed_list) + return looping_dict def process_extract_holog_chunk(extract_holog_params): @@ -989,15 +926,27 @@ def create_holog_json(holog_file, holog_dict): raise Exception(error) -def _convert_ant_name_to_id(ant_list, ant_names): +def _convert_ant_name_to_id( + ant_name_list, + ref_ant_names, + ref_ant_ids, +): """_summary_ Args: - ant_list (_type_): _description_ + ant_name_list (list): _description_ ant_names (_type_): _description_ Returns: _type_: _description_ """ + if not isinstance(ant_name_list, list): + ant_name_list = [ant_name_list] + + ant_ids_list = [] + for ant_name in ant_name_list: + if ant_name in ref_ant_names: + i_ids = ref_ant_names.index(ant_name) + ant_ids_list.append(ref_ant_ids[i_ids]) - return np.nonzero(np.isin(ant_list, ant_names))[0] + return ant_ids_list diff --git a/src/astrohack/extract_holog_2.py b/src/astrohack/extract_holog_2.py index cdbc282b..a9bafe2b 100644 --- a/src/astrohack/extract_holog_2.py +++ b/src/astrohack/extract_holog_2.py @@ -20,12 +20,13 @@ from rich.table import Table from astrohack import open_pointing +from astrohack.utils import print_dict_types from astrohack.utils.file import ( overwrite_file, ) from astrohack.core.extract_holog_2 import ( - extract_holog_processing, + extract_holog_preprocessing, ) from astrohack.core.extract_holog_2 import process_extract_holog_chunk from astrohack.utils.text import get_default_file_name @@ -220,7 +221,8 @@ def extract_holog( pnt_mds = open_pointing(point_name) - extract_holog_processing(extract_holog_params, pnt_mds) + looping_dict = extract_holog_preprocessing(extract_holog_params, pnt_mds) + print_dict_types(looping_dict, show_values=True) # if count > 0: # logger.info("Finished processing") From 2ed60e06e055441b371181ee18ba78083a81798c Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Mon, 26 Jan 2026 11:20:52 -0700 Subject: [PATCH 039/295] extract_holog_2.py now uses the general graph creation tool. --- src/astrohack/extract_holog_2.py | 48 ++++++++++++++------------------ 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/src/astrohack/extract_holog_2.py b/src/astrohack/extract_holog_2.py index a9bafe2b..d96378b7 100644 --- a/src/astrohack/extract_holog_2.py +++ b/src/astrohack/extract_holog_2.py @@ -35,6 +35,7 @@ from astrohack.io.point_mds import AstrohackPointFile from astrohack.extract_pointing import extract_pointing from astrohack.core.holog_obs_dict import HologObsDict +from astrohack.utils.graph import compute_graph_to_mds_tree from typing import Union, List, Tuple @@ -224,33 +225,26 @@ def extract_holog( looping_dict = extract_holog_preprocessing(extract_holog_params, pnt_mds) print_dict_types(looping_dict, show_values=True) - # if count > 0: - # logger.info("Finished processing") - # - # holog_dict = load_holog_file( - # file=extract_holog_params["holog_name"], dask_load=True, load_pnt_dict=False - # ) - # - # create_holog_json(extract_holog_params["holog_name"], holog_dict) - # - # holog_attr_file = "{name}/{ext}".format( - # name=extract_holog_params["holog_name"], ext=".holog_input" - # ) - # write_meta_data(holog_attr_file, input_pars) - # - # with open( - # f"{extract_holog_params['holog_name']}/holog_obs_dict.json", "w" - # ) as outfile: - # json.dump(holog_obs_dict, outfile, cls=NumpyEncoder) - # - # holog_mds = AstrohackHologFile(extract_holog_params["holog_name"]) - # holog_mds.open() - # - # return holog_mds - # - # else: - # logger.warning("No data to process") - # return None + holog_mds = AstrohackHologFile.create_from_input_parameters( + holog_name, extract_holog_params + ) + + extract_holog_params["pnt_mds"] = pnt_mds + + executed_graph = compute_graph_to_mds_tree( + looping_dict, + process_extract_holog_chunk, + extract_holog_params, + ["ddi", "map"], + holog_mds, + ) + + if executed_graph: + holog_mds.write(mode="a") + return holog_mds + else: + logger.warning("No data to process") + return None def generate_holog_obs_dict( From 315ea2a530d3dbe13e61c502b6c5ca938a57ea31 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Mon, 26 Jan 2026 11:40:58 -0700 Subject: [PATCH 040/295] Antenna stations now made an attribute of point_mds.root --- src/astrohack/core/extract_pointing_2.py | 1 + src/astrohack/extract_pointing_2.py | 1 + 2 files changed, 2 insertions(+) diff --git a/src/astrohack/core/extract_pointing_2.py b/src/astrohack/core/extract_pointing_2.py index 96c09b03..420c5626 100644 --- a/src/astrohack/core/extract_pointing_2.py +++ b/src/astrohack/core/extract_pointing_2.py @@ -102,6 +102,7 @@ def extract_pointing_preprocessing(input_params): "parallel": input_params["parallel"], "antenna_names": antenna_names, "antenna_ids": antenna_ids, + "antenna_stations": antenna_stations, } ant_dist_matrix = compute_antenna_baseline_distance_matrix_dict( diff --git a/src/astrohack/extract_pointing_2.py b/src/astrohack/extract_pointing_2.py index 042960b5..633594c2 100644 --- a/src/astrohack/extract_pointing_2.py +++ b/src/astrohack/extract_pointing_2.py @@ -93,6 +93,7 @@ def extract_pointing( point_mds.root.attrs["baseline_dist_matrix"] = ant_dist_matrix point_mds.root.attrs["antenna_names"] = pnt_params.pop("antenna_names") point_mds.root.attrs["antenna_ids"] = pnt_params.pop("antenna_ids") + point_mds.root.attrs["antenna_stations"] = pnt_params.pop("antenna_stations") executed_graph = compute_graph_to_mds_tree( looping_dict, From 1d66aa5194581ee9fcfb8ea627fcce8e4c419fb1 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Mon, 26 Jan 2026 11:41:40 -0700 Subject: [PATCH 041/295] Ported process_extract_holog_chunk up to create_holog_file. --- src/astrohack/core/extract_holog_2.py | 108 +++++++++++++++----------- src/astrohack/extract_holog_2.py | 5 +- 2 files changed, 65 insertions(+), 48 deletions(-) diff --git a/src/astrohack/core/extract_holog_2.py b/src/astrohack/core/extract_holog_2.py index ef8cab1c..b1ee87b0 100644 --- a/src/astrohack/core/extract_holog_2.py +++ b/src/astrohack/core/extract_holog_2.py @@ -47,6 +47,7 @@ def extract_holog_preprocessing(extract_holog_params, pnt_mds): ant_names = pnt_mds.root.attrs["antenna_names"] ant_ids = pnt_mds.root.attrs["antenna_ids"] + ant_stations = pnt_mds.root.attrs["antenna_stations"] # Create holog_obs_dict or modify user supplied holog_obs_dict. if holog_obs_dict is None: @@ -168,6 +169,7 @@ def extract_holog_preprocessing(extract_holog_params, pnt_mds): map_ant_name_list = [] ref_ant_per_map_ant_name_list = [] + ref_ant_stations_list = [] for map_ant_name, ref_ant_name_list in map_data["ant"].items(): ref_ant_ids = _convert_ant_name_to_id( @@ -186,6 +188,10 @@ def extract_holog_preprocessing(extract_holog_params, pnt_mds): ref_ant_per_map_ant_name_list.append( list(map_data["ant"][map_ant_name]) ) + ref_ant_stations = _convert_ant_name_to_id( + ref_ant_name_list, ant_names, ant_stations + ) + ref_ant_stations_list.append(ref_ant_stations) map_ant_name_list.append(map_ant_name) map_dict = { @@ -195,6 +201,7 @@ def extract_holog_preprocessing(extract_holog_params, pnt_mds): ref_ant_per_map_ant_name_list ), "map_ant_name_tuple": tuple(map_ant_name_list), + "ref_ant_stations": tuple(ref_ant_stations_list), "scans": scans, "sel_state_ids": state_ids, "ant_names": ant_names, @@ -216,7 +223,7 @@ def extract_holog_preprocessing(extract_holog_params, pnt_mds): return looping_dict -def process_extract_holog_chunk(extract_holog_params): +def process_extract_holog_chunk(extract_holog_params, holog_mds): """Perform data query on holography data chunk and get unique time and state_ids/ Args: @@ -224,20 +231,32 @@ def process_extract_holog_chunk(extract_holog_params): """ ms_name = extract_holog_params["ms_name"] - pnt_name = extract_holog_params["point_name"] data_column = extract_holog_params["data_column"] - ddi = extract_holog_params["ddi"] - scans = extract_holog_params["scans"] - ant_names = extract_holog_params["ant_names"] - ant_station = extract_holog_params["ant_station"] - ref_ant_per_map_ant_tuple = extract_holog_params["ref_ant_per_map_ant_tuple"] - map_ant_tuple = extract_holog_params["map_ant_tuple"] - map_ant_name_tuple = extract_holog_params["map_ant_name_tuple"] - holog_map_key = extract_holog_params["holog_map_key"] time_interval = extract_holog_params["time_smoothing_interval"] pointing_interpolation_method = extract_holog_params[ "pointing_interpolation_method" ] + holog_name = extract_holog_params["holog_name"] + + pnt_mds = extract_holog_params["pnt_mds"] + inp_data_dict = extract_holog_params["data_dict"] + + ddi_key = extract_holog_params["this_ddi"] + map_key = extract_holog_params["this_map"] + + ddi_id = int(ddi_key.replace("ddi_", "")) + + scans = inp_data_dict["scans"] + ant_names = inp_data_dict["ant_names"] + ant_stations = inp_data_dict["ref_ant_stations"] + ref_ant_per_map_ant_tuple = inp_data_dict["ref_ant_per_map_ant_tuple"] + map_ant_tuple = inp_data_dict["map_ant_tuple"] + map_ant_name_tuple = inp_data_dict["map_ant_name_tuple"] + sel_state_ids = inp_data_dict["sel_state_ids"] + chan_setup = inp_data_dict["chan_setup"] + pol = inp_data_dict["pol_setup"]["pol"] + + chan_freq = chan_setup["chan_freq"] # This piece of information is no longer used leaving them here commented out for completeness # ref_ant_per_map_ant_name_tuple = extract_holog_params["ref_ant_per_map_ant_name_tuple"] @@ -250,12 +269,6 @@ def process_extract_holog_chunk(extract_holog_params): "Inconsistancy between antenna list length, see error above for more info." ) - sel_state_ids = extract_holog_params["sel_state_ids"] - holog_name = extract_holog_params["holog_name"] - - chan_freq = extract_holog_params["chan_setup"]["chan_freq"] - pol = extract_holog_params["pol_setup"]["pol"] - table_obj = ctables.table( ms_name, readonly=True, lockoptions={"option": "usernoread"}, ack=False ) @@ -264,13 +277,13 @@ def process_extract_holog_chunk(extract_holog_params): ctb = ctables.taql( "select %s, SCAN_NUMBER, ANTENNA1, ANTENNA2, TIME, TIME_CENTROID, WEIGHT, FLAG_ROW, FLAG, FIELD_ID from " "$table_obj WHERE DATA_DESC_ID == %s AND SCAN_NUMBER in %s AND STATE_ID in %s" - % (data_column, ddi, scans, list(sel_state_ids)) + % (data_column, ddi_id, scans, list(sel_state_ids)) ) else: ctb = ctables.taql( "select %s, SCAN_NUMBER, ANTENNA1, ANTENNA2, TIME, TIME_CENTROID, WEIGHT, FLAG_ROW, FLAG, FIELD_ID from " "$table_obj WHERE DATA_DESC_ID == %s AND SCAN_NUMBER in %s" - % (data_column, ddi, scans) + % (data_column, ddi_id, scans) ) vis_data = ctb.getcol(data_column) weight = ctb.getcol("WEIGHT") @@ -294,7 +307,7 @@ def process_extract_holog_chunk(extract_holog_params): table_obj.close() map_ref_dict = _get_map_ref_dict( - map_ant_tuple, ref_ant_per_map_ant_tuple, ant_names, ant_station + map_ant_tuple, ref_ant_per_map_ant_tuple, ant_names, ant_stations ) ( @@ -325,9 +338,8 @@ def process_extract_holog_chunk(extract_holog_params): map_ant_name_list = ["_".join(("ant", i)) for i in map_ant_name_list] - pnt_ant_dict = load_point_file(pnt_name, map_ant_name_list, dask_load=False) pnt_map_dict = _extract_pointing_chunk( - map_ant_name_list, time_vis, pnt_ant_dict, pointing_interpolation_method + map_ant_name_list, time_vis, pnt_mds, pointing_interpolation_method ) grid_params = {} @@ -338,7 +350,7 @@ def process_extract_holog_chunk(extract_holog_params): antenna_name = "_".join(("ant", ant_names[ant_index])) telescope = get_proper_telescope(gen_info["telescope name"], antenna_name) n_pix, cell_size = calculate_optimal_grid_parameters( - pnt_map_dict, antenna_name, telescope.diameter, chan_freq, ddi + pnt_map_dict, antenna_name, telescope.diameter, chan_freq, ddi_id ) grid_params[antenna_name] = {"n_pix": n_pix, "cell_size": cell_size} @@ -349,43 +361,45 @@ def process_extract_holog_chunk(extract_holog_params): # vis_map_dict, weight_map_dict, flagged_mapping_antennas,time_vis,pnt_map_dict,ant_names) time_vis = time_vis + # over_flow_protector_constant - _create_holog_file( - holog_name, - vis_map_dict, - weight_map_dict, - pnt_map_dict, - time_vis, - used_samples_dict, - chan_freq, - pol, - flagged_mapping_antennas, - holog_map_key, - ddi, - ms_name, - ant_names, - ant_station, - grid_params, - time_interval, - gen_info, - map_ref_dict, - scan_time_ranges, - unq_scans, - ) + # _create_holog_file( + # holog_name, + # vis_map_dict, + # weight_map_dict, + # pnt_map_dict, + # time_vis, + # used_samples_dict, + # chan_freq, + # pol, + # flagged_mapping_antennas, + # map_key, + # ddi_id, + # ms_name, + # ant_names, + # ant_station, + # grid_params, + # time_interval, + # gen_info, + # map_ref_dict, + # scan_time_ranges, + # unq_scans, + # ) logger.info( "Finished extracting holography chunk for ddi: {ddi} holog_map_key: {holog_map_key}".format( - ddi=ddi, holog_map_key=holog_map_key + ddi=ddi_id, holog_map_key=map_key ) ) -def _get_map_ref_dict(map_ant_tuple, ref_ant_per_map_ant_tuple, ant_names, ant_station): +def _get_map_ref_dict( + map_ant_tuple, ref_ant_per_map_ant_tuple, ant_names, ant_stations +): map_dict = {} for ii, map_id in enumerate(map_ant_tuple): map_name = ant_names[map_id] ref_list = [] for ref_id in ref_ant_per_map_ant_tuple[ii]: - ref_list.append(f"{ant_names[ref_id]} @ {ant_station[ref_id]}") + ref_list.append(f"{ant_names[ref_id]} @ {ant_stations[ref_id]}") map_dict[map_name] = ref_list return map_dict diff --git a/src/astrohack/extract_holog_2.py b/src/astrohack/extract_holog_2.py index d96378b7..08fd08b3 100644 --- a/src/astrohack/extract_holog_2.py +++ b/src/astrohack/extract_holog_2.py @@ -212,6 +212,9 @@ def extract_holog( extract_holog_params = locals() + # VVV This is a temporary fix waiting for the implementation of a mapping parameter + extract_holog_params["map"] = "all" + assert pathlib.Path(extract_holog_params["ms_name"]).exists() is True, logger.error( f'File {extract_holog_params["ms_name"]} does not exists.' ) @@ -223,7 +226,7 @@ def extract_holog( pnt_mds = open_pointing(point_name) looping_dict = extract_holog_preprocessing(extract_holog_params, pnt_mds) - print_dict_types(looping_dict, show_values=True) + # print_dict_types(looping_dict, show_values=True) holog_mds = AstrohackHologFile.create_from_input_parameters( holog_name, extract_holog_params From 91b03c5255a8f3ea00260c1c979e2f1bf636659f Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Mon, 26 Jan 2026 15:17:35 -0700 Subject: [PATCH 042/295] extract_holog_2.py now produces an output holog_mds with all the extracted holography data. --- src/astrohack/core/extract_holog_2.py | 191 +++++++++----------------- src/astrohack/extract_holog_2.py | 17 +-- 2 files changed, 71 insertions(+), 137 deletions(-) diff --git a/src/astrohack/core/extract_holog_2.py b/src/astrohack/core/extract_holog_2.py index b1ee87b0..93d22b8e 100644 --- a/src/astrohack/core/extract_holog_2.py +++ b/src/astrohack/core/extract_holog_2.py @@ -1,7 +1,4 @@ import os -import json -import dask -import copy import numpy as np import xarray as xr @@ -27,9 +24,6 @@ from astrohack.core.holog_obs_dict import HologObsDict -from astrohack.utils.file import load_point_file - - def extract_holog_preprocessing(extract_holog_params, pnt_mds): holog_obs_dict = extract_holog_params["holog_obs_dict"] @@ -220,14 +214,15 @@ def extract_holog_preprocessing(extract_holog_params, pnt_mds): pol_ctb.close() obs_ctb.close() - return looping_dict + return looping_dict, holog_obs_dict def process_extract_holog_chunk(extract_holog_params, holog_mds): """Perform data query on holography data chunk and get unique time and state_ids/ Args: - extract_holog_params: parameters controlling work to be done on this chunk + extract_holog_params (dict): parameters controlling work to be done on this chunk + holog_mds (AstrohackHologFile): Output holog mds file """ ms_name = extract_holog_params["ms_name"] @@ -352,7 +347,6 @@ def process_extract_holog_chunk(extract_holog_params, holog_mds): n_pix, cell_size = calculate_optimal_grid_parameters( pnt_map_dict, antenna_name, telescope.diameter, chan_freq, ddi_id ) - grid_params[antenna_name] = {"n_pix": n_pix, "cell_size": cell_size} # ## To DO: ################## Average multiple repeated samples over_flow_protector_constant = float("%.5g" % @@ -361,35 +355,32 @@ def process_extract_holog_chunk(extract_holog_params, holog_mds): # vis_map_dict, weight_map_dict, flagged_mapping_antennas,time_vis,pnt_map_dict,ant_names) time_vis = time_vis + # over_flow_protector_constant - # _create_holog_file( - # holog_name, - # vis_map_dict, - # weight_map_dict, - # pnt_map_dict, - # time_vis, - # used_samples_dict, - # chan_freq, - # pol, - # flagged_mapping_antennas, - # map_key, - # ddi_id, - # ms_name, - # ant_names, - # ant_station, - # grid_params, - # time_interval, - # gen_info, - # map_ref_dict, - # scan_time_ranges, - # unq_scans, - # ) - - logger.info( - "Finished extracting holography chunk for ddi: {ddi} holog_map_key: {holog_map_key}".format( - ddi=ddi_id, holog_map_key=map_key - ) + _create_holog_file( + holog_name, + vis_map_dict, + weight_map_dict, + pnt_map_dict, + time_vis, + used_samples_dict, + chan_freq, + pol, + flagged_mapping_antennas, + map_key, + ddi_key, + ms_name, + ant_names, + grid_params, + time_interval, + gen_info, + map_ref_dict, + scan_time_ranges, + unq_scans, + holog_mds, + extract_holog_params["parallel"], ) + logger.info(f"Finished extracting holography chunk for DDI {ddi_id}, {map_key}.") + def _get_map_ref_dict( map_ant_tuple, ref_ant_per_map_ant_tuple, ant_names, ant_stations @@ -490,11 +481,10 @@ def _extract_holog_chunk_jit( time_index = 0 for row in range(n_row): - if flag_row is False: + if not flag_row: continue # Find index of time_vis_row[row] in time_samples, assumes time_vis_row is ordered in time - if time_vis_row[row] < time_samples[time_index] - half_int: continue else: @@ -603,17 +593,18 @@ def _create_holog_file( chan, pol, flagged_mapping_antennas, - holog_map_key, - ddi, + map_key, + ddi_key, ms_name, ant_names, - ant_station, grid_params, time_interval, gen_info, map_ref_dict, scan_time_ranges, unq_scans, + holog_mds, + parallel, ): """Create holog-structured, formatted output file and save to zarr. @@ -627,8 +618,8 @@ def _create_holog_file( chan (numpy.ndarray): channel values pol (numpy.ndarray): polarization values flagged_mapping_antennas (numpy.ndarray): list of mapping antennas that have been flagged. - holog_map_key(string): holog map id string - ddi (numpy.ndarray): data description id; a combination of polarization and spectral window + map_key(string): holog map id string + ddi_key (string): data description id; a combination of polarization and spectral window """ ctb = ctables.table("/".join((ms_name, "ANTENNA")), ack=False) @@ -636,21 +627,24 @@ def _create_holog_file( ctb.close() for map_ant_index in vis_map_dict.keys(): + dataset_label = create_dataset_label( + ant_names[map_ant_index], ddi_key.split("_")[0] + ) if map_ant_index not in flagged_mapping_antennas: + map_ant_key = f"ant_{ant_names[map_ant_index]}" + pnt_xds = pnt_map_dict[map_ant_key] + vis_data = vis_map_dict[map_ant_index] + wei_data = weight_map_dict[map_ant_index] valid_data = used_samples_dict[map_ant_index] == 1.0 - ant_time_vis = time_vis[valid_data] time_vis_days = ant_time_vis / (3600 * 24) astro_time_vis = astropy.time.Time(time_vis_days, format="mjd") time_samples, indicies = _get_time_samples(astro_time_vis) coords = {"time": ant_time_vis, "chan": chan, "pol": pol} - map_ant_tag = ( - "ant_" + ant_names[map_ant_index] - ) # 'ant_' + str(map_ant_index) direction = np.take( - pnt_map_dict[map_ant_tag]["DIRECTIONAL_COSINES"].values, + pnt_xds["DIRECTIONAL_COSINES"].values, indicies, axis=0, ) @@ -664,73 +658,52 @@ def _create_holog_file( xds = xr.Dataset() xds = xds.assign_coords(coords) xds["VIS"] = xr.DataArray( - vis_map_dict[map_ant_index][valid_data, ...], + vis_data[valid_data, ...], dims=["time", "chan", "pol"], ) xds["WEIGHT"] = xr.DataArray( - weight_map_dict[map_ant_index][valid_data, ...], + wei_data[valid_data, ...], dims=["time", "chan", "pol"], ) xds["DIRECTIONAL_COSINES"] = xr.DataArray( - pnt_map_dict[map_ant_tag]["DIRECTIONAL_COSINES"].values[ - valid_data, ... - ], + pnt_xds["DIRECTIONAL_COSINES"].values[valid_data, ...], dims=["time", "lm"], ) xds["IDEAL_DIRECTIONAL_COSINES"] = xr.DataArray( - pnt_map_dict[map_ant_tag]["POINTING_OFFSET"].values[valid_data, ...], + pnt_xds["POINTING_OFFSET"].values[valid_data, ...], dims=["time", "lm"], ) - xds.attrs["holog_map_key"] = holog_map_key - xds.attrs["ddi"] = ddi xds.attrs["parallactic_samples"] = parallactic_samples xds.attrs["time_smoothing_interval"] = time_interval xds.attrs["scan_time_ranges"] = scan_time_ranges xds.attrs["scan_list"] = unq_scans - xds.attrs["summary"] = _crate_observation_summary( - ant_names[map_ant_index], - ant_station[map_ant_index], + xds.attrs["summary"] = _create_observation_summary( gen_info, grid_params, xds["DIRECTIONAL_COSINES"].values, chan, - pnt_map_dict[map_ant_tag], + pnt_xds, valid_data, map_ref_dict, ) - holog_file = holog_name + holog_filename = holog_name - logger.debug( - f"Writing {create_dataset_label(ant_names[map_ant_index], ddi)} holog file to {holog_file}" - ) - xds.to_zarr( - os.path.join( - holog_file, - "ddi_" - + str(ddi) - + "/" - + str(holog_map_key) - + "/" - + "ant_" - + str(ant_names[map_ant_index]), - ), - mode="w", - compute=True, - consolidated=True, - ) + logger.debug(f"Writing {dataset_label} holog data to {holog_filename}") + dataset_name = "-".join([map_ant_key, f"ddi_{ddi_key}", map_key]) - else: - logger.warning( - "Mapping antenna {index} has no data".format( - index=ant_names[map_ant_index] - ) + holog_mds.add_node_to_tree( + xr.DataTree(name=dataset_name, dataset=xds), + dump_to_disk=True, + running_in_parallel=parallel, ) + else: + logger.warning(f"No holography data for {dataset_label}") def _extract_pointing_chunk( @@ -865,9 +838,7 @@ def _get_freq_summary(chan_axis): return freq_info -def _crate_observation_summary( - antenna_name, - station, +def _create_observation_summary( obs_info, grid_params, lm, @@ -876,6 +847,8 @@ def _crate_observation_summary( valid_data, map_ref_dict, ): + antenna_name = pnt_map_xds.attrs["ant_name"] + station = pnt_map_xds.attrs["ant_station"] spw_info = _get_freq_summary(chan_axis) obs_info["az el info"] = _get_az_el_characteristics(pnt_map_xds, valid_data) obs_info["reference antennas"] = map_ref_dict[antenna_name] @@ -903,43 +876,6 @@ def _crate_observation_summary( return summary -def create_holog_json(holog_file, holog_dict): - """Save holog file meta information to json file with the transformation - of the ordering (ddi, holog_map, ant) --> (ant, ddi, holog_map). - - Args: - input_params (): - holog_file (str): holog file name. - holog_dict (dict): Dictionary containing msdx data. - """ - - ant_holog_dict = {} - - for ddi, map_dict in holog_dict.items(): - if "ddi_" in ddi: - for mapping, ant_dict in map_dict.items(): - if "map_" in mapping: - for ant, xds in ant_dict.items(): - if "ant_" in ant: - if ant not in ant_holog_dict: - ant_holog_dict[ant] = {ddi: {mapping: {}}} - elif ddi not in ant_holog_dict[ant]: - ant_holog_dict[ant][ddi] = {mapping: {}} - - ant_holog_dict[ant][ddi][mapping] = xds.to_dict(data=False) - - output_meta_file = "{name}/{ext}".format(name=holog_file, ext=".holog_json") - - try: - with open(output_meta_file, "w") as json_file: - json.dump(ant_holog_dict, json_file) - - except Exception as error: - logger.error(f"{error}") - - raise Exception(error) - - def _convert_ant_name_to_id( ant_name_list, ref_ant_names, @@ -948,11 +884,12 @@ def _convert_ant_name_to_id( """_summary_ Args: - ant_name_list (list): _description_ - ant_names (_type_): _description_ + ant_name_list (list): List of antennas for which to fetch ids + ref_ant_names (list): Reference list of antenna names + ref_ant_ids (list): Reference list of antenna ids Returns: - _type_: _description_ + list: List of antenna ids """ if not isinstance(ant_name_list, list): ant_name_list = [ant_name_list] diff --git a/src/astrohack/extract_holog_2.py b/src/astrohack/extract_holog_2.py index 08fd08b3..6c99d044 100644 --- a/src/astrohack/extract_holog_2.py +++ b/src/astrohack/extract_holog_2.py @@ -1,5 +1,3 @@ -import copy -import json import os import pathlib import pickle @@ -8,19 +6,15 @@ import multiprocessing import toolviper.utils.parameter -import dask import psutil -import numpy as np import toolviper.utils.logger as logger -from casacore import tables as ctables from rich.console import Console from rich.table import Table from astrohack import open_pointing -from astrohack.utils import print_dict_types from astrohack.utils.file import ( overwrite_file, @@ -30,14 +24,13 @@ ) from astrohack.core.extract_holog_2 import process_extract_holog_chunk from astrohack.utils.text import get_default_file_name -from astrohack.utils.text import NumpyEncoder from astrohack.io.holog_mds import AstrohackHologFile from astrohack.io.point_mds import AstrohackPointFile from astrohack.extract_pointing import extract_pointing from astrohack.core.holog_obs_dict import HologObsDict from astrohack.utils.graph import compute_graph_to_mds_tree -from typing import Union, List, Tuple +from typing import Union, List # @toolviper.utils.parameter.validate(add_data_type=HologObsDict) @@ -225,11 +218,13 @@ def extract_holog( pnt_mds = open_pointing(point_name) - looping_dict = extract_holog_preprocessing(extract_holog_params, pnt_mds) + looping_dict, used_holog_obs_dict = extract_holog_preprocessing( + extract_holog_params, pnt_mds + ) # print_dict_types(looping_dict, show_values=True) holog_mds = AstrohackHologFile.create_from_input_parameters( - holog_name, extract_holog_params + holog_name, extract_holog_params.copy() ) extract_holog_params["pnt_mds"] = pnt_mds @@ -242,6 +237,8 @@ def extract_holog( holog_mds, ) + holog_mds.root.attrs["holog_obs_dict"] = used_holog_obs_dict + if executed_graph: holog_mds.write(mode="a") return holog_mds From 859ae4ef7debe50037003ac0e8ff8a74271a28a7 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Mon, 26 Jan 2026 15:39:02 -0700 Subject: [PATCH 043/295] Made flag_row detection more robust --- src/astrohack/core/extract_holog_2.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/astrohack/core/extract_holog_2.py b/src/astrohack/core/extract_holog_2.py index 93d22b8e..1e40f901 100644 --- a/src/astrohack/core/extract_holog_2.py +++ b/src/astrohack/core/extract_holog_2.py @@ -477,11 +477,21 @@ def _extract_holog_chunk_jit( (n_time, n_chan, n_pol), dtype=types.float64, ) + + # This code here is to uncommented and the snippet above commited for this function to work outside jit + # vis_map_dict[antenna_id] = np.zeros( + # (n_time, n_chan, n_pol), + # dtype=np.complex128, + # ) + # sum_weight_map_dict[antenna_id] = np.zeros( + # (n_time, n_chan, n_pol), + # dtype=np.float64, + # ) used_samples_dict[antenna_id] = np.full(n_time, False, dtype=bool) time_index = 0 for row in range(n_row): - if not flag_row: + if np.all(flag_row == False): continue # Find index of time_vis_row[row] in time_samples, assumes time_vis_row is ordered in time From 4a3f6d4c6e3efc46dbef7288b21fcacada619e3f Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Mon, 26 Jan 2026 15:42:42 -0700 Subject: [PATCH 044/295] Fixed a typo in dataset name creation. --- src/astrohack/core/extract_holog_2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/astrohack/core/extract_holog_2.py b/src/astrohack/core/extract_holog_2.py index 1e40f901..936d856b 100644 --- a/src/astrohack/core/extract_holog_2.py +++ b/src/astrohack/core/extract_holog_2.py @@ -705,7 +705,7 @@ def _create_holog_file( holog_filename = holog_name logger.debug(f"Writing {dataset_label} holog data to {holog_filename}") - dataset_name = "-".join([map_ant_key, f"ddi_{ddi_key}", map_key]) + dataset_name = "-".join([map_ant_key, ddi_key, map_key]) holog_mds.add_node_to_tree( xr.DataTree(name=dataset_name, dataset=xds), From e4ce940aa4f6bcbdecd2425060ad091c6758d8d9 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Mon, 26 Jan 2026 15:45:37 -0700 Subject: [PATCH 045/295] Black compliance --- docs/tutorials/beamcut_tutorial.ipynb | 7 +------ docs/tutorials/vla_holography_tutorial.ipynb | 3 +-- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/docs/tutorials/beamcut_tutorial.ipynb b/docs/tutorials/beamcut_tutorial.ipynb index 3566f9f9..18e52844 100644 --- a/docs/tutorials/beamcut_tutorial.ipynb +++ b/docs/tutorials/beamcut_tutorial.ipynb @@ -2421,12 +2421,7 @@ } ], "source": [ - "beamcut_mds.create_beam_fit_report(\n", - " beamcut_exports,\n", - " ant=\"ea17\",\n", - " ddi=0,\n", - " parallel=False\n", - ")\n", + "beamcut_mds.create_beam_fit_report(beamcut_exports, ant=\"ea17\", ddi=0, parallel=False)\n", "\n", "with open(\"beamcut_exports/beamcut_report_ant_ea17_ddi_0.txt\", \"r\") as infile:\n", " for line in infile:\n", diff --git a/docs/tutorials/vla_holography_tutorial.ipynb b/docs/tutorials/vla_holography_tutorial.ipynb index fa7f7f4e..432f854f 100644 --- a/docs/tutorials/vla_holography_tutorial.ipynb +++ b/docs/tutorials/vla_holography_tutorial.ipynb @@ -450,8 +450,7 @@ "toolviper.utils.data.download(\"heuristic_model\", folder=\"./\")\n", "# the elastic model download needs to be fixed\n", "model_memory_usage(\n", - " ms_name=\"data/ea25_cal_small_after_fixed.split.ms\",\n", - " holog_obs_dict=None\n", + " ms_name=\"data/ea25_cal_small_after_fixed.split.ms\", holog_obs_dict=None\n", ")" ] }, From b2775e919ebbdc066c2c273bb588fc64dbbff94b Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 27 Jan 2026 10:37:43 -0700 Subject: [PATCH 046/295] open_holog function now opens the new holog_mds files backed by xarray datatrees. --- src/astrohack/io/dio.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/astrohack/io/dio.py b/src/astrohack/io/dio.py index dc01d202..1abe93f4 100644 --- a/src/astrohack/io/dio.py +++ b/src/astrohack/io/dio.py @@ -12,7 +12,7 @@ check_if_file_can_be_opened_2, ) from astrohack.io.mds import AstrohackImageFile -from astrohack.io.mds import AstrohackHologFile +from astrohack.io.holog_mds import AstrohackHologFile from astrohack.io.mds import AstrohackPanelFile from astrohack.io.point_mds import AstrohackPointFile from astrohack.io.position_mds import AstrohackPositionFile @@ -97,7 +97,7 @@ def open_holog(file: str) -> Union[AstrohackHologFile, None]: ddi_m: … } """ - check_if_file_can_be_opened(file, "0.7.2") + check_if_file_can_be_opened_2(file, "extract_holog", "0.10.1") _data_file = AstrohackHologFile(file=file) if _data_file.open(): From f1c58a7ec5606f2cccbef04c235de6a9cb70cb7d Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 27 Jan 2026 10:38:13 -0700 Subject: [PATCH 047/295] Added retrieval of holog_obs_dict from holog_mds file on disk. --- src/astrohack/core/holog_obs_dict.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/astrohack/core/holog_obs_dict.py b/src/astrohack/core/holog_obs_dict.py index b7fc7eba..22f4c612 100644 --- a/src/astrohack/core/holog_obs_dict.py +++ b/src/astrohack/core/holog_obs_dict.py @@ -6,6 +6,7 @@ from typing import Union, List, Any from rich.console import Console +from astrohack import open_holog def _add_prefix_to_keys(prefix, key_list): @@ -55,6 +56,11 @@ def to_json_file(self, filepath): with open(filepath, "w") as file: json.dump(self, file, indent=4) + @classmethod + def from_holog_file(cls, filepath): + holog_mds = open_holog(filepath) + return cls(holog_mds.root.attrs["holog_obs_dict"]) + @classmethod def create_from_ms_info( cls, From 20f6d5408c755649a51e6e2fd76c120864c90dca Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 27 Jan 2026 10:53:39 -0700 Subject: [PATCH 048/295] Added append functionality to extract_holog. --- src/astrohack/extract_holog_2.py | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/astrohack/extract_holog_2.py b/src/astrohack/extract_holog_2.py index 6c99d044..ff47140a 100644 --- a/src/astrohack/extract_holog_2.py +++ b/src/astrohack/extract_holog_2.py @@ -14,7 +14,7 @@ from rich.console import Console from rich.table import Table -from astrohack import open_pointing +from astrohack import open_pointing, open_holog from astrohack.utils.file import ( overwrite_file, @@ -49,6 +49,7 @@ def extract_holog( pointing_interpolation_method: str = "linear", parallel: bool = False, overwrite: bool = False, + append: bool = False, ) -> Union[AstrohackHologFile, None]: """ Extract holography and optionally pointing data, from measurement set. Creates holography output file. @@ -111,6 +112,10 @@ def extract_holog( :param overwrite: Boolean for whether to overwrite current holog.zarr and point.zarr files, defaults to False. :type overwrite: bool, optional + :param append: Should data be appended to an existing holog file on disk, append and overwrite cannot be both true\ + defaults to False. + :type append: bool, optional + :return: Holography holog object. :rtype: AstrohackHologFile @@ -212,9 +217,8 @@ def extract_holog( f'File {extract_holog_params["ms_name"]} does not exists.' ) - overwrite_file( - extract_holog_params["holog_name"], extract_holog_params["overwrite"] - ) + if append and overwrite: + raise RuntimeError("Append and overwrite cannot be both set to True.") pnt_mds = open_pointing(point_name) @@ -223,9 +227,16 @@ def extract_holog( ) # print_dict_types(looping_dict, show_values=True) - holog_mds = AstrohackHologFile.create_from_input_parameters( - holog_name, extract_holog_params.copy() - ) + if append: + holog_mds = open_holog(holog_name) + holog_mds.root.attrs["input_parameters"] = extract_holog_params.copy() + else: + overwrite_file( + extract_holog_params["holog_name"], extract_holog_params["overwrite"] + ) + holog_mds = AstrohackHologFile.create_from_input_parameters( + holog_name, extract_holog_params.copy() + ) extract_holog_params["pnt_mds"] = pnt_mds From 65ec5f04a88637d8bd2a3add39f2ede722f0a438 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 27 Jan 2026 10:59:16 -0700 Subject: [PATCH 049/295] cosmetics --- src/astrohack/core/holog_obs_dict.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/astrohack/core/holog_obs_dict.py b/src/astrohack/core/holog_obs_dict.py index 22f4c612..c7e23bd0 100644 --- a/src/astrohack/core/holog_obs_dict.py +++ b/src/astrohack/core/holog_obs_dict.py @@ -40,7 +40,6 @@ def print(self, style: str = "static"): from IPython.display import JSON return JSON(self) - else: console = Console() console.log(self, log_locals=False) From 42910f99b8fa926e3bf8ba74fb2ed91e5ea7a5be Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 27 Jan 2026 11:01:24 -0700 Subject: [PATCH 050/295] Black compliance --- docs/tutorials/run-notebooks.py | 6 ++---- src/astrohack/antenna/panel_fitting.py | 1 - src/astrohack/holog.py | 1 - tests/unit/antenna_classes/test_class_antenna_surface.py | 1 - 4 files changed, 2 insertions(+), 7 deletions(-) diff --git a/docs/tutorials/run-notebooks.py b/docs/tutorials/run-notebooks.py index 953a9ef6..62335286 100644 --- a/docs/tutorials/run-notebooks.py +++ b/docs/tutorials/run-notebooks.py @@ -11,10 +11,8 @@ start = time.time() # Parse args -parser = argparse.ArgumentParser( - description="Runs a set of Jupyter \ - notebooks." -) +parser = argparse.ArgumentParser(description="Runs a set of Jupyter \ + notebooks.") file_text = """ Notebook file(s) to be run, e.g. '*.ipynb' (default), 'my_nb1.ipynb', 'my_nb1.ipynb my_nb2.ipynb', 'my_dir/*.ipynb' """ diff --git a/src/astrohack/antenna/panel_fitting.py b/src/astrohack/antenna/panel_fitting.py index fc05fcab..f2b323ca 100644 --- a/src/astrohack/antenna/panel_fitting.py +++ b/src/astrohack/antenna/panel_fitting.py @@ -5,7 +5,6 @@ from astrohack.utils import gauss_elimination, least_squares_jit - ################################### # General purpose # ################################### diff --git a/src/astrohack/holog.py b/src/astrohack/holog.py index 666f25b8..a5e6b3e3 100644 --- a/src/astrohack/holog.py +++ b/src/astrohack/holog.py @@ -15,7 +15,6 @@ from astrohack.utils.text import get_default_file_name from astrohack.io.mds import AstrohackImageFile - Array = NewType("Array", Union[np.array, List[int], List[float]]) diff --git a/tests/unit/antenna_classes/test_class_antenna_surface.py b/tests/unit/antenna_classes/test_class_antenna_surface.py index ced70d83..a214f701 100644 --- a/tests/unit/antenna_classes/test_class_antenna_surface.py +++ b/tests/unit/antenna_classes/test_class_antenna_surface.py @@ -8,7 +8,6 @@ import shutil import xarray as xr - datafolder = "paneldata/" From 4c51fb47cc3ac921bc9f0f289ebff285b2f82310 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Mon, 2 Feb 2026 15:30:41 -0700 Subject: [PATCH 051/295] Added method to plot pointing in time for point_mds. --- src/astrohack/core/extract_pointing_2.py | 138 ++++++++++++++++++++++- src/astrohack/io/point_mds.py | 34 ++++++ 2 files changed, 166 insertions(+), 6 deletions(-) diff --git a/src/astrohack/core/extract_pointing_2.py b/src/astrohack/core/extract_pointing_2.py index 420c5626..71ae53e8 100644 --- a/src/astrohack/core/extract_pointing_2.py +++ b/src/astrohack/core/extract_pointing_2.py @@ -13,12 +13,12 @@ from astrohack.utils import ( compute_antenna_baseline_distance_matrix_dict, - print_dict_types, + param_to_list, + convert_unit, ) from astrohack.utils.conversion import convert_dict_from_numba -from astrohack.utils.graph import compute_graph_to_mds_tree from astrohack.utils.tools import get_valid_state_ids -from astrohack.io.point_mds import AstrohackPointFile +from astrohack.visualization import create_figure_and_axes, close_figure def extract_pointing_preprocessing(input_params): @@ -28,7 +28,7 @@ def extract_pointing_preprocessing(input_params): input_params(dict): extract_pointing parameters Returns: - AstrohackPointFile: point mds file + tuple Containing execution parameters """ ms_name = input_params["ms_name"] @@ -166,7 +166,7 @@ def make_ant_pnt_chunk(pnt_params, output_mds): tb.close() table_obj.close() - evaluate_time_samping(direction_time, ant_name) + _evaluate_time_samping(direction_time, ant_name) pnt_xds = xr.Dataset() coords = {"time": direction_time} @@ -287,6 +287,132 @@ def make_ant_pnt_chunk(pnt_params, output_mds): ) +def _create_pointing_figure(input_params): + y_labels = ["azimuth", "elevation"] + fig, axes = create_figure_and_axes(input_params["figure_size"], [2, 1]) + return fig, axes, y_labels + + +def _finalize_pointing_figure( + input_params, target_column, ant_label, y_labels, axes, fig +): + title = f"Pointing [{target_column}] data for: {ant_label}" + filename = f"{input_params['destination']}/point_{target_column.lower()}_" + if len(ant_label.split(",")) > 1: + filename += "combined.png" + else: + filename += f"ant_{ant_label}.png" + for i_coord, y_label in enumerate(y_labels): + axes[i_coord].set_ylabel( + f"{y_label.capitalize()} [{input_params["azel_unit"]}]" + ) + if y_label == "Azimuth": + + if input_params["az_scale"] is not None: + axes[i_coord].set_ylim(input_params["az_scale"]) + + else: + if input_params["el_scale"] is not None: + axes[i_coord].set_ylim(input_params["el_scale"]) + if input_params["time_scale"] is not None: + axes[i_coord].set_xlim(input_params["time_scale"]) + axes[i_coord].set_xlabel( + f"Time Since Observation start [{input_params["time_unit"]}]" + ) + axes[i_coord].legend() + close_figure(fig, title, filename, input_params["dpi"], input_params["display"]) + + +def _plot_one_pnt_xds( + time_fac, ang_fac, ant_name, pnt_xds, target_column, y_labels, axes +): + time_ax = pnt_xds.coords["time"].values + # Set time from obs start + time_ax -= time_ax[0] + plot_data = pnt_xds[target_column].values + for i_coord, y_label in enumerate(y_labels): + axes[i_coord].plot( + time_fac * time_ax, + ang_fac * plot_data[:, i_coord], + label=ant_name, + ls="", + marker="o", + ms=5, + ) + + +def _get_plot_configuration(input_params, point_mds): + ant_list = param_to_list(input_params["ant"], point_mds, "ant") + time_fac = convert_unit("sec", input_params["time_unit"], "time") + ang_fac = convert_unit("rad", input_params["azel_unit"], "trigonometric") + target_column = input_params["pointing_key"].upper() + return ant_list, time_fac, ang_fac, target_column + + +def plot_pointing_in_time_separately(input_params, point_mds): + ant_list, time_fac, ang_fac, target_column = _get_plot_configuration( + input_params, point_mds + ) + + n_use_ants = 0 + for ant_key in ant_list: + ant_name = ant_key.split("_")[1] + if ant_key in point_mds.keys(): + n_use_ants = n_use_ants + 1 + fig, axes, y_labels = _create_pointing_figure(input_params) + _plot_one_pnt_xds( + time_fac, + ang_fac, + ant_name, + point_mds[ant_key].dataset, + target_column, + y_labels, + axes, + ) + _finalize_pointing_figure( + input_params, target_column, ant_name, y_labels, axes, fig + ) + else: + logger.warning(f"Antenna {ant_name} not found in dataset") + + if n_use_ants <= 0: + logger.warning(f"No valid antennas selected, no plot produced.") + return + + +def plot_pointing_in_time_together(input_params, point_mds): + ant_list, time_fac, ang_fac, target_column = _get_plot_configuration( + input_params, point_mds + ) + fig, axes, y_labels = _create_pointing_figure(input_params) + + n_use_ants = 0 + for ant_key in ant_list: + ant_name = ant_key.split("_")[1] + if ant_key in point_mds.keys(): + n_use_ants = n_use_ants + 1 + _plot_one_pnt_xds( + time_fac, + ang_fac, + ant_name, + point_mds[ant_key].dataset, + target_column, + y_labels, + axes, + ) + else: + logger.warning(f"Antenna {ant_name} not found in dataset") + + if n_use_ants > 0: + simple_ant_list = [ant_key.split("_")[1] for ant_key in ant_list] + _finalize_pointing_figure( + input_params, target_column, ", ".join(simple_ant_list), y_labels, axes, fig + ) + else: + logger.warning(f"No valid antennas selected, no plot produced.") + return + + def _extract_scan_time_dict(time, scan_ids, state_ids, ddi_ids, mapping_state_ids): """ [ddi][scan][start, stop] @@ -362,7 +488,7 @@ def _extract_scan_time_dict_jit(time, scan_ids, state_ids, ddi_ids, mapping_stat return scan_time_dict -def evaluate_time_samping( +def _evaluate_time_samping( time_sampling, data_label, threshold=0.01, expected_interval=0.1 ): bin_sz = expected_interval / 4 diff --git a/src/astrohack/io/point_mds.py b/src/astrohack/io/point_mds.py index ff0455c2..603ed663 100644 --- a/src/astrohack/io/point_mds.py +++ b/src/astrohack/io/point_mds.py @@ -1,3 +1,12 @@ +import numpy as np +import pathlib + +from typing import Union, List, Tuple + +from astrohack.core.extract_pointing_2 import ( + plot_pointing_in_time_together, + plot_pointing_in_time_separately, +) from astrohack.io.base_mds import AstrohackBaseFile @@ -13,3 +22,28 @@ def __init__(self, file: str): :rtype: AstrohackPointFile """ super().__init__(file=file) + + def plot_pointing_in_time( + self, + destination: str, + ant: Union[str, List[str]] = "all", + pointing_key: str = "TARGET", + plot_antennas_separately: bool = False, + azel_unit: str = "deg", + time_unit: str = "hour", + az_scale: list[float] = None, + el_scale: list[float] = None, + time_scale: list[float] = None, + figure_size: Union[Tuple, List[float], np.array] = (5.0, 6.4), + display: bool = False, + dpi: int = 300, + ): + + pathlib.Path(destination).mkdir(exist_ok=True) + input_params = locals() + + if plot_antennas_separately: + plot_pointing_in_time_separately(input_params, self) + else: + plot_pointing_in_time_together(input_params, self) + return From c4142d33e4d5c07b0f878cbd8f93eb387a9d11f7 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Mon, 2 Feb 2026 15:37:09 -0700 Subject: [PATCH 052/295] Renamed array configuration plotting function in locit to avoid confusion. --- src/astrohack/core/extract_locit.py | 2 +- src/astrohack/io/locit_mds.py | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/astrohack/core/extract_locit.py b/src/astrohack/core/extract_locit.py index 77dd199d..5efcb593 100644 --- a/src/astrohack/core/extract_locit.py +++ b/src/astrohack/core/extract_locit.py @@ -409,7 +409,7 @@ def plot_source_table( return -def plot_array_configuration(parm_dict, root_tree): +def plot_array_configuration_locit(parm_dict, root_tree): """backend for plotting array configuration Args: diff --git a/src/astrohack/io/locit_mds.py b/src/astrohack/io/locit_mds.py index d0660d3d..c1bc3a07 100644 --- a/src/astrohack/io/locit_mds.py +++ b/src/astrohack/io/locit_mds.py @@ -6,7 +6,10 @@ import toolviper.utils.parameter from astrohack.antenna import get_proper_telescope -from astrohack.core.extract_locit import plot_source_table, plot_array_configuration +from astrohack.core.extract_locit import ( + plot_source_table, + plot_array_configuration_locit, +) from astrohack.io.base_mds import AstrohackBaseFile from astrohack.utils import ( create_pretty_table, @@ -246,5 +249,5 @@ def plot_array_configuration( """ param_dict = locals() pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) - plot_array_configuration(param_dict, self.root) + plot_array_configuration_locit(param_dict, self.root) return From 3d14abf8d87efdd1e1278fff34cb9b88df029d73 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Mon, 2 Feb 2026 15:46:40 -0700 Subject: [PATCH 053/295] Telescope name is now fetched by extract_pointing and stored in the point_mds. --- src/astrohack/core/extract_holog_2.py | 9 +-------- src/astrohack/core/extract_pointing_2.py | 16 ++++++++++++++++ src/astrohack/extract_pointing_2.py | 1 + 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/astrohack/core/extract_holog_2.py b/src/astrohack/core/extract_holog_2.py index 936d856b..218fd0ee 100644 --- a/src/astrohack/core/extract_holog_2.py +++ b/src/astrohack/core/extract_holog_2.py @@ -92,14 +92,7 @@ def extract_holog_preprocessing(extract_holog_params, pnt_mds): ack=False, ) - obs_ctb = ctables.table( - os.path.join(extract_holog_params["ms_name"], "OBSERVATION"), - readonly=True, - lockoptions={"option": "usernoread"}, - ack=False, - ) - - telescope_name = obs_ctb.getcol("TELESCOPE_NAME")[0] + telescope_name = pnt_mds.root.attrs["telescope_name"] # start_time_unix = obs_ctb.getcol('TIME_RANGE')[0][0] - 3506716800.0 # time = Time(start_time_unix, format='unix').jyear diff --git a/src/astrohack/core/extract_pointing_2.py b/src/astrohack/core/extract_pointing_2.py index 71ae53e8..56ecea45 100644 --- a/src/astrohack/core/extract_pointing_2.py +++ b/src/astrohack/core/extract_pointing_2.py @@ -84,6 +84,16 @@ def extract_pointing_preprocessing(input_params): # scan intent (with subscan intent) is stored in the OBS_MODE column of the STATE sub-table. obs_modes = ctb.getcol("OBS_MODE") ctb.close() + + obs_ctb = ctables.table( + os.path.join(ms_name, "OBSERVATION"), + readonly=True, + lockoptions={"option": "usernoread"}, + ack=False, + ) + + telescope_name = obs_ctb.getcol("TELESCOPE_NAME")[0] + mapping_state_ids = get_valid_state_ids(obs_modes) mapping_state_ids = np.array(mapping_state_ids) @@ -103,6 +113,7 @@ def extract_pointing_preprocessing(input_params): "antenna_names": antenna_names, "antenna_ids": antenna_ids, "antenna_stations": antenna_stations, + "telescope_name": telescope_name, } ant_dist_matrix = compute_antenna_baseline_distance_matrix_dict( @@ -413,6 +424,11 @@ def plot_pointing_in_time_together(input_params, point_mds): return +def plot_array_configuration_pointing(input_params, point_mds): + + return + + def _extract_scan_time_dict(time, scan_ids, state_ids, ddi_ids, mapping_state_ids): """ [ddi][scan][start, stop] diff --git a/src/astrohack/extract_pointing_2.py b/src/astrohack/extract_pointing_2.py index 633594c2..a347189d 100644 --- a/src/astrohack/extract_pointing_2.py +++ b/src/astrohack/extract_pointing_2.py @@ -94,6 +94,7 @@ def extract_pointing( point_mds.root.attrs["antenna_names"] = pnt_params.pop("antenna_names") point_mds.root.attrs["antenna_ids"] = pnt_params.pop("antenna_ids") point_mds.root.attrs["antenna_stations"] = pnt_params.pop("antenna_stations") + point_mds.root.attrs["telescope_name"] = pnt_params.pop("telescope_name") executed_graph = compute_graph_to_mds_tree( looping_dict, From c65d4f5a9fdb1c430931e11350911ab6d17aba52 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Mon, 2 Feb 2026 16:03:23 -0700 Subject: [PATCH 054/295] Added more complete antenna_info to each antenna xds in the point_mds. --- src/astrohack/core/extract_pointing_2.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/astrohack/core/extract_pointing_2.py b/src/astrohack/core/extract_pointing_2.py index 56ecea45..d7e32cb4 100644 --- a/src/astrohack/core/extract_pointing_2.py +++ b/src/astrohack/core/extract_pointing_2.py @@ -287,9 +287,17 @@ def make_ant_pnt_chunk(pnt_params, output_mds): pnt_xds.attrs["mapping_scans_obs_dict"] = [mapping_scans_obs_dict] ############### - pnt_xds.attrs["ant_name"] = ant_name - pnt_xds.attrs["ant_pos"] = ant_pos - pnt_xds.attrs["ant_station"] = ant_station + ant_rad = np.sqrt(ant_pos[0] ** 2 + ant_pos[1] ** 2 + ant_pos[2] ** 2) + ant_lat = np.arcsin(ant_pos[2] / ant_rad) + ant_lon = -np.arccos(ant_pos[0] / (ant_rad * np.cos(ant_lat))) + + pnt_xds.attrs["antenna_info"] = { + "name": ant_name, + "station": ant_station, + "longitude": ant_lon, + "latitude": ant_lat, + "radius": ant_rad, + } output_mds.add_node_to_tree( xr.DataTree(dataset=pnt_xds, name=ant_key), @@ -424,11 +432,6 @@ def plot_pointing_in_time_together(input_params, point_mds): return -def plot_array_configuration_pointing(input_params, point_mds): - - return - - def _extract_scan_time_dict(time, scan_ids, state_ids, ddi_ids, mapping_state_ids): """ [ddi][scan][start, stop] From 24af59e7c9f314d1eab9ccc3e73a58d283872fcd Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Mon, 2 Feb 2026 16:08:45 -0700 Subject: [PATCH 055/295] Implemented a common array configuration plotting tool. --- src/astrohack/core/extract_locit.py | 83 +--------------------- src/astrohack/io/locit_mds.py | 4 +- src/astrohack/io/point_mds.py | 24 ++++++- src/astrohack/visualization/diagnostics.py | 75 +++++++++++++++++++ 4 files changed, 99 insertions(+), 87 deletions(-) diff --git a/src/astrohack/core/extract_locit.py b/src/astrohack/core/extract_locit.py index 5efcb593..6e505258 100644 --- a/src/astrohack/core/extract_locit.py +++ b/src/astrohack/core/extract_locit.py @@ -8,15 +8,11 @@ from astropy.coordinates import SkyCoord, CIRS from astropy.time import Time -from astrohack.antenna.telescope import get_proper_telescope from astrohack.utils.conversion import convert_unit, casa_time_to_mjd -from astrohack.utils.constants import figsize, twopi, fontsize -from astrohack.utils.tools import get_telescope_lat_lon_rad -from astrohack.utils.algorithms import compute_antenna_relative_off +from astrohack.utils.constants import figsize, twopi from astrohack.visualization.plot_tools import ( create_figure_and_axes, close_figure, - plot_boxes_limits_and_labels, scatter_plot, ) @@ -407,80 +403,3 @@ def plot_source_table( close_figure(fig, title, filename, dpi, display) return - - -def plot_array_configuration_locit(parm_dict, root_tree): - """backend for plotting array configuration - - Args: - parm_dict: Parameter dictionary crafted by the calling function - root_tree: Root of the Xarray DataTree in the locit_mds - """ - telescope_name = root_tree.attrs["telescope_name"] - telescope = get_proper_telescope(telescope_name) - stations = parm_dict["stations"] - display = parm_dict["display"] - figure_size = parm_dict["figure_size"] - dpi = parm_dict["dpi"] - filename = parm_dict["destination"] + "/locit_antenna_positions.png" - length_unit = parm_dict["unit"] - box_size = parm_dict["box_size"] # In user input unit - plot_zoff = parm_dict["zoff"] - - fig, axes = create_figure_and_axes(figure_size, [1, 2], default_figsize=[10, 5]) - - len_fac = convert_unit("m", length_unit, "length") - - inner_ax = axes[1] - outer_ax = axes[0] - - tel_lon, tel_lat, tel_rad = get_telescope_lat_lon_rad(telescope) - - for ant_xdtree in root_tree.values(): - ant_info = ant_xdtree.attrs["antenna_info"] - ew_off, ns_off, el_off, _ = compute_antenna_relative_off( - ant_info, tel_lon, tel_lat, tel_rad, len_fac - ) - text = f' {ant_info["name"]}' - if stations: - text += f'@{ant_info["station"]}' - if plot_zoff: - text += f" {el_off:.1f} {length_unit}" - plot_antenna_position(outer_ax, inner_ax, ew_off, ns_off, text, box_size) - - # axes labels - xlabel = f"East [{length_unit}]" - ylabel = f"North [{length_unit}]" - - plot_boxes_limits_and_labels( - outer_ax, inner_ax, xlabel, ylabel, box_size, "Outer array", "Inner array" - ) - - title = f"{len(root_tree.keys())} antennas during observation" - close_figure(fig, title, filename, dpi, display) - return - - -def plot_antenna_position( - outerax, innerax, xpos, ypos, text, box_size, marker="+", color="black" -): - """ - Plot an antenna to either the inner or outer array boxes - Args: - outerax: Plotting axis for the outer array box - innerax: Plotting axis for the inner array box - xpos: X antenna position (east-west) - ypos: Y antenna position (north-south) - text: Antenna label - box_size: Size of the inner array box - marker: Antenna position marker - color: Color for the antenna position marker - """ - half_box = box_size / 2 - if abs(xpos) > half_box or abs(ypos) > half_box: - outerax.plot(xpos, ypos, marker=marker, color=color) - outerax.text(xpos, ypos, text, fontsize=fontsize, ha="left", va="center") - else: - outerax.plot(xpos, ypos, marker=marker, color=color) - innerax.plot(xpos, ypos, marker=marker, color=color) - innerax.text(xpos, ypos, text, fontsize=fontsize, ha="left", va="center") diff --git a/src/astrohack/io/locit_mds.py b/src/astrohack/io/locit_mds.py index c1bc3a07..b5ef140f 100644 --- a/src/astrohack/io/locit_mds.py +++ b/src/astrohack/io/locit_mds.py @@ -8,7 +8,6 @@ from astrohack.antenna import get_proper_telescope from astrohack.core.extract_locit import ( plot_source_table, - plot_array_configuration_locit, ) from astrohack.io.base_mds import AstrohackBaseFile from astrohack.utils import ( @@ -20,6 +19,7 @@ ) from astrohack.utils.tools import get_telescope_lat_lon_rad from astrohack.utils.validation import custom_unit_checker +from astrohack.visualization.diagnostics import plot_array_configuration class AstrohackLocitFile(AstrohackBaseFile): @@ -249,5 +249,5 @@ def plot_array_configuration( """ param_dict = locals() pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) - plot_array_configuration_locit(param_dict, self.root) + plot_array_configuration(param_dict, self.root, "locit") return diff --git a/src/astrohack/io/point_mds.py b/src/astrohack/io/point_mds.py index 603ed663..15c1bace 100644 --- a/src/astrohack/io/point_mds.py +++ b/src/astrohack/io/point_mds.py @@ -3,6 +3,8 @@ from typing import Union, List, Tuple +from astrohack.visualization.diagnostics import plot_array_configuration + from astrohack.core.extract_pointing_2 import ( plot_pointing_in_time_together, plot_pointing_in_time_separately, @@ -31,9 +33,9 @@ def plot_pointing_in_time( plot_antennas_separately: bool = False, azel_unit: str = "deg", time_unit: str = "hour", - az_scale: list[float] = None, - el_scale: list[float] = None, - time_scale: list[float] = None, + az_scale: Union[Tuple, List[float], np.array] = None, + el_scale: Union[Tuple, List[float], np.array] = None, + time_scale: Union[Tuple, List[float], np.array] = None, figure_size: Union[Tuple, List[float], np.array] = (5.0, 6.4), display: bool = False, dpi: int = 300, @@ -47,3 +49,19 @@ def plot_pointing_in_time( else: plot_pointing_in_time_together(input_params, self) return + + def plot_array_configuration( + self, + destination: str, + stations: bool = True, + zoff: bool = False, + unit: str = "m", + box_size: Union[int, float] = 5000, + figure_size: Union[Tuple, List[float], np.array] = None, + display: bool = False, + dpi: int = 300, + ): + + pathlib.Path(destination).mkdir(exist_ok=True) + input_params = locals() + plot_array_configuration(input_params, self.root, "point") diff --git a/src/astrohack/visualization/diagnostics.py b/src/astrohack/visualization/diagnostics.py index ce242514..9675278d 100644 --- a/src/astrohack/visualization/diagnostics.py +++ b/src/astrohack/visualization/diagnostics.py @@ -12,11 +12,15 @@ ) from astrohack.utils.constants import fontsize, markersize from astrohack.utils.text import add_prefix +from astrohack.utils.tools import get_telescope_lat_lon_rad +from astrohack.utils.algorithms import compute_antenna_relative_off +from astrohack.antenna import get_proper_telescope from astrohack.visualization.plot_tools import ( create_figure_and_axes, close_figure, scatter_plot, simple_imshow_map_plot, + plot_boxes_limits_and_labels, ) @@ -678,3 +682,74 @@ def _plot_zernike_aperture_model( parm_dict["display"], tight_layout=True, ) + + +def plot_array_configuration(input_dict, xdtree, caller): + telescope_name = xdtree.attrs["telescope_name"] + telescope = get_proper_telescope(telescope_name) + stations = input_dict["stations"] + display = input_dict["display"] + figure_size = input_dict["figure_size"] + dpi = input_dict["dpi"] + filename = f"{input_dict["destination"]}/{caller}_array_configuration.png" + length_unit = input_dict["unit"] + box_size = input_dict["box_size"] # In user input unit + plot_zoff = input_dict["zoff"] + + fig, axes = create_figure_and_axes(figure_size, [1, 2], default_figsize=[10, 5]) + + len_fac = convert_unit("m", length_unit, "length") + + inner_ax = axes[1] + outer_ax = axes[0] + + tel_lon, tel_lat, tel_rad = get_telescope_lat_lon_rad(telescope) + + for ant_xdtree in xdtree.values(): + ant_info = ant_xdtree.attrs["antenna_info"] + ew_off, ns_off, el_off, _ = compute_antenna_relative_off( + ant_info, tel_lon, tel_lat, tel_rad, len_fac + ) + text = f' {ant_info["name"]}' + if stations: + text += f'@{ant_info["station"]}' + if plot_zoff: + text += f" {el_off:.1f} {length_unit}" + plot_one_antenna_position(outer_ax, inner_ax, ew_off, ns_off, text, box_size) + + # axes labels + xlabel = f"East [{length_unit}]" + ylabel = f"North [{length_unit}]" + + plot_boxes_limits_and_labels( + outer_ax, inner_ax, xlabel, ylabel, box_size, "Outer array", "Inner array" + ) + + title = f"{len(xdtree.keys())} antennas during observation" + close_figure(fig, title, filename, dpi, display) + return + + +def plot_one_antenna_position( + outerax, innerax, xpos, ypos, text, box_size, marker="+", color="black" +): + """ + Plot an antenna to either the inner or outer array boxes + Args: + outerax: Plotting axis for the outer array box + innerax: Plotting axis for the inner array box + xpos: X antenna position (east-west) + ypos: Y antenna position (north-south) + text: Antenna label + box_size: Size of the inner array box + marker: Antenna position marker + color: Color for the antenna position marker + """ + half_box = box_size / 2 + if abs(xpos) > half_box or abs(ypos) > half_box: + outerax.plot(xpos, ypos, marker=marker, color=color) + outerax.text(xpos, ypos, text, fontsize=fontsize, ha="left", va="center") + else: + outerax.plot(xpos, ypos, marker=marker, color=color) + innerax.plot(xpos, ypos, marker=marker, color=color) + innerax.text(xpos, ypos, text, fontsize=fontsize, ha="left", va="center") From 63142f1124604ffcf1d8f15f02ed9945d1aaa2be Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Mon, 2 Feb 2026 16:12:27 -0700 Subject: [PATCH 056/295] Fixed import --- src/astrohack/core/locit.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/astrohack/core/locit.py b/src/astrohack/core/locit.py index 5201a358..8b0de2ee 100644 --- a/src/astrohack/core/locit.py +++ b/src/astrohack/core/locit.py @@ -14,7 +14,7 @@ compute_antenna_relative_off, ) -from astrohack.core.extract_locit import plot_antenna_position +from astrohack.visualization.diagnostics import plot_one_antenna_position from astrohack.utils.conversion import convert_unit, hadec_to_elevation from astrohack.utils.algorithms import least_squares, phase_wrapping from astrohack.utils.constants import * @@ -158,8 +158,6 @@ def locit_difference_chunk(locit_parms, output_mds): locit_parms: the locit parameter dictionary output_mds: Output mds file onto which to add results - Returns: - xds save to disk in the .zarr format """ ant_xdt = locit_parms["xdt_data"] antenna_info = ant_xdt.attrs["antenna_info"] @@ -1297,13 +1295,13 @@ def plot_antenna_position_corrections_worker( text = " " + antenna["name"] if antenna["name"] == ref_ant: text += "*" - plot_antenna_position( + plot_one_antenna_position( xy_whole, xy_inner, ew_off, ns_off, text, box_size, marker="+" ) add_antenna_position_corrections_to_plot( xy_whole, xy_inner, ew_off, ns_off, corrections[0], corrections[1], box_size ) - plot_antenna_position( + plot_one_antenna_position( z_whole, z_inner, ew_off, ns_off, text, box_size, marker="+" ) add_antenna_position_corrections_to_plot( From 38f24807cfa2f8911aba4ba0b66a684e1e36993e Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Mon, 2 Feb 2026 16:23:00 -0700 Subject: [PATCH 057/295] Removed forgotten debug print. --- src/astrohack/core/locit.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/astrohack/core/locit.py b/src/astrohack/core/locit.py index 8b0de2ee..f7558311 100644 --- a/src/astrohack/core/locit.py +++ b/src/astrohack/core/locit.py @@ -688,8 +688,6 @@ def _create_output_xds( fit_rate = locit_parms["fit_delay_rate"] error = np.sqrt(variance) - # print(delays) - output_xds = xr.Dataset() output_xds.attrs["polarization"] = locit_parms["polarization"] output_xds.attrs["frequency"] = frequency @@ -723,14 +721,11 @@ def _create_output_xds( output_xds["ELEVATION"] = xr.DataArray(coordinates[2, :], dims=["time"]) output_xds["LST"] = xr.DataArray(lst, dims=["time"]) - # print(output_xds["DELAYS"].values) - if ddi_key is None: xdt_name = f"{ant_key}" else: xdt_name = f"{ant_key}-{ddi_key}" output_xdt = xr.DataTree(dataset=output_xds.assign_coords(coords), name=xdt_name) - print(output_xds["DELAYS"].values) return output_xdt From 14fba8de7a7460fcb8f7eb22c5849ee949a30792 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Mon, 2 Feb 2026 16:33:16 -0700 Subject: [PATCH 058/295] Revised documentation. --- src/astrohack/io/locit_mds.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/astrohack/io/locit_mds.py b/src/astrohack/io/locit_mds.py index b5ef140f..fc743fce 100644 --- a/src/astrohack/io/locit_mds.py +++ b/src/astrohack/io/locit_mds.py @@ -244,8 +244,7 @@ def plot_array_configuration( :type dpi: int, optional .. _Description: - - + Plot the array configuration from the antenna positions. """ param_dict = locals() pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) From 25a707c3b731eb9dac063f5389393cba654b4306 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Mon, 2 Feb 2026 16:34:04 -0700 Subject: [PATCH 059/295] Added documentation to point_mds plotting functions. --- src/astrohack/io/point_mds.py | 77 +++++++++++++++++++++++++++++++++-- 1 file changed, 74 insertions(+), 3 deletions(-) diff --git a/src/astrohack/io/point_mds.py b/src/astrohack/io/point_mds.py index 15c1bace..b4a41b45 100644 --- a/src/astrohack/io/point_mds.py +++ b/src/astrohack/io/point_mds.py @@ -29,7 +29,7 @@ def plot_pointing_in_time( self, destination: str, ant: Union[str, List[str]] = "all", - pointing_key: str = "TARGET", + pointing_key: str = "DIRECTIONAL_COSINES", plot_antennas_separately: bool = False, azel_unit: str = "deg", time_unit: str = "hour", @@ -39,7 +39,49 @@ def plot_pointing_in_time( figure_size: Union[Tuple, List[float], np.array] = (5.0, 6.4), display: bool = False, dpi: int = 300, - ): + ) -> None: + """Plot Pointing for antennas in time. + + :param destination: Name of the destination folder to contain plot(s) + :type destination: str + + :param ant: Antenna(s) to plot, default is "all" + :type ant: str, list, optional + + :param pointing_key: Which xds pointing data key to plot, defaults to "DIRECTIONAL_COSINES" + :type pointing_key: str, optional + + :param plot_antennas_separately: Create an individual plot file for each antenna? + :type plot_antennas_separately: bool, optional + + :param azel_unit: Unit for Azimuth and Elevation in the plot(s), valid values are trigonometric units, default \ + is deg + :type azel_unit: str, optional + + :param time_unit: Unit for time in the plot(s), valid values are time units, default is hour + :type time_unit: str, optional + + :param az_scale: Azimuth plot limits, defaults to all Azimuths present when None. + :type az_scale: Union[Tuple, List[float], np.array], optional + + :param el_scale: Elevation plot limits, defaults to all Elevations present when None. + :type el_scale: Union[Tuple, List[float], np.array], optional + + :param time_scale: Time plot limits, defaults to all times present when None + :type time_scale: Union[Tuple, List[float], np.array], optional + + :param display: Display plot(s) inline or suppress, defaults to True + :type display: bool, optional + + :param figure_size: 2 element array/list/tuple with the plot sizes in inches + :type figure_size: numpy.ndarray, list, tuple, optional + + :param dpi: dots per inch to be used in plots, default is 300 + :type dpi: int, optional + + .. _Description: + Plot antenna pointing info in time together in one plot, or individually for each antenna. + """ pathlib.Path(destination).mkdir(exist_ok=True) input_params = locals() @@ -60,7 +102,36 @@ def plot_array_configuration( figure_size: Union[Tuple, List[float], np.array] = None, display: bool = False, dpi: int = 300, - ): + ) -> None: + """Plot antenna positions. + + :param destination: Name of the destination folder to contain plot + :type destination: str + + :param stations: Add station names to the plot, defaults to True + :type stations: bool, optional + + :param zoff: Add Elevation offsets to the plots, defaults to False + :type zoff: bool, optional + + :param unit: Unit for the plot, valid values are length units, default is km + :type unit: str, optional + + :param box_size: Size of the box for plotting the inner part of the array in unit, default is 5 km + :type box_size: int, float, optional + + :param display: Display plots inline or suppress, defaults to True + :type display: bool, optional + + :param figure_size: 2 element array/list/tuple with the plot sizes in inches + :type figure_size: numpy.ndarray, list, tuple, optional + + :param dpi: dots per inch to be used in plots, default is 300 + :type dpi: int, optional + + .. _Description: + Plot the array configuration from the antenna positions. + """ pathlib.Path(destination).mkdir(exist_ok=True) input_params = locals() From 9988cc07dd37cbea1efbbfb881aba66a69c279c2 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Mon, 2 Feb 2026 16:42:21 -0700 Subject: [PATCH 060/295] Arguments are now properly passed to dask delayed. --- src/astrohack/utils/graph.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/astrohack/utils/graph.py b/src/astrohack/utils/graph.py index fd6379ff..ab837e30 100644 --- a/src/astrohack/utils/graph.py +++ b/src/astrohack/utils/graph.py @@ -24,7 +24,7 @@ def _construct_xdtree_graph_recursively( else: args = [param_dict, output_mds] if parallel: - delayed_list.append(dask.delayed(chunk_function)(dask.delayed(args))) + delayed_list.append(dask.delayed(chunk_function)(*args)) else: delayed_list.append((chunk_function, args)) else: @@ -78,7 +78,7 @@ def _construct_general_graph_recursively( else: args = [param_dict, output_mds] if parallel: - delayed_list.append(dask.delayed(chunk_function)(dask.delayed(args))) + delayed_list.append(dask.delayed(chunk_function)(*args)) else: delayed_list.append((chunk_function, args)) else: From 7142a9e76486b91405eedceb1eea9961c4bc6f11 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Mon, 2 Feb 2026 17:24:12 -0700 Subject: [PATCH 061/295] copied contents of AstrohackHologFile to holog_mds.py, nothing functional yet. --- src/astrohack/io/holog_mds.py | 232 ++++++++++++++++++++++++++++++++++ 1 file changed, 232 insertions(+) diff --git a/src/astrohack/io/holog_mds.py b/src/astrohack/io/holog_mds.py index daa6c830..108f6278 100644 --- a/src/astrohack/io/holog_mds.py +++ b/src/astrohack/io/holog_mds.py @@ -13,3 +13,235 @@ def __init__(self, file: str): :rtype: AstrohackHologFile """ super().__init__(file=file) + + # @toolviper.utils.parameter.validate(custom_checker=custom_plots_checker) + # def plot_diagnostics( + # self, + # destination: str, + # delta: float = 0.01, + # ant: Union[str, List[str]] = "all", + # ddi: Union[str, int, List[int]] = "all", + # map_id: Union[int, List[int]] = "all", + # complex_split: str = "polar", + # display: bool = False, + # figure_size: Union[Tuple, List[float], np.array] = None, + # dpi: int = 300, + # parallel: bool = False, + # ) -> None: + # """ Plot diagnostic calibration plots from the holography data file. + # + # :param destination: Name of the destination folder to contain diagnostic plots + # :type destination: str + # :param delta: Defines a fraction of cell_size around which to look for peaks., defaults to 0.01 + # :type delta: float, optional + # :param ant: antenna ID to use in subselection, defaults to "all" when None, ex. ea25 + # :type ant: list or str, optional + # :param ddi: data description ID to use in subselection, defaults to "all" when None, ex. 0 + # :type ddi: list or int, optional + # :param map_id: map ID to use in subselection. This relates to which antenna are in the mapping vs. scanning \ + # configuration, defaults to "all" when None, ex. 0 + # :type map_id: list or int, optional + # :param complex_split: How to split complex data, cartesian (real + imaginary) or polar (amplitude + phase), \ + # default is polar + # :type complex_split: str, optional + # :param display: Display plots inline or suppress, defaults to True + # :type display: bool, optional + # :param figure_size: 2 element array/list/tuple with the plot sizes in inches + # :type figure_size: numpy.ndarray, list, tuple, optional + # :param dpi: dots per inch to be used in plots, default is 300 + # :type dpi: int, optional + # :param parallel: Run in parallel, defaults to False + # :type parallel: bool, optional + # + # **Additional Information** + # The visibilities extracted by extract_holog are complex due to the nature of interferometric measurements. To + # ease the visualization of the complex data it can be split into real and imaginary parts (cartesian) or in + # amplitude and phase (polar). + # + # .. rubric:: Available complex splitting possibilities: + # - *cartesian*: Split is done to a real part and an imaginary part in the plots + # - *polar*: Split is done to an amplitude and a phase in the plots + # + # """ + # + # param_dict = locals() + # param_dict["map"] = map_id + # + # pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) + # key_order = ["ddi", "map", "ant"] + # compute_graph(self, calibration_plot_chunk, param_dict, key_order, parallel) + # + # @toolviper.utils.parameter.validate(custom_checker=custom_plots_checker) + # def plot_lm_sky_coverage( + # self, + # destination: str, + # ant: Union[str, List[str]] = "all", + # ddi: Union[str, int, List[int]] = "all", + # map_id: Union[int, List[int]] = "all", + # angle_unit: str = "deg", + # time_unit: str = "hour", + # plot_correlation: Union[str, List[str]] = None, + # complex_split: str = "polar", + # phase_unit: str = "deg", + # display: bool = False, + # figure_size: Union[Tuple, List[float], np.array] = None, + # dpi: int = 300, + # parallel: bool = False, + # ) -> None: + # """ Plot directional cosine coverage. + # + # :param destination: Name of the destination folder to contain plots + # :type destination: str + # :param ant: antenna ID to use in subselection, defaults to "all" when None, ex. ea25 + # :type ant: list or str, optional + # :param ddi: data description ID to use in subselection, defaults to "all" when None, ex. 0 + # :type ddi: list or int, optional + # :param map_id: map ID to use in subselection. This relates to which antenna are in the mapping vs. scanning \ + # configuration, defaults to "all" when None, ex. 0 + # :type map_id: list or int, optional + # :param angle_unit: Unit for L and M axes in plots, default is 'deg'. + # :type angle_unit: str, optional + # :param time_unit: Unit for time axis in plots, default is 'hour'. + # :type time_unit: str, optional + # :param plot_correlation: Which correlation to plot against L and M, default is None (no correlation plots). + # :type plot_correlation: str, list, optional + # :param complex_split: How to split complex data, cartesian (real + imaginary) or polar (amplitude + phase), \ + # default is polar + # :type complex_split: str, optional + # :param phase_unit: Unit for phase in 'polar' plots, default is 'deg'. + # :type phase_unit: str + # :param display: Display plots inline or suppress, defaults to True + # :type display: bool, optional + # :param figure_size: 2 element array/list/tuple with the plot sizes in inches + # :type figure_size: numpy.ndarray, list, tuple, optional + # :param dpi: dots per inch to be used in plots, default is 300 + # :type dpi: int, optional + # :param parallel: Run in parallel, defaults to False + # :type parallel: bool, optional + # + # **Additional Information** + # The visibilities extracted by extract_holog are complex due to the nature of interferometric measurements. To + # ease the visualization of the complex data it can be split into real and imaginary parts (cartesian) or in + # amplitude and phase (polar). + # + # .. rubric:: Available complex splitting possibilities: + # - *cartesian*: Split is done to a real part and an imaginary part in the plots + # - *polar*: Split is done to an amplitude and a phase in the plots + # + # .. rubric:: Plotting correlations: + # - *RR, RL, LR, LL*: Are available for circular systems + # - *XX, XY, YX, YY*: Are available for linear systems + # - *all*: Plot all correlations in dataset + # + # """ + # + # param_dict = locals() + # param_dict["map"] = map_id + # + # pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) + # key_order = ["ddi", "map", "ant"] + # compute_graph(self, plot_lm_coverage, param_dict, key_order, parallel) + # return + # + # @toolviper.utils.parameter.validate(custom_checker=custom_plots_checker) + # def export_to_aips( + # self, + # destination: str, + # ant: Union[str, List[str]] = "all", + # ddi: Union[str, int, List[int]] = "all", + # map_id: Union[int, List[int]] = "all", + # parallel: bool = False, + # ) -> None: + # """ Export data compatible to AIPS's HOLOG task + # + # :param destination: Name of the destination folder to contain SCII files + # :type destination: str + # :param ant: antenna ID to use in subselection, defaults to "all" when None, ex. ea25 + # :type ant: list or str, optional + # :param ddi: data description ID to use in subselection, defaults to "all" when None, ex. 0 + # :type ddi: list or int, optional + # :param map_id: map ID to use in subselection. This relates to which antenna are in the mapping vs. scanning \ + # configuration, defaults to "all" when None, ex. 0 + # :type map_id: list or int, optional + # :param parallel: Run in parallel, defaults to False + # :type parallel: bool, optional + # + # **Additional Information** + # + # This method converts the data for an Antenna mapping to the ASCII format used by AIPS's HOLOG task. + # Currently only stokes I is supported. + # """ + # param_dict = locals() + # param_dict["map"] = map_id + # + # pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) + # key_order = ["ddi", "map", "ant"] + # compute_graph(self, export_to_aips, param_dict, key_order, parallel) + # return + + # @toolviper.utils.parameter.validate(custom_checker=custom_unit_checker) + # def observation_summary( + # self, + # summary_file: str, + # ant: Union[str, List[str]] = "all", + # ddi: Union[str, int, List[int]] = "all", + # map_id: Union[int, List[int]] = "all", + # az_el_key: str = "center", + # phase_center_unit: str = "radec", + # az_el_unit: str = "deg", + # time_format: str = "%d %h %Y, %H:%M:%S", + # tab_size: int = 3, + # print_summary: bool = True, + # parallel: bool = False, + # ) -> None: + # """ Create a Summary of observation information + # + # :param summary_file: Text file to put the observation summary + # :type summary_file: str + # :param ant: antenna ID to use in subselection, defaults to "all" when None, ex. ea25 + # :type ant: list or str, optional + # :param ddi: data description ID to use in subselection, defaults to "all" when None, ex. 0 + # :type ddi: list or int, optional + # :param map_id: map ID to use in subselection. This relates to which antenna are in the mapping vs. scanning \ + # configuration, defaults to "all" when None, ex. 0 + # :type map_id: list or int, optional + # :param az_el_key: What type of Azimuth & Elevation information to print, 'mean', 'median' or 'center', default\ + # is 'center' + # :type az_el_key: str, optional + # :param phase_center_unit: What unit to display phase center coordinates, 'radec' and angle units supported, \ + # default is 'radec' + # :type phase_center_unit: str, optional + # :param az_el_unit: Angle unit used to display Azimuth & Elevation information, default is 'deg' + # :type az_el_unit: str, optional + # :param time_format: datetime time format for the start and end dates of observation, default is \ + # "%d %h %Y, %H:%M:%S" + # :type time_format: str, optional + # :param tab_size: Number of spaces in the tab levels, default is 3 + # :type tab_size: int, optional + # :param print_summary: Print the summary at the end of execution, default is True + # :type print_summary: bool, optional + # :param parallel: Run in parallel, defaults to False + # :type parallel: bool, optional + # + # **Additional Information** + # + # This method produces a summary of the data in the AstrohackHologFile displaying general information, + # spectral information and suggested beam image characteristics. + # """ + # + # param_dict = locals() + # param_dict["map"] = map_id + # key_order = ["ddi", "map", "ant"] + # execution, summary = compute_graph( + # self, + # generate_observation_summary, + # param_dict, + # key_order, + # parallel, + # fetch_returns=True, + # ) + # summary = "".join(summary) + # with open(summary_file, "w") as output_file: + # output_file.write(summary) + # if print_summary: + # print(summary) From a84b61da30f7520d19d8c52a5b385c9aeab53463 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 3 Feb 2026 11:12:11 -0700 Subject: [PATCH 062/295] Plot_array_configuration can now chooses an internal box of 0.2 times the size of the array when no box size is given. --- src/astrohack/visualization/diagnostics.py | 27 +++++++++++++++------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/src/astrohack/visualization/diagnostics.py b/src/astrohack/visualization/diagnostics.py index 9675278d..524b1e0e 100644 --- a/src/astrohack/visualization/diagnostics.py +++ b/src/astrohack/visualization/diagnostics.py @@ -684,7 +684,7 @@ def _plot_zernike_aperture_model( ) -def plot_array_configuration(input_dict, xdtree, caller): +def plot_array_configuration(input_dict, xdtree, caller, box_default_size=0.2): telescope_name = xdtree.attrs["telescope_name"] telescope = get_proper_telescope(telescope_name) stations = input_dict["stations"] @@ -696,15 +696,10 @@ def plot_array_configuration(input_dict, xdtree, caller): box_size = input_dict["box_size"] # In user input unit plot_zoff = input_dict["zoff"] - fig, axes = create_figure_and_axes(figure_size, [1, 2], default_figsize=[10, 5]) - len_fac = convert_unit("m", length_unit, "length") - - inner_ax = axes[1] - outer_ax = axes[0] - tel_lon, tel_lat, tel_rad = get_telescope_lat_lon_rad(telescope) - + ant_offs = [] + ant_texts = [] for ant_xdtree in xdtree.values(): ant_info = ant_xdtree.attrs["antenna_info"] ew_off, ns_off, el_off, _ = compute_antenna_relative_off( @@ -715,6 +710,22 @@ def plot_array_configuration(input_dict, xdtree, caller): text += f'@{ant_info["station"]}' if plot_zoff: text += f" {el_off:.1f} {length_unit}" + ant_offs.append([ew_off, ns_off]) + ant_texts.append(text) + + ant_offs = np.array(ant_offs) + if box_size is None: + ew_min, ew_max = np.min(ant_offs[:, 0]), np.max(ant_offs[:, 0]) + ew_range = ew_max - ew_min + ns_min, ns_max = np.min(ant_offs[:, 1]), np.max(ant_offs[:, 1]) + ns_range = ns_max - ns_min + box_size = box_default_size * np.max([ew_range, ns_range]) + + fig, axes = create_figure_and_axes(figure_size, [1, 2], default_figsize=[10, 5]) + inner_ax = axes[1] + outer_ax = axes[0] + for i_ant, text in enumerate(ant_texts): + ew_off, ns_off = ant_offs[i_ant] plot_one_antenna_position(outer_ax, inner_ax, ew_off, ns_off, text, box_size) # axes labels From 6fd86812afae1b274e2f1c0a2f9ffacf9860c1f6 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 3 Feb 2026 11:14:56 -0700 Subject: [PATCH 063/295] Plot_array_configuration methods for locit and point mdses now have a default of None for the inner array box. --- src/astrohack/config/locit_mds.param.json | 2 +- src/astrohack/io/locit_mds.py | 5 +++-- src/astrohack/io/point_mds.py | 5 +++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/astrohack/config/locit_mds.param.json b/src/astrohack/config/locit_mds.param.json index b44cbeba..80e26756 100644 --- a/src/astrohack/config/locit_mds.param.json +++ b/src/astrohack/config/locit_mds.param.json @@ -66,7 +66,7 @@ "check allowed with": "units.length" }, "box_size":{ - "nullable": false, + "nullable": true, "required": false, "type": ["int", "float"], "min": 0 diff --git a/src/astrohack/io/locit_mds.py b/src/astrohack/io/locit_mds.py index fc743fce..73fca6a3 100644 --- a/src/astrohack/io/locit_mds.py +++ b/src/astrohack/io/locit_mds.py @@ -212,7 +212,7 @@ def plot_array_configuration( stations: bool = True, zoff: bool = False, unit: str = "m", - box_size: Union[int, float] = 5000, + box_size: Union[int, float] = None, display: bool = False, figure_size: Union[Tuple, List[float], np.array] = None, dpi: int = 300, @@ -231,7 +231,8 @@ def plot_array_configuration( :param unit: Unit for the plot, valid values are length units, default is km :type unit: str, optional - :param box_size: Size of the box for plotting the inner part of the array in unit, default is 5 km + :param box_size: Size of the box for plotting the inner part of the array in unit, when none the box size is \ + 20% of the total size of the array, default is None :type box_size: int, float, optional :param display: Display plots inline or suppress, defaults to True diff --git a/src/astrohack/io/point_mds.py b/src/astrohack/io/point_mds.py index b4a41b45..0d26072b 100644 --- a/src/astrohack/io/point_mds.py +++ b/src/astrohack/io/point_mds.py @@ -98,7 +98,7 @@ def plot_array_configuration( stations: bool = True, zoff: bool = False, unit: str = "m", - box_size: Union[int, float] = 5000, + box_size: Union[int, float] = None, figure_size: Union[Tuple, List[float], np.array] = None, display: bool = False, dpi: int = 300, @@ -117,7 +117,8 @@ def plot_array_configuration( :param unit: Unit for the plot, valid values are length units, default is km :type unit: str, optional - :param box_size: Size of the box for plotting the inner part of the array in unit, default is 5 km + :param box_size: Size of the box for plotting the inner part of the array in unit, when none the box size is \ + 20% of the total size of the array, default is None :type box_size: int, float, optional :param display: Display plots inline or suppress, defaults to True From 999c69e1f8e01fc2c7bf51ac464eec27a9a938ab Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 3 Feb 2026 11:20:48 -0700 Subject: [PATCH 064/295] Corrections in extract_holog_2.py for the changes in antenna data format in the point_xds. --- src/astrohack/core/extract_holog_2.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/astrohack/core/extract_holog_2.py b/src/astrohack/core/extract_holog_2.py index 218fd0ee..843e7b0e 100644 --- a/src/astrohack/core/extract_holog_2.py +++ b/src/astrohack/core/extract_holog_2.py @@ -72,7 +72,6 @@ def extract_holog_preprocessing(extract_holog_params, pnt_mds): lockoptions={"option": "usernoread"}, ack=False, ) - # Scan intent (with subscan intent) is stored in the OBS_MODE column of the STATE sub-table. obs_modes = ctb.getcol("OBS_MODE") ctb.close() @@ -141,13 +140,15 @@ def extract_holog_preprocessing(extract_holog_params, pnt_mds): scans = map_data["scans"] if len(scans) > 1: logger.info( - "Processing ddi: {ddi}, scans: [{min} ... {max}]".format( + "Pre-processing ddi: {ddi}, scans: [{min} ... {max}]".format( ddi=ddi, min=scans[0], max=scans[-1] ) ) else: logger.info( - "Processing ddi: {ddi}, scan: {scan}".format(ddi=ddi, scan=scans) + "Pre-processing ddi: {ddi}, scan: {scan}".format( + ddi=ddi, scan=scans + ) ) if len(list(map_data["ant"].keys())) != 0: @@ -205,7 +206,6 @@ def extract_holog_preprocessing(extract_holog_params, pnt_mds): spw_ctb.close() pol_ctb.close() - obs_ctb.close() return looping_dict, holog_obs_dict @@ -850,8 +850,9 @@ def _create_observation_summary( valid_data, map_ref_dict, ): - antenna_name = pnt_map_xds.attrs["ant_name"] - station = pnt_map_xds.attrs["ant_station"] + antenna_info = pnt_map_xds.attrs["antenna_info"] + antenna_name = antenna_info["name"] + station = antenna_info["station"] spw_info = _get_freq_summary(chan_axis) obs_info["az el info"] = _get_az_el_characteristics(pnt_map_xds, valid_data) obs_info["reference antennas"] = map_ref_dict[antenna_name] From 56b74d734b7b1eed6f432ce19a56a422f0f2e701 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 3 Feb 2026 11:38:42 -0700 Subject: [PATCH 065/295] ported holog_mds.plot_diagnostics. --- src/astrohack/io/holog_mds.py | 226 +++++++++++++++++++++++++--------- 1 file changed, 171 insertions(+), 55 deletions(-) diff --git a/src/astrohack/io/holog_mds.py b/src/astrohack/io/holog_mds.py index 108f6278..f36e464d 100644 --- a/src/astrohack/io/holog_mds.py +++ b/src/astrohack/io/holog_mds.py @@ -1,4 +1,13 @@ +import numpy as np +import pathlib + +from astropy.time import Time +from typing import Union, Tuple, List + from astrohack.io.base_mds import AstrohackBaseFile +from astrohack.utils.constants import fontsize, markersize +from astrohack.visualization.plot_tools import close_figure, create_figure_and_axes +from astrohack.utils.graph import compute_graph class AstrohackHologFile(AstrohackBaseFile): @@ -15,61 +24,62 @@ def __init__(self, file: str): super().__init__(file=file) # @toolviper.utils.parameter.validate(custom_checker=custom_plots_checker) - # def plot_diagnostics( - # self, - # destination: str, - # delta: float = 0.01, - # ant: Union[str, List[str]] = "all", - # ddi: Union[str, int, List[int]] = "all", - # map_id: Union[int, List[int]] = "all", - # complex_split: str = "polar", - # display: bool = False, - # figure_size: Union[Tuple, List[float], np.array] = None, - # dpi: int = 300, - # parallel: bool = False, - # ) -> None: - # """ Plot diagnostic calibration plots from the holography data file. - # - # :param destination: Name of the destination folder to contain diagnostic plots - # :type destination: str - # :param delta: Defines a fraction of cell_size around which to look for peaks., defaults to 0.01 - # :type delta: float, optional - # :param ant: antenna ID to use in subselection, defaults to "all" when None, ex. ea25 - # :type ant: list or str, optional - # :param ddi: data description ID to use in subselection, defaults to "all" when None, ex. 0 - # :type ddi: list or int, optional - # :param map_id: map ID to use in subselection. This relates to which antenna are in the mapping vs. scanning \ - # configuration, defaults to "all" when None, ex. 0 - # :type map_id: list or int, optional - # :param complex_split: How to split complex data, cartesian (real + imaginary) or polar (amplitude + phase), \ - # default is polar - # :type complex_split: str, optional - # :param display: Display plots inline or suppress, defaults to True - # :type display: bool, optional - # :param figure_size: 2 element array/list/tuple with the plot sizes in inches - # :type figure_size: numpy.ndarray, list, tuple, optional - # :param dpi: dots per inch to be used in plots, default is 300 - # :type dpi: int, optional - # :param parallel: Run in parallel, defaults to False - # :type parallel: bool, optional - # - # **Additional Information** - # The visibilities extracted by extract_holog are complex due to the nature of interferometric measurements. To - # ease the visualization of the complex data it can be split into real and imaginary parts (cartesian) or in - # amplitude and phase (polar). - # - # .. rubric:: Available complex splitting possibilities: - # - *cartesian*: Split is done to a real part and an imaginary part in the plots - # - *polar*: Split is done to an amplitude and a phase in the plots - # - # """ - # - # param_dict = locals() - # param_dict["map"] = map_id - # - # pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) - # key_order = ["ddi", "map", "ant"] - # compute_graph(self, calibration_plot_chunk, param_dict, key_order, parallel) + def plot_diagnostics( + self, + destination: str, + delta: float = 0.01, + ant: Union[str, List[str]] = "all", + ddi: Union[str, int, List[int]] = "all", + map_id: Union[int, List[int]] = "all", + complex_split: str = "polar", + display: bool = False, + figure_size: Union[Tuple, List[float], np.array] = None, + dpi: int = 300, + parallel: bool = False, + ) -> None: + """ Plot diagnostic calibration plots from the holography data file. + + :param destination: Name of the destination folder to contain diagnostic plots + :type destination: str + :param delta: Defines a fraction of cell_size around which to look for peaks., defaults to 0.01 + :type delta: float, optional + :param ant: antenna ID to use in subselection, defaults to "all" when None, ex. ea25 + :type ant: list or str, optional + :param ddi: data description ID to use in subselection, defaults to "all" when None, ex. 0 + :type ddi: list or int, optional + :param map_id: map ID to use in subselection. This relates to which antenna are in the mapping vs. scanning \ + configuration, defaults to "all" when None, ex. 0 + :type map_id: list or int, optional + :param complex_split: How to split complex data, cartesian (real + imaginary) or polar (amplitude + phase), \ + default is polar + :type complex_split: str, optional + :param display: Display plots inline or suppress, defaults to True + :type display: bool, optional + :param figure_size: 2 element array/list/tuple with the plot sizes in inches + :type figure_size: numpy.ndarray, list, tuple, optional + :param dpi: dots per inch to be used in plots, default is 300 + :type dpi: int, optional + :param parallel: Run in parallel, defaults to False + :type parallel: bool, optional + + **Additional Information** + The visibilities extracted by extract_holog are complex due to the nature of interferometric measurements. To + ease the visualization of the complex data it can be split into real and imaginary parts (cartesian) or in + amplitude and phase (polar). + + .. rubric:: Available complex splitting possibilities: + - *cartesian*: Split is done to a real part and an imaginary part in the plots + - *polar*: Split is done to an amplitude and a phase in the plots + + """ + + param_dict = locals() + param_dict["map"] = map_id + + pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) + key_order = ["ant", "ddi", "map"] + compute_graph(self, _calibration_plot_chunk, param_dict, key_order, parallel) + # # @toolviper.utils.parameter.validate(custom_checker=custom_plots_checker) # def plot_lm_sky_coverage( @@ -245,3 +255,109 @@ def __init__(self, file: str): # output_file.write(summary) # if print_summary: # print(summary) + + +def _extract_indices(laxis, maxis, squared_radius): + indices = [] + + assert laxis.shape[0] == maxis.shape[0], "l, m must be same size." + + for i in range(laxis.shape[0]): + squared_sum = np.power(laxis[i], 2) + np.power(maxis[i], 2) + if squared_sum <= squared_radius: + indices.append(i) + + return np.array(indices) + + +def _calibration_plot_chunk(param_dict): + xds_data = param_dict["xdt_data"].dataset + delta = param_dict["delta"] + complex_split = param_dict["complex_split"] + display = param_dict["display"] + figuresize = param_dict["figure_size"] + destination = param_dict["destination"] + dpi = param_dict["dpi"] + thisfont = 1.2 * fontsize + + UNIX_CONVERSION = 3506716800 + + radius = np.power(xds_data.attrs["summary"]["beam"]["cell size"] * delta, 2) + + l_axis = xds_data.DIRECTIONAL_COSINES.values[..., 0] + m_axis = xds_data.DIRECTIONAL_COSINES.values[..., 1] + + assert l_axis.shape[0] == m_axis.shape[0], "l, m dimensions don't match!" + + indices = _extract_indices(laxis=l_axis, maxis=m_axis, squared_radius=radius) + + if complex_split == "cartesian": + vis_dict = { + "data": [ + xds_data.isel(time=indices).VIS.real, + xds_data.isel(time=indices).VIS.imag, + ], + "polarization": [0, 3], + "label": ["REAL", "IMAG"], + } + else: + vis_dict = { + "data": [ + xds_data.isel(time=indices).apply(np.abs).VIS, + xds_data.isel(time=indices).apply(np.angle).VIS, + ], + "polarization": [0, 3], + "label": ["AMP", "PHASE"], + } + + times = np.unique( + Time(vis_dict["data"][0].time.data - UNIX_CONVERSION, format="unix").iso + ) + + fig, axis = create_figure_and_axes(figuresize, [4, 1], sharex=True) + + chan = np.arange(0, xds_data.chan.data.shape[0]) + + length = times.shape[0] + + for i, vis in enumerate(vis_dict["data"]): + for j, pol in enumerate(vis_dict["polarization"]): + for time in range(length): + k = 2 * i + j + axis[k].plot( + chan, + vis[time, :, pol], + marker="o", + label=times[time], + markersize=markersize, + ) + axis[k].set_ylabel( + f'Vis ({vis_dict["label"][i]}; {xds_data.pol.values[pol]})', + fontsize=thisfont, + ) + axis[k].tick_params(axis="both", which="major", labelsize=thisfont) + + axis[3].set_xlabel("Channel", fontsize=thisfont) + axis[0].legend( + bbox_to_anchor=(0.0, 1.02, 1.0, 0.102), + loc="lower left", + ncols=4, + mode="expand", + borderaxespad=0.0, + fontsize=fontsize, + ) + + fig.suptitle( + f'Data Calibration Check: [{param_dict["this_ddi"]}, {param_dict["this_map"]}, {param_dict["this_ant"]}]', + ha="center", + va="center", + x=0.5, + y=0.95, + rotation=0, + fontsize=1.5 * thisfont, + ) + plotfile = ( + f'{destination}/holog_diagnostics_{param_dict["this_ant"]}_' + f'{param_dict["this_ddi"]}_{param_dict["this_map"]}.png' + ) + close_figure(fig, None, plotfile, dpi, display, tight_layout=False) From 87ec3363471269389700cdf74c49362f756e0c1f Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 3 Feb 2026 11:49:22 -0700 Subject: [PATCH 066/295] ported holog_mds.plot_lm_sky_coverage. --- src/astrohack/io/holog_mds.py | 372 +++++++++++++++++++++++++++------- 1 file changed, 299 insertions(+), 73 deletions(-) diff --git a/src/astrohack/io/holog_mds.py b/src/astrohack/io/holog_mds.py index f36e464d..3a362dde 100644 --- a/src/astrohack/io/holog_mds.py +++ b/src/astrohack/io/holog_mds.py @@ -4,10 +4,17 @@ from astropy.time import Time from typing import Union, Tuple, List +from toolviper.utils import logger as logger + from astrohack.io.base_mds import AstrohackBaseFile from astrohack.utils.constants import fontsize, markersize -from astrohack.visualization.plot_tools import close_figure, create_figure_and_axes +from astrohack.visualization.plot_tools import ( + close_figure, + create_figure_and_axes, + scatter_plot, +) from astrohack.utils.graph import compute_graph +from astrohack.utils.conversion import convert_unit class AstrohackHologFile(AstrohackBaseFile): @@ -41,24 +48,33 @@ def plot_diagnostics( :param destination: Name of the destination folder to contain diagnostic plots :type destination: str + :param delta: Defines a fraction of cell_size around which to look for peaks., defaults to 0.01 :type delta: float, optional + :param ant: antenna ID to use in subselection, defaults to "all" when None, ex. ea25 :type ant: list or str, optional + :param ddi: data description ID to use in subselection, defaults to "all" when None, ex. 0 :type ddi: list or int, optional + :param map_id: map ID to use in subselection. This relates to which antenna are in the mapping vs. scanning \ configuration, defaults to "all" when None, ex. 0 :type map_id: list or int, optional + :param complex_split: How to split complex data, cartesian (real + imaginary) or polar (amplitude + phase), \ default is polar :type complex_split: str, optional + :param display: Display plots inline or suppress, defaults to True :type display: bool, optional + :param figure_size: 2 element array/list/tuple with the plot sizes in inches :type figure_size: numpy.ndarray, list, tuple, optional + :param dpi: dots per inch to be used in plots, default is 300 :type dpi: int, optional + :param parallel: Run in parallel, defaults to False :type parallel: bool, optional @@ -80,78 +96,90 @@ def plot_diagnostics( key_order = ["ant", "ddi", "map"] compute_graph(self, _calibration_plot_chunk, param_dict, key_order, parallel) - # # @toolviper.utils.parameter.validate(custom_checker=custom_plots_checker) - # def plot_lm_sky_coverage( - # self, - # destination: str, - # ant: Union[str, List[str]] = "all", - # ddi: Union[str, int, List[int]] = "all", - # map_id: Union[int, List[int]] = "all", - # angle_unit: str = "deg", - # time_unit: str = "hour", - # plot_correlation: Union[str, List[str]] = None, - # complex_split: str = "polar", - # phase_unit: str = "deg", - # display: bool = False, - # figure_size: Union[Tuple, List[float], np.array] = None, - # dpi: int = 300, - # parallel: bool = False, - # ) -> None: - # """ Plot directional cosine coverage. - # - # :param destination: Name of the destination folder to contain plots - # :type destination: str - # :param ant: antenna ID to use in subselection, defaults to "all" when None, ex. ea25 - # :type ant: list or str, optional - # :param ddi: data description ID to use in subselection, defaults to "all" when None, ex. 0 - # :type ddi: list or int, optional - # :param map_id: map ID to use in subselection. This relates to which antenna are in the mapping vs. scanning \ - # configuration, defaults to "all" when None, ex. 0 - # :type map_id: list or int, optional - # :param angle_unit: Unit for L and M axes in plots, default is 'deg'. - # :type angle_unit: str, optional - # :param time_unit: Unit for time axis in plots, default is 'hour'. - # :type time_unit: str, optional - # :param plot_correlation: Which correlation to plot against L and M, default is None (no correlation plots). - # :type plot_correlation: str, list, optional - # :param complex_split: How to split complex data, cartesian (real + imaginary) or polar (amplitude + phase), \ - # default is polar - # :type complex_split: str, optional - # :param phase_unit: Unit for phase in 'polar' plots, default is 'deg'. - # :type phase_unit: str - # :param display: Display plots inline or suppress, defaults to True - # :type display: bool, optional - # :param figure_size: 2 element array/list/tuple with the plot sizes in inches - # :type figure_size: numpy.ndarray, list, tuple, optional - # :param dpi: dots per inch to be used in plots, default is 300 - # :type dpi: int, optional - # :param parallel: Run in parallel, defaults to False - # :type parallel: bool, optional - # - # **Additional Information** - # The visibilities extracted by extract_holog are complex due to the nature of interferometric measurements. To - # ease the visualization of the complex data it can be split into real and imaginary parts (cartesian) or in - # amplitude and phase (polar). - # - # .. rubric:: Available complex splitting possibilities: - # - *cartesian*: Split is done to a real part and an imaginary part in the plots - # - *polar*: Split is done to an amplitude and a phase in the plots - # - # .. rubric:: Plotting correlations: - # - *RR, RL, LR, LL*: Are available for circular systems - # - *XX, XY, YX, YY*: Are available for linear systems - # - *all*: Plot all correlations in dataset - # - # """ - # - # param_dict = locals() - # param_dict["map"] = map_id - # - # pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) - # key_order = ["ddi", "map", "ant"] - # compute_graph(self, plot_lm_coverage, param_dict, key_order, parallel) - # return + def plot_lm_sky_coverage( + self, + destination: str, + ant: Union[str, List[str]] = "all", + ddi: Union[str, int, List[int]] = "all", + map_id: Union[int, List[int]] = "all", + angle_unit: str = "deg", + time_unit: str = "hour", + plot_correlation: Union[str, List[str]] = None, + complex_split: str = "polar", + phase_unit: str = "deg", + display: bool = False, + figure_size: Union[Tuple, List[float], np.array] = None, + dpi: int = 300, + parallel: bool = False, + ) -> None: + """ Plot directional cosine coverage. + + :param destination: Name of the destination folder to contain plots + :type destination: str + + :param ant: antenna ID to use in subselection, defaults to "all" when None, ex. ea25 + :type ant: list or str, optional + + :param ddi: data description ID to use in subselection, defaults to "all" when None, ex. 0 + :type ddi: list or int, optional + + :param map_id: map ID to use in subselection. This relates to which antenna are in the mapping vs. scanning \ + configuration, defaults to "all" when None, ex. 0 + :type map_id: list or int, optional + + :param angle_unit: Unit for L and M axes in plots, default is 'deg'. + :type angle_unit: str, optional + + :param time_unit: Unit for time axis in plots, default is 'hour'. + :type time_unit: str, optional + + :param plot_correlation: Which correlation to plot against L and M, default is None (no correlation plots). + :type plot_correlation: str, list, optional + + :param complex_split: How to split complex data, cartesian (real + imaginary) or polar (amplitude + phase), \ + default is polar + :type complex_split: str, optional + + :param phase_unit: Unit for phase in 'polar' plots, default is 'deg'. + :type phase_unit: str + + :param display: Display plots inline or suppress, defaults to True + :type display: bool, optional + + :param figure_size: 2 element array/list/tuple with the plot sizes in inches + :type figure_size: numpy.ndarray, list, tuple, optional + + :param dpi: dots per inch to be used in plots, default is 300 + :type dpi: int, optional + + :param parallel: Run in parallel, defaults to False + :type parallel: bool, optional + + **Additional Information** + The visibilities extracted by extract_holog are complex due to the nature of interferometric measurements. To + ease the visualization of the complex data it can be split into real and imaginary parts (cartesian) or in + amplitude and phase (polar). + + .. rubric:: Available complex splitting possibilities: + - *cartesian*: Split is done to a real part and an imaginary part in the plots + - *polar*: Split is done to an amplitude and a phase in the plots + + .. rubric:: Plotting correlations: + - *RR, RL, LR, LL*: Are available for circular systems + - *XX, XY, YX, YY*: Are available for linear systems + - *all*: Plot all correlations in dataset + + """ + + param_dict = locals() + param_dict["map"] = map_id + + pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) + key_order = ["ant", "ddi", "map"] + compute_graph(self, _plot_lm_coverage_chunk, param_dict, key_order, parallel) + return + # # @toolviper.utils.parameter.validate(custom_checker=custom_plots_checker) # def export_to_aips( @@ -348,7 +376,7 @@ def _calibration_plot_chunk(param_dict): ) fig.suptitle( - f'Data Calibration Check: [{param_dict["this_ddi"]}, {param_dict["this_map"]}, {param_dict["this_ant"]}]', + f'Data Calibration Check: [{param_dict["this_ant"]}, {param_dict["this_ddi"]}, {param_dict["this_map"]}]', ha="center", va="center", x=0.5, @@ -361,3 +389,201 @@ def _calibration_plot_chunk(param_dict): f'{param_dict["this_ddi"]}_{param_dict["this_map"]}.png' ) close_figure(fig, None, plotfile, dpi, display, tight_layout=False) + + +def _plot_lm_coverage_chunk(param_dict): + xdt_data = param_dict["xdt_data"] + angle_fact = convert_unit("rad", param_dict["angle_unit"], "trigonometric") + real_lm = xdt_data["DIRECTIONAL_COSINES"] * angle_fact + ideal_lm = xdt_data["IDEAL_DIRECTIONAL_COSINES"] * angle_fact + time = xdt_data.time.values + time -= time[0] + time *= convert_unit("sec", param_dict["time_unit"], "time") + param_dict["l_label"] = f'L [{param_dict["angle_unit"]}]' + param_dict["m_label"] = f'M [{param_dict["angle_unit"]}]' + param_dict["time_label"] = ( + f'Time from observation start [{param_dict["time_unit"]}]' + ) + + param_dict["marker"] = "." + param_dict["linestyle"] = "-" + param_dict["color"] = "blue" + + _plot_lm_coverage_sub(time, real_lm, ideal_lm, param_dict) + + if ( + param_dict["plot_correlation"] is None + or param_dict["plot_correlation"] == "None" + ): + pass + else: + param_dict["linestyle"] = "" + visi = np.average(xdt_data["VIS"].values, axis=1) + weights = np.average(xdt_data["WEIGHT"].values, axis=1) + pol_axis = xdt_data.pol.values + if isinstance(param_dict["plot_correlation"], (list, tuple)): + for correlation in param_dict["plot_correlation"]: + _plot_correlation_sub( + visi, weights, correlation, pol_axis, time, real_lm, param_dict + ) + else: + if param_dict["plot_correlation"] == "all": + for correlation in pol_axis: + _plot_correlation_sub( + visi, weights, correlation, pol_axis, time, real_lm, param_dict + ) + else: + _plot_correlation_sub( + visi, + weights, + param_dict["plot_correlation"], + pol_axis, + time, + real_lm, + param_dict, + ) + + +def _plot_lm_coverage_sub(time, real_lm, ideal_lm, param_dict): + fig, ax = create_figure_and_axes(param_dict["figure_size"], [2, 2]) + scatter_plot( + ax[0, 0], + time, + param_dict["time_label"], + real_lm[:, 0], + param_dict["l_label"], + "Time vs Real L", + data_marker=param_dict["marker"], + data_linestyle=param_dict["linestyle"], + data_color=param_dict["color"], + add_legend=False, + ) + scatter_plot( + ax[0, 1], + time, + param_dict["time_label"], + real_lm[:, 1], + param_dict["m_label"], + "Time vs Real M", + data_marker=param_dict["marker"], + data_linestyle=param_dict["linestyle"], + data_color=param_dict["color"], + add_legend=False, + ) + scatter_plot( + ax[1, 0], + real_lm[:, 0], + param_dict["l_label"], + real_lm[:, 1], + param_dict["m_label"], + "Real L and M", + data_marker=param_dict["marker"], + data_linestyle=param_dict["linestyle"], + data_color=param_dict["color"], + add_legend=False, + ) + scatter_plot( + ax[1, 1], + ideal_lm[:, 0], + param_dict["l_label"], + ideal_lm[:, 1], + param_dict["m_label"], + "Ideal L and M", + data_marker=param_dict["marker"], + data_linestyle=param_dict["linestyle"], + data_color=param_dict["color"], + add_legend=False, + ) + plotfile = ( + f'{param_dict["destination"]}/holog_directional_cosines_' + f'{param_dict["this_ant"]}_{param_dict["this_ddi"]}_{param_dict["this_map"]}.png' + ) + close_figure( + fig, "Directional Cosines", plotfile, param_dict["dpi"], param_dict["display"] + ) + + +def _plot_correlation_sub(visi, weights, correlation, pol_axis, time, lm, param_dict): + if correlation in pol_axis: + ipol = pol_axis == correlation + loc_vis = visi[:, ipol] + loc_wei = weights[:, ipol] + if param_dict["complex_split"] == "polar": + y_data = [np.absolute(loc_vis)] + y_label = [f"{correlation} Amplitude [arb. units]"] + title = ["Amplitude"] + y_data.append( + np.angle(loc_vis) + * convert_unit("rad", param_dict["phase_unit"], "trigonometric") + ) + y_label.append(f'{correlation} Phase [{param_dict["phase_unit"]}]') + title.append("Phase") + else: + y_data = [loc_vis.real] + y_label = [f"Real {correlation} [arb. units]"] + title = ["real part"] + y_data.append(loc_vis.imag) + y_label.append(f"Imaginary {correlation} [arb. units]") + title.append("imaginary part") + + y_data.append(loc_wei) + y_label.append(f"{correlation} weights [arb. units]") + title.append("weights") + + fig, ax = create_figure_and_axes(param_dict["figure_size"], [3, 3]) + for isplit in range(3): + scatter_plot( + ax[isplit, 0], + time, + param_dict["time_label"], + y_data[isplit], + y_label[isplit], + f"Time vs {correlation} {title[isplit]}", + data_marker=param_dict["marker"], + data_linestyle=param_dict["linestyle"], + data_color=param_dict["color"], + add_legend=False, + ) + scatter_plot( + ax[isplit, 1], + lm[:, 0], + param_dict["l_label"], + y_data[isplit], + y_label[isplit], + f"L vs {correlation} {title[isplit]}", + data_marker=param_dict["marker"], + data_linestyle=param_dict["linestyle"], + data_color=param_dict["color"], + add_legend=False, + ) + scatter_plot( + ax[isplit, 2], + lm[:, 1], + param_dict["m_label"], + y_data[isplit], + y_label[isplit], + f"M vs {correlation} {title[isplit]}", + data_marker=param_dict["marker"], + data_linestyle=param_dict["linestyle"], + data_color=param_dict["color"], + add_legend=False, + ) + + plotfile = ( + f'{param_dict["destination"]}/holog_directional_cosines_{correlation}_' + f'{param_dict["this_ant"]}_{param_dict["this_ddi"]}_{param_dict["this_map"]}.png' + ) + close_figure( + fig, + f"Channel averaged {correlation} vs Directional Cosines", + plotfile, + param_dict["dpi"], + param_dict["display"], + ) + else: + + logger.warning( + f'Correlation {correlation} is not present for {param_dict["this_ant"]} {param_dict["this_ddi"]} ' + f'{param_dict["this_map"]}, skipping...' + ) + return From a322439acd493e173e31084eff9a4b809087a0e1 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 3 Feb 2026 11:59:52 -0700 Subject: [PATCH 067/295] ported holog_mds.export_to_aips. --- src/astrohack/io/holog_mds.py | 117 ++++++++++++++++++++++++---------- 1 file changed, 82 insertions(+), 35 deletions(-) diff --git a/src/astrohack/io/holog_mds.py b/src/astrohack/io/holog_mds.py index 3a362dde..86826e6d 100644 --- a/src/astrohack/io/holog_mds.py +++ b/src/astrohack/io/holog_mds.py @@ -1,6 +1,7 @@ import numpy as np import pathlib +from datetime import date from astropy.time import Time from typing import Union, Tuple, List @@ -15,6 +16,7 @@ ) from astrohack.utils.graph import compute_graph from astrohack.utils.conversion import convert_unit +from astrohack.utils.algorithms import compute_average_stokes_visibilities class AstrohackHologFile(AstrohackBaseFile): @@ -180,42 +182,41 @@ def plot_lm_sky_coverage( compute_graph(self, _plot_lm_coverage_chunk, param_dict, key_order, parallel) return - # # @toolviper.utils.parameter.validate(custom_checker=custom_plots_checker) - # def export_to_aips( - # self, - # destination: str, - # ant: Union[str, List[str]] = "all", - # ddi: Union[str, int, List[int]] = "all", - # map_id: Union[int, List[int]] = "all", - # parallel: bool = False, - # ) -> None: - # """ Export data compatible to AIPS's HOLOG task - # - # :param destination: Name of the destination folder to contain SCII files - # :type destination: str - # :param ant: antenna ID to use in subselection, defaults to "all" when None, ex. ea25 - # :type ant: list or str, optional - # :param ddi: data description ID to use in subselection, defaults to "all" when None, ex. 0 - # :type ddi: list or int, optional - # :param map_id: map ID to use in subselection. This relates to which antenna are in the mapping vs. scanning \ - # configuration, defaults to "all" when None, ex. 0 - # :type map_id: list or int, optional - # :param parallel: Run in parallel, defaults to False - # :type parallel: bool, optional - # - # **Additional Information** - # - # This method converts the data for an Antenna mapping to the ASCII format used by AIPS's HOLOG task. - # Currently only stokes I is supported. - # """ - # param_dict = locals() - # param_dict["map"] = map_id - # - # pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) - # key_order = ["ddi", "map", "ant"] - # compute_graph(self, export_to_aips, param_dict, key_order, parallel) - # return + def export_to_aips( + self, + destination: str, + ant: Union[str, List[str]] = "all", + ddi: Union[str, int, List[int]] = "all", + map_id: Union[int, List[int]] = "all", + parallel: bool = False, + ) -> None: + """ Export data compatible to AIPS's HOLOG task + + :param destination: Name of the destination folder to contain SCII files + :type destination: str + :param ant: antenna ID to use in subselection, defaults to "all" when None, ex. ea25 + :type ant: list or str, optional + :param ddi: data description ID to use in subselection, defaults to "all" when None, ex. 0 + :type ddi: list or int, optional + :param map_id: map ID to use in subselection. This relates to which antenna are in the mapping vs. scanning \ + configuration, defaults to "all" when None, ex. 0 + :type map_id: list or int, optional + :param parallel: Run in parallel, defaults to False + :type parallel: bool, optional + + **Additional Information** + + This method converts the data for an Antenna mapping to the ASCII format used by AIPS's HOLOG task. + Currently only stokes I is supported. + """ + param_dict = locals() + param_dict["map"] = map_id + + pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) + key_order = ["ant", "ddi", "map"] + compute_graph(self, _export_to_aips_chunk, param_dict, key_order, parallel) + return # @toolviper.utils.parameter.validate(custom_checker=custom_unit_checker) # def observation_summary( @@ -587,3 +588,49 @@ def _plot_correlation_sub(visi, weights, correlation, pol_axis, time, lm, param_ f'{param_dict["this_map"]}, skipping...' ) return + + +def _export_to_aips_chunk(param_dict): + xds_data = param_dict["xdt_data"] + stokes = "I" + stokes_vis = compute_average_stokes_visibilities(xds_data, stokes) + filename = ( + f'{param_dict["destination"]}/holog_visibilities_{param_dict["this_ant"]}_' + f'{param_dict["this_ddi"]}_{param_dict["this_map"]}.txt' + ) + ant_num = xds_data.attrs["summary"]["general"]["antenna name"].split("a")[1] + cmt = "#! " + + today = date.today().strftime("%y%m%d") + outstr = ( + cmt + + f"RefAnt = ** Antenna = {ant_num} Stokes = '{stokes}_' Freq = {stokes_vis.attrs['frequency']:.9f}" + f" DATE-OBS = '{today}'\n" + ) + outstr += cmt + "MINsamp = 0 Npoint = 1\n" + outstr += cmt + "IFnumber = 2 Channel = 32.0\n" + outstr += cmt + "TimeRange = -99, 0, 0, 0, 999, 0, 0, 0\n" + outstr += cmt + "Averaged Ref-Ants = 10, 15,\n" + outstr += cmt + "DOCAL = T DOPOL =-1\n" + outstr += cmt + "BCHAN= 4 ECHAN= 60 CHINC= 1 averaged\n" + outstr += ( + cmt + + " LL MM AMPLITUDE PHASE SIGMA(AMP) SIGMA(PHASE)\n" + ) + lm = xds_data["DIRECTIONAL_COSINES"].values + amp = stokes_vis["AMPLITUDE"].values + pha = stokes_vis["PHASE"].values + sigma_amp = stokes_vis["SIGMA_AMP"] + sigma_pha = stokes_vis["SIGMA_PHA"] + for i_time in range(len(xds_data.time)): + if np.isfinite(sigma_amp[i_time]): + outstr += ( + f"{lm[i_time, 0]:15.7f}{lm[i_time, 1]:15.7f}{amp[i_time]:15.7f}{pha[i_time]:15.7f}" + f"{sigma_amp[i_time]:15.7f}{sigma_pha[i_time]:15.7f}\n" + ) + outstr += f"{cmt}Average number samples per point = 1.000" + + with open(filename, "w") as outfile: + outfile.write(outstr) + + return From 44db3ff52d82a268bb68a9b3900fe07d364f363a Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 3 Feb 2026 12:06:45 -0700 Subject: [PATCH 068/295] ported holog_mds.observation_summary. --- src/astrohack/io/holog_mds.py | 145 +++++++++++--------- src/astrohack/visualization/textual_data.py | 4 +- 2 files changed, 82 insertions(+), 67 deletions(-) diff --git a/src/astrohack/io/holog_mds.py b/src/astrohack/io/holog_mds.py index 86826e6d..34fd4d6d 100644 --- a/src/astrohack/io/holog_mds.py +++ b/src/astrohack/io/holog_mds.py @@ -17,6 +17,7 @@ from astrohack.utils.graph import compute_graph from astrohack.utils.conversion import convert_unit from astrohack.utils.algorithms import compute_average_stokes_visibilities +from astrohack.visualization.textual_data import generate_observation_summary class AstrohackHologFile(AstrohackBaseFile): @@ -195,13 +196,17 @@ def export_to_aips( :param destination: Name of the destination folder to contain SCII files :type destination: str + :param ant: antenna ID to use in subselection, defaults to "all" when None, ex. ea25 :type ant: list or str, optional + :param ddi: data description ID to use in subselection, defaults to "all" when None, ex. 0 :type ddi: list or int, optional + :param map_id: map ID to use in subselection. This relates to which antenna are in the mapping vs. scanning \ configuration, defaults to "all" when None, ex. 0 :type map_id: list or int, optional + :param parallel: Run in parallel, defaults to False :type parallel: bool, optional @@ -219,71 +224,81 @@ def export_to_aips( return # @toolviper.utils.parameter.validate(custom_checker=custom_unit_checker) - # def observation_summary( - # self, - # summary_file: str, - # ant: Union[str, List[str]] = "all", - # ddi: Union[str, int, List[int]] = "all", - # map_id: Union[int, List[int]] = "all", - # az_el_key: str = "center", - # phase_center_unit: str = "radec", - # az_el_unit: str = "deg", - # time_format: str = "%d %h %Y, %H:%M:%S", - # tab_size: int = 3, - # print_summary: bool = True, - # parallel: bool = False, - # ) -> None: - # """ Create a Summary of observation information - # - # :param summary_file: Text file to put the observation summary - # :type summary_file: str - # :param ant: antenna ID to use in subselection, defaults to "all" when None, ex. ea25 - # :type ant: list or str, optional - # :param ddi: data description ID to use in subselection, defaults to "all" when None, ex. 0 - # :type ddi: list or int, optional - # :param map_id: map ID to use in subselection. This relates to which antenna are in the mapping vs. scanning \ - # configuration, defaults to "all" when None, ex. 0 - # :type map_id: list or int, optional - # :param az_el_key: What type of Azimuth & Elevation information to print, 'mean', 'median' or 'center', default\ - # is 'center' - # :type az_el_key: str, optional - # :param phase_center_unit: What unit to display phase center coordinates, 'radec' and angle units supported, \ - # default is 'radec' - # :type phase_center_unit: str, optional - # :param az_el_unit: Angle unit used to display Azimuth & Elevation information, default is 'deg' - # :type az_el_unit: str, optional - # :param time_format: datetime time format for the start and end dates of observation, default is \ - # "%d %h %Y, %H:%M:%S" - # :type time_format: str, optional - # :param tab_size: Number of spaces in the tab levels, default is 3 - # :type tab_size: int, optional - # :param print_summary: Print the summary at the end of execution, default is True - # :type print_summary: bool, optional - # :param parallel: Run in parallel, defaults to False - # :type parallel: bool, optional - # - # **Additional Information** - # - # This method produces a summary of the data in the AstrohackHologFile displaying general information, - # spectral information and suggested beam image characteristics. - # """ - # - # param_dict = locals() - # param_dict["map"] = map_id - # key_order = ["ddi", "map", "ant"] - # execution, summary = compute_graph( - # self, - # generate_observation_summary, - # param_dict, - # key_order, - # parallel, - # fetch_returns=True, - # ) - # summary = "".join(summary) - # with open(summary_file, "w") as output_file: - # output_file.write(summary) - # if print_summary: - # print(summary) + def observation_summary( + self, + summary_file: str, + ant: Union[str, List[str]] = "all", + ddi: Union[str, int, List[int]] = "all", + map_id: Union[int, List[int]] = "all", + az_el_key: str = "center", + phase_center_unit: str = "radec", + az_el_unit: str = "deg", + time_format: str = "%d %h %Y, %H:%M:%S", + tab_size: int = 3, + print_summary: bool = True, + parallel: bool = False, + ) -> None: + """ Create a Summary of observation information + + :param summary_file: Text file to put the observation summary + :type summary_file: str + + :param ant: antenna ID to use in subselection, defaults to "all" when None, ex. ea25 + :type ant: list or str, optional + + :param ddi: data description ID to use in subselection, defaults to "all" when None, ex. 0 + :type ddi: list or int, optional + + :param map_id: map ID to use in subselection. This relates to which antenna are in the mapping vs. scanning \ + configuration, defaults to "all" when None, ex. 0 + :type map_id: list or int, optional + + :param az_el_key: What type of Azimuth & Elevation information to print, 'mean', 'median' or 'center', default\ + is 'center' + :type az_el_key: str, optional + + :param phase_center_unit: What unit to display phase center coordinates, 'radec' and angle units supported, \ + default is 'radec' + :type phase_center_unit: str, optional + + :param az_el_unit: Angle unit used to display Azimuth & Elevation information, default is 'deg' + :type az_el_unit: str, optional + + :param time_format: datetime time format for the start and end dates of observation, default is \ + "%d %h %Y, %H:%M:%S" + :type time_format: str, optional + + :param tab_size: Number of spaces in the tab levels, default is 3 + :type tab_size: int, optional + + :param print_summary: Print the summary at the end of execution, default is True + :type print_summary: bool, optional + + :param parallel: Run in parallel, defaults to False + :type parallel: bool, optional + + **Additional Information** + + This method produces a summary of the data in the AstrohackHologFile displaying general information, + spectral information and suggested beam image characteristics. + """ + + param_dict = locals() + param_dict["map"] = map_id + key_order = ["ant", "ddi", "map"] + execution, summary = compute_graph( + self, + generate_observation_summary, + param_dict, + key_order, + parallel, + fetch_returns=True, + ) + summary = "".join(summary) + with open(summary_file, "w") as output_file: + output_file.write(summary) + if print_summary: + print(summary) def _extract_indices(laxis, maxis, squared_radius): diff --git a/src/astrohack/visualization/textual_data.py b/src/astrohack/visualization/textual_data.py index 446482c9..63cce86e 100644 --- a/src/astrohack/visualization/textual_data.py +++ b/src/astrohack/visualization/textual_data.py @@ -257,14 +257,14 @@ def generate_observation_summary(parm_dict): map_id = None is_holog_zarr = False - xds = parm_dict["xds_data"] + xds = parm_dict["xdt_data"] obs_sum = xds.attrs["summary"] tab_size = parm_dict["tab_size"] tab_count = 1 if is_holog_zarr: - header = f"{ddi}, {map_id}, {antenna}" + header = f"{antenna}, {ddi}, {map_id}" else: header = f"{antenna}, {ddi}" From ee7c91adcfc1c91c9a9429a9d35a6780ff7dba26 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 3 Feb 2026 12:10:58 -0700 Subject: [PATCH 069/295] Created image_mds class and also added a docstring stub for the other mds classes. --- src/astrohack/io/holog_mds.py | 4 ++++ src/astrohack/io/image_mds.py | 19 +++++++++++++++++++ src/astrohack/io/point_mds.py | 4 ++++ src/astrohack/io/position_mds.py | 4 ++++ 4 files changed, 31 insertions(+) create mode 100644 src/astrohack/io/image_mds.py diff --git a/src/astrohack/io/holog_mds.py b/src/astrohack/io/holog_mds.py index 34fd4d6d..a399cab2 100644 --- a/src/astrohack/io/holog_mds.py +++ b/src/astrohack/io/holog_mds.py @@ -21,6 +21,10 @@ class AstrohackHologFile(AstrohackBaseFile): + """Data class for holog data. + + Data within an object of this class can be selected for further inspection, plotted or produce a report + """ def __init__(self, file: str): """Initialize an AstrohackHologFile object. diff --git a/src/astrohack/io/image_mds.py b/src/astrohack/io/image_mds.py new file mode 100644 index 00000000..9b9974b8 --- /dev/null +++ b/src/astrohack/io/image_mds.py @@ -0,0 +1,19 @@ +from astrohack.io.base_mds import AstrohackBaseFile + + +class AstrohackImageFile(AstrohackBaseFile): + """Data class for image data. + + Data within an object of this class can be selected for further inspection, plotted or produce a report + """ + + def __init__(self, file: str): + """Initialize an AstrohackImageFile object. + + :param file: File to be linked to this object + :type file: str + + :return: AstrohackImageFile object + :rtype: AstrohackImageFile + """ + super().__init__(file=file) diff --git a/src/astrohack/io/point_mds.py b/src/astrohack/io/point_mds.py index 0d26072b..c05299c3 100644 --- a/src/astrohack/io/point_mds.py +++ b/src/astrohack/io/point_mds.py @@ -13,6 +13,10 @@ class AstrohackPointFile(AstrohackBaseFile): + """Data class for point data. + + Data within an object of this class can be selected for further inspection, plotted or produce a report + """ def __init__(self, file: str): """Initialize an AstrohackPointFile object. diff --git a/src/astrohack/io/position_mds.py b/src/astrohack/io/position_mds.py index 2ea77fa3..d0ad2a77 100644 --- a/src/astrohack/io/position_mds.py +++ b/src/astrohack/io/position_mds.py @@ -29,6 +29,10 @@ class AstrohackPositionFile(AstrohackBaseFile): + """Data class for position data. + + Data within an object of this class can be selected for further inspection, plotted or produce a report + """ def __init__(self, file: str): """Initialize an AstrohackPositionFile object. From 64af8012638a2ebee2bca95d267d76e565cebabd Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 3 Feb 2026 12:12:16 -0700 Subject: [PATCH 070/295] Copied holog files to work on porting holog to use the new data files. --- src/astrohack/core/holog_2.py | 391 ++++++++++++++++++++++++++++++++++ src/astrohack/holog_2.py | 230 ++++++++++++++++++++ 2 files changed, 621 insertions(+) create mode 100644 src/astrohack/core/holog_2.py create mode 100644 src/astrohack/holog_2.py diff --git a/src/astrohack/core/holog_2.py b/src/astrohack/core/holog_2.py new file mode 100644 index 00000000..ea2d5a61 --- /dev/null +++ b/src/astrohack/core/holog_2.py @@ -0,0 +1,391 @@ +import numpy as np +import xarray as xr + +from astrohack.utils import format_angular_distance +from astrohack.antenna.telescope import get_proper_telescope, RingedCassegrain +from astrohack.utils.text import create_dataset_label +from astrohack.utils.conversion import convert_5d_grid_to_stokes +from astrohack.utils.algorithms import phase_wrapping +from astrohack.utils.zernike_aperture_fitting import fit_zernike_coefficients +from astrohack.utils.file import load_holog_file +from astrohack.utils.imaging import ( + calculate_far_field_aperture, + calculate_near_field_aperture, +) +from astrohack.utils.gridding import grid_beam +from astrohack.utils.imaging import parallactic_derotation +from astrohack.utils.phase_fitting import ( + clic_like_phase_fitting, + skip_phase_fitting, + aips_like_phase_fitting, +) + +import toolviper.utils.logger as logger + + +def process_holog_chunk(holog_chunk_params): + """Process chunk holography data along the antenna axis. Works with holography file to properly grid , normalize, + average and correct data and returns the aperture pattern. + + Args: + holog_chunk_params (dict): Dictionary containing holography parameters. + """ + holog_file, ant_data_dict = load_holog_file( + holog_chunk_params["holog_name"], + dask_load=False, + load_pnt_dict=False, + ant_id=holog_chunk_params["this_ant"], + ddi_id=holog_chunk_params["this_ddi"], + ) + label = create_dataset_label( + holog_chunk_params["this_ant"], holog_chunk_params["this_ddi"], separator="," + ) + logger.info(f"Processing {label}") + ddi = holog_chunk_params["this_ddi"] + convert_to_stokes = holog_chunk_params["to_stokes"] + ref_xds = ant_data_dict[ddi]["map_0"] + summary = ref_xds.attrs["summary"] + + user_grid_size = holog_chunk_params["grid_size"] + + if user_grid_size is None: + grid_size = np.array(summary["beam"]["grid size"]) + elif isinstance(user_grid_size, int): + grid_size = np.array([user_grid_size, user_grid_size]) + elif isinstance(user_grid_size, (list, np.ndarray)): + grid_size = np.array(user_grid_size) + else: + raise Exception( + f"Don't know what due with grid size of type {type(user_grid_size)}" + ) + + logger.info( + f"{label}: Using a grid of {grid_size[0]} by {grid_size[1]} pixels for the beam" + ) + + user_cell_size = holog_chunk_params["cell_size"] + if user_cell_size is None: + cell_size = np.array( + [-summary["beam"]["cell size"], summary["beam"]["cell size"]] + ) + elif isinstance(user_cell_size, (int, float)): + cell_size = np.array([-user_cell_size, user_cell_size]) + elif isinstance(user_cell_size, (list, np.ndarray)): + cell_size = np.array(user_cell_size) + else: + raise Exception( + f"Don't know what due with cell size of type {type(user_cell_size)}" + ) + + logger.info( + f"{label}: Using a cell size of {format_angular_distance(cell_size[0])} by " + f"{format_angular_distance(cell_size[1])} for the beam" + ) + + telescope = get_proper_telescope( + summary["general"]["telescope name"], summary["general"]["antenna name"] + ) + try: + is_near_field = ref_xds.attrs["near_field"] + except KeyError: + is_near_field = False + + ( + beam_grid, + time_centroid, + freq_axis, + pol_axis, + l_axis, + m_axis, + grid_corr, + summary, + ) = grid_beam( + ant_ddi_dict=ant_data_dict[ddi], + grid_size=grid_size, + sky_cell_size=cell_size, + avg_chan=holog_chunk_params["chan_average"], + chan_tol_fac=holog_chunk_params["chan_tolerance_factor"], + telescope=telescope, + grid_interpolation_mode=holog_chunk_params["grid_interpolation_mode"], + observation_summary=summary, + label=label, + ) + + if not is_near_field: + beam_grid = parallactic_derotation( + data=beam_grid, parallactic_angle_dict=ant_data_dict[ddi] + ) + + if holog_chunk_params["scan_average"]: + beam_grid = np.mean(beam_grid, axis=0)[None, ...] + time_centroid = np.mean(np.array(time_centroid)) + + # Current bottleneck + if is_near_field: + distance, focus_offset = telescope.station_distance_dict[ + holog_chunk_params["alma_osf_pad"] + ] + aperture_grid, u_axis, v_axis, _, used_wavelength = ( + calculate_near_field_aperture( + grid=beam_grid, + sky_cell_size=holog_chunk_params["cell_size"], + distance=distance, + freq=freq_axis, + padding_factor=holog_chunk_params["padding_factor"], + focus_offset=focus_offset, + telescope=telescope, + apply_grid_correction=grid_corr, + label=label, + ) + ) + else: + focus_offset = 0 + aperture_grid, u_axis, v_axis, _, used_wavelength = ( + calculate_far_field_aperture( + grid=beam_grid, + padding_factor=holog_chunk_params["padding_factor"], + freq=freq_axis, + telescope=telescope, + sky_cell_size=cell_size, + apply_grid_correction=grid_corr, + label=label, + ) + ) + zernike_n_order = holog_chunk_params["zernike_n_order"] + zernike_coeffs, zernike_model, zernike_rms, osa_coeff_list = ( + fit_zernike_coefficients( + aperture_grid, u_axis, v_axis, zernike_n_order, telescope + ) + ) + + orig_pol_axis = pol_axis + if convert_to_stokes: + beam_grid = convert_5d_grid_to_stokes(beam_grid, pol_axis) + aperture_grid = convert_5d_grid_to_stokes(aperture_grid, pol_axis) + pol_axis = ["I", "Q", "U", "V"] + + amplitude, phase, u_prime, v_prime = _crop_and_split_aperture( + aperture_grid, u_axis, v_axis, telescope + ) + + phase_fit_engine = holog_chunk_params["phase_fit_engine"] + if phase_fit_engine == "perturbations" and not isinstance( + telescope, RingedCassegrain + ): + logger.warning( + f"Pertubation phase fitting is not supported for {telescope.name}, changing phase fit engine to" + f" zernike" + ) + phase_fit_engine = "zernike" + + if phase_fit_engine is None or phase_fit_engine == "none": + phase_corrected_angle, phase_fit_results = skip_phase_fitting(label, phase) + else: + if is_near_field: + phase_corrected_angle, phase_fit_results = clic_like_phase_fitting( + phase, freq_axis, telescope, focus_offset, u_prime, v_prime, label + ) + else: + if phase_fit_engine == "perturbations": + phase_corrected_angle, phase_fit_results = aips_like_phase_fitting( + amplitude, + phase, + pol_axis, + freq_axis, + telescope, + u_axis, + v_axis, + holog_chunk_params["phase_fit_control"], + label, + ) + elif phase_fit_engine == "zernike": + if zernike_n_order > 4: + logger.warning( + "Using a Zernike order > 4 for phase fitting may result in overfitting" + ) + + if convert_to_stokes: + zernike_grid = convert_5d_grid_to_stokes( + zernike_model, orig_pol_axis + ) + else: + zernike_grid = zernike_model.copy() + + _, zernike_phase, _, _ = _crop_and_split_aperture( + zernike_grid, u_axis, v_axis, telescope + ) + + phase_corrected_angle = phase_wrapping( + np.where(np.isfinite(zernike_phase), phase - zernike_phase, phase) + ) + phase_fit_results = None + else: + logger.error(f"Unsupported phase fitting engine: {phase_fit_engine}") + raise ValueError + + summary["aperture"] = _get_aperture_summary( + u_axis, v_axis, _compute_aperture_resolution(l_axis, m_axis, used_wavelength) + ) + + _export_to_xds( + beam_grid, + aperture_grid, + amplitude, + phase_corrected_angle, + holog_chunk_params["this_ant"], + time_centroid, + ddi, + phase_fit_results, + pol_axis, + freq_axis, + l_axis, + m_axis, + u_axis, + v_axis, + u_prime, + v_prime, + orig_pol_axis, + osa_coeff_list, + zernike_coeffs, + zernike_model, + zernike_rms, + zernike_n_order, + holog_chunk_params["image_name"], + summary, + ) + + logger.info(f"Finished processing {label}") + + +def _crop_and_split_aperture(aperture_grid, u_axis, v_axis, telescope, scaling=1.5): + # Default scaling factor is now 1.5 to allow for better analysis of the noise around the aperture. + # This will probably mean no cropping for most apertures, but may be important if dish appears too small in the + # aperture. + max_aperture_radius = 0.5 * telescope.diameter + + image_slice = aperture_grid[0, 0, 0, ...] + center_pixel = np.array(image_slice.shape[0:2]) // 2 + radius_u = int( + np.where(np.abs(u_axis) < max_aperture_radius * scaling)[0].max() + - center_pixel[0] + ) + radius_v = int( + np.where(np.abs(v_axis) < max_aperture_radius * scaling)[0].max() + - center_pixel[1] + ) + + if radius_v > radius_u: + radius = radius_v + else: + radius = radius_u + + start_cut = center_pixel - radius + end_cut = center_pixel + radius + + amplitude = np.absolute( + aperture_grid[..., start_cut[0] : end_cut[0], start_cut[1] : end_cut[1]] + ) + phase = np.angle( + aperture_grid[..., start_cut[0] : end_cut[0], start_cut[1] : end_cut[1]] + ) + return ( + amplitude, + phase, + u_axis[start_cut[0] : end_cut[0]], + v_axis[start_cut[1] : end_cut[1]], + ) + + +def _compute_aperture_resolution(l_axis, m_axis, wavelength): + # Here we compute the aperture resolution from Equation 7 In EVLA memo 212 + # https://library.nrao.edu/public/memos/evla/EVLAM_212.pdf + deltal = np.max(l_axis) - np.min(l_axis) + deltam = np.max(m_axis) - np.min(m_axis) + aperture_resolution = np.array([1 / deltal, 1 / deltam]) + aperture_resolution *= 1.27 * wavelength + return aperture_resolution + + +def _export_to_xds( + beam_grid, + aperture_grid, + amplitude, + phase_corrected_angle, + ant_id, + time_centroid, + ddi, + phase_fit_results, + pol_axis, + freq_axis, + l_axis, + m_axis, + u_axis, + v_axis, + u_prime, + v_prime, + orig_pol_axis, + osa_coeff_list, + zernike_coeffs, + zernike_model, + zernike_rms, + zernike_n_order, + image_name, + summary, +): + # Todo: Add Paralactic angle as a non-dimension coordinate dependant on time. + xds = xr.Dataset() + + xds["BEAM"] = xr.DataArray(beam_grid, dims=["time", "chan", "pol", "l", "m"]) + xds["APERTURE"] = xr.DataArray( + aperture_grid, dims=["time", "chan", "pol", "u", "v"] + ) + + xds["AMPLITUDE"] = xr.DataArray( + amplitude, dims=["time", "chan", "pol", "u_prime", "v_prime"] + ) + xds["CORRECTED_PHASE"] = xr.DataArray( + phase_corrected_angle, dims=["time", "chan", "pol", "u_prime", "v_prime"] + ) + + xds["ZERNIKE_COEFFICIENTS"] = xr.DataArray( + zernike_coeffs, dims=["time", "chan", "orig_pol", "osa"] + ) + xds["ZERNIKE_MODEL"] = xr.DataArray( + zernike_model, dims=["time", "chan", "orig_pol", "u", "v"] + ) + xds["ZERNIKE_FIT_RMS"] = xr.DataArray( + zernike_rms, dims=["time", "chan", "orig_pol"] + ) + + xds.attrs["ant_id"] = ant_id + xds.attrs["time_centroid"] = np.array(time_centroid) + xds.attrs["ddi"] = ddi + xds.attrs["phase_fitting"] = phase_fit_results + xds.attrs["zernike_N_order"] = zernike_n_order + xds.attrs["summary"] = summary + + coords = { + "orig_pol": orig_pol_axis, + "pol": pol_axis, + "l": l_axis, + "m": m_axis, + "u": u_axis, + "v": v_axis, + "u_prime": u_prime, + "v_prime": v_prime, + "chan": freq_axis, + "osa": osa_coeff_list, + } + xds = xds.assign_coords(coords) + xds.to_zarr( + f"{image_name}/{ant_id}/{ddi}", mode="w", compute=True, consolidated=True + ) + + +def _get_aperture_summary(u_axis, v_axis, aperture_resolution): + aperture_dict = { + "grid size": [u_axis.shape[0], v_axis.shape[0]], + "cell size": [u_axis[1] - u_axis[0], v_axis[1] - v_axis[0]], + "resolution": aperture_resolution.tolist(), + } + return aperture_dict diff --git a/src/astrohack/holog_2.py b/src/astrohack/holog_2.py new file mode 100644 index 00000000..a5e6b3e3 --- /dev/null +++ b/src/astrohack/holog_2.py @@ -0,0 +1,230 @@ +import json +import pathlib +import numpy as np + +import toolviper.utils.logger as logger +import toolviper + +from numbers import Number +from typing import List, Union, NewType, Tuple + +from astrohack.utils.graph import compute_graph +from astrohack.utils.file import overwrite_file, check_if_file_can_be_opened +from astrohack.utils.data import write_meta_data +from astrohack.core.holog import process_holog_chunk +from astrohack.utils.text import get_default_file_name +from astrohack.io.mds import AstrohackImageFile + +Array = NewType("Array", Union[np.array, List[int], List[float]]) + + +@toolviper.utils.parameter.validate() +def holog( + holog_name: str, + grid_size: Union[int, Array, List] = None, + cell_size: Union[float, Array, List] = None, + image_name: str = None, + padding_factor: int = 10, + grid_interpolation_mode: str = "gaussian", + chan_average: bool = True, + chan_tolerance_factor: float = 0.005, + scan_average: bool = True, + alma_osf_pad: str = None, + ant: Union[str, List[str]] = "all", + ddi: Union[str, int, List[int]] = "all", + zernike_n_order: int = 4, + phase_fit_engine: str = "perturbations", + phase_fit_control: Union[List[bool], Tuple[bool]] = (True, True, True, True, True), + to_stokes: bool = True, + overwrite: bool = False, + parallel: bool = False, +) -> Union[AstrohackImageFile, None]: + """ Process holography data and derive aperture illumination pattern. + + :param holog_name: Name of holography .holog.zarr file to process. + :type holog_name: str + + :param grid_size: Numpy array specifying the dimensions of the grid used in data gridding. If not specified \ + grid_size is calculated using POINTING_OFFSET in pointing table. + :type grid_size: numpy.ndarray, dtype int, list optional + + :param cell_size: Size 2 array defining the cell size of each beam grid bin in radians. If not specified, the used \ + cell_size is the one given in the observation_summary of the input holog file. + :type cell_size: numpy.ndarray, dtype float, list optional + + :param image_name: Defines the name of the output image name. If value is None, the name will be set to \ + .image.zarr, defaults to None + :type image_name: str, optional + + :param padding_factor: Padding factor applied to beam grid before computing the fast-fourier transform. The default\ + has been set for operation on most systems. The user should be aware of memory constraints before increasing this\ + parameter significantly., defaults to 10 + :type padding_factor: int, optional + + :param parallel: Run in parallel with Dask or in serial., defaults to False + :type parallel: bool, optional + + :param grid_interpolation_mode: Method of interpolation used when gridding data. For modes 'linear', 'nearest' and \ + 'cubic' this is done using the `scipy.interpolate.griddata` method. For more information see `scipy.interpolate \ + `_.\ + The remaining mode 'gaussian' convolves the visibilities with a gaussian kernel with a FWHM equal HPBW for the \ + primary beam main lobe at the given frequency, this is slower than `scipy.interpolate.griddata` but better at\ + preserving the small scales variations in the beam. Defaults to "gaussian". + :type grid_interpolation_mode: str, optional. Available options: {"gaussian", "linear", "nearest", "cubic"} + + :param chan_average: Boolean dictating whether the channel average is computed and written to the output holog \ + file., defaults to True + :type chan_average: bool, optional + + :param chan_tolerance_factor: Tolerance used in channel averaging to determine the number of primary beam \ + channels., defaults to 0.005 + :type chan_tolerance_factor: float, optional + + :param scan_average: Boolean dictating whether averaging is done over scan., defaults to True + :type scan_average: bool, optional + + :param alma_osf_pad: Pad on which the antenna was poitioned at the ALMA OSF (only relevant for ALMA near field \ + holographies). + :type alma_osf_pad: str, optional + + :param ant: List of antennas/antenna to be processed, defaults to "all" when None, ex. ea25 + :type ant: list or str, optional + + :param ddi: List of ddi to be processed, defaults to "all" when None, ex. 0 + :type ddi: list or int, optional + + :param zernike_n_order: Maximal N order for the Zernike Polynomials to be fitted to the aperture data. + :type zernike_n_order: int, optional + + :param phase_fit_engine: Choose between the two available phase fitting engines, "perturbations" which assumes \ + cassegrain optics and "zernike" which makes no assumption about the optical system but may overfit the aperture \ + phase, default is "perturbations". + :type phase_fit_engine: str, optional. + + :param phase_fit_control: Controls which type of optical perturbations are to be fitted when phase_fit_engine is \ + set to "perturbations". + + Available phase fit controls: + + - [0]: pointing offset; + - [1]: focus xy offsets; + - [2]: focus z offset; + - [3]: subreflector tilt (off by default except for VLA and VLBA) + - [4]: cassegrain offset + + :type phase_fit_control: bool array, optional + + :param to_stokes: Dictates whether polarization is computed according to stokes values., defaults to True + :type to_stokes: bool, optional + + :param overwrite: Overwrite existing files on disk, defaults to False + :type overwrite: bool, optional + + :return: Holography image object. + :rtype: AstrohackImageFile + + .. _Description: + **AstrohackImageFile** + + Image object allows the user to access image data via compound dictionary keys with values, in order of depth,\ + `ant` -> `ddi`. The image object also provides a `summary()` helper function to list available keys for each file.\ + An outline of the image object structure is show below: + + .. parsed-literal:: + image_mds = + { + ant_0:{ + ddi_0: image_ds, + ⋮ + ddi_m: image_ds + }, + ⋮ + ant_n: … + } + + **Example Usage** + + .. parsed-literal:: + from astrohack.holog import holog + + holog( + holog_name="astrohack_observation.holog.zarr", + padding_factor=50, + grid_interpolation_mode='linear', + chan_average = True, + scan_average = True, + ant='ea25', + overwrite=True, + parallel=True + ) + + """ + + check_if_file_can_be_opened(holog_name, "0.7.2") + + # Doing this here allows it to get captured by locals() + if image_name is None: + image_name = get_default_file_name( + input_file=holog_name, output_type=".image.zarr" + ) + + holog_params = locals() + + input_params = holog_params.copy() + assert pathlib.Path(holog_params["holog_name"]).exists() is True, logger.error( + f'File {holog_params["holog_name"]} does not exists.' + ) + + overwrite_file(holog_params["image_name"], holog_params["overwrite"]) + + json_data = "/".join((holog_params["holog_name"], ".holog_json")) + + with open(json_data, "r") as json_file: + holog_json = json.load(json_file) + + if compute_graph( + holog_json, process_holog_chunk, holog_params, ["ant", "ddi"], parallel=parallel + ): + + output_attr_file = "{name}/{ext}".format( + name=holog_params["image_name"], ext=".image_attr" + ) + write_meta_data(output_attr_file, holog_params) + + output_attr_file = "{name}/{ext}".format( + name=holog_params["image_name"], ext=".image_input" + ) + write_meta_data(output_attr_file, input_params) + + image_mds = AstrohackImageFile(holog_params["image_name"]) + image_mds.open() + + logger.info("Finished processing") + + return image_mds + + else: + logger.warning("No data to process") + return None + + +def _convert_gridding_parameter( + gridding_parameter: Union[List, Array], reflect_on_axis=False +) -> np.ndarray: + if isinstance(gridding_parameter, Number): + gridding_parameter = np.array( + [np.power(-1, reflect_on_axis) * gridding_parameter, gridding_parameter] + ) + + elif isinstance(gridding_parameter, list): + gridding_parameter = np.array(gridding_parameter) + + elif isinstance(gridding_parameter, np.ndarray): + pass + + else: + logger.error( + "Unknown dtype for gridding parameter: {}".format(gridding_parameter) + ) + + return gridding_parameter From 6181954ddcc162bfc3d01070e8a9b1fec673382b Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 3 Feb 2026 12:20:27 -0700 Subject: [PATCH 071/295] holog user facing function ported to new data format in holog_2.py. --- src/astrohack/holog_2.py | 89 +++++++++++----------------------------- 1 file changed, 23 insertions(+), 66 deletions(-) diff --git a/src/astrohack/holog_2.py b/src/astrohack/holog_2.py index a5e6b3e3..9ddb607b 100644 --- a/src/astrohack/holog_2.py +++ b/src/astrohack/holog_2.py @@ -1,24 +1,20 @@ -import json -import pathlib import numpy as np -import toolviper.utils.logger as logger -import toolviper - -from numbers import Number from typing import List, Union, NewType, Tuple -from astrohack.utils.graph import compute_graph -from astrohack.utils.file import overwrite_file, check_if_file_can_be_opened -from astrohack.utils.data import write_meta_data -from astrohack.core.holog import process_holog_chunk +import toolviper.utils.logger as logger + +from astrohack import open_holog +from astrohack.utils.graph import compute_graph_to_mds_tree +from astrohack.utils.file import overwrite_file +from astrohack.core.holog_2 import process_holog_chunk from astrohack.utils.text import get_default_file_name -from astrohack.io.mds import AstrohackImageFile +from astrohack.io.image_mds import AstrohackImageFile Array = NewType("Array", Union[np.array, List[int], List[float]]) -@toolviper.utils.parameter.validate() +# @toolviper.utils.parameter.validate() def holog( holog_name: str, grid_size: Union[int, Array, List] = None, @@ -160,8 +156,6 @@ def holog( """ - check_if_file_can_be_opened(holog_name, "0.7.2") - # Doing this here allows it to get captured by locals() if image_name is None: image_name = get_default_file_name( @@ -170,61 +164,24 @@ def holog( holog_params = locals() - input_params = holog_params.copy() - assert pathlib.Path(holog_params["holog_name"]).exists() is True, logger.error( - f'File {holog_params["holog_name"]} does not exists.' - ) - - overwrite_file(holog_params["image_name"], holog_params["overwrite"]) - - json_data = "/".join((holog_params["holog_name"], ".holog_json")) - - with open(json_data, "r") as json_file: - holog_json = json.load(json_file) - - if compute_graph( - holog_json, process_holog_chunk, holog_params, ["ant", "ddi"], parallel=parallel - ): - - output_attr_file = "{name}/{ext}".format( - name=holog_params["image_name"], ext=".image_attr" - ) - write_meta_data(output_attr_file, holog_params) - - output_attr_file = "{name}/{ext}".format( - name=holog_params["image_name"], ext=".image_input" - ) - write_meta_data(output_attr_file, input_params) - - image_mds = AstrohackImageFile(holog_params["image_name"]) - image_mds.open() + holog_mds = open_holog(holog_name) - logger.info("Finished processing") + overwrite_file(image_name, overwrite) + image_mds = AstrohackImageFile.create_from_input_parameters( + image_name, holog_params + ) - return image_mds + executed_graph = compute_graph_to_mds_tree( + holog_mds, + process_holog_chunk, + holog_params, + ["ant", "ddi"], + holog_mds, + ) + if executed_graph: + holog_mds.write(mode="a") + return holog_mds else: logger.warning("No data to process") return None - - -def _convert_gridding_parameter( - gridding_parameter: Union[List, Array], reflect_on_axis=False -) -> np.ndarray: - if isinstance(gridding_parameter, Number): - gridding_parameter = np.array( - [np.power(-1, reflect_on_axis) * gridding_parameter, gridding_parameter] - ) - - elif isinstance(gridding_parameter, list): - gridding_parameter = np.array(gridding_parameter) - - elif isinstance(gridding_parameter, np.ndarray): - pass - - else: - logger.error( - "Unknown dtype for gridding parameter: {}".format(gridding_parameter) - ) - - return gridding_parameter From 75725ba428530b4b3be9c40710629d17dc9bc6db Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 3 Feb 2026 15:05:40 -0700 Subject: [PATCH 072/295] Changed order of parameters in holog, image_name is now the second parameter. --- src/astrohack/holog_2.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/astrohack/holog_2.py b/src/astrohack/holog_2.py index 9ddb607b..c621fe3a 100644 --- a/src/astrohack/holog_2.py +++ b/src/astrohack/holog_2.py @@ -17,9 +17,9 @@ # @toolviper.utils.parameter.validate() def holog( holog_name: str, + image_name: str = None, grid_size: Union[int, Array, List] = None, cell_size: Union[float, Array, List] = None, - image_name: str = None, padding_factor: int = 10, grid_interpolation_mode: str = "gaussian", chan_average: bool = True, @@ -40,6 +40,10 @@ def holog( :param holog_name: Name of holography .holog.zarr file to process. :type holog_name: str + :param image_name: Defines the name of the output image name. If value is None, the name will be set to \ + .image.zarr, defaults to None + :type image_name: str, optional + :param grid_size: Numpy array specifying the dimensions of the grid used in data gridding. If not specified \ grid_size is calculated using POINTING_OFFSET in pointing table. :type grid_size: numpy.ndarray, dtype int, list optional @@ -48,10 +52,6 @@ def holog( cell_size is the one given in the observation_summary of the input holog file. :type cell_size: numpy.ndarray, dtype float, list optional - :param image_name: Defines the name of the output image name. If value is None, the name will be set to \ - .image.zarr, defaults to None - :type image_name: str, optional - :param padding_factor: Padding factor applied to beam grid before computing the fast-fourier transform. The default\ has been set for operation on most systems. The user should be aware of memory constraints before increasing this\ parameter significantly., defaults to 10 From bcc152c12255b2dd4cb59d2c72cf8cbdbe07d128 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 3 Feb 2026 15:32:06 -0700 Subject: [PATCH 073/295] Fixed a typo --- src/astrohack/utils/imaging.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/astrohack/utils/imaging.py b/src/astrohack/utils/imaging.py index e15318db..2cf9b3b3 100644 --- a/src/astrohack/utils/imaging.py +++ b/src/astrohack/utils/imaging.py @@ -78,7 +78,7 @@ def parallactic_derotation(data, parallactic_angle_dict): # Find the middle index of the array. This is calculated because there might be a desire to change # the array length at some point and I don't want to hard code the middle value. # - # It is assumed, and should be true, that the parallacitc angle array size is consistent over map. + # It is assumed, and should be true, that the parallactic angle array size is consistent over map. maps = list(parallactic_angle_dict.keys()) # Get the median index for the first map (this should be the same for every map). From 5e7a307692d2412db4b9c936c07863ded06caf54 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 3 Feb 2026 15:33:23 -0700 Subject: [PATCH 074/295] Created grid_beam_2 to use with the new holog. --- src/astrohack/utils/gridding.py | 128 ++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) diff --git a/src/astrohack/utils/gridding.py b/src/astrohack/utils/gridding.py index 7f15b9f4..330e88c1 100644 --- a/src/astrohack/utils/gridding.py +++ b/src/astrohack/utils/gridding.py @@ -1,5 +1,6 @@ import time import numpy as np +import xarray from toolviper.utils import logger as logger from scipy.interpolate import griddata from numba import njit @@ -22,6 +23,133 @@ ) +def grid_beam_2( + ant_ddi_xdt: xarray.DataTree, + grid_size, + sky_cell_size, + avg_chan, + chan_tol_fac, + telescope, + grid_interpolation_mode, + observation_summary, + label, +): + """ + Grids the visibilities onto a 2D plane based on their Sky coordinates, using scipy griddata or a gaussian + convolution + Args: + ant_ddi_xdt: Xarray DataTree containing the visibilities + grid_size: The size of the beam image grid (pixels) + sky_cell_size: Size of the beam grid cell in the sky (radians) + avg_chan: Average cahnnels? (boolean) + chan_tol_fac: Frequency tolerance to chunk channels together + telescope: Telescope object containing optical description of the telescope + grid_interpolation_mode: linear, nearest, cubic or gaussian (convolution) + observation_summary: Dictionaty containing a summary of observation information. + label: label to be used in messages + + Returns: + The gridded beam, its time centroid, frequency axis, polarization axis, L and M axes and a boolean about the + necessity of gridding corrections after fourier transform. + """ + + n_holog_map = len(ant_ddi_xdt.keys()) + map_0_key = list(ant_ddi_xdt.keys())[0] + freq_axis = ant_ddi_xdt[map_0_key].chan.values + pol_axis = ant_ddi_xdt[map_0_key].pol.values + n_chan = ant_ddi_xdt[map_0_key].sizes["chan"] + n_pol = ant_ddi_xdt[map_0_key].sizes["pol"] + + observation_summary["beam"]["grid size"] = [int(grid_size[0]), int(grid_size[1])] + observation_summary["beam"]["cell size"] = [sky_cell_size[0], sky_cell_size[1]] + + reference_scaling_frequency = np.mean(freq_axis) + if avg_chan: + n_chan = 1 + avg_chan_map, avg_freq_axis = _create_average_chan_map(freq_axis, chan_tol_fac) + output_freq_axis = [np.mean(avg_freq_axis)] + observation_summary["spectral"]["channel width"] *= observation_summary[ + "spectral" + ]["number of channels"] + observation_summary["spectral"]["number of channels"] = 1 + else: + avg_chan_map = None + avg_freq_axis = None + output_freq_axis = freq_axis + l_axis, m_axis, l_grid, m_grid, beam_grid = _create_beam_grid( + grid_size, sky_cell_size, n_chan, n_pol, n_holog_map + ) + scipy_interp = ["linear", "nearest", "cubic"] + + time_centroid = [] + grid_corr = False + for holog_map_index, map_xdt in enumerate(ant_ddi_xdt.values()): + # Grid the data + vis = map_xdt.VIS.values + vis[vis == np.nan] = 0.0 + lm = map_xdt.DIRECTIONAL_COSINES.values + weight = map_xdt.WEIGHT.values + + if avg_chan: + vis_avg, weight_sum = chunked_average( + vis, weight, avg_chan_map, avg_freq_axis + ) + lm_freq_scaled = lm[:, :, None] * ( + avg_freq_axis / reference_scaling_frequency + ) + else: + vis_avg = vis + weight_sum = weight + lm_freq_scaled = lm[:, :, None] * np.full_like(freq_axis, 1.0) + + if grid_interpolation_mode in scipy_interp: + beam_grid[holog_map_index, ...] = _scipy_gridding( + vis_avg, + lm_freq_scaled, + l_grid, + m_grid, + grid_interpolation_mode, + avg_chan, + label, + ) + elif grid_interpolation_mode == "gaussian": + grid_corr = True + beam_grid[holog_map_index, ...] = _convolution_gridding( + vis_avg, + weight_sum, + lm_freq_scaled, + telescope.diameter, + l_axis, + m_axis, + sky_cell_size, + reference_scaling_frequency, + avg_chan, + label, + ) + else: + msg = f"Unknown grid type {grid_interpolation_mode}." + logger.error(msg) + raise Exception(msg) + + time_centroid_index = map_xdt.sizes["time"] // 2 + time_centroid.append(map_xdt.coords["time"][time_centroid_index].values) + + beam_grid[holog_map_index, ...] = _normalize_beam( + beam_grid[holog_map_index, ...], n_chan, pol_axis + ) + + return ( + beam_grid, + time_centroid, + output_freq_axis, + pol_axis, + l_axis, + m_axis, + grid_corr, + observation_summary, + ) + + def grid_beam( ant_ddi_dict, grid_size, From 553732fe22d33b4f14b392e03262f31a0a9b38d1 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 3 Feb 2026 15:41:38 -0700 Subject: [PATCH 075/295] Fixed output file. --- src/astrohack/holog_2.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/astrohack/holog_2.py b/src/astrohack/holog_2.py index c621fe3a..34318ff3 100644 --- a/src/astrohack/holog_2.py +++ b/src/astrohack/holog_2.py @@ -176,12 +176,12 @@ def holog( process_holog_chunk, holog_params, ["ant", "ddi"], - holog_mds, + image_mds, ) if executed_graph: - holog_mds.write(mode="a") - return holog_mds + image_mds.write(mode="a") + return image_mds else: logger.warning("No data to process") return None From 6b490bfa5981494cceec8e4bb25f01f7e475e737 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 3 Feb 2026 15:42:08 -0700 Subject: [PATCH 076/295] core/holog_2.py ported to use the new data formats. --- src/astrohack/core/holog_2.py | 67 ++++++++++++++++------------------- 1 file changed, 31 insertions(+), 36 deletions(-) diff --git a/src/astrohack/core/holog_2.py b/src/astrohack/core/holog_2.py index ea2d5a61..743bb837 100644 --- a/src/astrohack/core/holog_2.py +++ b/src/astrohack/core/holog_2.py @@ -1,18 +1,18 @@ import numpy as np import xarray as xr -from astrohack.utils import format_angular_distance +from copy import deepcopy +from astrohack.utils import format_angular_distance, print_dict_types from astrohack.antenna.telescope import get_proper_telescope, RingedCassegrain from astrohack.utils.text import create_dataset_label from astrohack.utils.conversion import convert_5d_grid_to_stokes from astrohack.utils.algorithms import phase_wrapping from astrohack.utils.zernike_aperture_fitting import fit_zernike_coefficients -from astrohack.utils.file import load_holog_file from astrohack.utils.imaging import ( calculate_far_field_aperture, calculate_near_field_aperture, ) -from astrohack.utils.gridding import grid_beam +from astrohack.utils.gridding import grid_beam_2 from astrohack.utils.imaging import parallactic_derotation from astrohack.utils.phase_fitting import ( clic_like_phase_fitting, @@ -23,31 +23,26 @@ import toolviper.utils.logger as logger -def process_holog_chunk(holog_chunk_params): +def process_holog_chunk(holog_chunk_params, output_mds): """Process chunk holography data along the antenna axis. Works with holography file to properly grid , normalize, average and correct data and returns the aperture pattern. Args: holog_chunk_params (dict): Dictionary containing holography parameters. """ - holog_file, ant_data_dict = load_holog_file( - holog_chunk_params["holog_name"], - dask_load=False, - load_pnt_dict=False, - ant_id=holog_chunk_params["this_ant"], - ddi_id=holog_chunk_params["this_ddi"], - ) - label = create_dataset_label( - holog_chunk_params["this_ant"], holog_chunk_params["this_ddi"], separator="," - ) + + ant_key = holog_chunk_params["this_ant"] + ddi_key = holog_chunk_params["this_ddi"] + ant_ddi_xdt = holog_chunk_params["xdt_data"] + + label = create_dataset_label(ant_key, ddi_key, separator=",") logger.info(f"Processing {label}") - ddi = holog_chunk_params["this_ddi"] + + summary = deepcopy(ant_ddi_xdt["map_0"].attrs["summary"]) + convert_to_stokes = holog_chunk_params["to_stokes"] - ref_xds = ant_data_dict[ddi]["map_0"] - summary = ref_xds.attrs["summary"] user_grid_size = holog_chunk_params["grid_size"] - if user_grid_size is None: grid_size = np.array(summary["beam"]["grid size"]) elif isinstance(user_grid_size, int): @@ -58,7 +53,6 @@ def process_holog_chunk(holog_chunk_params): raise Exception( f"Don't know what due with grid size of type {type(user_grid_size)}" ) - logger.info( f"{label}: Using a grid of {grid_size[0]} by {grid_size[1]} pixels for the beam" ) @@ -86,7 +80,7 @@ def process_holog_chunk(holog_chunk_params): summary["general"]["telescope name"], summary["general"]["antenna name"] ) try: - is_near_field = ref_xds.attrs["near_field"] + is_near_field = ant_ddi_xdt["map_0"].attrs["near_field"] except KeyError: is_near_field = False @@ -99,8 +93,8 @@ def process_holog_chunk(holog_chunk_params): m_axis, grid_corr, summary, - ) = grid_beam( - ant_ddi_dict=ant_data_dict[ddi], + ) = grid_beam_2( + ant_ddi_xdt=ant_ddi_xdt, grid_size=grid_size, sky_cell_size=cell_size, avg_chan=holog_chunk_params["chan_average"], @@ -110,10 +104,9 @@ def process_holog_chunk(holog_chunk_params): observation_summary=summary, label=label, ) - if not is_near_field: beam_grid = parallactic_derotation( - data=beam_grid, parallactic_angle_dict=ant_data_dict[ddi] + data=beam_grid, parallactic_angle_dict=ant_ddi_xdt ) if holog_chunk_params["scan_average"]: @@ -226,15 +219,14 @@ def process_holog_chunk(holog_chunk_params): summary["aperture"] = _get_aperture_summary( u_axis, v_axis, _compute_aperture_resolution(l_axis, m_axis, used_wavelength) ) - _export_to_xds( beam_grid, aperture_grid, amplitude, phase_corrected_angle, - holog_chunk_params["this_ant"], + ant_key, time_centroid, - ddi, + ddi_key, phase_fit_results, pol_axis, freq_axis, @@ -250,8 +242,9 @@ def process_holog_chunk(holog_chunk_params): zernike_model, zernike_rms, zernike_n_order, - holog_chunk_params["image_name"], summary, + output_mds, + holog_chunk_params["parallel"], ) logger.info(f"Finished processing {label}") @@ -311,9 +304,9 @@ def _export_to_xds( aperture_grid, amplitude, phase_corrected_angle, - ant_id, + ant_key, time_centroid, - ddi, + ddi_key, phase_fit_results, pol_axis, freq_axis, @@ -329,10 +322,11 @@ def _export_to_xds( zernike_model, zernike_rms, zernike_n_order, - image_name, summary, + output_mds, + parallel, ): - # Todo: Add Paralactic angle as a non-dimension coordinate dependant on time. + # Todo: Add Parallactic angle as a non-dimension coordinate dependant on time. xds = xr.Dataset() xds["BEAM"] = xr.DataArray(beam_grid, dims=["time", "chan", "pol", "l", "m"]) @@ -357,9 +351,7 @@ def _export_to_xds( zernike_rms, dims=["time", "chan", "orig_pol"] ) - xds.attrs["ant_id"] = ant_id xds.attrs["time_centroid"] = np.array(time_centroid) - xds.attrs["ddi"] = ddi xds.attrs["phase_fitting"] = phase_fit_results xds.attrs["zernike_N_order"] = zernike_n_order xds.attrs["summary"] = summary @@ -377,8 +369,11 @@ def _export_to_xds( "osa": osa_coeff_list, } xds = xds.assign_coords(coords) - xds.to_zarr( - f"{image_name}/{ant_id}/{ddi}", mode="w", compute=True, consolidated=True + dataset_name = f"{ant_key}-{ddi_key}" + output_mds.add_node_to_tree( + xr.DataTree(name=dataset_name, dataset=xds), + dump_to_disk=True, + running_in_parallel=parallel, ) From 2b34b003b4e9d1aa0b392084a74aebfa9df7c37a Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 3 Feb 2026 15:58:27 -0700 Subject: [PATCH 077/295] check_if_file_can_be_opened_2 now supports multiple creators, this was done to support image mdses that might be created by holog or combine, also implemented open_image with the new data format. --- src/astrohack/io/dio.py | 16 ++++++++-------- src/astrohack/utils/file.py | 6 ++++-- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/astrohack/io/dio.py b/src/astrohack/io/dio.py index 1abe93f4..8e37edf0 100644 --- a/src/astrohack/io/dio.py +++ b/src/astrohack/io/dio.py @@ -11,7 +11,7 @@ check_if_file_can_be_opened, check_if_file_can_be_opened_2, ) -from astrohack.io.mds import AstrohackImageFile +from astrohack.io.image_mds import AstrohackImageFile from astrohack.io.holog_mds import AstrohackHologFile from astrohack.io.mds import AstrohackPanelFile from astrohack.io.point_mds import AstrohackPointFile @@ -84,17 +84,17 @@ def open_holog(file: str) -> Union[AstrohackHologFile, None]: .. parsed-literal:: holog_mds = { - ddi_0:{ - map_0:{ - ant_0: holog_ds, + ant_0:{ + ddi_0:{ + map_0: holog_ds, ⋮ - ant_n: holog_ds + map_n: holog_ds }, ⋮ - map_p: … + ddi_p: … }, ⋮ - ddi_m: … + ant_m: … } """ check_if_file_can_be_opened_2(file, "extract_holog", "0.10.1") @@ -136,7 +136,7 @@ def open_image(file: str) -> Union[AstrohackImageFile, None]: } """ - check_if_file_can_be_opened(file, "0.7.2") + check_if_file_can_be_opened_2(file, ["holog", "combine"], "0.10.1") _data_file = AstrohackImageFile(file=file) if _data_file.open(): diff --git a/src/astrohack/utils/file.py b/src/astrohack/utils/file.py index 70d905cf..8c8f3d80 100644 --- a/src/astrohack/utils/file.py +++ b/src/astrohack/utils/file.py @@ -79,9 +79,11 @@ def check_if_file_can_be_opened_2(filename, file_creator, minimal_version): if origin_info["origin"] != "astrohack": raise ValueError(f"{filename} was not created by astrohack") - if origin_info["creator_function"] != file_creator: + if not isinstance(file_creator, list): + file_creator = [file_creator] + if origin_info["creator_function"] not in file_creator: raise ValueError( - f'{filename} was created by {origin_info["creator_function"]} but {file_creator} was expected' + f'{filename} was created by {origin_info["creator_function"]} but {" or ".join(file_creator)} was expected' ) file_version = origin_info["version"] From ff223c9bc4b7736566b5febc3310d83aed96de7a Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 3 Feb 2026 16:05:25 -0700 Subject: [PATCH 078/295] ported image_mds.export_to_fits method to work with the new image_mds class. --- src/astrohack/io/image_mds.py | 538 ++++++++++++++++++++++++++++++++++ 1 file changed, 538 insertions(+) diff --git a/src/astrohack/io/image_mds.py b/src/astrohack/io/image_mds.py index 9b9974b8..59ab8872 100644 --- a/src/astrohack/io/image_mds.py +++ b/src/astrohack/io/image_mds.py @@ -1,4 +1,21 @@ +import pathlib +import numpy as np + +from typing import List, Union + +import toolviper.utils.logger as logger + from astrohack.io.base_mds import AstrohackBaseFile +from astrohack.utils.graph import compute_graph +from astrohack.utils.constants import clight +from astrohack.utils.conversion import convert_unit +from astrohack.utils.fits import ( + write_fits, + add_prefix, + put_axis_in_fits_header, + put_stokes_axis_in_fits_header, + put_resolution_in_fits_header, +) class AstrohackImageFile(AstrohackBaseFile): @@ -17,3 +34,524 @@ def __init__(self, file: str): :rtype: AstrohackImageFile """ super().__init__(file=file) + + # @toolviper.utils.parameter.validate(custom_checker=custom_split_checker) + def export_to_fits( + self, + destination: str, + complex_split: str = "cartesian", + ant: Union[str, List[str]] = "all", + ddi: Union[str, int, List[int]] = "all", + parallel: bool = False, + ) -> None: + """Export contents of an AstrohackImageFile object to several FITS files in the destination folder + + :param destination: Name of the destination folder to contain plots + :type destination: str + + :param complex_split: How to split complex data, cartesian (real + imag, default) or polar (amplitude + phase) + :type complex_split: str, optional + + :param ant: List of antennas/antenna to be plotted, defaults to "all" when None, ex. ea25 + :type ant: list or str, optional + + :param ddi: List of ddi to be plotted, defaults to "all" when None, ex. 0 + :type ddi: list or int, optional + + :param parallel: If True will use an existing astrohack client to export FITS in parallel, default is False + :type parallel: bool, optional + + .. _Description: + Export the products from the holog mds onto FITS files to be read by other software packages + + **Additional Information** + The image products of holog are complex images due to the nature of interferometric measurements and Fourier + transforms, currently complex128 FITS files are not supported by astropy, hence the need to split complex images + onto two real image products, we present the user with two options to carry out this split. + + .. rubric:: Available complex splitting possibilities: + - *cartesian*: Split is done to a real part and an imaginary part FITS files + - *polar*: Split is done to an amplitude and a phase FITS files + + + The FITS files produced by this function have been tested and are known to work with CARTA and DS9 + """ + + param_dict = locals() + pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) + param_dict["input_params"] = self.root.attrs["input_parameters"] + compute_graph( + self, + _export_to_fits_chunk, + param_dict, + ["ant", "ddi"], + parallel=parallel, + ) + + # @toolviper.utils.parameter.validate(custom_checker=custom_plots_checker) + # def plot_apertures( + # self, + # destination: str, + # ant: Union[str, List[str]] = "all", + # ddi: Union[str, int, List[int]] = "all", + # polarization_state: Union[str, List[str]] = "I", + # plot_screws: bool = False, + # amplitude_limits: Union[List[float], Tuple, np.array] = None, + # phase_unit: str = "deg", + # phase_limits: Union[List[float], Tuple, np.array] = None, + # deviation_unit: str = "mm", + # deviation_limits: Union[List[float], Tuple, np.array] = None, + # panel_labels: bool = False, + # display: bool = False, + # colormap: str = "viridis", + # figure_size: Union[Tuple, List[float], np.array] = None, + # dpi: int = 300, + # parallel: bool = False, + # ) -> None: + # """ Aperture amplitude and phase plots from the data in an AstrohackImageFIle object. + # + # :param destination: Name of the destination folder to contain plots + # :type destination: str + # :param ant: List of antennas/antenna to be plotted, defaults to "all" when None, ex. ea25 + # :type ant: list or str, optional + # :param ddi: List of ddis/ddi to be plotted, defaults to "all" when None, ex. 0 + # :type ddi: list or int, optional + # :param polarization_state: List of polarization states/ polarization state to be plotted, defaults to "I" + # :type polarization_state: list or str, optional + # :param plot_screws: Add screw positions to plot, default is False + # :type plot_screws: bool, optional + # :param amplitude_limits: Lower than Upper limit for amplitude in volts default is None (Guess from data) + # :type amplitude_limits: numpy.ndarray, list, tuple, optional + # :param phase_unit: Unit for phase plots, defaults is 'deg' + # :type phase_unit: str, optional + # :param phase_limits: Lower than Upper limit for phase, value in phase_unit, default is None (Guess from data) + # :type phase_limits: numpy.ndarray, list, tuple, optional + # :param deviation_unit: Unit for deviation plots, defaults is 'mm' + # :type deviation_unit: str, optional + # :param deviation_limits: Lower than Upper limit for deviation, value in deviation_unit, default is None (Guess\ + # from data) + # :type deviation_limits: numpy.ndarray, list, tuple, optional + # :param panel_labels: Add panel labels to antenna surface plots, default is False + # :type panel_labels: bool, optional + # :param display: Display plots inline or suppress, defaults to True + # :type display: bool, optional + # :param colormap: Colormap for plots, default is viridis + # :type colormap: str, optional + # :param figure_size: 2 element array/list/tuple with the plot sizes in inches + # :type figure_size: numpy.ndarray, list, tuple, optional + # :param dpi: dots per inch to be used in plots, default is 300 + # :type dpi: int, optional + # :param parallel: If True will use an existing astrohack client to produce plots in parallel, default is False + # :type parallel: bool, optional + # + # .. _Description: + # + # Produce plots from ``astrohack.holog`` results for analysis + # """ + # param_dict = locals() + # + # pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) + # compute_graph( + # self, plot_aperture_chunk, param_dict, ["ant", "ddi"], parallel=parallel + # ) + # + # @toolviper.utils.parameter.validate(custom_checker=custom_plots_checker) + # def plot_beams( + # self, + # destination: str, + # ant: Union[str, List[str]] = "all", + # ddi: Union[str, int, List[int]] = "all", + # complex_split: str = "polar", + # angle_unit: str = "deg", + # phase_unit: str = "deg", + # display: bool = False, + # colormap: str = "viridis", + # figure_size: Union[Tuple, List[float], np.array] = (8, 4.5), + # dpi: int = 300, + # parallel: bool = False, + # ) -> None: + # """ Beam plots from the data in an AstrohackImageFIle object. + # + # :param destination: Name of the destination folder to contain plots + # :type destination: str + # :param ant: List of antennas/antenna to be plotted, defaults to "all" when None, ex. ea25 + # :type ant: list or str, optional + # :param ddi: List of ddis/ddi to be plotted, defaults to "all" when None, ex. 0 + # :type ddi: list or int, optional + # :param angle_unit: Unit for L and M axes in plots, default is 'deg'. + # :type angle_unit: str, optional + # :param complex_split: How to split complex beam data, cartesian (real + imag) or polar (amplitude + phase, \ + # default) + # :type complex_split: str, optional + # :param phase_unit: Unit for phase in 'polar' plots, default is 'deg'. + # :type phase_unit: str + # :param display: Display plots inline or suppress, defaults to True + # :type display: bool, optional + # :param colormap: Colormap for plots, default is viridis + # :type colormap: str, optional + # :param figure_size: 2 element array/list/tuple with the plot sizes in inches + # :type figure_size: numpy.ndarray, list, tuple, optional + # :param dpi: dots per inch to be used in plots, default is 300 + # :type dpi: int, optional + # :param parallel: If True will use an existing astrohack client to produce plots in parallel, default is False + # :type parallel: bool, optional + # + # .. _Description: + # + # Produce plots from ``astrohack.holog`` results for analysis + # """ + # param_dict = locals() + # + # pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) + # compute_graph( + # self, plot_beam_chunk, param_dict, ["ant", "ddi"], parallel=parallel + # ) + # + # @toolviper.utils.parameter.validate(custom_checker=custom_unit_checker) + # def export_phase_fit_results( + # self, + # destination: str, + # ant: Union[str, List[str]] = "all", + # ddi: Union[str, int, List[int]] = "all", + # angle_unit: str = "deg", + # length_unit: str = "mm", + # parallel: bool = False, + # ) -> None: + # """Export perturbations phase fit results from the data in an AstrohackImageFIle object to ASCII files. + # + # :param destination: Name of the destination folder to contain ASCII files + # :type destination: str + # :param ant: List of antennas/antenna to be exported, defaults to "all" when None, ex. ea25 + # :type ant: list or str, optional + # :param ddi: List of ddis/ddi to be exported, defaults to "all" when None, ex. 0 + # :type ddi: list or int, optional + # :param angle_unit: Unit for results that are angles. + # :type angle_unit: str, optional + # :param length_unit: Unit for results that are displacements. + # :type length_unit: str, optional + # :param parallel: If True will use an existing astrohack client to produce ASCII files in parallel, default is False + # :type parallel: bool, optional + # + # .. _Description: + # + # Export the results of the phase fitting process in ``astrohack.holog`` for analysis + # """ + # param_dict = locals() + # + # pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) + # compute_graph( + # self, export_phase_fit_chunk, param_dict, ["ant", "ddi"], parallel=parallel + # ) + # + # @toolviper.utils.parameter.validate() + # def export_zernike_fit_results( + # self, + # destination: str, + # ant: Union[str, List[str]] = "all", + # ddi: Union[str, int, List[int]] = "all", + # parallel: bool = False, + # ) -> None: + # """Export Zernike coefficients from the data in an AstrohackImageFIle object to ASCII files. + # + # :param destination: Name of the destination folder to contain ASCII files + # :type destination: str + # :param ant: List of antennas/antenna to be exported, defaults to "all" when None, ex. ea25 + # :type ant: list or str, optional + # :param ddi: List of ddis/ddi to be exported, defaults to "all" when None, ex. 0 + # :type ddi: list or int, optional + # :param parallel: If True will use an existing astrohack client to produce ASCII files in parallel, default is False + # :type parallel: bool, optional + # + # .. _Description: + # + # Export Zernike coefficients from the AstrohackImageFile object obtained during processing in \ + # ``astrohack.holog`` for analysis. + # """ + # param_dict = locals() + # + # pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) + # compute_graph( + # self, + # export_zernike_fit_chunk, + # param_dict, + # ["ant", "ddi"], + # parallel=parallel, + # ) + # + # @toolviper.utils.parameter.validate(custom_checker=custom_plots_checker) + # def plot_zernike_model( + # self, + # destination: str, + # ant: Union[str, List[str]] = "all", + # ddi: Union[str, int, List[int]] = "all", + # display: bool = False, + # colormap: str = "viridis", + # figure_size: Union[Tuple, List[float], np.array] = (16, 9), + # dpi: int = 300, + # parallel: bool = False, + # ) -> None: + # """Plot Zernike models from the data in an AstrohackImageFile object. + # + # :param destination: Name of the destination folder to contain the model plots + # :type destination: str + # :param ant: List of antennas/antenna to be exported, defaults to "all" when None, ex. ea25 + # :type ant: list or str, optional + # :param ddi: List of ddis/ddi to be exported, defaults to "all" when None, ex. 0 + # :type ddi: list or int, optional + # :param display: Display plots inline or suppress, defaults to True + # :type display: bool, optional + # :param colormap: Colormap for plots, default is viridis + # :type colormap: str, optional + # :param figure_size: 2 element array/list/tuple with the plot sizes in inches + # :type figure_size: numpy.ndarray, list, tuple, optional + # :param dpi: dots per inch to be used in plots, default is 300 + # :type dpi: int, optional + # :param parallel: If True will use an existing astrohack client to produce plots in parallel, default is False + # :type parallel: bool, optional + # + # .. _Description: + # + # Export Zernike coefficients from the AstrohackImageFile object obtained during processing in \ + # ``astrohack.holog`` for analysis. + # """ + # param_dict = locals() + # + # pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) + # compute_graph( + # self, + # plot_zernike_model_chunk, + # param_dict, + # ["ant", "ddi"], + # parallel=parallel, + # ) + # + # @toolviper.utils.parameter.validate(custom_checker=custom_unit_checker) + # def observation_summary( + # self, + # summary_file: str, + # ant: Union[str, List[str]] = "all", + # ddi: Union[str, int, List[int]] = "all", + # az_el_key: str = "center", + # phase_center_unit: str = "radec", + # az_el_unit: str = "deg", + # time_format: str = "%d %h %Y, %H:%M:%S", + # tab_size: int = 3, + # print_summary: bool = True, + # parallel: bool = False, + # ) -> None: + # """ Create a Summary of observation information + # + # :param summary_file: Text file to put the observation summary + # :type summary_file: str + # :param ant: antenna ID to use in subselection, defaults to "all" when None, ex. ea25 + # :type ant: list or str, optional + # :param ddi: data description ID to use in subselection, defaults to "all" when None, ex. 0 + # :type ddi: list or int, optional + # :param az_el_key: What type of Azimuth & Elevation information to print, 'mean', 'median' or 'center', default\ + # is 'center' + # :type az_el_key: str, optional + # :param phase_center_unit: What unit to display phase center coordinates, 'radec' and angle units supported, \ + # default is 'radec' + # :type phase_center_unit: str, optional + # :param az_el_unit: Angle unit used to display Azimuth & Elevation information, default is 'deg' + # :type az_el_unit: str, optional + # :param time_format: datetime time format for the start and end dates of observation, default is \ + # "%d %h %Y, %H:%M:%S" + # :type time_format: str, optional + # :param tab_size: Number of spaces in the tab levels, default is 3 + # :type tab_size: int, optional + # :param print_summary: Print the summary at the end of execution, default is True + # :type print_summary: bool, optional + # :param parallel: Run in parallel, defaults to False + # :type parallel: bool, optional + # + # **Additional Information** + # + # This method produces a summary of the data in the AstrohackImageFile displaying general information, + # spectral information, beam image characteristics and aperture image characteristics. + # """ + # + # param_dict = locals() + # key_order = ["ant", "ddi"] + # execution, summary = compute_graph( + # self, + # generate_observation_summary, + # param_dict, + # key_order, + # parallel, + # fetch_returns=True, + # ) + # summary = "".join(summary) + # with open(summary_file, "w") as output_file: + # output_file.write(summary) + # if print_summary: + # print(summary) + + +def _export_to_fits_chunk(param_dict): + """ + Holog side chunk function for the user facing function export_to_fits + Args: + param_dict: parameter dictionary + """ + input_xds = param_dict["xdt_data"] + input_params = param_dict["input_params"] + antenna = param_dict["this_ant"] + ddi = param_dict["this_ddi"] + destination = param_dict["destination"] + basename = f"{destination}/{antenna}_{ddi}" + + logger.info( + f"Exporting image contents of {antenna} {ddi} to FITS files in {destination}" + ) + + aperture_resolution = input_xds.attrs["summary"]["aperture"]["cell size"] + + nchan = len(input_xds.chan) + + if nchan == 1: + reffreq = input_xds.chan.values[0] + + else: + reffreq = input_xds.chan.values[nchan // 2] + + telname = input_xds.attrs["summary"]["general"]["telescope name"] + + if telname in ["EVLA", "VLA", "JVLA"]: + telname = "VLA" + + polist = [] + + for pol in input_xds.pol: + polist.append(str(pol.values)) + + base_header = { + "STOKES": ", ".join(polist), + "WAVELENG": clight / reffreq, + "FREQUENC": reffreq, + "TELESCOP": input_xds.attrs["summary"]["general"]["antenna name"], + "INSTRUME": telname, + "TIME_CEN": input_xds.attrs["time_centroid"], + "PADDING": input_params["padding_factor"], + "GRD_INTR": input_params["grid_interpolation_mode"], + "CHAN_AVE": "yes" if input_params["chan_average"] is True else "no", + "CHAN_TOL": input_params["chan_tolerance_factor"], + "SCAN_AVE": "yes" if input_params["scan_average"] is True else "no", + "TO_STOKE": "yes" if input_params["to_stokes"] is True else "no", + } + + ntime = len(input_xds.time) + if ntime != 1: + raise Exception("Data with multiple times not supported for FITS export") + + base_header = put_axis_in_fits_header( + base_header, input_xds.chan.values, 3, "Frequency", "Hz" + ) + base_header = put_stokes_axis_in_fits_header(base_header, 4) + rad_to_deg = convert_unit("rad", "deg", "trigonometric") + beam_header = put_axis_in_fits_header( + base_header, -input_xds.l.values * rad_to_deg, 1, "RA---SIN", "deg" + ) + beam_header = put_axis_in_fits_header( + beam_header, input_xds.m.values * rad_to_deg, 2, "DEC--SIN", "deg" + ) + beam_header["RADESYSA"] = "FK5" + beam = input_xds["BEAM"].values + if param_dict["complex_split"] == "cartesian": + write_fits( + beam_header, + "Complex beam real part", + beam.real, + add_prefix(basename, "beam_real") + ".fits", + "Normalized", + "image", + ) + write_fits( + beam_header, + "Complex beam imag part", + beam.imag, + add_prefix(basename, "beam_imag") + ".fits", + "Normalized", + "image", + ) + else: + write_fits( + beam_header, + "Complex beam amplitude", + np.absolute(beam), + add_prefix(basename, "beam_amplitude") + ".fits", + "Normalized", + "image", + ) + write_fits( + beam_header, + "Complex beam phase", + np.angle(beam), + add_prefix(basename, "beam_phase") + ".fits", + "Radians", + "image", + ) + wavelength = clight / input_xds.chan.values[0] + aperture_header = put_axis_in_fits_header( + base_header, input_xds.u.values * wavelength, 1, "X----LIN", "m" + ) + aperture_header = put_axis_in_fits_header( + aperture_header, input_xds.u.values * wavelength, 2, "Y----LIN", "m" + ) + aperture_header = put_resolution_in_fits_header( + aperture_header, aperture_resolution + ) + aperture = input_xds["APERTURE"].values + if param_dict["complex_split"] == "cartesian": + write_fits( + aperture_header, + "Complex aperture real part", + aperture.real, + add_prefix(basename, "aperture_real") + ".fits", + "Normalized", + "image", + ) + write_fits( + aperture_header, + "Complex aperture imag part", + aperture.imag, + add_prefix(basename, "aperture_imag") + ".fits", + "Normalized", + "image", + ) + else: + write_fits( + aperture_header, + "Complex aperture amplitude", + np.absolute(aperture), + add_prefix(basename, "aperture_amplitude") + ".fits", + "Normalized", + "image", + ) + write_fits( + aperture_header, + "Complex aperture phase", + np.angle(aperture), + add_prefix(basename, "aperture_phase") + ".fits", + "rad", + "image", + ) + + phase_amp_header = put_axis_in_fits_header( + base_header, input_xds.u_prime.values * wavelength, 1, "X----LIN", "m" + ) + phase_amp_header = put_axis_in_fits_header( + phase_amp_header, input_xds.v_prime.values * wavelength, 2, "Y----LIN", "m" + ) + phase_amp_header = put_resolution_in_fits_header( + phase_amp_header, aperture_resolution + ) + write_fits( + phase_amp_header, + "Cropped aperture corrected phase", + input_xds["CORRECTED_PHASE"].values, + add_prefix(basename, "corrected_phase") + ".fits", + "rad", + "image", + ) + return From 2184393a883f763ffed1593ec6fd7a1a53df2fb4 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 3 Feb 2026 16:24:58 -0700 Subject: [PATCH 079/295] ported image_mds.plot_apertures method to work with the new image_mds class. --- src/astrohack/core/holog_2.py | 1 + src/astrohack/io/image_mds.py | 192 ++++++++++++++++++++++------------ 2 files changed, 126 insertions(+), 67 deletions(-) diff --git a/src/astrohack/core/holog_2.py b/src/astrohack/core/holog_2.py index 743bb837..2aa62c3c 100644 --- a/src/astrohack/core/holog_2.py +++ b/src/astrohack/core/holog_2.py @@ -355,6 +355,7 @@ def _export_to_xds( xds.attrs["phase_fitting"] = phase_fit_results xds.attrs["zernike_N_order"] = zernike_n_order xds.attrs["summary"] = summary + xds.attrs["ddi"] = ddi_key coords = { "orig_pol": orig_pol_axis, diff --git a/src/astrohack/io/image_mds.py b/src/astrohack/io/image_mds.py index 59ab8872..cb56e047 100644 --- a/src/astrohack/io/image_mds.py +++ b/src/astrohack/io/image_mds.py @@ -1,10 +1,11 @@ import pathlib import numpy as np -from typing import List, Union +from typing import List, Union, Tuple import toolviper.utils.logger as logger +from astrohack.antenna.antenna_surface import AntennaSurface from astrohack.io.base_mds import AstrohackBaseFile from astrohack.utils.graph import compute_graph from astrohack.utils.constants import clight @@ -89,72 +90,87 @@ def export_to_fits( ) # @toolviper.utils.parameter.validate(custom_checker=custom_plots_checker) - # def plot_apertures( - # self, - # destination: str, - # ant: Union[str, List[str]] = "all", - # ddi: Union[str, int, List[int]] = "all", - # polarization_state: Union[str, List[str]] = "I", - # plot_screws: bool = False, - # amplitude_limits: Union[List[float], Tuple, np.array] = None, - # phase_unit: str = "deg", - # phase_limits: Union[List[float], Tuple, np.array] = None, - # deviation_unit: str = "mm", - # deviation_limits: Union[List[float], Tuple, np.array] = None, - # panel_labels: bool = False, - # display: bool = False, - # colormap: str = "viridis", - # figure_size: Union[Tuple, List[float], np.array] = None, - # dpi: int = 300, - # parallel: bool = False, - # ) -> None: - # """ Aperture amplitude and phase plots from the data in an AstrohackImageFIle object. - # - # :param destination: Name of the destination folder to contain plots - # :type destination: str - # :param ant: List of antennas/antenna to be plotted, defaults to "all" when None, ex. ea25 - # :type ant: list or str, optional - # :param ddi: List of ddis/ddi to be plotted, defaults to "all" when None, ex. 0 - # :type ddi: list or int, optional - # :param polarization_state: List of polarization states/ polarization state to be plotted, defaults to "I" - # :type polarization_state: list or str, optional - # :param plot_screws: Add screw positions to plot, default is False - # :type plot_screws: bool, optional - # :param amplitude_limits: Lower than Upper limit for amplitude in volts default is None (Guess from data) - # :type amplitude_limits: numpy.ndarray, list, tuple, optional - # :param phase_unit: Unit for phase plots, defaults is 'deg' - # :type phase_unit: str, optional - # :param phase_limits: Lower than Upper limit for phase, value in phase_unit, default is None (Guess from data) - # :type phase_limits: numpy.ndarray, list, tuple, optional - # :param deviation_unit: Unit for deviation plots, defaults is 'mm' - # :type deviation_unit: str, optional - # :param deviation_limits: Lower than Upper limit for deviation, value in deviation_unit, default is None (Guess\ - # from data) - # :type deviation_limits: numpy.ndarray, list, tuple, optional - # :param panel_labels: Add panel labels to antenna surface plots, default is False - # :type panel_labels: bool, optional - # :param display: Display plots inline or suppress, defaults to True - # :type display: bool, optional - # :param colormap: Colormap for plots, default is viridis - # :type colormap: str, optional - # :param figure_size: 2 element array/list/tuple with the plot sizes in inches - # :type figure_size: numpy.ndarray, list, tuple, optional - # :param dpi: dots per inch to be used in plots, default is 300 - # :type dpi: int, optional - # :param parallel: If True will use an existing astrohack client to produce plots in parallel, default is False - # :type parallel: bool, optional - # - # .. _Description: - # - # Produce plots from ``astrohack.holog`` results for analysis - # """ - # param_dict = locals() - # - # pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) - # compute_graph( - # self, plot_aperture_chunk, param_dict, ["ant", "ddi"], parallel=parallel - # ) - # + def plot_apertures( + self, + destination: str, + ant: Union[str, List[str]] = "all", + ddi: Union[str, int, List[int]] = "all", + polarization_state: Union[str, List[str]] = "I", + plot_screws: bool = False, + amplitude_limits: Union[List[float], Tuple, np.array] = None, + phase_unit: str = "deg", + phase_limits: Union[List[float], Tuple, np.array] = None, + deviation_unit: str = "mm", + deviation_limits: Union[List[float], Tuple, np.array] = None, + panel_labels: bool = False, + display: bool = False, + colormap: str = "viridis", + figure_size: Union[Tuple, List[float], np.array] = None, + dpi: int = 300, + parallel: bool = False, + ) -> None: + """ Aperture amplitude and phase plots from the data in an AstrohackImageFIle object. + + :param destination: Name of the destination folder to contain plots + :type destination: str + + :param ant: List of antennas/antenna to be plotted, defaults to "all" when None, ex. ea25 + :type ant: list or str, optional + + :param ddi: List of ddis/ddi to be plotted, defaults to "all" when None, ex. 0 + :type ddi: list or int, optional + + :param polarization_state: List of polarization states/ polarization state to be plotted, defaults to "I" + :type polarization_state: list or str, optional + + :param plot_screws: Add screw positions to plot, default is False + :type plot_screws: bool, optional + + :param amplitude_limits: Lower than Upper limit for amplitude in volts default is None (Guess from data) + :type amplitude_limits: numpy.ndarray, list, tuple, optional + + :param phase_unit: Unit for phase plots, defaults is 'deg' + :type phase_unit: str, optional + + :param phase_limits: Lower than Upper limit for phase, value in phase_unit, default is None (Guess from data) + :type phase_limits: numpy.ndarray, list, tuple, optional + + :param deviation_unit: Unit for deviation plots, defaults is 'mm' + :type deviation_unit: str, optional + + :param deviation_limits: Lower than Upper limit for deviation, value in deviation_unit, default is None (Guess\ + from data) + :type deviation_limits: numpy.ndarray, list, tuple, optional + + :param panel_labels: Add panel labels to antenna surface plots, default is False + :type panel_labels: bool, optional + + :param display: Display plots inline or suppress, defaults to True + :type display: bool, optional + + :param colormap: Colormap for plots, default is viridis + :type colormap: str, optional + + :param figure_size: 2 element array/list/tuple with the plot sizes in inches + :type figure_size: numpy.ndarray, list, tuple, optional + + :param dpi: dots per inch to be used in plots, default is 300 + :type dpi: int, optional + + :param parallel: If True will use an existing astrohack client to produce plots in parallel, default is False + :type parallel: bool, optional + + .. _Description: + + Produce plots from ``astrohack.holog`` results for analysis + """ + param_dict = locals() + + pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) + compute_graph( + self, _plot_aperture_chunk, param_dict, ["ant", "ddi"], parallel=parallel + ) + # @toolviper.utils.parameter.validate(custom_checker=custom_plots_checker) # def plot_beams( # self, @@ -555,3 +571,45 @@ def _export_to_fits_chunk(param_dict): "image", ) return + + +def _plot_aperture_chunk(parm_dict): + """ + Chunk function for the user facing function plot_apertures + Args: + parm_dict: parameter dictionary + """ + antenna = parm_dict["this_ant"] + ddi = parm_dict["this_ddi"] + destination = parm_dict["destination"] + input_xds = parm_dict["xdt_data"] + input_xds.attrs["AIPS"] = False + + asked_pol_states = parm_dict["polarization_state"] + avail_pol_states = input_xds.pol.values + if asked_pol_states == "all": + plot_pol_states = avail_pol_states + elif type(asked_pol_states) is str: + plot_pol_states = [asked_pol_states] + elif type(asked_pol_states) is list: + plot_pol_states = asked_pol_states + else: + msg = f"Uncomprehensible polarization state: {asked_pol_states}" + logger.error(msg) + raise Exception(msg) + + for pol_state in plot_pol_states: + if pol_state in avail_pol_states: + surface = AntennaSurface( + input_xds.dataset, + nan_out_of_bounds=False, + pol_state=str(pol_state), + clip_type="absolute", + clip_level=0, + ) + basename = f"{destination}/{antenna}_{ddi}_pol_{pol_state}" + surface.plot_phase(basename, "image_aperture", parm_dict) + surface.plot_deviation(basename, "image_aperture", parm_dict) + surface.plot_amplitude(basename, "image_aperture", parm_dict) + else: + logger.warning(f"Polarization state {pol_state} not available in data") From 3bb336fe43a6d426aacf959c3df00069e5c8fec1 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 3 Feb 2026 16:30:01 -0700 Subject: [PATCH 080/295] ported image_mds.plot_beams method to work with the new image_mds class. --- src/astrohack/io/image_mds.py | 236 ++++++++++++++++++++++++++-------- 1 file changed, 185 insertions(+), 51 deletions(-) diff --git a/src/astrohack/io/image_mds.py b/src/astrohack/io/image_mds.py index cb56e047..f1abe202 100644 --- a/src/astrohack/io/image_mds.py +++ b/src/astrohack/io/image_mds.py @@ -17,6 +17,11 @@ put_stokes_axis_in_fits_header, put_resolution_in_fits_header, ) +from astrohack.visualization.plot_tools import ( + create_figure_and_axes, + close_figure, + simple_imshow_map_plot, +) class AstrohackImageFile(AstrohackBaseFile): @@ -172,57 +177,67 @@ def plot_apertures( ) # @toolviper.utils.parameter.validate(custom_checker=custom_plots_checker) - # def plot_beams( - # self, - # destination: str, - # ant: Union[str, List[str]] = "all", - # ddi: Union[str, int, List[int]] = "all", - # complex_split: str = "polar", - # angle_unit: str = "deg", - # phase_unit: str = "deg", - # display: bool = False, - # colormap: str = "viridis", - # figure_size: Union[Tuple, List[float], np.array] = (8, 4.5), - # dpi: int = 300, - # parallel: bool = False, - # ) -> None: - # """ Beam plots from the data in an AstrohackImageFIle object. - # - # :param destination: Name of the destination folder to contain plots - # :type destination: str - # :param ant: List of antennas/antenna to be plotted, defaults to "all" when None, ex. ea25 - # :type ant: list or str, optional - # :param ddi: List of ddis/ddi to be plotted, defaults to "all" when None, ex. 0 - # :type ddi: list or int, optional - # :param angle_unit: Unit for L and M axes in plots, default is 'deg'. - # :type angle_unit: str, optional - # :param complex_split: How to split complex beam data, cartesian (real + imag) or polar (amplitude + phase, \ - # default) - # :type complex_split: str, optional - # :param phase_unit: Unit for phase in 'polar' plots, default is 'deg'. - # :type phase_unit: str - # :param display: Display plots inline or suppress, defaults to True - # :type display: bool, optional - # :param colormap: Colormap for plots, default is viridis - # :type colormap: str, optional - # :param figure_size: 2 element array/list/tuple with the plot sizes in inches - # :type figure_size: numpy.ndarray, list, tuple, optional - # :param dpi: dots per inch to be used in plots, default is 300 - # :type dpi: int, optional - # :param parallel: If True will use an existing astrohack client to produce plots in parallel, default is False - # :type parallel: bool, optional - # - # .. _Description: - # - # Produce plots from ``astrohack.holog`` results for analysis - # """ - # param_dict = locals() - # - # pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) - # compute_graph( - # self, plot_beam_chunk, param_dict, ["ant", "ddi"], parallel=parallel - # ) - # + def plot_beams( + self, + destination: str, + ant: Union[str, List[str]] = "all", + ddi: Union[str, int, List[int]] = "all", + complex_split: str = "polar", + angle_unit: str = "deg", + phase_unit: str = "deg", + display: bool = False, + colormap: str = "viridis", + figure_size: Union[Tuple, List[float], np.array] = (8, 4.5), + dpi: int = 300, + parallel: bool = False, + ) -> None: + """ Beam plots from the data in an AstrohackImageFIle object. + + :param destination: Name of the destination folder to contain plots + :type destination: str + + :param ant: List of antennas/antenna to be plotted, defaults to "all" when None, ex. ea25 + :type ant: list or str, optional + + :param ddi: List of ddis/ddi to be plotted, defaults to "all" when None, ex. 0 + :type ddi: list or int, optional + + :param angle_unit: Unit for L and M axes in plots, default is 'deg'. + :type angle_unit: str, optional + + :param complex_split: How to split complex beam data, cartesian (real + imag) or polar (amplitude + phase, \ + default) + :type complex_split: str, optional + + :param phase_unit: Unit for phase in 'polar' plots, default is 'deg'. + :type phase_unit: str + + :param display: Display plots inline or suppress, defaults to True + :type display: bool, optional + + :param colormap: Colormap for plots, default is viridis + :type colormap: str, optional + + :param figure_size: 2 element array/list/tuple with the plot sizes in inches + :type figure_size: numpy.ndarray, list, tuple, optional + + :param dpi: dots per inch to be used in plots, default is 300 + :type dpi: int, optional + + :param parallel: If True will use an existing astrohack client to produce plots in parallel, default is False + :type parallel: bool, optional + + .. _Description: + + Produce plots from ``astrohack.holog`` results for analysis + """ + param_dict = locals() + + pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) + compute_graph( + self, _plot_beam_chunk, param_dict, ["ant", "ddi"], parallel=parallel + ) + # @toolviper.utils.parameter.validate(custom_checker=custom_unit_checker) # def export_phase_fit_results( # self, @@ -613,3 +628,122 @@ def _plot_aperture_chunk(parm_dict): surface.plot_amplitude(basename, "image_aperture", parm_dict) else: logger.warning(f"Polarization state {pol_state} not available in data") + + +def _plot_beam_chunk(parm_dict): + """ + Chunk function for the user facing function plot_beams + Args: + parm_dict: parameter dictionary + """ + antenna = parm_dict["this_ant"] + ddi = parm_dict["this_ddi"] + destination = parm_dict["destination"] + input_xds = parm_dict["xdt_data"] + laxis = input_xds.l.values * convert_unit( + "rad", parm_dict["angle_unit"], "trigonometric" + ) + maxis = input_xds.m.values * convert_unit( + "rad", parm_dict["angle_unit"], "trigonometric" + ) + if input_xds.sizes["chan"] != 1: + raise Exception("Only single channel holographies supported") + + if input_xds.sizes["time"] != 1: + raise Exception("Only single mapping holographies supported") + + full_beam = input_xds.BEAM.isel(time=0, chan=0).values + pol_axis = input_xds.pol.values + + for i_pol, pol in enumerate(pol_axis): + basename = f"{destination}/{antenna}_{ddi}_pol_{pol}" + _plot_beam_by_pol(laxis, maxis, pol, full_beam[i_pol, ...], basename, parm_dict) + + +def _plot_beam_by_pol(laxis, maxis, pol, beam_image, basename, parm_dict): + """ + Plot a beam + Args: + laxis: L axis + maxis: M axis + pol: Polarization state + beam_image: Beam data + basename: Basename for output file + parm_dict: dictionary with general and plotting parameters + """ + + fig, axes = create_figure_and_axes(parm_dict["figure_size"], [1, 2]) + norm_z_label = f"Z Scale [Normalized]" + x_label = f'L axis [{parm_dict["angle_unit"]}]' + y_label = f'M axis [{parm_dict["angle_unit"]}]' + + if parm_dict["complex_split"] == "cartesian": + vmin = np.min([np.nanmin(beam_image.real), np.nanmin(beam_image.imag)]) + vmax = np.max([np.nanmax(beam_image.real), np.nanmax(beam_image.imag)]) + simple_imshow_map_plot( + axes[0], + fig, + laxis, + maxis, + beam_image.real, + "Real part", + parm_dict["colormap"], + [vmin, vmax], + x_label=x_label, + y_label=y_label, + z_label=norm_z_label, + ) + simple_imshow_map_plot( + axes[1], + fig, + laxis, + maxis, + beam_image.imag, + "Imaginary part", + parm_dict["colormap"], + [vmin, vmax], + x_label=x_label, + y_label=y_label, + z_label=norm_z_label, + ) + else: + scale = convert_unit("rad", parm_dict["phase_unit"], "trigonometric") + amplitude = np.absolute(beam_image) + phase = np.angle(beam_image) * scale + + simple_imshow_map_plot( + axes[0], + fig, + laxis, + maxis, + amplitude, + "Amplitude", + parm_dict["colormap"], + [np.nanmin(amplitude[amplitude > 1e-8]), np.nanmax(amplitude)], + x_label=x_label, + y_label=y_label, + z_label=norm_z_label, + ) + simple_imshow_map_plot( + axes[1], + fig, + laxis, + maxis, + phase, + "Phase", + parm_dict["colormap"], + [-np.pi * scale, np.pi * scale], + x_label=x_label, + y_label=y_label, + z_label=f"Phase [{parm_dict['phase_unit']}]", + ) + + plot_name = add_prefix( + add_prefix(basename, parm_dict["complex_split"]), "image_beam" + ) + suptitle = ( + f'Beam for Antenna: {parm_dict["this_ant"].split("_")[1]}, DDI: {parm_dict["this_ddi"].split("_")[1]}, ' + f"pol. State: {pol}" + ) + close_figure(fig, suptitle, plot_name, parm_dict["dpi"], parm_dict["display"]) + return From 69801e88733df7ae2e465375f6662449ecd50176 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 3 Feb 2026 16:52:55 -0700 Subject: [PATCH 081/295] ported image_mds.export_phase_fit_results method to work with the new image_mds class. --- src/astrohack/io/image_mds.py | 137 +++++++++++++++++++++++++--------- 1 file changed, 102 insertions(+), 35 deletions(-) diff --git a/src/astrohack/io/image_mds.py b/src/astrohack/io/image_mds.py index f1abe202..3be0fe5f 100644 --- a/src/astrohack/io/image_mds.py +++ b/src/astrohack/io/image_mds.py @@ -8,8 +8,18 @@ from astrohack.antenna.antenna_surface import AntennaSurface from astrohack.io.base_mds import AstrohackBaseFile from astrohack.utils.graph import compute_graph -from astrohack.utils.constants import clight +from astrohack.utils.constants import clight, length_units, trigo_units from astrohack.utils.conversion import convert_unit +from astrohack.utils.phase_fitting import aips_par_names + +from astrohack.utils.text import ( + format_label, + format_frequency, + create_pretty_table, + string_to_ascii_file, + create_dataset_label, + format_value_error, +) from astrohack.utils.fits import ( write_fits, add_prefix, @@ -239,40 +249,46 @@ def plot_beams( ) # @toolviper.utils.parameter.validate(custom_checker=custom_unit_checker) - # def export_phase_fit_results( - # self, - # destination: str, - # ant: Union[str, List[str]] = "all", - # ddi: Union[str, int, List[int]] = "all", - # angle_unit: str = "deg", - # length_unit: str = "mm", - # parallel: bool = False, - # ) -> None: - # """Export perturbations phase fit results from the data in an AstrohackImageFIle object to ASCII files. - # - # :param destination: Name of the destination folder to contain ASCII files - # :type destination: str - # :param ant: List of antennas/antenna to be exported, defaults to "all" when None, ex. ea25 - # :type ant: list or str, optional - # :param ddi: List of ddis/ddi to be exported, defaults to "all" when None, ex. 0 - # :type ddi: list or int, optional - # :param angle_unit: Unit for results that are angles. - # :type angle_unit: str, optional - # :param length_unit: Unit for results that are displacements. - # :type length_unit: str, optional - # :param parallel: If True will use an existing astrohack client to produce ASCII files in parallel, default is False - # :type parallel: bool, optional - # - # .. _Description: - # - # Export the results of the phase fitting process in ``astrohack.holog`` for analysis - # """ - # param_dict = locals() - # - # pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) - # compute_graph( - # self, export_phase_fit_chunk, param_dict, ["ant", "ddi"], parallel=parallel - # ) + def export_phase_fit_results( + self, + destination: str, + ant: Union[str, List[str]] = "all", + ddi: Union[str, int, List[int]] = "all", + angle_unit: str = "deg", + length_unit: str = "mm", + parallel: bool = False, + ) -> None: + """Export perturbations phase fit results from the data in an AstrohackImageFIle object to ASCII files. + + :param destination: Name of the destination folder to contain ASCII files + :type destination: str + + :param ant: List of antennas/antenna to be exported, defaults to "all" when None, ex. ea25 + :type ant: list or str, optional + + :param ddi: List of ddis/ddi to be exported, defaults to "all" when None, ex. 0 + :type ddi: list or int, optional + + :param angle_unit: Unit for results that are angles. + :type angle_unit: str, optional + + :param length_unit: Unit for results that are displacements. + :type length_unit: str, optional + + :param parallel: If True will use an existing astrohack client to produce ASCII files in parallel, default is False + :type parallel: bool, optional + + .. _Description: + + Export the results of the phase fitting process in ``astrohack.holog`` for analysis + """ + param_dict = locals() + + pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) + compute_graph( + self, _export_phase_fit_chunk, param_dict, ["ant", "ddi"], parallel=parallel + ) + # # @toolviper.utils.parameter.validate() # def export_zernike_fit_results( @@ -747,3 +763,54 @@ def _plot_beam_by_pol(laxis, maxis, pol, beam_image, basename, parm_dict): ) close_figure(fig, suptitle, plot_name, parm_dict["dpi"], parm_dict["display"]) return + + +def _export_phase_fit_chunk(parm_dict): + antenna = parm_dict["this_ant"] + ddi = parm_dict["this_ddi"] + destination = parm_dict["destination"] + phase_fit_results = parm_dict["xdt_data"].attrs["phase_fitting"] + if phase_fit_results is None: + logger.warning( + f"No phase fit results to export for {create_dataset_label(antenna, ddi)}" + ) + return + + angle_unit = parm_dict["angle_unit"] + length_unit = parm_dict["length_unit"] + field_names = ["Parameter", "Value", "Unit"] + alignment = ["l", "r", "c"] + outstr = "" + + for mapkey, map_dict in phase_fit_results.items(): + for freq, freq_dict in map_dict.items(): + for pol, pol_dict in freq_dict.items(): + outstr += ( + f'* {mapkey.replace("_", " ")}, Frequency {format_frequency(freq)}, ' + f"polarization state {pol}:\n\n " + ) + table = create_pretty_table(field_names, alignment) + for par_name in aips_par_names: + item = pol_dict[par_name] + val = item["value"] + err = item["error"] + unit = item["unit"] + if unit in length_units: + fac = convert_unit(unit, length_unit, "length") + elif unit in trigo_units: + fac = convert_unit(unit, angle_unit, "trigonometric") + else: + msg = f"Unknown unit {unit}" + logger.error(msg) + raise Exception(msg) + + row = [ + format_label(par_name), + format_value_error(fac * val, fac * err, 1.0, 1e-4), + unit, + ] + table.add_row(row) + + outstr += table.get_string() + "\n\n" + + string_to_ascii_file(outstr, f"{destination}/image_phase_fit_{antenna}_{ddi}.txt") From ec7bbdce1ea945f28c591349917ef4637ef4ba93 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 3 Feb 2026 16:57:06 -0700 Subject: [PATCH 082/295] ported image_mds.export_zernike_fit_results method to work with the new image_mds class. --- src/astrohack/io/image_mds.py | 110 +++++++++++++++++++++++----------- 1 file changed, 75 insertions(+), 35 deletions(-) diff --git a/src/astrohack/io/image_mds.py b/src/astrohack/io/image_mds.py index 3be0fe5f..374fdb29 100644 --- a/src/astrohack/io/image_mds.py +++ b/src/astrohack/io/image_mds.py @@ -289,42 +289,44 @@ def export_phase_fit_results( self, _export_phase_fit_chunk, param_dict, ["ant", "ddi"], parallel=parallel ) - # # @toolviper.utils.parameter.validate() - # def export_zernike_fit_results( - # self, - # destination: str, - # ant: Union[str, List[str]] = "all", - # ddi: Union[str, int, List[int]] = "all", - # parallel: bool = False, - # ) -> None: - # """Export Zernike coefficients from the data in an AstrohackImageFIle object to ASCII files. - # - # :param destination: Name of the destination folder to contain ASCII files - # :type destination: str - # :param ant: List of antennas/antenna to be exported, defaults to "all" when None, ex. ea25 - # :type ant: list or str, optional - # :param ddi: List of ddis/ddi to be exported, defaults to "all" when None, ex. 0 - # :type ddi: list or int, optional - # :param parallel: If True will use an existing astrohack client to produce ASCII files in parallel, default is False - # :type parallel: bool, optional - # - # .. _Description: - # - # Export Zernike coefficients from the AstrohackImageFile object obtained during processing in \ - # ``astrohack.holog`` for analysis. - # """ - # param_dict = locals() - # - # pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) - # compute_graph( - # self, - # export_zernike_fit_chunk, - # param_dict, - # ["ant", "ddi"], - # parallel=parallel, - # ) - # + def export_zernike_fit_results( + self, + destination: str, + ant: Union[str, List[str]] = "all", + ddi: Union[str, int, List[int]] = "all", + parallel: bool = False, + ) -> None: + """Export Zernike coefficients from the data in an AstrohackImageFIle object to ASCII files. + + :param destination: Name of the destination folder to contain ASCII files + :type destination: str + + :param ant: List of antennas/antenna to be exported, defaults to "all" when None, ex. ea25 + :type ant: list or str, optional + + :param ddi: List of ddis/ddi to be exported, defaults to "all" when None, ex. 0 + :type ddi: list or int, optional + + :param parallel: If True will use an existing astrohack client to produce ASCII files in parallel, default is False + :type parallel: bool, optional + + .. _Description: + + Export Zernike coefficients from the AstrohackImageFile object obtained during processing in \ + ``astrohack.holog`` for analysis. + """ + param_dict = locals() + + pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) + compute_graph( + self, + _export_zernike_fit_chunk, + param_dict, + ["ant", "ddi"], + parallel=parallel, + ) + # @toolviper.utils.parameter.validate(custom_checker=custom_plots_checker) # def plot_zernike_model( # self, @@ -814,3 +816,41 @@ def _export_phase_fit_chunk(parm_dict): outstr += table.get_string() + "\n\n" string_to_ascii_file(outstr, f"{destination}/image_phase_fit_{antenna}_{ddi}.txt") + + +def _export_zernike_fit_chunk(parm_dict): + antenna = parm_dict["this_ant"] + ddi = parm_dict["this_ddi"] + xdt_data = parm_dict["xdt_data"] + zernike_coeffs = xdt_data["ZERNIKE_COEFFICIENTS"].values + rms = xdt_data["ZERNIKE_FIT_RMS"].values + corr_axis = xdt_data.orig_pol.values + freq_axis = xdt_data.chan.values + ntime = zernike_coeffs.shape[0] + osa_indices = xdt_data.osa.values + destination = parm_dict["destination"] + + field_names = ["Indices", "Real", "Imaginary"] + alignment = ["l", "c", "c"] + outstr = "" + + for itime in range(ntime): + for ichan, freq in enumerate(freq_axis): + for icorr, corr in enumerate(corr_axis): + outstr += f"* map {itime}, Frequency {format_frequency(freq)}, Correlation {corr}:\n" + outstr += ( + f" Fit RMS = {rms[itime, ichan, icorr].real:.8f} + {rms[itime, ichan, icorr].imag:.8f}*i" + f"\n\n" + ) + table = create_pretty_table(field_names, alignment) + for icoeff, coeff in enumerate(zernike_coeffs[itime, ichan, icorr]): + row = [ + osa_indices[icoeff], + f"{coeff.real:.8f}", + f"{coeff.imag:.8f}", + ] + table.add_row(row) + + outstr += table.get_string() + "\n\n" + + string_to_ascii_file(outstr, f"{destination}/image_zernike_fit_{antenna}_{ddi}.txt") From 2f33bf7881f579d3669ec43d9c13347e52d7c0ae Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 3 Feb 2026 17:03:18 -0700 Subject: [PATCH 083/295] ported image_mds.plot_zernike_model method to work with the new image_mds class. --- src/astrohack/io/image_mds.py | 227 +++++++++++++++++++++++++++------- 1 file changed, 182 insertions(+), 45 deletions(-) diff --git a/src/astrohack/io/image_mds.py b/src/astrohack/io/image_mds.py index 374fdb29..efb30c46 100644 --- a/src/astrohack/io/image_mds.py +++ b/src/astrohack/io/image_mds.py @@ -7,6 +7,7 @@ from astrohack.antenna.antenna_surface import AntennaSurface from astrohack.io.base_mds import AstrohackBaseFile +from astrohack.utils.conversion import convert_5d_grid_from_stokes from astrohack.utils.graph import compute_graph from astrohack.utils.constants import clight, length_units, trigo_units from astrohack.utils.conversion import convert_unit @@ -328,51 +329,59 @@ def export_zernike_fit_results( ) # @toolviper.utils.parameter.validate(custom_checker=custom_plots_checker) - # def plot_zernike_model( - # self, - # destination: str, - # ant: Union[str, List[str]] = "all", - # ddi: Union[str, int, List[int]] = "all", - # display: bool = False, - # colormap: str = "viridis", - # figure_size: Union[Tuple, List[float], np.array] = (16, 9), - # dpi: int = 300, - # parallel: bool = False, - # ) -> None: - # """Plot Zernike models from the data in an AstrohackImageFile object. - # - # :param destination: Name of the destination folder to contain the model plots - # :type destination: str - # :param ant: List of antennas/antenna to be exported, defaults to "all" when None, ex. ea25 - # :type ant: list or str, optional - # :param ddi: List of ddis/ddi to be exported, defaults to "all" when None, ex. 0 - # :type ddi: list or int, optional - # :param display: Display plots inline or suppress, defaults to True - # :type display: bool, optional - # :param colormap: Colormap for plots, default is viridis - # :type colormap: str, optional - # :param figure_size: 2 element array/list/tuple with the plot sizes in inches - # :type figure_size: numpy.ndarray, list, tuple, optional - # :param dpi: dots per inch to be used in plots, default is 300 - # :type dpi: int, optional - # :param parallel: If True will use an existing astrohack client to produce plots in parallel, default is False - # :type parallel: bool, optional - # - # .. _Description: - # - # Export Zernike coefficients from the AstrohackImageFile object obtained during processing in \ - # ``astrohack.holog`` for analysis. - # """ - # param_dict = locals() - # - # pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) - # compute_graph( - # self, - # plot_zernike_model_chunk, - # param_dict, - # ["ant", "ddi"], - # parallel=parallel, - # ) + def plot_zernike_model( + self, + destination: str, + ant: Union[str, List[str]] = "all", + ddi: Union[str, int, List[int]] = "all", + display: bool = False, + colormap: str = "viridis", + figure_size: Union[Tuple, List[float], np.array] = (16, 9), + dpi: int = 300, + parallel: bool = False, + ) -> None: + """Plot Zernike models from the data in an AstrohackImageFile object. + + :param destination: Name of the destination folder to contain the model plots + :type destination: str + + :param ant: List of antennas/antenna to be exported, defaults to "all" when None, ex. ea25 + :type ant: list or str, optional + + :param ddi: List of ddis/ddi to be exported, defaults to "all" when None, ex. 0 + :type ddi: list or int, optional + + :param display: Display plots inline or suppress, defaults to True + :type display: bool, optional + + :param colormap: Colormap for plots, default is viridis + :type colormap: str, optional + + :param figure_size: 2 element array/list/tuple with the plot sizes in inches + :type figure_size: numpy.ndarray, list, tuple, optional + + :param dpi: dots per inch to be used in plots, default is 300 + :type dpi: int, optional + + :param parallel: If True will use an existing astrohack client to produce plots in parallel, default is False + :type parallel: bool, optional + + .. _Description: + + Export Zernike coefficients from the AstrohackImageFile object obtained during processing in \ + ``astrohack.holog`` for analysis. + """ + param_dict = locals() + + pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) + compute_graph( + self, + plot_zernike_model_chunk, + param_dict, + ["ant", "ddi"], + parallel=parallel, + ) + # # @toolviper.utils.parameter.validate(custom_checker=custom_unit_checker) # def observation_summary( @@ -854,3 +863,131 @@ def _export_zernike_fit_chunk(parm_dict): outstr += table.get_string() + "\n\n" string_to_ascii_file(outstr, f"{destination}/image_zernike_fit_{antenna}_{ddi}.txt") + + +def plot_zernike_model_chunk(parm_dict): + """ + Chunk function for the user facing function plot_zernike_model + Args: + parm_dict: the parameter dict containing the parameters for the plot and the data. + + Returns: + Plots of the Zernike models along with residuals in png files inside destination. + """ + antenna = parm_dict["this_ant"] + ddi = parm_dict["this_ddi"] + destination = parm_dict["destination"] + input_xds = parm_dict["xdt_data"] + + if input_xds.sizes["chan"] != 1: + raise Exception("Only single channel holographies supported") + + if input_xds.sizes["time"] != 1: + raise Exception("Only single mapping holographies supported") + + # Data retrieval + u_axis = input_xds.u.values + v_axis = input_xds.v.values + pol_axis = input_xds.pol.values + corr_axis = input_xds.orig_pol.values + zernike_model = input_xds.ZERNIKE_MODEL.isel(time=0, chan=0).values + aperture = input_xds.APERTURE.values + zernike_n_order = input_xds.attrs["zernike_N_order"] + corr_aperture = convert_5d_grid_from_stokes(aperture, pol_axis, corr_axis)[ + 0, 0, :, :, : + ] + suptitle = ( + f"Zernike model with N<={zernike_n_order} for {create_dataset_label(antenna, ddi, ',')} " + f"correlation: " + ) + + for icorr, corr in enumerate(corr_axis): + filename = f"{destination}/image_zernike_model_{antenna}_{ddi}_corr_{corr}.png" + _plot_zernike_aperture_model( + suptitle + f"{corr}", + corr_aperture[icorr], + u_axis, + v_axis, + zernike_model[icorr], + filename, + parm_dict, + ) + + return + + +def _plot_zernike_cartesian_component( + ax, fig, aperture, model, u_axis, v_axis, colormap, comp_label +): + maxabs = np.nanmax(np.abs(aperture)) + zlim = [-maxabs, maxabs] + residuals = aperture - model + nvalid = np.sum(np.isfinite(model)) + rms = np.sqrt(np.nansum(residuals**2)) / nvalid + simple_imshow_map_plot( + ax[0], + fig, + u_axis, + v_axis, + aperture, + f"Aperture {comp_label} part", + colormap, + zlim, + z_label="EM intensity", + ) + simple_imshow_map_plot( + ax[1], + fig, + u_axis, + v_axis, + model, + f"Model {comp_label} part", + colormap, + zlim, + z_label="EM intensity", + ) + simple_imshow_map_plot( + ax[2], + fig, + u_axis, + v_axis, + residuals, + f"Residuals {comp_label} part, RMS={rms:.5f}", + colormap, + zlim, + z_label="EM intensity", + ) + + +def _plot_zernike_aperture_model( + suptitle, aperture, u_axis, v_axis, model_aperture, filename, parm_dict +): + fig, ax = create_figure_and_axes(parm_dict["figure_size"], [2, 3]) + _plot_zernike_cartesian_component( + ax[0], + fig, + aperture.real, + model_aperture.real, + u_axis, + v_axis, + parm_dict["colormap"], + "real", + ) + _plot_zernike_cartesian_component( + ax[1], + fig, + aperture.imag, + model_aperture.imag, + u_axis, + v_axis, + parm_dict["colormap"], + "imaginary", + ) + close_figure( + fig, + suptitle, + filename, + parm_dict["dpi"], + parm_dict["display"], + tight_layout=True, + ) From 67821cca2eac78e28d69774a4858be940bef6cc7 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 3 Feb 2026 17:06:53 -0700 Subject: [PATCH 084/295] ported image_mds.observation_summary method to work with the new image_mds class. --- src/astrohack/io/image_mds.py | 130 ++++++++++++++++++---------------- 1 file changed, 70 insertions(+), 60 deletions(-) diff --git a/src/astrohack/io/image_mds.py b/src/astrohack/io/image_mds.py index efb30c46..62a851bc 100644 --- a/src/astrohack/io/image_mds.py +++ b/src/astrohack/io/image_mds.py @@ -33,6 +33,7 @@ close_figure, simple_imshow_map_plot, ) +from astrohack.visualization.textual_data import generate_observation_summary class AstrohackImageFile(AstrohackBaseFile): @@ -384,66 +385,75 @@ def plot_zernike_model( # # @toolviper.utils.parameter.validate(custom_checker=custom_unit_checker) - # def observation_summary( - # self, - # summary_file: str, - # ant: Union[str, List[str]] = "all", - # ddi: Union[str, int, List[int]] = "all", - # az_el_key: str = "center", - # phase_center_unit: str = "radec", - # az_el_unit: str = "deg", - # time_format: str = "%d %h %Y, %H:%M:%S", - # tab_size: int = 3, - # print_summary: bool = True, - # parallel: bool = False, - # ) -> None: - # """ Create a Summary of observation information - # - # :param summary_file: Text file to put the observation summary - # :type summary_file: str - # :param ant: antenna ID to use in subselection, defaults to "all" when None, ex. ea25 - # :type ant: list or str, optional - # :param ddi: data description ID to use in subselection, defaults to "all" when None, ex. 0 - # :type ddi: list or int, optional - # :param az_el_key: What type of Azimuth & Elevation information to print, 'mean', 'median' or 'center', default\ - # is 'center' - # :type az_el_key: str, optional - # :param phase_center_unit: What unit to display phase center coordinates, 'radec' and angle units supported, \ - # default is 'radec' - # :type phase_center_unit: str, optional - # :param az_el_unit: Angle unit used to display Azimuth & Elevation information, default is 'deg' - # :type az_el_unit: str, optional - # :param time_format: datetime time format for the start and end dates of observation, default is \ - # "%d %h %Y, %H:%M:%S" - # :type time_format: str, optional - # :param tab_size: Number of spaces in the tab levels, default is 3 - # :type tab_size: int, optional - # :param print_summary: Print the summary at the end of execution, default is True - # :type print_summary: bool, optional - # :param parallel: Run in parallel, defaults to False - # :type parallel: bool, optional - # - # **Additional Information** - # - # This method produces a summary of the data in the AstrohackImageFile displaying general information, - # spectral information, beam image characteristics and aperture image characteristics. - # """ - # - # param_dict = locals() - # key_order = ["ant", "ddi"] - # execution, summary = compute_graph( - # self, - # generate_observation_summary, - # param_dict, - # key_order, - # parallel, - # fetch_returns=True, - # ) - # summary = "".join(summary) - # with open(summary_file, "w") as output_file: - # output_file.write(summary) - # if print_summary: - # print(summary) + def observation_summary( + self, + summary_file: str, + ant: Union[str, List[str]] = "all", + ddi: Union[str, int, List[int]] = "all", + az_el_key: str = "center", + phase_center_unit: str = "radec", + az_el_unit: str = "deg", + time_format: str = "%d %h %Y, %H:%M:%S", + tab_size: int = 3, + print_summary: bool = True, + parallel: bool = False, + ) -> None: + """ Create a Summary of observation information + + :param summary_file: Text file to put the observation summary + :type summary_file: str + + :param ant: antenna ID to use in subselection, defaults to "all" when None, ex. ea25 + :type ant: list or str, optional + + :param ddi: data description ID to use in subselection, defaults to "all" when None, ex. 0 + :type ddi: list or int, optional + + :param az_el_key: What type of Azimuth & Elevation information to print, 'mean', 'median' or 'center', default\ + is 'center' + :type az_el_key: str, optional + + :param phase_center_unit: What unit to display phase center coordinates, 'radec' and angle units supported, \ + default is 'radec' + :type phase_center_unit: str, optional + + :param az_el_unit: Angle unit used to display Azimuth & Elevation information, default is 'deg' + :type az_el_unit: str, optional + + :param time_format: datetime time format for the start and end dates of observation, default is \ + "%d %h %Y, %H:%M:%S" + :type time_format: str, optional + + :param tab_size: Number of spaces in the tab levels, default is 3 + :type tab_size: int, optional + + :param print_summary: Print the summary at the end of execution, default is True + :type print_summary: bool, optional + + :param parallel: Run in parallel, defaults to False + :type parallel: bool, optional + + **Additional Information** + + This method produces a summary of the data in the AstrohackImageFile displaying general information, + spectral information, beam image characteristics and aperture image characteristics. + """ + + param_dict = locals() + key_order = ["ant", "ddi"] + execution, summary = compute_graph( + self, + generate_observation_summary, + param_dict, + key_order, + parallel, + fetch_returns=True, + ) + summary = "".join(summary) + with open(summary_file, "w") as output_file: + output_file.write(summary) + if print_summary: + print(summary) def _export_to_fits_chunk(param_dict): From c15709f2a1e7c7a20d1694899ea57e9b96ba29ee Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Wed, 4 Feb 2026 10:17:06 -0700 Subject: [PATCH 085/295] First pass in reactivating and porting combine to the new datatree format. --- src/astrohack/combine_2.py | 113 ++++++++++++++++++++++++++ src/astrohack/core/combine_2.py | 135 ++++++++++++++++++++++++++++++++ 2 files changed, 248 insertions(+) create mode 100644 src/astrohack/combine_2.py create mode 100644 src/astrohack/core/combine_2.py diff --git a/src/astrohack/combine_2.py b/src/astrohack/combine_2.py new file mode 100644 index 00000000..eb7144be --- /dev/null +++ b/src/astrohack/combine_2.py @@ -0,0 +1,113 @@ +import pathlib +import toolviper.utils.parameter +import toolviper.utils.logger as logger + +from typing import Union, List + +from astrohack import open_image +from astrohack.core.combine import process_combine_chunk + +from astrohack.utils.graph import compute_graph_to_mds_tree +from astrohack.utils.file import overwrite_file +from astrohack.utils.data import write_meta_data +from astrohack.utils.text import get_default_file_name + +from astrohack.io.image_mds import AstrohackImageFile + + +@toolviper.utils.parameter.validate() +def combine( + image_name: str, + combine_name: str = None, + ant: Union[str, List[str]] = "all", + ddi: Union[int, List[int], str] = "all", + weighted: bool = False, + parallel: bool = False, + overwrite: bool = False, +) -> Union[AstrohackImageFile, None]: + """Combine DDIs in a Holography image to increase SNR + + :param image_name: Input holography data file name. Accepted data format is the output from ``astrohack.holog.holog`` + :type image_name: str + + :param combine_name: Name of output file; File name will be appended with suffix *.combine.zarr*. Defaults to \ + *basename* of input file plus holography panel file suffix. + :type combine_name: str, optional + + :param ant: List of antennas to be processed. None will use all antennas. Defaults to None, ex. ea25. + :type ant: list or str, optional + + :param ddi: List of DDIs to be combined. None will use all DDIs. Defaults to None, ex. [0, ..., 8]. + :type ddi: list of int, optional + + :param weighted: Weight phases by the corresponding amplitudes. + :type weighted: bool, optional + + :param parallel: Run in parallel. Defaults to False. + :type parallel: bool, optional + + :param overwrite: Overwrite files on disk. Defaults to False. + :type overwrite: bool, optional + + :return: Holography image object. + :rtype: AstrohackImageFile + + .. _Description: + **AstrohackImageFile** + + Image object allows the user to access image data via compound dictionary keys with values, in order of depth, \ + `ant` -> `ddi`. The image object produced by combine is special because it will always contain a single DDI.\ + The image object also provides a `summary()` helper function to list available keys for each file.\ + An outline of the image object structure when produced by combine is show below: + + .. parsed-literal:: + image_mds = + { + ant_0:{ + ddi_n: image_ds, + }, + ⋮ + ant_n: … + } + + **Example Usage** + + .. parsed-literal:: + from astrohack.combine import combine + + combine( + "astrohack_obs.image.zarr", + ant = "ea25" + weight = False + ) + """ + + if combine_name is None: + combine_name = get_default_file_name( + input_file=image_name, output_type=".combine.zarr" + ) + + combine_params = locals() + + overwrite_file(combine_params["combine_name"], combine_params["overwrite"]) + + image_mds = open_image(image_name) + + combine_mds = AstrohackImageFile.create_from_input_parameters( + combine_name, combine_params + ) + + executed_graph = compute_graph_to_mds_tree( + image_mds, + process_combine_chunk, + combine_params, + ["ant"], + combine_mds, + ) + + if executed_graph: + image_mds.write(mode="a") + return image_mds + else: + logger.warning("No data to process") + return None diff --git a/src/astrohack/core/combine_2.py b/src/astrohack/core/combine_2.py new file mode 100644 index 00000000..4fec01be --- /dev/null +++ b/src/astrohack/core/combine_2.py @@ -0,0 +1,135 @@ +from copy import deepcopy + +import numpy as np +import xarray as xr + +import toolviper.utils.logger as logger +from imageio.config.plugins import summary + +from astrohack.utils import create_dataset_label +from astrohack.utils.file import load_image_xds +from scipy.interpolate import griddata +from astrohack.utils.constants import clight +from astrohack.utils.text import param_to_list + + +def process_combine_chunk(combine_chunk_params, output_mds): + """ + Process a combine chunk + Args: + combine_chunk_params: Param dictionary for combine chunk + output_mds: output mds file that contains combined data + """ + + ant_key = combine_chunk_params["this_ant"] + ant_xdt = combine_chunk_params["xdt_data"] + user_ddi_sel = combine_chunk_params["ddi"] + ddi_list = param_to_list(user_ddi_sel, ant_xdt, "ddi") + dataset_label = create_dataset_label(ant_key, None) + + nddi = len(ddi_list) + if nddi == 0: + logger.warning(f"Nothing to process for {ant_key}") + return + elif nddi == 1: + ddi_key = ddi_list[0] + + if ddi_key in list(ant_xdt.keys()): + logger.info( + f"{dataset_label} has a single ddi to be combined, data copied from input file" + ) + + # Dataset already has the propper name! + output_mds.add_node_to_tree( + ant_xdt[ddi_key], + dump_to_disk=True, + running_in_parallel=combine_chunk_params["parallel"], + ) + else: + logger.warning( + f"{dataset_label} has no {ddi_key}, nothing to process for this antenna" + ) + return + else: + ddi_in_xdt_list = list(ant_xdt.keys()) + ddi_present_list = [ddi_key in ddi_in_xdt_list for ddi_key in ddi_list] + if np.sum(ddi_present_list) == 0: + logger.warning( + f"{dataset_label} has no valid DDI in user selection (ddi = {user_ddi_sel})" + ) + return + ddi_ref_key = ddi_list[ddi_present_list.index(True)] + + out_xds = deepcopy(ant_xdt[ddi_ref_key].dataset) + nddi = len(ddi_list) + shape = list(out_xds["CORRECTED_PHASE"].values.shape) + if out_xds.sizes["chan"] != 1: + msg = f"Only single channel holographies supported" + logger.error(msg) + raise Exception(msg) + npol = shape[2] + npoints = shape[3] * shape[4] + amp_sum = np.zeros((npol, npoints)) + pha_sum = np.zeros((npol, npoints)) + + u, v = np.meshgrid(out_xds.u_prime.values, out_xds.v_prime.values) + dest_u_axis = u.ravel() + dest_v_axis = v.ravel() + summary_list = [] + for i_ddi, ddi_key in enumerate(ddi_list): + this_dataset_label = create_dataset_label(ant_key, ddi_key) + if not ddi_present_list[i_ddi]: + logger.warning( + f"{this_dataset_label} does not exist in input mds, skipping" + ) + continue + + logger.info(f"Regridding {this_dataset_label}") + this_xds = ant_xdt[ddi_key].dataset + summary_list.append(this_xds.attrs["summary"]) + u, v = np.meshgrid( + this_xds.u_prime.values, + this_xds.v_prime.values, + ) + loca_u_axis = u.ravel() + loca_v_axis = v.ravel() + for ipol in range(npol): + thispha = this_xds["CORRECTED_PHASE"].values[0, 0, ipol, :, :].ravel() + thisamp = this_xds["AMPLITUDE"].values[0, 0, ipol, :, :].ravel() + repha = griddata( + (loca_u_axis, loca_v_axis), + thispha, + (dest_u_axis, dest_v_axis), + method="linear", + ) + reamp = griddata( + (loca_u_axis, loca_v_axis), + thisamp, + (dest_u_axis, dest_v_axis), + method="linear", + ) + amp_sum[ipol, :] += reamp + if combine_chunk_params["weighted"]: + pha_sum[ipol, :] += repha * reamp + else: + pha_sum[ipol, :] += repha + + if combine_chunk_params["weighted"]: + phase = pha_sum / amp_sum + else: + phase = pha_sum / nddi + amplitude = amp_sum / nddi + + out_xds["AMPLITUDE"] = xr.DataArray( + amplitude.reshape(shape), dims=["time", "chan", "pol", "u_prime", "v_prime"] + ) + out_xds["CORRECTED_PHASE"] = xr.DataArray( + phase.reshape(shape), dims=["time", "chan", "pol", "u_prime", "v_prime"] + ) + + out_dataset_name = f"{ant_key}-ddi_99" + output_mds.add_node_to_tree( + xr.DataTree(name=out_dataset_name, dataset=out_xds), + dump_to_disk=True, + running_in_parallel=combine_chunk_params["parallel"], + ) From 5f660321d2b9ca71d49441aed1bcc4602a3f8562 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Wed, 4 Feb 2026 10:56:01 -0700 Subject: [PATCH 086/295] Improved print_dict_types --- src/astrohack/utils/text.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/astrohack/utils/text.py b/src/astrohack/utils/text.py index 7de2c965..19247b85 100644 --- a/src/astrohack/utils/text.py +++ b/src/astrohack/utils/text.py @@ -350,7 +350,7 @@ def print_dict_table( print(table) -def print_dict_types(le_dict, ident=0, show_values=False): +def print_dict_types(le_dict, ident=4, show_values=False): spc = " " for key, value in le_dict.items(): if isinstance(value, dict): From 53ab9c1cb3d90d1187bfa13ffe1fa1fce8a454f2 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Wed, 4 Feb 2026 15:27:04 -0700 Subject: [PATCH 087/295] Combine_2 now only performs regridding if aperture pixel coordinates are different by more than 1e-6 of a pixel; also in this commit the output summary in the combined dataset is an aggregation of the specified DDIs summaries. --- src/astrohack/core/combine_2.py | 165 ++++++++++++++++++++++++-------- 1 file changed, 125 insertions(+), 40 deletions(-) diff --git a/src/astrohack/core/combine_2.py b/src/astrohack/core/combine_2.py index 4fec01be..1bca821f 100644 --- a/src/astrohack/core/combine_2.py +++ b/src/astrohack/core/combine_2.py @@ -4,12 +4,9 @@ import xarray as xr import toolviper.utils.logger as logger -from imageio.config.plugins import summary -from astrohack.utils import create_dataset_label -from astrohack.utils.file import load_image_xds +from astrohack.utils import create_dataset_label, clight from scipy.interpolate import griddata -from astrohack.utils.constants import clight from astrohack.utils.text import param_to_list @@ -33,12 +30,10 @@ def process_combine_chunk(combine_chunk_params, output_mds): return elif nddi == 1: ddi_key = ddi_list[0] - if ddi_key in list(ant_xdt.keys()): logger.info( f"{dataset_label} has a single ddi to be combined, data copied from input file" ) - # Dataset already has the propper name! output_mds.add_node_to_tree( ant_xdt[ddi_key], @@ -58,24 +53,42 @@ def process_combine_chunk(combine_chunk_params, output_mds): f"{dataset_label} has no valid DDI in user selection (ddi = {user_ddi_sel})" ) return - ddi_ref_key = ddi_list[ddi_present_list.index(True)] + + min_freq = 1e34 + ddi_ref_key = None + summary_dict = {} + for i_ddi, ddi_key in enumerate(ddi_list): + if not ddi_present_list[i_ddi]: + continue + summary = ant_xdt[ddi_key].attrs["summary"] + summary_dict[ddi_key] = summary + rep_freq = summary["spectral"]["rep. frequency"] + if rep_freq < min_freq: + min_freq = rep_freq + ddi_ref_key = ddi_key out_xds = deepcopy(ant_xdt[ddi_ref_key].dataset) - nddi = len(ddi_list) shape = list(out_xds["CORRECTED_PHASE"].values.shape) if out_xds.sizes["chan"] != 1: msg = f"Only single channel holographies supported" logger.error(msg) - raise Exception(msg) + raise RuntimeError(msg) + + nmap = shape[0] + if nmap != 1: + msg = f"Only single mapping holographies supported" + logger.error(msg) + raise RuntimeError(msg) + npol = shape[2] npoints = shape[3] * shape[4] amp_sum = np.zeros((npol, npoints)) pha_sum = np.zeros((npol, npoints)) - u, v = np.meshgrid(out_xds.u_prime.values, out_xds.v_prime.values) - dest_u_axis = u.ravel() - dest_v_axis = v.ravel() - summary_list = [] + u_mesh, v_mesh = np.meshgrid(out_xds.u_prime.values, out_xds.v_prime.values) + dest_u_axis = u_mesh.ravel() + dest_v_axis = v_mesh.ravel() + for i_ddi, ddi_key in enumerate(ddi_list): this_dataset_label = create_dataset_label(ant_key, ddi_key) if not ddi_present_list[i_ddi]: @@ -83,42 +96,69 @@ def process_combine_chunk(combine_chunk_params, output_mds): f"{this_dataset_label} does not exist in input mds, skipping" ) continue - - logger.info(f"Regridding {this_dataset_label}") this_xds = ant_xdt[ddi_key].dataset - summary_list.append(this_xds.attrs["summary"]) - u, v = np.meshgrid( + + u_mesh, v_mesh = np.meshgrid( this_xds.u_prime.values, this_xds.v_prime.values, ) - loca_u_axis = u.ravel() - loca_v_axis = v.ravel() - for ipol in range(npol): - thispha = this_xds["CORRECTED_PHASE"].values[0, 0, ipol, :, :].ravel() - thisamp = this_xds["AMPLITUDE"].values[0, 0, ipol, :, :].ravel() - repha = griddata( - (loca_u_axis, loca_v_axis), - thispha, - (dest_u_axis, dest_v_axis), - method="linear", + loca_u_axis = u_mesh.ravel() + loca_v_axis = v_mesh.ravel() + + if loca_u_axis.shape[0] == dest_u_axis.shape[0]: + resample_needed = not ( + np.allclose(loca_u_axis, dest_u_axis, rtol=1e-6) + and np.allclose(loca_v_axis, dest_v_axis, rtol=1e-6) ) - reamp = griddata( - (loca_u_axis, loca_v_axis), - thisamp, - (dest_u_axis, dest_v_axis), - method="linear", + else: + resample_needed = True + + if resample_needed: + logger.info(f"Regridding {this_dataset_label}") + for ipol in range(npol): + thispha = ( + this_xds["CORRECTED_PHASE"].values[0, 0, ipol, :, :].ravel() + ) + thisamp = this_xds["AMPLITUDE"].values[0, 0, ipol, :, :].ravel() + + repha = griddata( + (loca_u_axis, loca_v_axis), + thispha, + (dest_u_axis, dest_v_axis), + method="linear", + ) + reamp = griddata( + (loca_u_axis, loca_v_axis), + thisamp, + (dest_u_axis, dest_v_axis), + method="linear", + ) + amp_sum[ipol, :] += reamp + if combine_chunk_params["weighted"]: + pha_sum[ipol, :] += repha * reamp + else: + pha_sum[ipol, :] += repha + else: + logger.info( + f"{this_dataset_label} already has the proper sampling, simple addition" ) - amp_sum[ipol, :] += reamp - if combine_chunk_params["weighted"]: - pha_sum[ipol, :] += repha * reamp - else: - pha_sum[ipol, :] += repha + for ipol in range(npol): + thispha = ( + this_xds["CORRECTED_PHASE"].values[0, 0, ipol, :, :].ravel() + ) + thisamp = this_xds["AMPLITUDE"].values[0, 0, ipol, :, :].ravel() + amp_sum[ipol, :] += thisamp + if combine_chunk_params["weighted"]: + pha_sum[ipol, :] += thispha * thisamp + else: + pha_sum[ipol, :] += thispha + n_used_ddi = np.sum(ddi_present_list) if combine_chunk_params["weighted"]: phase = pha_sum / amp_sum else: - phase = pha_sum / nddi - amplitude = amp_sum / nddi + phase = pha_sum / n_used_ddi + amplitude = amp_sum / n_used_ddi out_xds["AMPLITUDE"] = xr.DataArray( amplitude.reshape(shape), dims=["time", "chan", "pol", "u_prime", "v_prime"] @@ -127,9 +167,54 @@ def process_combine_chunk(combine_chunk_params, output_mds): phase.reshape(shape), dims=["time", "chan", "pol", "u_prime", "v_prime"] ) - out_dataset_name = f"{ant_key}-ddi_99" + out_ddi_key = "ddi_99" + out_xds.attrs["ddi"] = out_ddi_key + out_xds.attrs["summary"] = _merge_summary_dict(summary_dict, ddi_ref_key) + out_dataset_name = f"{ant_key}-{out_ddi_key}" output_mds.add_node_to_tree( xr.DataTree(name=out_dataset_name, dataset=out_xds), dump_to_disk=True, running_in_parallel=combine_chunk_params["parallel"], ) + + +def _merge_summary_dict(summary_dict, ddi_ref_key): + out_summary = deepcopy(summary_dict[ddi_ref_key]) + + aperture_resolution = out_summary["aperture"]["resolution"] + frequency_range = out_summary["spectral"]["frequency range"] + + channel_width = 0.0 + rep_freq = 0.0 + + n_used_ddi = 0 + for ddi_key, ddi_summary in summary_dict.items(): + # Spectral part + n_used_ddi += 1 + channel_width += ddi_summary["spectral"]["channel width"] + rep_freq += ddi_summary["spectral"]["rep. frequency"] + frequency_range[0] = float( + np.min([frequency_range[0], ddi_summary["spectral"]["frequency range"][0]]) + ) + frequency_range[1] = float( + np.max([frequency_range[1], ddi_summary["spectral"]["frequency range"][1]]) + ) + for i_coord in range(2): + aperture_resolution[i_coord] = float( + np.max( + [ + aperture_resolution[i_coord], + ddi_summary["aperture"]["resolution"][i_coord], + ] + ) + ) + + rep_freq /= n_used_ddi + + out_summary["aperture"]["resolution"] = aperture_resolution + out_summary["spectral"]["frequency range"] = frequency_range + out_summary["spectral"]["rep. frequency"] = rep_freq + out_summary["spectral"]["channel width"] = channel_width + out_summary["spectral"]["rep. wavelength"] = clight / rep_freq + + return out_summary From 01b0f8fe5508ba8cff5436767bccbc8361ed3ede Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Wed, 4 Feb 2026 15:27:34 -0700 Subject: [PATCH 088/295] Added further explanations on how the output combine_mds file is structured. --- src/astrohack/combine_2.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/astrohack/combine_2.py b/src/astrohack/combine_2.py index eb7144be..67bffa56 100644 --- a/src/astrohack/combine_2.py +++ b/src/astrohack/combine_2.py @@ -1,21 +1,17 @@ -import pathlib +from typing import Union, List + import toolviper.utils.parameter import toolviper.utils.logger as logger -from typing import Union, List - from astrohack import open_image -from astrohack.core.combine import process_combine_chunk - +from astrohack.core.combine_2 import process_combine_chunk from astrohack.utils.graph import compute_graph_to_mds_tree from astrohack.utils.file import overwrite_file -from astrohack.utils.data import write_meta_data from astrohack.utils.text import get_default_file_name - from astrohack.io.image_mds import AstrohackImageFile -@toolviper.utils.parameter.validate() +# @toolviper.utils.parameter.validate() def combine( image_name: str, combine_name: str = None, @@ -53,6 +49,13 @@ def combine( :rtype: AstrohackImageFile .. _Description: + **combine** + + Combine combines the amplitude and corrected_phase members of the selected DDIs in the input image file. Currently, \ + combine only supports the combination of these two quantities to avoid long regridding times. Hence, the output \ + image file (.combine.zarr file name) contains the combined amplitude and corrected_phase images, but the aperture \ + and beam images present in this file will be those present in the DDI with the lowest frequency. + **AstrohackImageFile** Image object allows the user to access image data via compound dictionary keys with values, in order of depth, \ @@ -106,8 +109,8 @@ def combine( ) if executed_graph: - image_mds.write(mode="a") - return image_mds + combine_mds.write(mode="a") + return combine_mds else: logger.warning("No data to process") return None From 7aaad9040d478b3649e0ae74e81a820a99ee8862 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Wed, 4 Feb 2026 15:38:29 -0700 Subject: [PATCH 089/295] compute_graph_to_mds_tree now checks to see if there has been any node added to the output tree, if no node has been added it returns False on execution. --- src/astrohack/utils/graph.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/astrohack/utils/graph.py b/src/astrohack/utils/graph.py index ab837e30..45ea97f8 100644 --- a/src/astrohack/utils/graph.py +++ b/src/astrohack/utils/graph.py @@ -144,9 +144,10 @@ def compute_graph_to_mds_tree( if len(delayed_list) == 0: logger.warning(f"List of delayed processing jobs is empty: No data to process") - - return False, None - + if fetch_returns: + return False, None + else: + return False else: if parallel: return_list = dask.compute(delayed_list)[0] @@ -155,6 +156,12 @@ def compute_graph_to_mds_tree( for function, args in delayed_list: return_list.append(function(*args)) + if len(output_mds.keys()) == 0: + logger.warning("Processing did not yield any data") + if fetch_returns: + return False, None + else: + return False if fetch_returns: return True, return_list else: From 6d5a7ffd7d288738b2c261ebc0c5dbf78d459862 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Wed, 4 Feb 2026 15:42:17 -0700 Subject: [PATCH 090/295] Changed return and warnings about data contents due to the change in compute_graph_to_mds_tree. --- src/astrohack/beamcut.py | 1 - src/astrohack/combine_2.py | 1 - src/astrohack/extract_holog_2.py | 1 - src/astrohack/extract_pointing_2.py | 1 - src/astrohack/holog_2.py | 1 - src/astrohack/locit.py | 5 ----- 6 files changed, 10 deletions(-) diff --git a/src/astrohack/beamcut.py b/src/astrohack/beamcut.py index af98a991..2ea1e0bb 100644 --- a/src/astrohack/beamcut.py +++ b/src/astrohack/beamcut.py @@ -153,5 +153,4 @@ def beamcut( beamcut_mds.write(mode="a") return beamcut_mds else: - logger.warning("No data to process") return None diff --git a/src/astrohack/combine_2.py b/src/astrohack/combine_2.py index 67bffa56..f0b52430 100644 --- a/src/astrohack/combine_2.py +++ b/src/astrohack/combine_2.py @@ -112,5 +112,4 @@ def combine( combine_mds.write(mode="a") return combine_mds else: - logger.warning("No data to process") return None diff --git a/src/astrohack/extract_holog_2.py b/src/astrohack/extract_holog_2.py index ff47140a..097a8f07 100644 --- a/src/astrohack/extract_holog_2.py +++ b/src/astrohack/extract_holog_2.py @@ -254,7 +254,6 @@ def extract_holog( holog_mds.write(mode="a") return holog_mds else: - logger.warning("No data to process") return None diff --git a/src/astrohack/extract_pointing_2.py b/src/astrohack/extract_pointing_2.py index a347189d..7bb6b2f1 100644 --- a/src/astrohack/extract_pointing_2.py +++ b/src/astrohack/extract_pointing_2.py @@ -108,5 +108,4 @@ def extract_pointing( point_mds.write(mode="a") return point_mds else: - logger.warning("No data to process") return None diff --git a/src/astrohack/holog_2.py b/src/astrohack/holog_2.py index 34318ff3..323e7271 100644 --- a/src/astrohack/holog_2.py +++ b/src/astrohack/holog_2.py @@ -183,5 +183,4 @@ def holog( image_mds.write(mode="a") return image_mds else: - logger.warning("No data to process") return None diff --git a/src/astrohack/locit.py b/src/astrohack/locit.py index cedcee75..861b1cfd 100644 --- a/src/astrohack/locit.py +++ b/src/astrohack/locit.py @@ -196,10 +196,6 @@ def locit( position_mds, parallel=parallel, ) - if len(position_mds.keys()) == 0: - logger.warning("Processing did not yield any data") - executed_graph = False - if executed_graph: position_mds.root.attrs.update( { @@ -211,5 +207,4 @@ def locit( position_mds.write(mode="a") return position_mds else: - logger.warning("No data to process") return None From 27884c0658879df965b2694af4a3194ea43dfb6f Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Wed, 4 Feb 2026 15:49:32 -0700 Subject: [PATCH 091/295] Fixed the naming of the xdtree in the case of the selection of a single ddi for combining. --- src/astrohack/core/combine_2.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/astrohack/core/combine_2.py b/src/astrohack/core/combine_2.py index 1bca821f..8a1a1749 100644 --- a/src/astrohack/core/combine_2.py +++ b/src/astrohack/core/combine_2.py @@ -34,9 +34,11 @@ def process_combine_chunk(combine_chunk_params, output_mds): logger.info( f"{dataset_label} has a single ddi to be combined, data copied from input file" ) - # Dataset already has the propper name! + ddi_xdt = xr.DataTree( + name=f"{ant_key}-{ddi_key}", dataset=ant_xdt[ddi_key].dataset + ) output_mds.add_node_to_tree( - ant_xdt[ddi_key], + ddi_xdt, dump_to_disk=True, running_in_parallel=combine_chunk_params["parallel"], ) From 7f9f095dab8216b094f05f2a86b8c70003afed6b Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Fri, 6 Feb 2026 08:33:42 -0700 Subject: [PATCH 092/295] Ported panel to work with the new datatree file architecture. --- src/astrohack/core/panel_2.py | 53 ++++++++ src/astrohack/io/panel_mds.py | 19 +++ src/astrohack/panel_2.py | 234 ++++++++++++++++++++++++++++++++++ 3 files changed, 306 insertions(+) create mode 100644 src/astrohack/core/panel_2.py create mode 100644 src/astrohack/io/panel_mds.py create mode 100644 src/astrohack/panel_2.py diff --git a/src/astrohack/core/panel_2.py b/src/astrohack/core/panel_2.py new file mode 100644 index 00000000..cdf82193 --- /dev/null +++ b/src/astrohack/core/panel_2.py @@ -0,0 +1,53 @@ +import xarray as xr + +import toolviper.utils.logger as logger + +from astrohack.antenna.antenna_surface import AntennaSurface +from astrohack.utils import create_dataset_label + + +def process_panel_chunk(panel_chunk_params, output_mds): + """ + Process a chunk of the holographies, usually a chunk consists of an antenna over a ddi + Args: + panel_chunk_params: dictionary of inputs + output_mds: Output panel mds object + """ + + clip_level = panel_chunk_params["clip_level"] + ddi_key = panel_chunk_params["this_ddi"] + ant_key = panel_chunk_params["this_ant"] + inputxds = panel_chunk_params["xdt_data"].dataset + dataset_label = create_dataset_label(ant_key, ddi_key) + logger.info(f"processing {dataset_label}") + if isinstance(clip_level, dict): + ant_name = ant_key.split("_")[1] + ddi_name = int(ddi_key.split("_")[1]) + try: + clip_level = clip_level[ant_name][ddi_name] + except KeyError: + msg = f"{dataset_label} combination not found in clip_level dictionary" + logger.error(msg) + raise RuntimeError(msg) + + surface = AntennaSurface( + inputxds, + clip_type=panel_chunk_params["clip_type"], + pol_state=panel_chunk_params["polarization_state"], + clip_level=clip_level, + pmodel=panel_chunk_params["panel_model"], + panel_margins=panel_chunk_params["panel_margins"], + patch_phase=False, + use_detailed_mask=panel_chunk_params["use_detailed_mask"], + ) + + surface.fit_surface() + surface.correct_surface() + + xds = surface.export_xds() + dataset_name = f"{ant_key}-{ddi_key}" + output_mds.add_node_to_tree( + xr.DataTree(name=dataset_name, dataset=xds), + dump_to_disk=True, + running_in_parallel=panel_chunk_params["parallel"], + ) diff --git a/src/astrohack/io/panel_mds.py b/src/astrohack/io/panel_mds.py new file mode 100644 index 00000000..bf6d698c --- /dev/null +++ b/src/astrohack/io/panel_mds.py @@ -0,0 +1,19 @@ +from astrohack.io.base_mds import AstrohackBaseFile + + +class AstrohackPanelFile(AstrohackBaseFile): + """Data class for panel data. + + Data within an object of this class can be selected for further inspection, plotted or produce a report + """ + + def __init__(self, file: str): + """Initialize an AstrohackPanelFile object. + + :param file: File to be linked to this object + :type file: str + + :return: AstrohackPanelFile object + :rtype: AstrohackPanelFile + """ + super().__init__(file=file) diff --git a/src/astrohack/panel_2.py b/src/astrohack/panel_2.py new file mode 100644 index 00000000..173dfa73 --- /dev/null +++ b/src/astrohack/panel_2.py @@ -0,0 +1,234 @@ +import os +import pathlib +import toolviper.utils.logger as logger +import toolviper.utils.parameter + +from astrohack.antenna.panel_fitting import PANEL_MODEL_DICT +from astrohack.utils.file import overwrite_file, check_if_file_can_be_opened + +from astrohack.utils.data import write_meta_data +from astrohack.core.panel_2 import process_panel_chunk +from astrohack.utils.validation import custom_panel_checker +from astrohack.utils.text import get_default_file_name +from astrohack.utils.graph import compute_graph_to_mds_tree + +from astrohack.io.panel_mds import AstrohackPanelFile +from astrohack.io.dio import open_image + +from typing import Union, List + + +# @toolviper.utils.parameter.validate(custom_checker=custom_panel_checker) +def panel( + image_name: str, + panel_name: str = None, + clip_type: str = "sigma", + clip_level: Union[float, dict[dict[float]]] = 3.0, + use_detailed_mask: bool = True, + panel_model: str = "rigid", + panel_margins: float = 0.05, + polarization_state: str = "I", + ant: Union[str, List[str]] = "all", + ddi: Union[int, List[str]] = "all", + parallel: bool = False, + overwrite: bool = False, +): + """Analyze holography images to derive panel adjustments + + :param image_name: Input holography data file name. Accepted data formats are the output from \ + ``astrohack.holog.holog`` and AIPS holography data prepackaged using ``astrohack.panel.aips_holog_to_astrohack``. + :type image_name: str + + :param panel_name: Name of output file; File name will be appended with suffix *.panel.zarr*. Defaults to \ + *basename* of input file plus holography panel file suffix. + :type panel_name: str, optional + + :param clip_type: Choose the amplitude clipping algorithm: none, absolute, relative, sigma or noise_threshold, \ + default is sigma + :type clip_type: str, optional + + :param clip_level: Choose level of clipping, can also be specified for specific antenna and DDI combinations by \ + passing a dictionary, default is 3 (appropriate for sigma clipping) + :type clip_level: float, dict, optional + + :param use_detailed_mask: Use a detailed aperture mask, ie. include arm shadows for the VLA or include regions \ + outside central circular aperture for the ngvla, default is True. + :type use_detailed_mask: bool, optional + + :param panel_model: Model of surface fitting function used to fit panel surfaces, None will default to "rigid". \ + Possible models are listed below. + :type panel_model: str, optional + + :param panel_margins: Relative margin from the edge of the panel used to decide which points are margin points or \ + internal points of each panel. Defaults to 0.05. + :type panel_margins: float, optional + + :param polarization_state: Select the polarization state over which to run panel, only parallel hands or stokes I \ + should be used, default is I. + :type polarization_state: str, optional + + :param ant: List of antennas/antenna to be processed, defaults to "all" when None, ex. ea25 + :type ant: list or str, optional + + :param ddi: List of ddi to be processed, defaults to "all" when None, ex. 0 + :type ddi: list or int, optional + + :param parallel: Run in parallel. Defaults to False. + :type parallel: bool, optional + + :param overwrite: Overwrite files on disk. Defaults to False. + :type overwrite: bool, optional + + :return: Holography panel object. + :rtype: AstrohackPanelFile + + .. _Description: + + Each Stokes I aperture image in the input image file is processed in the following steps: + + .. rubric:: Code Outline + - Phase image is converted to a physical surface deviation image. + - A mask of valid signals is created by using the relative cutoff on the amplitude image. + - From telescope panel and layout information, an image describing the panel assignment of each pixel is created + - Using panel image and mask, a list of pixels in each panel is created. + - Pixels in each panel are divided into two groups: margin pixels and internal pixels. + - For each panel: + * Internal pixels are fitted to a surface model. + * The fitted surface model is used to derive corrections for all pixels in the panel, internal and margins. + * The fitted surface model is used to derive corrections for the positions of the screws. + - A corrected deviation image is produced. + - RMS is computed for both the corrected and uncorrected deviation images. + - All images produced are stored in the output *.panel.zarr file*. + + .. rubric:: Available panel surface models: + * AIPS fitting models: + - *mean*: The panel is corrected by the mean of its samples. + - *rigid*: The panel samples are fitted to a rigid surface (DEFAULT model). + * Corotated Paraboloids: (the two bending axes of the paraboloid are parallel and perpendicular to a radius \ + of the antenna crossing the middle point of the panel): + - *corotated_scipy*: Paraboloid is fitted using scipy.optimize, robust but slow. + - *corotated_lst_sq*: Paraboloid is fitted using the linear algebra least squares method, fast but \ + unreliable. + - *corotated_robust*: Tries corotated_lst_sq, if it diverges falls back to corotated_scipy, fast and robust. + * Experimental fitting models: + - *xy_paraboloid*: fitted using scipy.optimize, bending axes are parallel to the x and y axes. + - *rotated_paraboloid*: fitted using scipy.optimize, bending axes can be rotated by any arbitrary angle. + - *full_paraboloid_lst_sq*: Full 9 parameter paraboloid fitted using least_squares method, tends to \ + heavily overfit surface irregularities. + + .. rubric:: Amplitude clipping: + + In order to produce results of good quality parts of the aperture with low signal (e.g. the shadow of the + secondary mirror support) a mask is defined based on the amplitude of the aperture. There are 5 methods + (clip_type parameter) available to define at which level (clip_level) the amplitude is clipped: + + * none: In this method no amplitude clip is performed, i.e. the clipping value is set to -infinity. + + * absolute: In this method the clipping value is taken directly from the clip_level parameter, e.g.: \ + if the user calls `panel(..., clip_type='absolute', clip_level=3.5)` everything below 3.5 in \ + amplitude will be clipped + * relative: In this method the clipping value is derived from the amplitude maximum, e.g.: if the user calls \ + `panel(..., clip_type='relative', clip_level=0.2) everything below 20% of the maximum amplitude \ + will be clipped + * sigma: In this method the clipping value is computed from the RMS noise in the amplitude outside the \ + physical dish, e.g.: if the user calls `panel(clip_type='sigma', clip_level=3)` everything below 3 \ + times the RMS noise in amplitude will be clipped. + + * noise_threshold: In this model the cut is first set to the maximum amplitude outside the disk, a proxy for \ + the noise maximum in amplitude, if this preserves a fraction of the aperture disk that is larger \ + than the clip_level this is the chosen amplitude cutoff, if not, the cutoff is iteratively lowered \ + by 10% until it preservers a fraction of the disk that is larger thatn clip_level. This heuristic \ + was created with help from VLA operations. + + The default clipping is set to 3 sigma. + + .. rubric:: Passing a dictionary for amplitude clipping: + + The dictionary used for specifying amplitude clippings specific for each antenna and DDI combination must + follow the following scheme: + + .. parsed-literal:: + amp_clip_dict = { + ant1_name: { + 0: 0.3 + 1: 0.5 + } + ant2_name: { + 0: 0.45 + 1: 0.21 + } + } + + Where the antenna name is the usual antenna designation e.g. ea24 for the VLA or DV42 for ALMA, and the DDI + number must be given as an integer. + + + .. _Description: + **AstrohackPanelFile** + Panel object allows the user to access panel data via compound dictionary keys with values, in order of depth, \ + `ant` -> `ddi`. The panel object also provides a `summary()` helper function to list available keys for each file.\ + An outline of the panel object structure is show below: + + .. parsed-literal:: + panel_mds = + { + ant_0:{ + ddi_0: panel_ds, + ⋮ + ddi_m: panel_ds + }, + ⋮ + ant_n: … + } + + **Example Usage** + + .. parsed-literal:: + from astrohack.panel import panel + + # Fit the panels in the aperture image by using a rigid panel model and + # excluding the border 5% of each panel from the fitting. + panel_mds = panel( + "myholo.image.zarr", + panel_model='rigid', + panel_margin=0.05 + ) + + # fit the panels in the aperture image by using a rigid panel model and + # excluding points in the aperture image which have an amplitude that is less than 20% of the peak amplitude. + panel_mds = panel( + "myholo.image.zarr", + clip_type='relative', + clip_level=0.2 + ) + + """ + + # Doing this here allows it to get captured by locals() + if panel_name is None: + panel_name = get_default_file_name( + input_file=image_name, output_type=".panel.zarr" + ) + + panel_params = locals() + + image_mds = open_image(image_name) + + overwrite_file(panel_name, overwrite) + panel_mds = AstrohackPanelFile.create_from_input_parameters( + panel_name, panel_params + ) + + executed_graph = compute_graph_to_mds_tree( + image_mds, + process_panel_chunk, + panel_params, + ["ant", "ddi"], + panel_mds, + ) + + if executed_graph: + panel_mds.write(mode="a") + return panel_mds + else: + return None From dee146b367e3de1a1abe89be9d129e80f6d3e495 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Fri, 6 Feb 2026 08:51:09 -0700 Subject: [PATCH 093/295] open_panel now points to the new AstrohackPanelFile class. --- src/astrohack/io/dio.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/astrohack/io/dio.py b/src/astrohack/io/dio.py index 8e37edf0..c8899e9f 100644 --- a/src/astrohack/io/dio.py +++ b/src/astrohack/io/dio.py @@ -13,7 +13,7 @@ ) from astrohack.io.image_mds import AstrohackImageFile from astrohack.io.holog_mds import AstrohackHologFile -from astrohack.io.mds import AstrohackPanelFile +from astrohack.io.panel_mds import AstrohackPanelFile from astrohack.io.point_mds import AstrohackPointFile from astrohack.io.position_mds import AstrohackPositionFile @@ -102,7 +102,6 @@ def open_holog(file: str) -> Union[AstrohackHologFile, None]: if _data_file.open(): return _data_file - else: return None @@ -141,7 +140,6 @@ def open_image(file: str) -> Union[AstrohackImageFile, None]: if _data_file.open(): return _data_file - else: return None @@ -175,12 +173,11 @@ def open_panel(file: str) -> Union[AstrohackPanelFile, None]: } """ - check_if_file_can_be_opened(file, "0.7.2") + check_if_file_can_be_opened_2(file, "panel", "0.10.1") _data_file = AstrohackPanelFile(file=file) if _data_file.open(): return _data_file - else: return None From c4f858da65516d06e80e9fb75cb9301b9a76418c Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Fri, 6 Feb 2026 08:52:33 -0700 Subject: [PATCH 094/295] Changed the name of AstrohackBaseFile.file to .filename for clarity, disabled chunks='auto' to avoid issues when opening panel. --- src/astrohack/io/base_mds.py | 22 ++++++++++++++-------- src/astrohack/utils/file.py | 6 +++--- tests/unit/mdses/test_base_mds.py | 4 ++-- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/astrohack/io/base_mds.py b/src/astrohack/io/base_mds.py index 610bff32..9b21ce5d 100644 --- a/src/astrohack/io/base_mds.py +++ b/src/astrohack/io/base_mds.py @@ -29,7 +29,7 @@ def __init__(self, file: str): :return: AstrohackBaseFile object :rtype: AstrohackBaseFile """ - self.file = file + self.filename = file self._file_is_open = False self.root = None @@ -128,19 +128,25 @@ def open(self, file: str = None) -> bool: """ if file is None: - file = self.file + file = self.filename try: # Chunks='auto' means lazy dask loading with automatic choice of chunk size # chunks=None is direct opening. - self.root = xr.open_datatree(file, engine="zarr", chunks="auto") + self.root = xr.open_datatree(file, engine="zarr") # , chunks="auto") self._file_is_open = True - self.file = file + self.filename = file + + except FileNotFoundError: + self._file_is_open = False + msg = f"File not found at {self.filename}" except Exception as error: - logger.error(f"There was an exception opening the file: {error}") self._file_is_open = False + msg = f"There was an exception opening the file: {error}" + logger.error(msg) + raise RuntimeError(msg) return self._file_is_open @@ -151,7 +157,7 @@ def write(self, mode="w"): :param mode: File mode :type mode: str """ - self.root.to_zarr(self.file, mode=mode, consolidated=True) + self.root.to_zarr(self.filename, mode=mode, consolidated=True) def summary(self) -> None: """ @@ -160,7 +166,7 @@ def summary(self) -> None: :return: None :rtype: NoneType """ - outstr = get_summary_header(self.file) + outstr = get_summary_header(self.filename) outstr += get_property_string(self.root.attrs) outstr += get_method_list_string(self) outstr += get_data_content_string(self.root) @@ -254,6 +260,6 @@ def __repr__(self): :return: Print contents """ outstr = f"<{type(self).__name__}>{lnbr}" - outstr += f"File on disk: {self.file}{lnbr}" + outstr += f"File on disk: {self.filename}{lnbr}" outstr += f"Data tree: {lnbr}{self.root.__repr__()}" return outstr diff --git a/src/astrohack/utils/file.py b/src/astrohack/utils/file.py index 8c8f3d80..2f0d077d 100644 --- a/src/astrohack/utils/file.py +++ b/src/astrohack/utils/file.py @@ -580,10 +580,10 @@ def mds_equality_test(mds_a, mds_b): metadata_different, msg = _is_mds_metadata_different(mds_a, mds_b) if metadata_different: - return False, f"{mds_a.file} and {mds_b.file} {msg}." + return False, f"{mds_a.filename} and {mds_b.filename} {msg}." data_dicts_different, msg = _are_data_dicts_different(mds_a, mds_b) if data_dicts_different: - return False, f"{mds_a.file} and {mds_b.file} {msg}." + return False, f"{mds_a.filename} and {mds_b.filename} {msg}." - return True, f"{mds_a.file} and {mds_b.file} are equal" + return True, f"{mds_a.filename} and {mds_b.filename} are equal" diff --git a/tests/unit/mdses/test_base_mds.py b/tests/unit/mdses/test_base_mds.py index 83495e3e..c76c94fb 100644 --- a/tests/unit/mdses/test_base_mds.py +++ b/tests/unit/mdses/test_base_mds.py @@ -42,7 +42,7 @@ def test_init_and_open_base_mds(self): base_mds = AstrohackBaseFile(self.silly_name) assert ( - base_mds.file == self.silly_name + base_mds.filename == self.silly_name ), "base mds file name should be the same as the one given as argument to __init__" assert not base_mds.is_open, "base mds file should not be opened yet" @@ -60,7 +60,7 @@ def test_init_and_open_base_mds(self): base_mds.is_open ), "is_open property needs to return True now that the file has been opened" assert ( - base_mds.file == self.beamcut_file_name + base_mds.filename == self.beamcut_file_name ), ".file attribute should now be set to the name of the given file." return From a70319c661f32f11f3c314a766e8e8498f8679c5 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Fri, 6 Feb 2026 09:07:29 -0700 Subject: [PATCH 095/295] Panel model and panel label attributes of AntennaSurface class have been change from object dtype to fixed length unicode string dtype for compatibility with automatic chunking. --- src/astrohack/antenna/antenna_surface.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/astrohack/antenna/antenna_surface.py b/src/astrohack/antenna/antenna_surface.py index 69d92014..515657f7 100644 --- a/src/astrohack/antenna/antenna_surface.py +++ b/src/astrohack/antenna/antenna_surface.py @@ -663,8 +663,8 @@ def _build_panel_data_arrays(self): nscrews = self.panels[0].screws.shape[0] - self.panel_labels = np.ndarray([npanels], dtype=object) - self.panel_model_array = np.ndarray([npanels], dtype=object) + self.panel_labels = np.ndarray([npanels], dtype="U22") + self.panel_model_array = np.ndarray([npanels], dtype="U22") self.panel_pars = np.full((npanels, max_par), np.nan, dtype=float) self.screw_adjustments = np.ndarray((npanels, nscrews), dtype=float) self.panel_fallback = np.ndarray([npanels], dtype=bool) From 67393ccc9cdee26ff1cea2424221916b1b2d1f65 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Fri, 6 Feb 2026 09:07:46 -0700 Subject: [PATCH 096/295] Re-enabled auto chunking on opening. --- src/astrohack/io/base_mds.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/astrohack/io/base_mds.py b/src/astrohack/io/base_mds.py index 9b21ce5d..f66df828 100644 --- a/src/astrohack/io/base_mds.py +++ b/src/astrohack/io/base_mds.py @@ -133,7 +133,7 @@ def open(self, file: str = None) -> bool: try: # Chunks='auto' means lazy dask loading with automatic choice of chunk size # chunks=None is direct opening. - self.root = xr.open_datatree(file, engine="zarr") # , chunks="auto") + self.root = xr.open_datatree(file, engine="zarr", chunks="auto") self._file_is_open = True self.filename = file From 3fb037ca35da56da80186c8524dd0135d41674de Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Fri, 6 Feb 2026 12:18:57 -0700 Subject: [PATCH 097/295] ported panel_mds.get_antenna to the new data format. --- src/astrohack/io/panel_mds.py | 334 ++++++++++++++++++++++++++++++++++ 1 file changed, 334 insertions(+) diff --git a/src/astrohack/io/panel_mds.py b/src/astrohack/io/panel_mds.py index bf6d698c..04dbc90f 100644 --- a/src/astrohack/io/panel_mds.py +++ b/src/astrohack/io/panel_mds.py @@ -1,3 +1,4 @@ +from astrohack.antenna.antenna_surface import AntennaSurface from astrohack.io.base_mds import AstrohackBaseFile @@ -17,3 +18,336 @@ def __init__(self, file: str): :rtype: AstrohackPanelFile """ super().__init__(file=file) + + # @toolviper.utils.parameter.validate() + def get_antenna(self, ant: str, ddi: int) -> AntennaSurface: + """Retrieve an AntennaSurface object for interaction + + :param ant: Antenna to be retrieved, ex. ea25. + :type ant: str + + :param ddi: DDI to be retrieved for ant_id, ex. 0 + :type ddi: int + + :return: AntennaSurface object describing for further interaction + :rtype: AntennaSurface + """ + ant = "ant_" + ant + ddi = f"ddi_{ddi}" + xds = self[ant][ddi].dataset + return AntennaSurface(xds, reread=True) + + # @toolviper.utils.parameter.validate(custom_checker=custom_plots_checker) + # def export_screws( + # self, + # destination: str, + # ant: Union[str, List[str]] = "all", + # ddi: Union[str, int, List[int]] = "all", + # unit: str = "mm", + # threshold: float = None, + # panel_labels: bool = True, + # display: bool = False, + # colormap: str = "RdBu_r", + # figure_size: Union[Tuple, List[float], np.array] = None, + # dpi: int = 300, + # ) -> None: + # """ Export screw adjustments to text files and optionally plots. + # + # :param destination: Name of the destination folder to contain exported screw adjustments + # :type destination: str + # :param ant: List of antennas/antenna to be exported, defaults to "all" when None, ex. ea25 + # :type ant: list or str, optional + # :param ddi: List of ddis/ddi to be exported, defaults to "all" when None, ex. 0 + # :type ddi: list or int, optional + # :param unit: Unit for screws adjustments, most length units supported, defaults to "mm" + # :type unit: str, optional + # :param threshold: Threshold below which data is considered negligible, value is assumed to be in the same unit\ + # as the plot, if not given defaults to 10% of the maximal deviation + # :type threshold: float, optional + # :param panel_labels: Add panel labels to antenna surface plots, default is True + # :type panel_labels: bool, optional + # :param display: Display plots inline or suppress, defaults to True + # :type display: bool, optional + # :param colormap: Colormap for screw adjustment map, default is RdBu_r + # :type colormap: str, optional + # :param figure_size: 2 element array/list/tuple with the screw adjustment map size in inches + # :type figure_size: numpy.ndarray, list, tuple, optional + # :param dpi: Screw adjustment map resolution in pixels per inch, default is 300 + # :type dpi: int, optional + # + # .. _Description: + # + # Produce the screw adjustments from ``astrohack.panel`` results to be used at the antenna site to improve \ + # the antenna surface + # + # """ + # param_dict = locals() + # + # pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) + # compute_graph( + # self, export_screws_chunk, param_dict, ["ant", "ddi"], parallel=False + # ) + # + # @toolviper.utils.parameter.validate(custom_checker=custom_plots_checker) + # def plot_antennas( + # self, + # destination: str, + # ant: Union[str, List[str]] = "all", + # ddi: Union[str, int, List[int]] = "all", + # plot_type: str = "deviation", + # plot_screws: bool = False, + # amplitude_limits: Union[Tuple, List[float], np.array] = None, + # phase_unit: str = "deg", + # phase_limits: Union[Tuple, List[float], np.array] = None, + # deviation_unit: str = "mm", + # deviation_limits: Union[Tuple, List[float], np.array] = None, + # panel_labels: bool = False, + # display: bool = False, + # colormap: str = "viridis", + # figure_size: Union[Tuple, List[float], np.array] = (8.0, 6.4), + # dpi: int = 300, + # parallel: bool = False, + # ) -> None: + # """ Create diagnostic plots of antenna surfaces from panel data file. + # + # :param destination: Name of the destination folder to contain plots + # :type destination: str + # + # :param ant: List of antennas/antenna to be plotted, defaults to "all" when None, ex. ea25 + # :type ant: list or str, optional + # + # :param ddi: List of ddis/ddi to be plotted, defaults to "all" when None, ex. 0 + # :type ddi: list or int, optional + # + # :param plot_type: type of plot to be produced, deviation, phase, ancillary or all, default is deviation + # :type plot_type: str, optional + # + # :param plot_screws: Add screw positions to plot + # :type plot_screws: bool, optional + # + # :param amplitude_limits: Lower than Upper limit for amplitude in volts default is None (Guess from data) + # :type amplitude_limits: numpy.ndarray, list, tuple, optional + # + # :param phase_unit: Unit for phase plots, defaults is 'deg' + # :type phase_unit: str, optional + # + # :param phase_limits: Lower than Upper limit for phase, value in phase_unit, default is None (Guess from data) + # :type phase_limits: numpy.ndarray, list, tuple, optional + # + # :param deviation_unit: Unit for deviation plots, defaults is 'mm' + # :type deviation_unit: str, optional + # + # :param deviation_limits: Lower than Upper limit for deviation, value in deviation_unit, default is None (Guess \ + # from data) + # :type deviation_limits: numpy.ndarray, list, tuple, optional + # + # :param panel_labels: Add panel labels to antenna surface plots, default is False + # :type panel_labels: bool, optional + # + # :param display: Display plots inline or suppress, defaults to True + # :type display: bool, optional + # + # :param colormap: Colormap for plots, default is viridis + # :type colormap: str, optional + # + # :param figure_size: 2 element array/list/tuple with the plot sizes in inches + # :type figure_size: numpy.ndarray, list, tuple, optional + # + # :param dpi: dots per inch to be used in plots, default is 300 + # :type dpi: int, optional + # + # :param parallel: If True will use an existing astrohack client to produce plots in parallel, default is False + # :type parallel: bool, optional + # + # .. _Description: + # + # Produce plots from ``astrohack.panel`` results to be analyzed to judge the quality of the results + # + # **Additional Information** + # .. rubric:: Available plot types: + # - *deviation*: Surface deviation estimated from phase and wavelength, three plots are produced for each antenna \ + # and ddi combination, surface before correction, the corrections applied and the corrected \ + # surface, most length units available + # - *phase*: Phase deviations over the surface, three plots are produced for each antenna and ddi combination, \ + # phase before correction, the corrections applied and the corrected phase, deg and rad available as \ + # units + # - *ancillary*: Two ancillary plots with useful information: The mask used to select data to be fitted, the \ + # amplitude data used to derive the mask, units are irrelevant for these plots + # - *all*: All the plots listed above. In this case the unit parameter is taken to mean the deviation unit, the \ + # phase unit is set to degrees + # """ + # + # param_dict = locals() + # + # pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) + # compute_graph( + # self, plot_antenna_chunk, param_dict, ["ant", "ddi"], parallel=parallel + # ) + # + # @toolviper.utils.parameter.validate() + # def export_to_fits( + # self, + # destination: str, + # ant: Union[str, List[str]] = "all", + # ddi: Union[str, int, List[int]] = "all", + # parallel: bool = False, + # ) -> None: + # """Export contents of an Astrohack MDS file to several FITS files in the destination folder + # + # :param destination: Name of the destination folder to contain plots + # :type destination: str + # + # :param ant: List of antennas/antenna to be plotted, defaults to "all" when None, ex. ea25 + # :type ant: list or str, optional + # + # :param ddi: List of ddis/ddi to be plotted, defaults to "all" when None, ex. 0 + # :type ddi: list or int, optional + # + # :param parallel: If True will use an existing astrohack client to export FITS in parallel, default is False + # :type parallel: bool, optional + # + # .. _Description: + # Export the products from the panel mds onto FITS files to be read by other software packages + # + # **Additional Information** + # + # The FITS fils produced by this method have been tested and are known to work with CARTA and DS9 + # """ + # + # param_dict = locals() + # + # pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) + # compute_graph( + # self, + # export_to_fits_panel_chunk, + # param_dict, + # ["ant", "ddi"], + # parallel=parallel, + # ) + # + # @toolviper.utils.parameter.validate(custom_checker=custom_unit_checker) + # def export_gain_tables( + # self, + # destination: str, + # ant: Union[str, List[str]] = "all", + # ddi: Union[str, int, List[int]] = "all", + # wavelengths: Union[float, List[float]] = None, + # wavelength_unit: str = "cm", + # frequencies: Union[float, List[float]] = None, + # frequency_unit: str = "GHz", + # rms_unit: str = "mm", + # parallel: bool = False, + # ) -> None: + # """ Compute estimated antenna gains in dB and saves them to ASCII files. + # + # :param destination: Name of the destination folder to contain ASCII files + # :type destination: str + # + # :param ant: List of antennas/antenna to be exported, defaults to "all" when None, ex. ea25 + # :type ant: list or str, optional + # + # :param ddi: List of ddis/ddi to be exported, defaults to "all" when None, ex. 0 + # :type ddi: list or int, optional + # + # :param wavelengths: List of wavelengths at which to compute the gains. + # :type wavelengths: list or float, optional + # + # :param wavelength_unit: Unit for the wavelengths being used, default is cm. + # :type wavelength_unit: str, optional + # + # :param frequencies: List of frequencies at which to compute the gains. + # :type frequencies: list or float, optional + # + # :param frequency_unit: Unit for the frequencies being used, default is GHz. + # :type frequency_unit: str, optional + # + # :param rms_unit: Unit for the Antenna surface RMS, default is mm. + # :type rms_unit: str, optional + # + # :param parallel: If True will use an existing astrohack client to produce ASCII files in parallel, default is False + # :type parallel: bool, optional + # + # .. _Description: + # + # Export antenna gains in dB from ``astrohack.panel`` for analysis. + # + # **Additional Information** + # + # .. rubric:: Selecting frequencies and wavelengths: + # + # If neither a frequency list nor a wavelength list is provided, ``export_gains_table`` will try to use a\ + # predefined list set for the telescope associated with the dataset. If both are provided, ``export_gains_table``\ + # will combine both lists. + # """ + # + # param_dict = locals() + # pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) + # compute_graph( + # self, + # export_gains_table_chunk, + # param_dict, + # ["ant", "ddi"], + # parallel=parallel, + # ) + # + # @toolviper.utils.parameter.validate(custom_checker=custom_unit_checker) + # def observation_summary( + # self, + # summary_file: str, + # ant: Union[str, List[str]] = "all", + # ddi: Union[str, int, List[int]] = "all", + # az_el_key: str = "center", + # phase_center_unit: str = "radec", + # az_el_unit: str = "deg", + # time_format: str = "%d %h %Y, %H:%M:%S", + # tab_size: int = 3, + # print_summary: bool = True, + # parallel: bool = False, + # ) -> None: + # """ Create a Summary of observation information + # + # :param summary_file: Text file to put the observation summary + # :type summary_file: str + # :param ant: antenna ID to use in subselection, defaults to "all" when None, ex. ea25 + # :type ant: list or str, optional + # :param ddi: data description ID to use in subselection, defaults to "all" when None, ex. 0 + # :type ddi: list or int, optional + # :param az_el_key: What type of Azimuth & Elevation information to print, 'mean', 'median' or 'center', default\ + # is 'center' + # :type az_el_key: str, optional + # :param phase_center_unit: What unit to display phase center coordinates, 'radec' and angle units supported, \ + # default is 'radec' + # :type phase_center_unit: str, optional + # :param az_el_unit: Angle unit used to display Azimuth & Elevation information, default is 'deg' + # :type az_el_unit: str, optional + # :param time_format: datetime time format for the start and end dates of observation, default is \ + # "%d %h %Y, %H:%M:%S" + # :type time_format: str, optional + # :param tab_size: Number of spaces in the tab levels, default is 3 + # :type tab_size: int, optional + # :param print_summary: Print the summary at the end of execution, default is True + # :type print_summary: bool, optional + # :param parallel: Run in parallel, defaults to False + # :type parallel: bool, optional + # + # **Additional Information** + # + # This method produces a summary of the data in the AstrohackPanelFile displaying general information, + # spectral information, beam image characteristics and aperture image characteristics. + # """ + # + # param_dict = locals() + # key_order = ["ant", "ddi"] + # execution, summary = compute_graph( + # self, + # generate_observation_summary, + # param_dict, + # key_order, + # parallel, + # fetch_returns=True, + # ) + # summary = "".join(summary) + # with open(summary_file, "w") as output_file: + # output_file.write(summary) + # if print_summary: + # print(summary) From 869ea923c0c72c9edbfa4f2eff8755ffea697c71 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Fri, 6 Feb 2026 15:12:03 -0700 Subject: [PATCH 098/295] Simplified graph machinery by consolidating graph execution functionality in a single function. Function parameter calls have been made explicit to avoid problems, except in mds.py which will be deprecated in the near future. --- src/astrohack/beamcut.py | 17 ++- src/astrohack/combine.py | 10 +- src/astrohack/combine_2.py | 14 +- src/astrohack/extract_holog_2.py | 14 +- src/astrohack/extract_pointing_2.py | 14 +- src/astrohack/holog.py | 10 +- src/astrohack/holog_2.py | 14 +- src/astrohack/io/beamcut_mds.py | 52 ++++---- src/astrohack/io/holog_mds.py | 38 ++++-- src/astrohack/io/image_mds.py | 68 ++++++---- src/astrohack/io/mds.py | 40 +++--- src/astrohack/io/position_mds.py | 30 +++-- src/astrohack/locit.py | 14 +- src/astrohack/panel.py | 12 +- src/astrohack/panel_2.py | 14 +- src/astrohack/utils/graph.py | 195 ++++++---------------------- 16 files changed, 251 insertions(+), 305 deletions(-) diff --git a/src/astrohack/beamcut.py b/src/astrohack/beamcut.py index 2ea1e0bb..d5007236 100644 --- a/src/astrohack/beamcut.py +++ b/src/astrohack/beamcut.py @@ -1,15 +1,14 @@ import pathlib import json -import xarray as xr import toolviper.utils.logger as logger from toolviper.utils.parameter import validate from astrohack.core.beamcut import process_beamcut_chunk -from astrohack.utils import get_default_file_name, add_caller_and_version_to_dict +from astrohack.utils import get_default_file_name from astrohack.utils.file import overwrite_file, check_if_file_can_be_opened -from astrohack.utils.graph import compute_graph, compute_graph_to_mds_tree +from astrohack.utils.graph import create_and_execute_graph_from_dict from astrohack.io.beamcut_mds import AstrohackBeamcutFile from astrohack.utils.validation import custom_plots_checker @@ -140,12 +139,12 @@ def beamcut( beamcut_params["beamcut_name"], beamcut_params ) - executed_graph = compute_graph_to_mds_tree( - holog_json, - process_beamcut_chunk, - beamcut_params, - ["ant", "ddi"], - beamcut_mds, + executed_graph = create_and_execute_graph_from_dict( + looping_dict=holog_json, + chunk_function=process_beamcut_chunk, + param_dict=beamcut_params, + key_order=["ant", "ddi"], + output_mds=beamcut_mds, parallel=parallel, ) diff --git a/src/astrohack/combine.py b/src/astrohack/combine.py index 8ed3f943..b7c54ff4 100644 --- a/src/astrohack/combine.py +++ b/src/astrohack/combine.py @@ -6,7 +6,7 @@ from astrohack.core.combine import process_combine_chunk -from astrohack.utils.graph import compute_graph +from astrohack.utils.graph import create_and_execute_graph_from_dict from astrohack.utils.file import overwrite_file from astrohack.utils.data import write_meta_data from astrohack.utils.text import get_default_file_name @@ -102,8 +102,12 @@ def combine( combine_params["image_mds"] = image_mds image_attr = image_mds._meta_data - if compute_graph( - image_mds, process_combine_chunk, combine_params, ["ant"], parallel=parallel + if create_and_execute_graph_from_dict( + looping_dict=image_mds, + chunk_function=process_combine_chunk, + param_dict=combine_params, + key_order=["ant"], + parallel=parallel, ): logger.info("Finished processing") diff --git a/src/astrohack/combine_2.py b/src/astrohack/combine_2.py index f0b52430..4f7fb044 100644 --- a/src/astrohack/combine_2.py +++ b/src/astrohack/combine_2.py @@ -5,7 +5,7 @@ from astrohack import open_image from astrohack.core.combine_2 import process_combine_chunk -from astrohack.utils.graph import compute_graph_to_mds_tree +from astrohack.utils.graph import create_and_execute_graph_from_dict from astrohack.utils.file import overwrite_file from astrohack.utils.text import get_default_file_name from astrohack.io.image_mds import AstrohackImageFile @@ -100,12 +100,12 @@ def combine( combine_name, combine_params ) - executed_graph = compute_graph_to_mds_tree( - image_mds, - process_combine_chunk, - combine_params, - ["ant"], - combine_mds, + executed_graph = create_and_execute_graph_from_dict( + looping_dict=image_mds, + chunk_function=process_combine_chunk, + param_dict=combine_params, + key_order=["ant"], + output_mds=combine_mds, ) if executed_graph: diff --git a/src/astrohack/extract_holog_2.py b/src/astrohack/extract_holog_2.py index 097a8f07..49490e66 100644 --- a/src/astrohack/extract_holog_2.py +++ b/src/astrohack/extract_holog_2.py @@ -28,7 +28,7 @@ from astrohack.io.point_mds import AstrohackPointFile from astrohack.extract_pointing import extract_pointing from astrohack.core.holog_obs_dict import HologObsDict -from astrohack.utils.graph import compute_graph_to_mds_tree +from astrohack.utils.graph import create_and_execute_graph_from_dict from typing import Union, List @@ -240,12 +240,12 @@ def extract_holog( extract_holog_params["pnt_mds"] = pnt_mds - executed_graph = compute_graph_to_mds_tree( - looping_dict, - process_extract_holog_chunk, - extract_holog_params, - ["ddi", "map"], - holog_mds, + executed_graph = create_and_execute_graph_from_dict( + looping_dict=looping_dict, + chunk_function=process_extract_holog_chunk, + param_dict=extract_holog_params, + key_order=["ddi", "map"], + output_mds=holog_mds, ) holog_mds.root.attrs["holog_obs_dict"] = used_holog_obs_dict diff --git a/src/astrohack/extract_pointing_2.py b/src/astrohack/extract_pointing_2.py index 7bb6b2f1..1c4632ad 100644 --- a/src/astrohack/extract_pointing_2.py +++ b/src/astrohack/extract_pointing_2.py @@ -3,7 +3,7 @@ import toolviper.utils.logger as logger from astrohack.utils import print_dict_types -from astrohack.utils.graph import compute_graph_to_mds_tree +from astrohack.utils.graph import create_and_execute_graph_from_dict from astrohack.utils.text import get_default_file_name from astrohack.utils.file import overwrite_file from astrohack.core.extract_pointing_2 import ( @@ -96,12 +96,12 @@ def extract_pointing( point_mds.root.attrs["antenna_stations"] = pnt_params.pop("antenna_stations") point_mds.root.attrs["telescope_name"] = pnt_params.pop("telescope_name") - executed_graph = compute_graph_to_mds_tree( - looping_dict, - make_ant_pnt_chunk, - pnt_params, - ["ant"], - point_mds, + executed_graph = create_and_execute_graph_from_dict( + looping_dict=looping_dict, + chunk_function=make_ant_pnt_chunk, + param_dict=pnt_params, + key_order=["ant"], + output_mds=point_mds, ) if executed_graph: diff --git a/src/astrohack/holog.py b/src/astrohack/holog.py index a5e6b3e3..d796271f 100644 --- a/src/astrohack/holog.py +++ b/src/astrohack/holog.py @@ -8,7 +8,7 @@ from numbers import Number from typing import List, Union, NewType, Tuple -from astrohack.utils.graph import compute_graph +from astrohack.utils.graph import create_and_execute_graph_from_dict from astrohack.utils.file import overwrite_file, check_if_file_can_be_opened from astrohack.utils.data import write_meta_data from astrohack.core.holog import process_holog_chunk @@ -182,8 +182,12 @@ def holog( with open(json_data, "r") as json_file: holog_json = json.load(json_file) - if compute_graph( - holog_json, process_holog_chunk, holog_params, ["ant", "ddi"], parallel=parallel + if create_and_execute_graph_from_dict( + looping_dict=holog_json, + chunk_function=process_holog_chunk, + param_dict=holog_params, + key_order=["ant", "ddi"], + parallel=parallel, ): output_attr_file = "{name}/{ext}".format( diff --git a/src/astrohack/holog_2.py b/src/astrohack/holog_2.py index 323e7271..55118ce3 100644 --- a/src/astrohack/holog_2.py +++ b/src/astrohack/holog_2.py @@ -5,7 +5,7 @@ import toolviper.utils.logger as logger from astrohack import open_holog -from astrohack.utils.graph import compute_graph_to_mds_tree +from astrohack.utils.graph import create_and_execute_graph_from_dict from astrohack.utils.file import overwrite_file from astrohack.core.holog_2 import process_holog_chunk from astrohack.utils.text import get_default_file_name @@ -171,12 +171,12 @@ def holog( image_name, holog_params ) - executed_graph = compute_graph_to_mds_tree( - holog_mds, - process_holog_chunk, - holog_params, - ["ant", "ddi"], - image_mds, + executed_graph = create_and_execute_graph_from_dict( + looping_dict=holog_mds, + chunk_function=process_holog_chunk, + param_dict=holog_params, + key_order=["ant", "ddi"], + output_mds=image_mds, ) if executed_graph: diff --git a/src/astrohack/io/beamcut_mds.py b/src/astrohack/io/beamcut_mds.py index 124c86a0..077db6e5 100644 --- a/src/astrohack/io/beamcut_mds.py +++ b/src/astrohack/io/beamcut_mds.py @@ -15,7 +15,7 @@ from astrohack.visualization.textual_data import ( generate_observation_summary_for_beamcut, ) -from astrohack.utils.graph import compute_graph +from astrohack.utils.graph import create_and_execute_graph_from_dict from astrohack.utils.validation import custom_plots_checker, custom_unit_checker @@ -97,12 +97,12 @@ def observation_summary( param_dict = locals() key_order = ["ant", "ddi"] - execution, summary_list = compute_graph( - self, - generate_observation_summary_for_beamcut, - param_dict, - key_order, - parallel, + execution, summary_list = create_and_execute_graph_from_dict( + looping_dict=self, + chunk_function=generate_observation_summary_for_beamcut, + param_dict=param_dict, + key_order=key_order, + parallel=parallel, fetch_returns=True, ) full_summary = "".join(summary_list) @@ -161,11 +161,11 @@ def plot_beamcut_in_amplitude( param_dict = locals() pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) - compute_graph( - self, - plot_beamcut_in_amplitude_chunk, - param_dict, - ["ant", "ddi"], + create_and_execute_graph_from_dict( + looping_dict=self, + chunk_function=plot_beamcut_in_amplitude_chunk, + param_dict=param_dict, + key_order=["ant", "ddi"], parallel=parallel, ) return @@ -220,11 +220,11 @@ def plot_beamcut_in_attenuation( param_dict = locals() pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) - compute_graph( - self, - plot_beamcut_in_attenuation_chunk, - param_dict, - ["ant", "ddi"], + create_and_execute_graph_from_dict( + looping_dict=self, + chunk_function=plot_beamcut_in_attenuation_chunk, + param_dict=param_dict, + key_order=["ant", "ddi"], parallel=parallel, ) return @@ -275,11 +275,11 @@ def plot_beam_cuts_over_sky( param_dict = locals() pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) - compute_graph( - self, - plot_cuts_in_lm_chunk, - param_dict, - ["ant", "ddi"], + create_and_execute_graph_from_dict( + looping_dict=self, + chunk_function=plot_cuts_in_lm_chunk, + param_dict=param_dict, + key_order=["ant", "ddi"], parallel=parallel, ) return @@ -322,7 +322,11 @@ def create_beam_fit_report( param_dict = locals() pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) - compute_graph( - self, create_report_chunk, param_dict, ["ant", "ddi"], parallel=parallel + create_and_execute_graph_from_dict( + looping_dict=self, + chunk_function=create_report_chunk, + param_dict=param_dict, + key_order=["ant", "ddi"], + parallel=parallel, ) return diff --git a/src/astrohack/io/holog_mds.py b/src/astrohack/io/holog_mds.py index a399cab2..fb7e8127 100644 --- a/src/astrohack/io/holog_mds.py +++ b/src/astrohack/io/holog_mds.py @@ -14,7 +14,7 @@ create_figure_and_axes, scatter_plot, ) -from astrohack.utils.graph import compute_graph +from astrohack.utils.graph import create_and_execute_graph_from_dict from astrohack.utils.conversion import convert_unit from astrohack.utils.algorithms import compute_average_stokes_visibilities from astrohack.visualization.textual_data import generate_observation_summary @@ -101,7 +101,13 @@ def plot_diagnostics( pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) key_order = ["ant", "ddi", "map"] - compute_graph(self, _calibration_plot_chunk, param_dict, key_order, parallel) + create_and_execute_graph_from_dict( + looping_dict=self, + chunk_function=_calibration_plot_chunk, + param_dict=param_dict, + key_order=key_order, + parallel=parallel, + ) # @toolviper.utils.parameter.validate(custom_checker=custom_plots_checker) def plot_lm_sky_coverage( @@ -184,7 +190,13 @@ def plot_lm_sky_coverage( pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) key_order = ["ant", "ddi", "map"] - compute_graph(self, _plot_lm_coverage_chunk, param_dict, key_order, parallel) + create_and_execute_graph_from_dict( + looping_dict=self, + chunk_function=_plot_lm_coverage_chunk, + param_dict=param_dict, + key_order=key_order, + parallel=parallel, + ) return # @toolviper.utils.parameter.validate(custom_checker=custom_plots_checker) @@ -224,7 +236,13 @@ def export_to_aips( pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) key_order = ["ant", "ddi", "map"] - compute_graph(self, _export_to_aips_chunk, param_dict, key_order, parallel) + create_and_execute_graph_from_dict( + looping_dict=self, + chunk_function=_export_to_aips_chunk, + param_dict=param_dict, + key_order=key_order, + parallel=parallel, + ) return # @toolviper.utils.parameter.validate(custom_checker=custom_unit_checker) @@ -290,12 +308,12 @@ def observation_summary( param_dict = locals() param_dict["map"] = map_id key_order = ["ant", "ddi", "map"] - execution, summary = compute_graph( - self, - generate_observation_summary, - param_dict, - key_order, - parallel, + execution, summary = create_and_execute_graph_from_dict( + looping_dict=self, + chunk_function=generate_observation_summary, + param_dict=param_dict, + key_order=key_order, + parallel=parallel, fetch_returns=True, ) summary = "".join(summary) diff --git a/src/astrohack/io/image_mds.py b/src/astrohack/io/image_mds.py index 62a851bc..6f9c5bb5 100644 --- a/src/astrohack/io/image_mds.py +++ b/src/astrohack/io/image_mds.py @@ -8,7 +8,7 @@ from astrohack.antenna.antenna_surface import AntennaSurface from astrohack.io.base_mds import AstrohackBaseFile from astrohack.utils.conversion import convert_5d_grid_from_stokes -from astrohack.utils.graph import compute_graph +from astrohack.utils.graph import create_and_execute_graph_from_dict from astrohack.utils.constants import clight, length_units, trigo_units from astrohack.utils.conversion import convert_unit from astrohack.utils.phase_fitting import aips_par_names @@ -98,11 +98,11 @@ def export_to_fits( param_dict = locals() pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) param_dict["input_params"] = self.root.attrs["input_parameters"] - compute_graph( - self, - _export_to_fits_chunk, - param_dict, - ["ant", "ddi"], + create_and_execute_graph_from_dict( + looping_dict=self, + chunk_function=_export_to_fits_chunk, + param_dict=param_dict, + key_order=["ant", "ddi"], parallel=parallel, ) @@ -184,8 +184,12 @@ def plot_apertures( param_dict = locals() pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) - compute_graph( - self, _plot_aperture_chunk, param_dict, ["ant", "ddi"], parallel=parallel + create_and_execute_graph_from_dict( + looping_dict=self, + chunk_function=_plot_aperture_chunk, + param_dict=param_dict, + key_order=["ant", "ddi"], + parallel=parallel, ) # @toolviper.utils.parameter.validate(custom_checker=custom_plots_checker) @@ -246,8 +250,12 @@ def plot_beams( param_dict = locals() pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) - compute_graph( - self, _plot_beam_chunk, param_dict, ["ant", "ddi"], parallel=parallel + create_and_execute_graph_from_dict( + looping_dict=self, + chunk_function=_plot_beam_chunk, + param_dict=param_dict, + key_order=["ant", "ddi"], + parallel=parallel, ) # @toolviper.utils.parameter.validate(custom_checker=custom_unit_checker) @@ -287,8 +295,12 @@ def export_phase_fit_results( param_dict = locals() pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) - compute_graph( - self, _export_phase_fit_chunk, param_dict, ["ant", "ddi"], parallel=parallel + create_and_execute_graph_from_dict( + looping_dict=self, + chunk_function=_export_phase_fit_chunk, + param_dict=param_dict, + key_order=["ant", "ddi"], + parallel=parallel, ) # @toolviper.utils.parameter.validate() @@ -321,11 +333,11 @@ def export_zernike_fit_results( param_dict = locals() pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) - compute_graph( - self, - _export_zernike_fit_chunk, - param_dict, - ["ant", "ddi"], + create_and_execute_graph_from_dict( + looping_dict=self, + chunk_function=_export_zernike_fit_chunk, + param_dict=param_dict, + key_order=["ant", "ddi"], parallel=parallel, ) @@ -375,11 +387,11 @@ def plot_zernike_model( param_dict = locals() pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) - compute_graph( - self, - plot_zernike_model_chunk, - param_dict, - ["ant", "ddi"], + create_and_execute_graph_from_dict( + looping_dict=self, + chunk_function=plot_zernike_model_chunk, + param_dict=param_dict, + key_order=["ant", "ddi"], parallel=parallel, ) @@ -441,12 +453,12 @@ def observation_summary( param_dict = locals() key_order = ["ant", "ddi"] - execution, summary = compute_graph( - self, - generate_observation_summary, - param_dict, - key_order, - parallel, + execution, summary = create_and_execute_graph_from_dict( + looping_dict=self, + chunk_function=generate_observation_summary, + param_dict=param_dict, + key_order=key_order, + parallel=parallel, fetch_returns=True, ) summary = "".join(summary) diff --git a/src/astrohack/io/mds.py b/src/astrohack/io/mds.py index bcd9be1c..c2a13c66 100644 --- a/src/astrohack/io/mds.py +++ b/src/astrohack/io/mds.py @@ -10,7 +10,7 @@ from astrohack.utils.validation import custom_unit_checker from astrohack.utils.validation import custom_split_checker -from astrohack.utils.graph import compute_graph +from astrohack.utils.graph import create_and_execute_graph_from_dict from astrohack.visualization.diagnostics import ( calibration_plot_chunk, @@ -260,7 +260,7 @@ def export_to_fits( param_dict = locals() pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) param_dict["metadata"] = self._meta_data - compute_graph( + create_and_execute_graph_from_dict( self, export_to_fits_holog_chunk, param_dict, @@ -331,7 +331,7 @@ def plot_apertures( param_dict = locals() pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) - compute_graph( + create_and_execute_graph_from_dict( self, plot_aperture_chunk, param_dict, ["ant", "ddi"], parallel=parallel ) @@ -383,7 +383,7 @@ def plot_beams( param_dict = locals() pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) - compute_graph( + create_and_execute_graph_from_dict( self, plot_beam_chunk, param_dict, ["ant", "ddi"], parallel=parallel ) @@ -419,7 +419,7 @@ def export_phase_fit_results( param_dict = locals() pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) - compute_graph( + create_and_execute_graph_from_dict( self, export_phase_fit_chunk, param_dict, ["ant", "ddi"], parallel=parallel ) @@ -450,7 +450,7 @@ def export_zernike_fit_results( param_dict = locals() pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) - compute_graph( + create_and_execute_graph_from_dict( self, export_zernike_fit_chunk, param_dict, @@ -497,7 +497,7 @@ def plot_zernike_model( param_dict = locals() pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) - compute_graph( + create_and_execute_graph_from_dict( self, plot_zernike_model_chunk, param_dict, @@ -553,7 +553,7 @@ def observation_summary( param_dict = locals() key_order = ["ant", "ddi"] - execution, summary = compute_graph( + execution, summary = create_and_execute_graph_from_dict( self, generate_observation_summary, param_dict, @@ -727,7 +727,9 @@ def plot_diagnostics( pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) key_order = ["ddi", "map", "ant"] - compute_graph(self, calibration_plot_chunk, param_dict, key_order, parallel) + create_and_execute_graph_from_dict( + self, calibration_plot_chunk, param_dict, key_order, parallel + ) @toolviper.utils.parameter.validate(custom_checker=custom_plots_checker) def plot_lm_sky_coverage( @@ -798,7 +800,9 @@ def plot_lm_sky_coverage( pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) key_order = ["ddi", "map", "ant"] - compute_graph(self, plot_lm_coverage, param_dict, key_order, parallel) + create_and_execute_graph_from_dict( + self, plot_lm_coverage, param_dict, key_order, parallel + ) return @toolviper.utils.parameter.validate(custom_checker=custom_plots_checker) @@ -834,7 +838,9 @@ def export_to_aips( pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) key_order = ["ddi", "map", "ant"] - compute_graph(self, export_to_aips, param_dict, key_order, parallel) + create_and_execute_graph_from_dict( + self, export_to_aips, param_dict, key_order, parallel + ) return @toolviper.utils.parameter.validate(custom_checker=custom_unit_checker) @@ -890,7 +896,7 @@ def observation_summary( param_dict = locals() param_dict["map"] = map_id key_order = ["ddi", "map", "ant"] - execution, summary = compute_graph( + execution, summary = create_and_execute_graph_from_dict( self, generate_observation_summary, param_dict, @@ -1045,7 +1051,7 @@ def export_screws( param_dict = locals() pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) - compute_graph( + create_and_execute_graph_from_dict( self, export_screws_chunk, param_dict, ["ant", "ddi"], parallel=False ) @@ -1141,7 +1147,7 @@ def plot_antennas( param_dict = locals() pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) - compute_graph( + create_and_execute_graph_from_dict( self, plot_antenna_chunk, param_dict, ["ant", "ddi"], parallel=parallel ) @@ -1178,7 +1184,7 @@ def export_to_fits( param_dict = locals() pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) - compute_graph( + create_and_execute_graph_from_dict( self, export_to_fits_panel_chunk, param_dict, @@ -1243,7 +1249,7 @@ def export_gain_tables( param_dict = locals() pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) - compute_graph( + create_and_execute_graph_from_dict( self, export_gains_table_chunk, param_dict, @@ -1299,7 +1305,7 @@ def observation_summary( param_dict = locals() key_order = ["ant", "ddi"] - execution, summary = compute_graph( + execution, summary = create_and_execute_graph_from_dict( self, generate_observation_summary, param_dict, diff --git a/src/astrohack/io/position_mds.py b/src/astrohack/io/position_mds.py index d0ad2a77..ceb85425 100644 --- a/src/astrohack/io/position_mds.py +++ b/src/astrohack/io/position_mds.py @@ -24,7 +24,7 @@ add_prefix, string_to_ascii_file, ) -from astrohack.utils.graph import compute_graph +from astrohack.utils.graph import create_and_execute_graph_from_dict from astrohack.utils.validation import custom_unit_checker @@ -308,15 +308,19 @@ def plot_sky_coverage( param_dict["combined"] = self.root.attrs["combined"] if self.root.attrs["combined"]: - compute_graph( - self, plot_sky_coverage_chunk, param_dict, ["ant"], parallel=parallel + create_and_execute_graph_from_dict( + looping_dict=self, + chunk_function=plot_sky_coverage_chunk, + param_dict=param_dict, + key_order=["ant"], + parallel=parallel, ) else: - compute_graph( - self, - plot_sky_coverage_chunk, - param_dict, - ["ant", "ddi"], + create_and_execute_graph_from_dict( + looping_dict=self, + chunk_function=plot_sky_coverage_chunk, + param_dict=param_dict, + key_order=["ant", "ddi"], parallel=parallel, ) @@ -389,11 +393,15 @@ def plot_delays( param_dict["combined"] = self.root.attrs["combined"] param_dict["comb_type"] = self.root.attrs["input_parameters"]["combine_ddis"] if self.root.attrs["combined"]: - compute_graph( - self, plot_delays_chunk, param_dict, ["ant"], parallel=parallel + create_and_execute_graph_from_dict( + looping_dict=self, + chunk_function=plot_delays_chunk, + param_dict=param_dict, + key_order=["ant"], + parallel=parallel, ) else: - compute_graph( + create_and_execute_graph_from_dict( self, plot_delays_chunk, param_dict, ["ant", "ddi"], parallel=parallel ) diff --git a/src/astrohack/locit.py b/src/astrohack/locit.py index 861b1cfd..96d5b838 100644 --- a/src/astrohack/locit.py +++ b/src/astrohack/locit.py @@ -3,7 +3,7 @@ import toolviper.utils.parameter import toolviper.utils.logger as logger -from astrohack.utils.graph import compute_graph_to_mds_tree +from astrohack.utils.graph import create_and_execute_graph_from_dict from astrohack.utils.file import overwrite_file, check_if_file_can_be_opened_2 from astrohack.core.locit import ( locit_separated_chunk, @@ -188,12 +188,12 @@ def locit( locit_params["position_name"], locit_params ) - executed_graph = compute_graph_to_mds_tree( - locit_mds, - function, - locit_params, - key_order, - position_mds, + executed_graph = create_and_execute_graph_from_dict( + looping_dict=locit_mds, + chunk_function=function, + param_dict=locit_params, + key_order=key_order, + output_mds=position_mds, parallel=parallel, ) if executed_graph: diff --git a/src/astrohack/panel.py b/src/astrohack/panel.py index e3f99e61..b1ca5eb6 100644 --- a/src/astrohack/panel.py +++ b/src/astrohack/panel.py @@ -10,7 +10,7 @@ from astrohack.core.panel import process_panel_chunk from astrohack.utils.validation import custom_panel_checker from astrohack.utils.text import get_default_file_name -from astrohack.utils.graph import compute_graph +from astrohack.utils.graph import create_and_execute_graph_from_dict from astrohack.io.mds import AstrohackPanelFile, AstrohackImageFile @@ -237,11 +237,11 @@ def panel( else: panel_params["origin"] = "astrohack" panel_params["version"] = image_mds._input_pars["version"] - if compute_graph( - image_mds, - process_panel_chunk, - panel_params, - ["ant", "ddi"], + if create_and_execute_graph_from_dict( + looping_dict=image_mds, + chunk_function=process_panel_chunk, + param_dict=panel_params, + key_order=["ant", "ddi"], parallel=parallel, ): logger.info("Finished processing") diff --git a/src/astrohack/panel_2.py b/src/astrohack/panel_2.py index 173dfa73..d753cee7 100644 --- a/src/astrohack/panel_2.py +++ b/src/astrohack/panel_2.py @@ -10,7 +10,7 @@ from astrohack.core.panel_2 import process_panel_chunk from astrohack.utils.validation import custom_panel_checker from astrohack.utils.text import get_default_file_name -from astrohack.utils.graph import compute_graph_to_mds_tree +from astrohack.utils.graph import create_and_execute_graph_from_dict from astrohack.io.panel_mds import AstrohackPanelFile from astrohack.io.dio import open_image @@ -219,12 +219,12 @@ def panel( panel_name, panel_params ) - executed_graph = compute_graph_to_mds_tree( - image_mds, - process_panel_chunk, - panel_params, - ["ant", "ddi"], - panel_mds, + executed_graph = create_and_execute_graph_from_dict( + looping_dict=image_mds, + chunk_function=process_panel_chunk, + param_dict=panel_params, + key_order=["ant", "ddi"], + output_mds=panel_mds, ) if executed_graph: diff --git a/src/astrohack/utils/graph.py b/src/astrohack/utils/graph.py index 45ea97f8..ec723a18 100644 --- a/src/astrohack/utils/graph.py +++ b/src/astrohack/utils/graph.py @@ -7,71 +7,25 @@ from astrohack.utils.text import param_to_list -def _construct_xdtree_graph_recursively( - xr_datatree, - chunk_function, - param_dict, - delayed_list, - key_order, - output_mds=None, - parallel=False, - oneup=None, -): - if len(key_order) == 0: - param_dict["xdt_data"] = xr_datatree - if output_mds is None: - args = [param_dict] - else: - args = [param_dict, output_mds] - if parallel: - delayed_list.append(dask.delayed(chunk_function)(*args)) - else: - delayed_list.append((chunk_function, args)) - else: - key_base = key_order[0] - exec_list = param_to_list(param_dict[key_base], xr_datatree, key_base) - - white_list = [key for key in exec_list if approve_prefix(key)] - - for item in white_list: - this_param_dict = copy.deepcopy(param_dict) - this_param_dict[f"this_{key_base}"] = item - - if item in xr_datatree: - _construct_xdtree_graph_recursively( - xr_datatree=xr_datatree[item], - chunk_function=chunk_function, - param_dict=this_param_dict, - delayed_list=delayed_list, - key_order=key_order[1:], - parallel=parallel, - output_mds=output_mds, - oneup=item, - ) - - else: - if oneup is None: - logger.warning(f"{item} is not present in DataTree") - else: - logger.warning(f"{item} is not present for {oneup}") - - def _construct_general_graph_recursively( looping_dict, chunk_function, param_dict, delayed_list, key_order, - output_mds=None, - parallel=False, + output_mds, + parallel, oneup=None, ): if len(key_order) == 0: - if isinstance(looping_dict, xr.Dataset): + if isinstance(looping_dict, xr.DataTree): + param_dict["xdt_data"] = looping_dict + elif isinstance(looping_dict, xr.Dataset): param_dict["xds_data"] = looping_dict - elif isinstance(looping_dict, dict): - param_dict["data_dict"] = looping_dict + param_dict["dic_data"] = looping_dict + else: + param_dict["unk_data"] = looping_dict if output_mds is None: args = [param_dict] @@ -85,10 +39,10 @@ def _construct_general_graph_recursively( key = key_order[0] exec_list = param_to_list(param_dict[key], looping_dict, key) - white_list = [key for key in exec_list if approve_prefix(key)] for item in white_list: + print(item) this_param_dict = copy.deepcopy(param_dict) this_param_dict[f"this_{key}"] = item @@ -111,122 +65,59 @@ def _construct_general_graph_recursively( logger.warning(f"{item} is not present for {oneup}") -def compute_graph_to_mds_tree( +def create_and_execute_graph_from_dict( looping_dict, chunk_function, param_dict, key_order, - output_mds, + output_mds=None, parallel=False, fetch_returns=False, ): - delayed_list = [] - if hasattr(looping_dict, "root"): - _construct_xdtree_graph_recursively( - xr_datatree=looping_dict.root, - chunk_function=chunk_function, - param_dict=param_dict, - delayed_list=delayed_list, - key_order=key_order, - output_mds=output_mds, - parallel=parallel, - ) - else: - _construct_general_graph_recursively( - looping_dict=looping_dict, - chunk_function=chunk_function, - param_dict=param_dict, - delayed_list=delayed_list, - key_order=key_order, - output_mds=output_mds, - parallel=parallel, - ) - - if len(delayed_list) == 0: - logger.warning(f"List of delayed processing jobs is empty: No data to process") - if fetch_returns: - return False, None - else: - return False - else: - if parallel: - return_list = dask.compute(delayed_list)[0] - else: - return_list = [] - for function, args in delayed_list: - return_list.append(function(*args)) - - if len(output_mds.keys()) == 0: - logger.warning("Processing did not yield any data") - if fetch_returns: - return False, None + def _factorized_return(status, ret_list, fetch_ret): + print(status, len(ret_list), fetch_ret) + if fetch_ret: + if status: + return status, ret_list else: - return False - if fetch_returns: - return True, return_list + return status, None else: - return True - + return status -def compute_graph( - looping_dict, - chunk_function, - param_dict, - key_order, - parallel=False, - fetch_returns=False, -): - """ - General tool for looping over the data and constructing graphs for dask parallel processing - Args: - looping_dict: The dictionary containing the keys over which the loops are to be executed - chunk_function: The chunk function to be executed - param_dict: The parameter dictionary for the chunk function - key_order: The order over which to loop over the keys inside the looping dictionary - parallel: Are loops to be executed in parallel? - fetch_returns: retrieve returns and return them to the caller - - Returns: True if processing has occurred, False if no data was processed - - """ + if hasattr(looping_dict, "root"): + looping_dict = looping_dict.root + # List created here to avoid complicated returns due to recursion. delayed_list = [] - if hasattr(looping_dict, "root"): - _construct_xdtree_graph_recursively( - xr_datatree=looping_dict.root, - chunk_function=chunk_function, - param_dict=param_dict, - delayed_list=delayed_list, - key_order=key_order, - parallel=parallel, - ) - else: - _construct_general_graph_recursively( - looping_dict=looping_dict, - chunk_function=chunk_function, - param_dict=param_dict, - delayed_list=delayed_list, - key_order=key_order, - parallel=parallel, - ) + _construct_general_graph_recursively( + looping_dict=looping_dict, + chunk_function=chunk_function, + param_dict=param_dict, + delayed_list=delayed_list, + key_order=key_order, + output_mds=output_mds, + parallel=parallel, + ) if len(delayed_list) == 0: logger.warning(f"List of delayed processing jobs is empty: No data to process") + return _factorized_return(False, [], fetch_returns) - return False - + if parallel: + return_list = dask.compute(delayed_list)[0] else: - if parallel: - return_list = dask.compute(delayed_list)[0] - else: - return_list = [] - for function, args in delayed_list: - return_list.append(function(*args)) + return_list = [] + for function, args in delayed_list: + return_list.append(function(*args)) - if fetch_returns: - return True, return_list + if output_mds is not None: + if len(output_mds.keys()) == 0: + logger.warning("Processing did not yield any data") + return _factorized_return(False, return_list, fetch_returns) else: - return True + return _factorized_return(True, return_list, fetch_returns) + + return _factorized_return(True, return_list, fetch_returns) def compute_graph_from_lists( From 02eb472cd843eb6088ecaecb0ed92716018ea793 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Fri, 6 Feb 2026 15:35:07 -0700 Subject: [PATCH 099/295] Ported panel_mds.export_screws to the new datatree file format. --- src/astrohack/io/panel_mds.py | 141 +++++++++++++++++++++------------- 1 file changed, 88 insertions(+), 53 deletions(-) diff --git a/src/astrohack/io/panel_mds.py b/src/astrohack/io/panel_mds.py index 04dbc90f..9e8265d6 100644 --- a/src/astrohack/io/panel_mds.py +++ b/src/astrohack/io/panel_mds.py @@ -1,5 +1,11 @@ +import numpy as np +import pathlib + +from typing import Union, List, Tuple + from astrohack.antenna.antenna_surface import AntennaSurface from astrohack.io.base_mds import AstrohackBaseFile +from astrohack.utils.graph import create_and_execute_graph_from_dict class AstrohackPanelFile(AstrohackBaseFile): @@ -38,55 +44,69 @@ def get_antenna(self, ant: str, ddi: int) -> AntennaSurface: return AntennaSurface(xds, reread=True) # @toolviper.utils.parameter.validate(custom_checker=custom_plots_checker) - # def export_screws( - # self, - # destination: str, - # ant: Union[str, List[str]] = "all", - # ddi: Union[str, int, List[int]] = "all", - # unit: str = "mm", - # threshold: float = None, - # panel_labels: bool = True, - # display: bool = False, - # colormap: str = "RdBu_r", - # figure_size: Union[Tuple, List[float], np.array] = None, - # dpi: int = 300, - # ) -> None: - # """ Export screw adjustments to text files and optionally plots. - # - # :param destination: Name of the destination folder to contain exported screw adjustments - # :type destination: str - # :param ant: List of antennas/antenna to be exported, defaults to "all" when None, ex. ea25 - # :type ant: list or str, optional - # :param ddi: List of ddis/ddi to be exported, defaults to "all" when None, ex. 0 - # :type ddi: list or int, optional - # :param unit: Unit for screws adjustments, most length units supported, defaults to "mm" - # :type unit: str, optional - # :param threshold: Threshold below which data is considered negligible, value is assumed to be in the same unit\ - # as the plot, if not given defaults to 10% of the maximal deviation - # :type threshold: float, optional - # :param panel_labels: Add panel labels to antenna surface plots, default is True - # :type panel_labels: bool, optional - # :param display: Display plots inline or suppress, defaults to True - # :type display: bool, optional - # :param colormap: Colormap for screw adjustment map, default is RdBu_r - # :type colormap: str, optional - # :param figure_size: 2 element array/list/tuple with the screw adjustment map size in inches - # :type figure_size: numpy.ndarray, list, tuple, optional - # :param dpi: Screw adjustment map resolution in pixels per inch, default is 300 - # :type dpi: int, optional - # - # .. _Description: - # - # Produce the screw adjustments from ``astrohack.panel`` results to be used at the antenna site to improve \ - # the antenna surface - # - # """ - # param_dict = locals() - # - # pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) - # compute_graph( - # self, export_screws_chunk, param_dict, ["ant", "ddi"], parallel=False - # ) + def export_screws( + self, + destination: str, + ant: Union[str, List[str]] = "all", + ddi: Union[str, int, List[int]] = "all", + unit: str = "mm", + threshold: float = None, + panel_labels: bool = True, + display: bool = False, + colormap: str = "RdBu_r", + figure_size: Union[Tuple, List[float], np.array] = None, + dpi: int = 300, + ) -> None: + """ Export screw adjustments to text files and optionally plots. + + :param destination: Name of the destination folder to contain exported screw adjustments + :type destination: str + + :param ant: List of antennas/antenna to be exported, defaults to "all" when None, ex. ea25 + :type ant: list or str, optional + + :param ddi: List of ddis/ddi to be exported, defaults to "all" when None, ex. 0 + :type ddi: list or int, optional + + :param unit: Unit for screws adjustments, most length units supported, defaults to "mm" + :type unit: str, optional + + :param threshold: Threshold below which data is considered negligible, value is assumed to be in the same unit\ + as the plot, if not given defaults to 10% of the maximal deviation + :type threshold: float, optional + + :param panel_labels: Add panel labels to antenna surface plots, default is True + :type panel_labels: bool, optional + + :param display: Display plots inline or suppress, defaults to True + :type display: bool, optional + + :param colormap: Colormap for screw adjustment map, default is RdBu_r + :type colormap: str, optional + + :param figure_size: 2 element array/list/tuple with the screw adjustment map size in inches + :type figure_size: numpy.ndarray, list, tuple, optional + + :param dpi: Screw adjustment map resolution in pixels per inch, default is 300 + :type dpi: int, optional + + .. _Description: + + Produce the screw adjustments from ``astrohack.panel`` results to be used at the antenna site to improve \ + the antenna surface + + """ + param_dict = locals() + + pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) + create_and_execute_graph_from_dict( + looping_dict=self, + chunk_function=_export_screws_chunk, + param_dict=param_dict, + key_order=["ant", "ddi"], + parallel=False, + ) + # # @toolviper.utils.parameter.validate(custom_checker=custom_plots_checker) # def plot_antennas( @@ -180,7 +200,7 @@ def get_antenna(self, ant: str, ddi: int) -> AntennaSurface: # param_dict = locals() # # pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) - # compute_graph( + # create_and_execute_graph_from_dict( # self, plot_antenna_chunk, param_dict, ["ant", "ddi"], parallel=parallel # ) # @@ -217,7 +237,7 @@ def get_antenna(self, ant: str, ddi: int) -> AntennaSurface: # param_dict = locals() # # pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) - # compute_graph( + # create_and_execute_graph_from_dict( # self, # export_to_fits_panel_chunk, # param_dict, @@ -282,7 +302,7 @@ def get_antenna(self, ant: str, ddi: int) -> AntennaSurface: # # param_dict = locals() # pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) - # compute_graph( + # create_and_execute_graph_from_dict( # self, # export_gains_table_chunk, # param_dict, @@ -338,7 +358,7 @@ def get_antenna(self, ant: str, ddi: int) -> AntennaSurface: # # param_dict = locals() # key_order = ["ant", "ddi"] - # execution, summary = compute_graph( + # execution, summary = create_and_execute_graph_from_dict( # self, # generate_observation_summary, # param_dict, @@ -351,3 +371,18 @@ def get_antenna(self, ant: str, ddi: int) -> AntennaSurface: # output_file.write(summary) # if print_summary: # print(summary) + + +def _export_screws_chunk(parm_dict): + """ + Chunk function for the user facing function export_screws + Args: + parm_dict: parameter dictionary + """ + antenna = parm_dict["this_ant"] + ddi = parm_dict["this_ddi"] + export_name = parm_dict["destination"] + f"/panel_screws_{antenna}_{ddi}." + xds = parm_dict["xdt_data"] + surface = AntennaSurface(xds, reread=True) + surface.export_screws(export_name + "txt", unit=parm_dict["unit"]) + surface.plot_screw_adjustments(export_name + "png", parm_dict) From e03c0c0e6491bcdcc3dabe6f6373dd000530a9b1 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Fri, 6 Feb 2026 15:36:38 -0700 Subject: [PATCH 100/295] Removed debug prints. --- src/astrohack/utils/graph.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/astrohack/utils/graph.py b/src/astrohack/utils/graph.py index ec723a18..6d956109 100644 --- a/src/astrohack/utils/graph.py +++ b/src/astrohack/utils/graph.py @@ -42,7 +42,6 @@ def _construct_general_graph_recursively( white_list = [key for key in exec_list if approve_prefix(key)] for item in white_list: - print(item) this_param_dict = copy.deepcopy(param_dict) this_param_dict[f"this_{key}"] = item @@ -75,7 +74,6 @@ def create_and_execute_graph_from_dict( fetch_returns=False, ): def _factorized_return(status, ret_list, fetch_ret): - print(status, len(ret_list), fetch_ret) if fetch_ret: if status: return status, ret_list From e4fd2124add65a0d015ea7ede0cc6e99945f2f12 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Fri, 6 Feb 2026 15:41:57 -0700 Subject: [PATCH 101/295] Ported panel_mds.plot_antennas to the new datatree file format. --- src/astrohack/io/panel_mds.py | 218 +++++++++++++++++++--------------- 1 file changed, 123 insertions(+), 95 deletions(-) diff --git a/src/astrohack/io/panel_mds.py b/src/astrohack/io/panel_mds.py index 9e8265d6..e985baa4 100644 --- a/src/astrohack/io/panel_mds.py +++ b/src/astrohack/io/panel_mds.py @@ -4,6 +4,7 @@ from typing import Union, List, Tuple from astrohack.antenna.antenna_surface import AntennaSurface +from astrohack.utils.constants import plot_types from astrohack.io.base_mds import AstrohackBaseFile from astrohack.utils.graph import create_and_execute_graph_from_dict @@ -107,102 +108,102 @@ def export_screws( parallel=False, ) - # # @toolviper.utils.parameter.validate(custom_checker=custom_plots_checker) - # def plot_antennas( - # self, - # destination: str, - # ant: Union[str, List[str]] = "all", - # ddi: Union[str, int, List[int]] = "all", - # plot_type: str = "deviation", - # plot_screws: bool = False, - # amplitude_limits: Union[Tuple, List[float], np.array] = None, - # phase_unit: str = "deg", - # phase_limits: Union[Tuple, List[float], np.array] = None, - # deviation_unit: str = "mm", - # deviation_limits: Union[Tuple, List[float], np.array] = None, - # panel_labels: bool = False, - # display: bool = False, - # colormap: str = "viridis", - # figure_size: Union[Tuple, List[float], np.array] = (8.0, 6.4), - # dpi: int = 300, - # parallel: bool = False, - # ) -> None: - # """ Create diagnostic plots of antenna surfaces from panel data file. - # - # :param destination: Name of the destination folder to contain plots - # :type destination: str - # - # :param ant: List of antennas/antenna to be plotted, defaults to "all" when None, ex. ea25 - # :type ant: list or str, optional - # - # :param ddi: List of ddis/ddi to be plotted, defaults to "all" when None, ex. 0 - # :type ddi: list or int, optional - # - # :param plot_type: type of plot to be produced, deviation, phase, ancillary or all, default is deviation - # :type plot_type: str, optional - # - # :param plot_screws: Add screw positions to plot - # :type plot_screws: bool, optional - # - # :param amplitude_limits: Lower than Upper limit for amplitude in volts default is None (Guess from data) - # :type amplitude_limits: numpy.ndarray, list, tuple, optional - # - # :param phase_unit: Unit for phase plots, defaults is 'deg' - # :type phase_unit: str, optional - # - # :param phase_limits: Lower than Upper limit for phase, value in phase_unit, default is None (Guess from data) - # :type phase_limits: numpy.ndarray, list, tuple, optional - # - # :param deviation_unit: Unit for deviation plots, defaults is 'mm' - # :type deviation_unit: str, optional - # - # :param deviation_limits: Lower than Upper limit for deviation, value in deviation_unit, default is None (Guess \ - # from data) - # :type deviation_limits: numpy.ndarray, list, tuple, optional - # - # :param panel_labels: Add panel labels to antenna surface plots, default is False - # :type panel_labels: bool, optional - # - # :param display: Display plots inline or suppress, defaults to True - # :type display: bool, optional - # - # :param colormap: Colormap for plots, default is viridis - # :type colormap: str, optional - # - # :param figure_size: 2 element array/list/tuple with the plot sizes in inches - # :type figure_size: numpy.ndarray, list, tuple, optional - # - # :param dpi: dots per inch to be used in plots, default is 300 - # :type dpi: int, optional - # - # :param parallel: If True will use an existing astrohack client to produce plots in parallel, default is False - # :type parallel: bool, optional - # - # .. _Description: - # - # Produce plots from ``astrohack.panel`` results to be analyzed to judge the quality of the results - # - # **Additional Information** - # .. rubric:: Available plot types: - # - *deviation*: Surface deviation estimated from phase and wavelength, three plots are produced for each antenna \ - # and ddi combination, surface before correction, the corrections applied and the corrected \ - # surface, most length units available - # - *phase*: Phase deviations over the surface, three plots are produced for each antenna and ddi combination, \ - # phase before correction, the corrections applied and the corrected phase, deg and rad available as \ - # units - # - *ancillary*: Two ancillary plots with useful information: The mask used to select data to be fitted, the \ - # amplitude data used to derive the mask, units are irrelevant for these plots - # - *all*: All the plots listed above. In this case the unit parameter is taken to mean the deviation unit, the \ - # phase unit is set to degrees - # """ - # - # param_dict = locals() - # - # pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) - # create_and_execute_graph_from_dict( - # self, plot_antenna_chunk, param_dict, ["ant", "ddi"], parallel=parallel - # ) + def plot_antennas( + self, + destination: str, + ant: Union[str, List[str]] = "all", + ddi: Union[str, int, List[int]] = "all", + plot_type: str = "deviation", + plot_screws: bool = False, + amplitude_limits: Union[Tuple, List[float], np.array] = None, + phase_unit: str = "deg", + phase_limits: Union[Tuple, List[float], np.array] = None, + deviation_unit: str = "mm", + deviation_limits: Union[Tuple, List[float], np.array] = None, + panel_labels: bool = False, + display: bool = False, + colormap: str = "viridis", + figure_size: Union[Tuple, List[float], np.array] = (8.0, 6.4), + dpi: int = 300, + parallel: bool = False, + ) -> None: + """ Create diagnostic plots of antenna surfaces from panel data file. + + :param destination: Name of the destination folder to contain plots + :type destination: str + + :param ant: List of antennas/antenna to be plotted, defaults to "all" when None, ex. ea25 + :type ant: list or str, optional + + :param ddi: List of ddis/ddi to be plotted, defaults to "all" when None, ex. 0 + :type ddi: list or int, optional + + :param plot_type: type of plot to be produced, deviation, phase, ancillary or all, default is deviation + :type plot_type: str, optional + + :param plot_screws: Add screw positions to plot + :type plot_screws: bool, optional + + :param amplitude_limits: Lower than Upper limit for amplitude in volts default is None (Guess from data) + :type amplitude_limits: numpy.ndarray, list, tuple, optional + + :param phase_unit: Unit for phase plots, defaults is 'deg' + :type phase_unit: str, optional + + :param phase_limits: Lower than Upper limit for phase, value in phase_unit, default is None (Guess from data) + :type phase_limits: numpy.ndarray, list, tuple, optional + + :param deviation_unit: Unit for deviation plots, defaults is 'mm' + :type deviation_unit: str, optional + + :param deviation_limits: Lower than Upper limit for deviation, value in deviation_unit, default is None (Guess \ + from data) + :type deviation_limits: numpy.ndarray, list, tuple, optional + + :param panel_labels: Add panel labels to antenna surface plots, default is False + :type panel_labels: bool, optional + + :param display: Display plots inline or suppress, defaults to True + :type display: bool, optional + + :param colormap: Colormap for plots, default is viridis + :type colormap: str, optional + + :param figure_size: 2 element array/list/tuple with the plot sizes in inches + :type figure_size: numpy.ndarray, list, tuple, optional + + :param dpi: dots per inch to be used in plots, default is 300 + :type dpi: int, optional + + :param parallel: If True will use an existing astrohack client to produce plots in parallel, default is False + :type parallel: bool, optional + + .. _Description: + + Produce plots from ``astrohack.panel`` results to be analyzed to judge the quality of the results + + **Additional Information** + .. rubric:: Available plot types: + - *deviation*: Surface deviation estimated from phase and wavelength, three plots are produced for each antenna \ + and ddi combination, surface before correction, the corrections applied and the corrected \ + surface, most length units available + - *phase*: Phase deviations over the surface, three plots are produced for each antenna and ddi combination, \ + phase before correction, the corrections applied and the corrected phase, deg and rad available as \ + units + - *ancillary*: Two ancillary plots with useful information: The mask used to select data to be fitted, the \ + amplitude data used to derive the mask, units are irrelevant for these plots + - *all*: All the plots listed above. In this case the unit parameter is taken to mean the deviation unit, the \ + phase unit is set to degrees + """ + + param_dict = locals() + + pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) + create_and_execute_graph_from_dict( + self, _plot_antenna_chunk, param_dict, ["ant", "ddi"], parallel=parallel + ) + # # @toolviper.utils.parameter.validate() # def export_to_fits( @@ -386,3 +387,30 @@ def _export_screws_chunk(parm_dict): surface = AntennaSurface(xds, reread=True) surface.export_screws(export_name + "txt", unit=parm_dict["unit"]) surface.plot_screw_adjustments(export_name + "png", parm_dict) + + +def _plot_antenna_chunk(parm_dict): + """ + Chunk function for the user facing function plot_antenna + Args: + parm_dict: parameter dictionary + """ + antenna = parm_dict["this_ant"] + ddi = parm_dict["this_ddi"] + destination = parm_dict["destination"] + plot_type = parm_dict["plot_type"] + basename = f"{destination}/{antenna}_{ddi}" + xds = parm_dict["xdt_data"] + surface = AntennaSurface(xds, reread=True) + if plot_type == plot_types[0]: # deviation plot + surface.plot_deviation(basename, "panel", parm_dict) + elif plot_type == plot_types[1]: # phase plot + surface.plot_phase(basename, "panel", parm_dict) + elif plot_type == plot_types[2]: # Ancillary plot + surface.plot_mask(basename, "panel", parm_dict) + surface.plot_amplitude(basename, "panel", parm_dict) + else: # all plots + surface.plot_deviation(basename, "panel", parm_dict) + surface.plot_phase(basename, "panel", parm_dict) + surface.plot_mask(basename, "panel", parm_dict) + surface.plot_amplitude(basename, "panel", parm_dict) From de19abe55c5fdf8988a94a1c9bd7d4694b9a06bf Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Fri, 6 Feb 2026 15:52:29 -0700 Subject: [PATCH 102/295] Ported panel_mds.export_to_fits to the new datatree file format. --- src/astrohack/io/image_mds.py | 4 +- src/astrohack/io/panel_mds.py | 102 +++++++++++++++++++++------------- 2 files changed, 64 insertions(+), 42 deletions(-) diff --git a/src/astrohack/io/image_mds.py b/src/astrohack/io/image_mds.py index 6f9c5bb5..742b65c2 100644 --- a/src/astrohack/io/image_mds.py +++ b/src/astrohack/io/image_mds.py @@ -389,7 +389,7 @@ def plot_zernike_model( pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) create_and_execute_graph_from_dict( looping_dict=self, - chunk_function=plot_zernike_model_chunk, + chunk_function=_plot_zernike_model_chunk, param_dict=param_dict, key_order=["ant", "ddi"], parallel=parallel, @@ -887,7 +887,7 @@ def _export_zernike_fit_chunk(parm_dict): string_to_ascii_file(outstr, f"{destination}/image_zernike_fit_{antenna}_{ddi}.txt") -def plot_zernike_model_chunk(parm_dict): +def _plot_zernike_model_chunk(parm_dict): """ Chunk function for the user facing function plot_zernike_model Args: diff --git a/src/astrohack/io/panel_mds.py b/src/astrohack/io/panel_mds.py index e985baa4..792f749e 100644 --- a/src/astrohack/io/panel_mds.py +++ b/src/astrohack/io/panel_mds.py @@ -3,6 +3,8 @@ from typing import Union, List, Tuple +import toolviper.utils.logger as logger + from astrohack.antenna.antenna_surface import AntennaSurface from astrohack.utils.constants import plot_types from astrohack.io.base_mds import AstrohackBaseFile @@ -204,47 +206,47 @@ def plot_antennas( self, _plot_antenna_chunk, param_dict, ["ant", "ddi"], parallel=parallel ) - # # @toolviper.utils.parameter.validate() - # def export_to_fits( - # self, - # destination: str, - # ant: Union[str, List[str]] = "all", - # ddi: Union[str, int, List[int]] = "all", - # parallel: bool = False, - # ) -> None: - # """Export contents of an Astrohack MDS file to several FITS files in the destination folder - # - # :param destination: Name of the destination folder to contain plots - # :type destination: str - # - # :param ant: List of antennas/antenna to be plotted, defaults to "all" when None, ex. ea25 - # :type ant: list or str, optional - # - # :param ddi: List of ddis/ddi to be plotted, defaults to "all" when None, ex. 0 - # :type ddi: list or int, optional - # - # :param parallel: If True will use an existing astrohack client to export FITS in parallel, default is False - # :type parallel: bool, optional - # - # .. _Description: - # Export the products from the panel mds onto FITS files to be read by other software packages - # - # **Additional Information** - # - # The FITS fils produced by this method have been tested and are known to work with CARTA and DS9 - # """ - # - # param_dict = locals() - # - # pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) - # create_and_execute_graph_from_dict( - # self, - # export_to_fits_panel_chunk, - # param_dict, - # ["ant", "ddi"], - # parallel=parallel, - # ) + def export_to_fits( + self, + destination: str, + ant: Union[str, List[str]] = "all", + ddi: Union[str, int, List[int]] = "all", + parallel: bool = False, + ) -> None: + """Export contents of an Astrohack MDS file to several FITS files in the destination folder + + :param destination: Name of the destination folder to contain plots + :type destination: str + + :param ant: List of antennas/antenna to be plotted, defaults to "all" when None, ex. ea25 + :type ant: list or str, optional + + :param ddi: List of ddis/ddi to be plotted, defaults to "all" when None, ex. 0 + :type ddi: list or int, optional + + :param parallel: If True will use an existing astrohack client to export FITS in parallel, default is False + :type parallel: bool, optional + + .. _Description: + Export the products from the panel mds onto FITS files to be read by other software packages + + **Additional Information** + + The FITS fils produced by this method have been tested and are known to work with CARTA and DS9 + """ + + param_dict = locals() + + pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) + create_and_execute_graph_from_dict( + self, + _export_to_fits_chunk, + param_dict, + ["ant", "ddi"], + parallel=parallel, + ) + # # @toolviper.utils.parameter.validate(custom_checker=custom_unit_checker) # def export_gain_tables( @@ -414,3 +416,23 @@ def _plot_antenna_chunk(parm_dict): surface.plot_phase(basename, "panel", parm_dict) surface.plot_mask(basename, "panel", parm_dict) surface.plot_amplitude(basename, "panel", parm_dict) + + +def _export_to_fits_chunk(parm_dict): + """ + Panel side chunk function for the user facing function export_to_fits + Args: + parm_dict: parameter dictionary + """ + + antenna = parm_dict["this_ant"] + ddi = parm_dict["this_ddi"] + destination = parm_dict["destination"] + logger.info( + f"Exporting panel contents of {antenna} {ddi} to FITS files in {destination}" + ) + xds = parm_dict["xdt_data"] + surface = AntennaSurface(xds, reread=True) + basename = f"{destination}/{antenna}_{ddi}" + surface.export_to_fits(basename) + return From 3133f065f866d836aa3c4182c7812fd2c79f4c8b Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Fri, 6 Feb 2026 16:09:31 -0700 Subject: [PATCH 103/295] Ported panel_mds.export_gain_tables to the new datatree file format. --- src/astrohack/io/panel_mds.py | 222 +++++++++++++++++++++++----------- 1 file changed, 153 insertions(+), 69 deletions(-) diff --git a/src/astrohack/io/panel_mds.py b/src/astrohack/io/panel_mds.py index 792f749e..0c63a727 100644 --- a/src/astrohack/io/panel_mds.py +++ b/src/astrohack/io/panel_mds.py @@ -9,6 +9,15 @@ from astrohack.utils.constants import plot_types from astrohack.io.base_mds import AstrohackBaseFile from astrohack.utils.graph import create_and_execute_graph_from_dict +from astrohack.utils.conversion import convert_unit +from astrohack.utils.constants import clight +from astrohack.utils.text import ( + format_frequency, + format_wavelength, + create_pretty_table, + string_to_ascii_file, + format_value_unit, +) class AstrohackPanelFile(AstrohackBaseFile): @@ -203,7 +212,11 @@ def plot_antennas( pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) create_and_execute_graph_from_dict( - self, _plot_antenna_chunk, param_dict, ["ant", "ddi"], parallel=parallel + looping_dict=self, + chunk_function=_plot_antenna_chunk, + param_dict=param_dict, + key_order=["ant", "ddi"], + parallel=parallel, ) # @toolviper.utils.parameter.validate() @@ -240,78 +253,78 @@ def export_to_fits( pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) create_and_execute_graph_from_dict( - self, - _export_to_fits_chunk, - param_dict, - ["ant", "ddi"], + looping_dict=self, + chunk_function=_export_to_fits_chunk, + param_dict=param_dict, + key_order=["ant", "ddi"], parallel=parallel, ) - # # @toolviper.utils.parameter.validate(custom_checker=custom_unit_checker) - # def export_gain_tables( - # self, - # destination: str, - # ant: Union[str, List[str]] = "all", - # ddi: Union[str, int, List[int]] = "all", - # wavelengths: Union[float, List[float]] = None, - # wavelength_unit: str = "cm", - # frequencies: Union[float, List[float]] = None, - # frequency_unit: str = "GHz", - # rms_unit: str = "mm", - # parallel: bool = False, - # ) -> None: - # """ Compute estimated antenna gains in dB and saves them to ASCII files. - # - # :param destination: Name of the destination folder to contain ASCII files - # :type destination: str - # - # :param ant: List of antennas/antenna to be exported, defaults to "all" when None, ex. ea25 - # :type ant: list or str, optional - # - # :param ddi: List of ddis/ddi to be exported, defaults to "all" when None, ex. 0 - # :type ddi: list or int, optional - # - # :param wavelengths: List of wavelengths at which to compute the gains. - # :type wavelengths: list or float, optional - # - # :param wavelength_unit: Unit for the wavelengths being used, default is cm. - # :type wavelength_unit: str, optional - # - # :param frequencies: List of frequencies at which to compute the gains. - # :type frequencies: list or float, optional - # - # :param frequency_unit: Unit for the frequencies being used, default is GHz. - # :type frequency_unit: str, optional - # - # :param rms_unit: Unit for the Antenna surface RMS, default is mm. - # :type rms_unit: str, optional - # - # :param parallel: If True will use an existing astrohack client to produce ASCII files in parallel, default is False - # :type parallel: bool, optional - # - # .. _Description: - # - # Export antenna gains in dB from ``astrohack.panel`` for analysis. - # - # **Additional Information** - # - # .. rubric:: Selecting frequencies and wavelengths: - # - # If neither a frequency list nor a wavelength list is provided, ``export_gains_table`` will try to use a\ - # predefined list set for the telescope associated with the dataset. If both are provided, ``export_gains_table``\ - # will combine both lists. - # """ - # - # param_dict = locals() - # pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) - # create_and_execute_graph_from_dict( - # self, - # export_gains_table_chunk, - # param_dict, - # ["ant", "ddi"], - # parallel=parallel, - # ) + def export_gain_tables( + self, + destination: str, + ant: Union[str, List[str]] = "all", + ddi: Union[str, int, List[int]] = "all", + wavelengths: Union[float, List[float]] = None, + wavelength_unit: str = "cm", + frequencies: Union[float, List[float]] = None, + frequency_unit: str = "GHz", + rms_unit: str = "mm", + parallel: bool = False, + ) -> None: + """ Compute estimated antenna gains in dB and saves them to ASCII files. + + :param destination: Name of the destination folder to contain ASCII files + :type destination: str + + :param ant: List of antennas/antenna to be exported, defaults to "all" when None, ex. ea25 + :type ant: list or str, optional + + :param ddi: List of ddis/ddi to be exported, defaults to "all" when None, ex. 0 + :type ddi: list or int, optional + + :param wavelengths: List of wavelengths at which to compute the gains. + :type wavelengths: list or float, optional + + :param wavelength_unit: Unit for the wavelengths being used, default is cm. + :type wavelength_unit: str, optional + + :param frequencies: List of frequencies at which to compute the gains. + :type frequencies: list or float, optional + + :param frequency_unit: Unit for the frequencies being used, default is GHz. + :type frequency_unit: str, optional + + :param rms_unit: Unit for the Antenna surface RMS, default is mm. + :type rms_unit: str, optional + + :param parallel: If True will use an existing astrohack client to produce ASCII files in parallel, default is False + :type parallel: bool, optional + + .. _Description: + + Export antenna gains in dB from ``astrohack.panel`` for analysis. + + **Additional Information** + + .. rubric:: Selecting frequencies and wavelengths: + + If neither a frequency list nor a wavelength list is provided, ``export_gains_table`` will try to use a\ + predefined list set for the telescope associated with the dataset. If both are provided, ``export_gains_table``\ + will combine both lists. + """ + + param_dict = locals() + pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) + create_and_execute_graph_from_dict( + looping_dict=self, + chunk_function=_export_gain_tables_chunk, + param_dict=param_dict, + key_order=["ant", "ddi"], + parallel=parallel, + ) + # # @toolviper.utils.parameter.validate(custom_checker=custom_unit_checker) # def observation_summary( @@ -436,3 +449,74 @@ def _export_to_fits_chunk(parm_dict): basename = f"{destination}/{antenna}_{ddi}" surface.export_to_fits(basename) return + + +def _export_gain_tables_chunk(parm_dict): + in_waves = parm_dict["wavelengths"] + in_freqs = parm_dict["frequencies"] + ant = parm_dict["this_ant"] + ddi = parm_dict["this_ddi"] + xds = parm_dict["xdt_data"] + antenna = AntennaSurface(xds, reread=True) + frequency = clight / antenna.wavelength + + if in_waves is None and in_freqs is None: + try: + wavelengths = antenna.telescope.gain_wavelengths + except AttributeError: + msg = f"Telescope {antenna.telescope.name} has no predefined list of wavelengths to compute gains" + logger.error(msg) + logger.info("Please provide one in the arguments") + raise Exception(msg) + else: + wave_fac = convert_unit(parm_dict["wavelength_unit"], "m", "length") + freq_fac = convert_unit(parm_dict["frequency_unit"], "Hz", "frequency") + wavelengths = [] + if in_waves is not None: + if isinstance(in_waves, float) or isinstance(in_waves, int): + in_waves = [in_waves] + for in_wave in in_waves: + wavelengths.append(wave_fac * in_wave) + if in_freqs is not None: + if isinstance(in_freqs, float) or isinstance(in_freqs, int): + in_freqs = [in_freqs] + for in_freq in in_freqs: + wavelengths.append(clight / freq_fac / in_freq) + + db = "dB" + rmsunit = parm_dict["rms_unit"] + rmses = antenna.get_rms(rmsunit) + + field_names = [ + "Frequency", + "Wavelength", + "Before panel", + "After panel", + "Theoretical Max.", + ] + table = create_pretty_table(field_names) + + outstr = ( + f'# Gain estimates for {antenna.telescope.name} antenna {ant.split("_")[1]}\n' + ) + outstr += f"# Based on a measurement at {format_frequency(frequency)}, {format_wavelength(antenna.wavelength)}\n" + outstr += f"# Antenna surface RMS before adjustment: {format_value_unit(rmses[0], rmsunit)}\n" + outstr += f"# Antenna surface RMS after adjustment: {format_value_unit(rmses[1], rmsunit)}\n" + outstr += 1 * "\n" + + for wavelength in wavelengths: + prior, theo = antenna.gain_at_wavelength(False, wavelength) + after, _ = antenna.gain_at_wavelength(True, wavelength) + row = [ + format_frequency(clight / wavelength), + format_wavelength(wavelength), + format_value_unit(prior, db), + format_value_unit(after, db), + format_value_unit(theo, db), + ] + table.add_row(row) + + outstr += table.get_string() + string_to_ascii_file( + outstr, parm_dict["destination"] + f"/panel_gains_{ant}_{ddi}.txt" + ) From 282fa55dd426145cc5f6b430c00faa9ce067cd59 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Fri, 6 Feb 2026 16:13:10 -0700 Subject: [PATCH 104/295] Ported panel_mds.observation_summary to the new datatree file format. --- src/astrohack/io/panel_mds.py | 131 ++++++++++++++++++---------------- 1 file changed, 70 insertions(+), 61 deletions(-) diff --git a/src/astrohack/io/panel_mds.py b/src/astrohack/io/panel_mds.py index 0c63a727..d96a201f 100644 --- a/src/astrohack/io/panel_mds.py +++ b/src/astrohack/io/panel_mds.py @@ -18,6 +18,7 @@ string_to_ascii_file, format_value_unit, ) +from astrohack.visualization.textual_data import generate_observation_summary class AstrohackPanelFile(AstrohackBaseFile): @@ -325,68 +326,76 @@ def export_gain_tables( parallel=parallel, ) - # # @toolviper.utils.parameter.validate(custom_checker=custom_unit_checker) - # def observation_summary( - # self, - # summary_file: str, - # ant: Union[str, List[str]] = "all", - # ddi: Union[str, int, List[int]] = "all", - # az_el_key: str = "center", - # phase_center_unit: str = "radec", - # az_el_unit: str = "deg", - # time_format: str = "%d %h %Y, %H:%M:%S", - # tab_size: int = 3, - # print_summary: bool = True, - # parallel: bool = False, - # ) -> None: - # """ Create a Summary of observation information - # - # :param summary_file: Text file to put the observation summary - # :type summary_file: str - # :param ant: antenna ID to use in subselection, defaults to "all" when None, ex. ea25 - # :type ant: list or str, optional - # :param ddi: data description ID to use in subselection, defaults to "all" when None, ex. 0 - # :type ddi: list or int, optional - # :param az_el_key: What type of Azimuth & Elevation information to print, 'mean', 'median' or 'center', default\ - # is 'center' - # :type az_el_key: str, optional - # :param phase_center_unit: What unit to display phase center coordinates, 'radec' and angle units supported, \ - # default is 'radec' - # :type phase_center_unit: str, optional - # :param az_el_unit: Angle unit used to display Azimuth & Elevation information, default is 'deg' - # :type az_el_unit: str, optional - # :param time_format: datetime time format for the start and end dates of observation, default is \ - # "%d %h %Y, %H:%M:%S" - # :type time_format: str, optional - # :param tab_size: Number of spaces in the tab levels, default is 3 - # :type tab_size: int, optional - # :param print_summary: Print the summary at the end of execution, default is True - # :type print_summary: bool, optional - # :param parallel: Run in parallel, defaults to False - # :type parallel: bool, optional - # - # **Additional Information** - # - # This method produces a summary of the data in the AstrohackPanelFile displaying general information, - # spectral information, beam image characteristics and aperture image characteristics. - # """ - # - # param_dict = locals() - # key_order = ["ant", "ddi"] - # execution, summary = create_and_execute_graph_from_dict( - # self, - # generate_observation_summary, - # param_dict, - # key_order, - # parallel, - # fetch_returns=True, - # ) - # summary = "".join(summary) - # with open(summary_file, "w") as output_file: - # output_file.write(summary) - # if print_summary: - # print(summary) + def observation_summary( + self, + summary_file: str, + ant: Union[str, List[str]] = "all", + ddi: Union[str, int, List[int]] = "all", + az_el_key: str = "center", + phase_center_unit: str = "radec", + az_el_unit: str = "deg", + time_format: str = "%d %h %Y, %H:%M:%S", + tab_size: int = 3, + print_summary: bool = True, + parallel: bool = False, + ) -> None: + """ Create a Summary of observation information + + :param summary_file: Text file to put the observation summary + :type summary_file: str + + :param ant: antenna ID to use in subselection, defaults to "all" when None, ex. ea25 + :type ant: list or str, optional + + :param ddi: data description ID to use in subselection, defaults to "all" when None, ex. 0 + :type ddi: list or int, optional + + :param az_el_key: What type of Azimuth & Elevation information to print, 'mean', 'median' or 'center', default\ + is 'center' + :type az_el_key: str, optional + + :param phase_center_unit: What unit to display phase center coordinates, 'radec' and angle units supported, \ + default is 'radec' + :type phase_center_unit: str, optional + + :param az_el_unit: Angle unit used to display Azimuth & Elevation information, default is 'deg' + :type az_el_unit: str, optional + + :param time_format: datetime time format for the start and end dates of observation, default is \ + "%d %h %Y, %H:%M:%S" + :type time_format: str, optional + + :param tab_size: Number of spaces in the tab levels, default is 3 + :type tab_size: int, optional + + :param print_summary: Print the summary at the end of execution, default is True + :type print_summary: bool, optional + + :param parallel: Run in parallel, defaults to False + :type parallel: bool, optional + + **Additional Information** + + This method produces a summary of the data in the AstrohackPanelFile displaying general information, + spectral information, beam image characteristics and aperture image characteristics. + """ + + param_dict = locals() + key_order = ["ant", "ddi"] + execution, summary = create_and_execute_graph_from_dict( + looping_dict=self, + chunk_function=generate_observation_summary, + param_dict=param_dict, + key_order=key_order, + parallel=parallel, + fetch_returns=True, + ) + summary = "".join(summary) + with open(summary_file, "w") as output_file: + output_file.write(summary) + if print_summary: + print(summary) def _export_screws_chunk(parm_dict): From b5451b5239d9538a001fe8182e3f33d84b425c21 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Fri, 6 Feb 2026 16:31:22 -0700 Subject: [PATCH 105/295] General Observation summary generation now also encompasses beamcut. --- src/astrohack/io/beamcut_mds.py | 5 ++- src/astrohack/io/holog_mds.py | 1 + src/astrohack/io/image_mds.py | 1 + src/astrohack/io/panel_mds.py | 1 + src/astrohack/visualization/textual_data.py | 48 ++++----------------- 5 files changed, 15 insertions(+), 41 deletions(-) diff --git a/src/astrohack/io/beamcut_mds.py b/src/astrohack/io/beamcut_mds.py index 077db6e5..70f8dd7a 100644 --- a/src/astrohack/io/beamcut_mds.py +++ b/src/astrohack/io/beamcut_mds.py @@ -13,7 +13,7 @@ plot_cuts_in_lm_chunk, ) from astrohack.visualization.textual_data import ( - generate_observation_summary_for_beamcut, + generate_observation_summary, ) from astrohack.utils.graph import create_and_execute_graph_from_dict from astrohack.utils.validation import custom_plots_checker, custom_unit_checker @@ -97,9 +97,10 @@ def observation_summary( param_dict = locals() key_order = ["ant", "ddi"] + param_dict["dtype"] = "beamcut" execution, summary_list = create_and_execute_graph_from_dict( looping_dict=self, - chunk_function=generate_observation_summary_for_beamcut, + chunk_function=generate_observation_summary, param_dict=param_dict, key_order=key_order, parallel=parallel, diff --git a/src/astrohack/io/holog_mds.py b/src/astrohack/io/holog_mds.py index fb7e8127..04a8b66b 100644 --- a/src/astrohack/io/holog_mds.py +++ b/src/astrohack/io/holog_mds.py @@ -307,6 +307,7 @@ def observation_summary( param_dict = locals() param_dict["map"] = map_id + param_dict["dtype"] = "holog" key_order = ["ant", "ddi", "map"] execution, summary = create_and_execute_graph_from_dict( looping_dict=self, diff --git a/src/astrohack/io/image_mds.py b/src/astrohack/io/image_mds.py index 742b65c2..41e63965 100644 --- a/src/astrohack/io/image_mds.py +++ b/src/astrohack/io/image_mds.py @@ -453,6 +453,7 @@ def observation_summary( param_dict = locals() key_order = ["ant", "ddi"] + param_dict["dtype"] = "image" execution, summary = create_and_execute_graph_from_dict( looping_dict=self, chunk_function=generate_observation_summary, diff --git a/src/astrohack/io/panel_mds.py b/src/astrohack/io/panel_mds.py index d96a201f..439d1fa4 100644 --- a/src/astrohack/io/panel_mds.py +++ b/src/astrohack/io/panel_mds.py @@ -383,6 +383,7 @@ def observation_summary( param_dict = locals() key_order = ["ant", "ddi"] + param_dict["dtype"] = "panel" execution, summary = create_and_execute_graph_from_dict( looping_dict=self, chunk_function=generate_observation_summary, diff --git a/src/astrohack/visualization/textual_data.py b/src/astrohack/visualization/textual_data.py index 63cce86e..e62c1061 100644 --- a/src/astrohack/visualization/textual_data.py +++ b/src/astrohack/visualization/textual_data.py @@ -250,20 +250,16 @@ def create_fits_comparison_rms_table(parameters, xdt): def generate_observation_summary(parm_dict): antenna = parm_dict["this_ant"] ddi = parm_dict["this_ddi"] - try: - map_id = parm_dict["this_map"] - is_holog_zarr = True - except KeyError: - map_id = None - is_holog_zarr = False - + data_type = parm_dict["dtype"] xds = parm_dict["xdt_data"] obs_sum = xds.attrs["summary"] - tab_size = parm_dict["tab_size"] + tab_count = 1 + spc = " " - if is_holog_zarr: + if data_type == "holog": + map_id = parm_dict["this_map"] header = f"{antenna}, {ddi}, {map_id}" else: header = f"{antenna}, {ddi}" @@ -283,35 +279,9 @@ def generate_observation_summary(parm_dict): + "\n" ) - return outstr - - -def generate_observation_summary_for_beamcut(parm_dict): - xdt = parm_dict["xdt_data"] - antenna = parm_dict["this_ant"] - ddi = parm_dict["this_ddi"] - obs_sum = xdt.attrs["summary"] - - tab_size = parm_dict["tab_size"] - tab_count = 1 - header = f"{antenna}, {ddi}" - outstr = make_header(header, "#", 60, 3) - spc = " " - - outstr += ( - format_observation_summary( - obs_sum, - tab_size, - tab_count, - az_el_key=parm_dict["az_el_key"], - phase_center_unit=parm_dict["phase_center_unit"], - az_el_unit=parm_dict["az_el_unit"], - time_format=parm_dict["time_format"], - ) - + "\n" - ) - for cut in xdt.children.values(): - outstr += f"{tab_count*tab_size*spc}{cut.name}:\n" - outstr += f'{(tab_count+1)*tab_size*spc}{cut.attrs["direction"]} at {cut.attrs["time_string"]} UTC\n\n' + if data_type == "beamcut": + for cut in xds.children.values(): + outstr += f"{tab_count*tab_size*spc}{cut.name}:\n" + outstr += f'{(tab_count+1)*tab_size*spc}{cut.attrs["direction"]} at {cut.attrs["time_string"]} UTC\n\n' return outstr From 4a2d61b34062e3854144f842903ee36d47771575 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Fri, 6 Feb 2026 16:35:47 -0700 Subject: [PATCH 106/295] Cosmetics. --- src/astrohack/io/holog_mds.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/astrohack/io/holog_mds.py b/src/astrohack/io/holog_mds.py index 04a8b66b..196f9e6f 100644 --- a/src/astrohack/io/holog_mds.py +++ b/src/astrohack/io/holog_mds.py @@ -103,7 +103,7 @@ def plot_diagnostics( key_order = ["ant", "ddi", "map"] create_and_execute_graph_from_dict( looping_dict=self, - chunk_function=_calibration_plot_chunk, + chunk_function=_plot_diagnostics_chunk, param_dict=param_dict, key_order=key_order, parallel=parallel, @@ -192,7 +192,7 @@ def plot_lm_sky_coverage( key_order = ["ant", "ddi", "map"] create_and_execute_graph_from_dict( looping_dict=self, - chunk_function=_plot_lm_coverage_chunk, + chunk_function=_plot_lm_sky_coverage_chunk, param_dict=param_dict, key_order=key_order, parallel=parallel, @@ -337,7 +337,7 @@ def _extract_indices(laxis, maxis, squared_radius): return np.array(indices) -def _calibration_plot_chunk(param_dict): +def _plot_diagnostics_chunk(param_dict): xds_data = param_dict["xdt_data"].dataset delta = param_dict["delta"] complex_split = param_dict["complex_split"] @@ -430,7 +430,7 @@ def _calibration_plot_chunk(param_dict): close_figure(fig, None, plotfile, dpi, display, tight_layout=False) -def _plot_lm_coverage_chunk(param_dict): +def _plot_lm_sky_coverage_chunk(param_dict): xdt_data = param_dict["xdt_data"] angle_fact = convert_unit("rad", param_dict["angle_unit"], "trigonometric") real_lm = xdt_data["DIRECTIONAL_COSINES"] * angle_fact From da841a397f728dd1f0c8dcbca3cac3d8a36aca0f Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Fri, 6 Feb 2026 16:47:05 -0700 Subject: [PATCH 107/295] Adapted extract_pointing and point_mds to the new cosmetic standards. --- src/astrohack/core/extract_pointing_2.py | 128 +------------------- src/astrohack/extract_pointing_2.py | 4 +- src/astrohack/io/point_mds.py | 142 +++++++++++++++++++++-- 3 files changed, 138 insertions(+), 136 deletions(-) diff --git a/src/astrohack/core/extract_pointing_2.py b/src/astrohack/core/extract_pointing_2.py index d7e32cb4..c43242ad 100644 --- a/src/astrohack/core/extract_pointing_2.py +++ b/src/astrohack/core/extract_pointing_2.py @@ -132,7 +132,7 @@ def extract_pointing_preprocessing(input_params): return ant_dist_matrix, looping_dict, pnt_params, mapping_state_ids -def make_ant_pnt_chunk(pnt_params, output_mds): +def extract_pointing_chunk(pnt_params, output_mds): """Extract subset of pointing table data into a dictionary of xarray data arrays. This is written to disk as a zarr file. This function processes a chunk the overall data and is managed by Dask. @@ -306,132 +306,6 @@ def make_ant_pnt_chunk(pnt_params, output_mds): ) -def _create_pointing_figure(input_params): - y_labels = ["azimuth", "elevation"] - fig, axes = create_figure_and_axes(input_params["figure_size"], [2, 1]) - return fig, axes, y_labels - - -def _finalize_pointing_figure( - input_params, target_column, ant_label, y_labels, axes, fig -): - title = f"Pointing [{target_column}] data for: {ant_label}" - filename = f"{input_params['destination']}/point_{target_column.lower()}_" - if len(ant_label.split(",")) > 1: - filename += "combined.png" - else: - filename += f"ant_{ant_label}.png" - for i_coord, y_label in enumerate(y_labels): - axes[i_coord].set_ylabel( - f"{y_label.capitalize()} [{input_params["azel_unit"]}]" - ) - if y_label == "Azimuth": - - if input_params["az_scale"] is not None: - axes[i_coord].set_ylim(input_params["az_scale"]) - - else: - if input_params["el_scale"] is not None: - axes[i_coord].set_ylim(input_params["el_scale"]) - if input_params["time_scale"] is not None: - axes[i_coord].set_xlim(input_params["time_scale"]) - axes[i_coord].set_xlabel( - f"Time Since Observation start [{input_params["time_unit"]}]" - ) - axes[i_coord].legend() - close_figure(fig, title, filename, input_params["dpi"], input_params["display"]) - - -def _plot_one_pnt_xds( - time_fac, ang_fac, ant_name, pnt_xds, target_column, y_labels, axes -): - time_ax = pnt_xds.coords["time"].values - # Set time from obs start - time_ax -= time_ax[0] - plot_data = pnt_xds[target_column].values - for i_coord, y_label in enumerate(y_labels): - axes[i_coord].plot( - time_fac * time_ax, - ang_fac * plot_data[:, i_coord], - label=ant_name, - ls="", - marker="o", - ms=5, - ) - - -def _get_plot_configuration(input_params, point_mds): - ant_list = param_to_list(input_params["ant"], point_mds, "ant") - time_fac = convert_unit("sec", input_params["time_unit"], "time") - ang_fac = convert_unit("rad", input_params["azel_unit"], "trigonometric") - target_column = input_params["pointing_key"].upper() - return ant_list, time_fac, ang_fac, target_column - - -def plot_pointing_in_time_separately(input_params, point_mds): - ant_list, time_fac, ang_fac, target_column = _get_plot_configuration( - input_params, point_mds - ) - - n_use_ants = 0 - for ant_key in ant_list: - ant_name = ant_key.split("_")[1] - if ant_key in point_mds.keys(): - n_use_ants = n_use_ants + 1 - fig, axes, y_labels = _create_pointing_figure(input_params) - _plot_one_pnt_xds( - time_fac, - ang_fac, - ant_name, - point_mds[ant_key].dataset, - target_column, - y_labels, - axes, - ) - _finalize_pointing_figure( - input_params, target_column, ant_name, y_labels, axes, fig - ) - else: - logger.warning(f"Antenna {ant_name} not found in dataset") - - if n_use_ants <= 0: - logger.warning(f"No valid antennas selected, no plot produced.") - return - - -def plot_pointing_in_time_together(input_params, point_mds): - ant_list, time_fac, ang_fac, target_column = _get_plot_configuration( - input_params, point_mds - ) - fig, axes, y_labels = _create_pointing_figure(input_params) - - n_use_ants = 0 - for ant_key in ant_list: - ant_name = ant_key.split("_")[1] - if ant_key in point_mds.keys(): - n_use_ants = n_use_ants + 1 - _plot_one_pnt_xds( - time_fac, - ang_fac, - ant_name, - point_mds[ant_key].dataset, - target_column, - y_labels, - axes, - ) - else: - logger.warning(f"Antenna {ant_name} not found in dataset") - - if n_use_ants > 0: - simple_ant_list = [ant_key.split("_")[1] for ant_key in ant_list] - _finalize_pointing_figure( - input_params, target_column, ", ".join(simple_ant_list), y_labels, axes, fig - ) - else: - logger.warning(f"No valid antennas selected, no plot produced.") - return - - def _extract_scan_time_dict(time, scan_ids, state_ids, ddi_ids, mapping_state_ids): """ [ddi][scan][start, stop] diff --git a/src/astrohack/extract_pointing_2.py b/src/astrohack/extract_pointing_2.py index 1c4632ad..73cdf4c6 100644 --- a/src/astrohack/extract_pointing_2.py +++ b/src/astrohack/extract_pointing_2.py @@ -8,7 +8,7 @@ from astrohack.utils.file import overwrite_file from astrohack.core.extract_pointing_2 import ( extract_pointing_preprocessing, - make_ant_pnt_chunk, + extract_pointing_chunk, ) from astrohack.io.point_mds import AstrohackPointFile @@ -98,7 +98,7 @@ def extract_pointing( executed_graph = create_and_execute_graph_from_dict( looping_dict=looping_dict, - chunk_function=make_ant_pnt_chunk, + chunk_function=extract_pointing_chunk, param_dict=pnt_params, key_order=["ant"], output_mds=point_mds, diff --git a/src/astrohack/io/point_mds.py b/src/astrohack/io/point_mds.py index c05299c3..f495ca52 100644 --- a/src/astrohack/io/point_mds.py +++ b/src/astrohack/io/point_mds.py @@ -3,13 +3,13 @@ from typing import Union, List, Tuple -from astrohack.visualization.diagnostics import plot_array_configuration +import toolviper.utils.logger as logger -from astrohack.core.extract_pointing_2 import ( - plot_pointing_in_time_together, - plot_pointing_in_time_separately, -) +from astrohack.visualization.diagnostics import plot_array_configuration +from astrohack.visualization.plot_tools import create_figure_and_axes, close_figure from astrohack.io.base_mds import AstrohackBaseFile +from astrohack.utils.conversion import convert_unit +from astrohack.utils import param_to_list class AstrohackPointFile(AstrohackBaseFile): @@ -91,9 +91,83 @@ def plot_pointing_in_time( input_params = locals() if plot_antennas_separately: - plot_pointing_in_time_separately(input_params, self) + self._plot_pointing_in_time_separately(input_params) + else: + self._plot_pointing_in_time_together(input_params) + return + + def _get_plot_configuration(self, input_params): + ant_list = param_to_list(input_params["ant"], self, "ant") + time_fac = convert_unit("sec", input_params["time_unit"], "time") + ang_fac = convert_unit("rad", input_params["azel_unit"], "trigonometric") + target_column = input_params["pointing_key"].upper() + return ant_list, time_fac, ang_fac, target_column + + def _plot_pointing_in_time_separately(self, input_params): + ant_list, time_fac, ang_fac, target_column = self._get_plot_configuration( + input_params + ) + + n_use_ants = 0 + for ant_key in ant_list: + ant_name = ant_key.split("_")[1] + if ant_key in self.keys(): + n_use_ants = n_use_ants + 1 + fig, axes, y_labels = _create_pointing_figure(input_params) + _plot_one_pnt_xds( + time_fac, + ang_fac, + ant_name, + self[ant_key].dataset, + target_column, + y_labels, + axes, + ) + _finalize_pointing_figure( + input_params, target_column, ant_name, y_labels, axes, fig + ) + else: + logger.warning(f"Antenna {ant_name} not found in dataset") + + if n_use_ants <= 0: + logger.warning(f"No valid antennas selected, no plot produced.") + return + + def _plot_pointing_in_time_together(self, input_params): + ant_list, time_fac, ang_fac, target_column = self._get_plot_configuration( + input_params, + ) + fig, axes, y_labels = _create_pointing_figure(input_params) + + n_use_ants = 0 + for ant_key in ant_list: + ant_name = ant_key.split("_")[1] + if ant_key in self.keys(): + n_use_ants = n_use_ants + 1 + _plot_one_pnt_xds( + time_fac, + ang_fac, + ant_name, + self[ant_key].dataset, + target_column, + y_labels, + axes, + ) + else: + logger.warning(f"Antenna {ant_name} not found in dataset") + + if n_use_ants > 0: + simple_ant_list = [ant_key.split("_")[1] for ant_key in ant_list] + _finalize_pointing_figure( + input_params, + target_column, + ", ".join(simple_ant_list), + y_labels, + axes, + fig, + ) else: - plot_pointing_in_time_together(input_params, self) + logger.warning(f"No valid antennas selected, no plot produced.") return def plot_array_configuration( @@ -141,3 +215,57 @@ def plot_array_configuration( pathlib.Path(destination).mkdir(exist_ok=True) input_params = locals() plot_array_configuration(input_params, self.root, "point") + + +def _create_pointing_figure(input_params): + y_labels = ["azimuth", "elevation"] + fig, axes = create_figure_and_axes(input_params["figure_size"], [2, 1]) + return fig, axes, y_labels + + +def _finalize_pointing_figure( + input_params, target_column, ant_label, y_labels, axes, fig +): + title = f"Pointing [{target_column}] data for: {ant_label}" + filename = f"{input_params['destination']}/point_{target_column.lower()}_" + if len(ant_label.split(",")) > 1: + filename += "combined.png" + else: + filename += f"ant_{ant_label}.png" + for i_coord, y_label in enumerate(y_labels): + axes[i_coord].set_ylabel( + f"{y_label.capitalize()} [{input_params["azel_unit"]}]" + ) + if y_label == "Azimuth": + + if input_params["az_scale"] is not None: + axes[i_coord].set_ylim(input_params["az_scale"]) + + else: + if input_params["el_scale"] is not None: + axes[i_coord].set_ylim(input_params["el_scale"]) + if input_params["time_scale"] is not None: + axes[i_coord].set_xlim(input_params["time_scale"]) + axes[i_coord].set_xlabel( + f"Time Since Observation start [{input_params["time_unit"]}]" + ) + axes[i_coord].legend() + close_figure(fig, title, filename, input_params["dpi"], input_params["display"]) + + +def _plot_one_pnt_xds( + time_fac, ang_fac, ant_name, pnt_xds, target_column, y_labels, axes +): + time_ax = pnt_xds.coords["time"].values + # Set time from obs start + time_ax -= time_ax[0] + plot_data = pnt_xds[target_column].values + for i_coord, y_label in enumerate(y_labels): + axes[i_coord].plot( + time_fac * time_ax, + ang_fac * plot_data[:, i_coord], + label=ant_name, + ls="", + marker="o", + ms=5, + ) From 5325e38ca124b78ba3ea8ee7494bffcec1cba737 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Mon, 9 Feb 2026 11:10:49 -0700 Subject: [PATCH 108/295] Fixed issue where extract_pointing and extract_holog were fetching the wrong data. Fixed an issue in extract_holgo related to the reference antenna stations. --- src/astrohack/core/extract_holog_2.py | 6 +++--- src/astrohack/core/extract_pointing_2.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/astrohack/core/extract_holog_2.py b/src/astrohack/core/extract_holog_2.py index 843e7b0e..56d4934b 100644 --- a/src/astrohack/core/extract_holog_2.py +++ b/src/astrohack/core/extract_holog_2.py @@ -189,7 +189,7 @@ def extract_holog_preprocessing(extract_holog_params, pnt_mds): ref_ant_per_map_ant_name_list ), "map_ant_name_tuple": tuple(map_ant_name_list), - "ref_ant_stations": tuple(ref_ant_stations_list), + "ant_stations": ant_stations, "scans": scans, "sel_state_ids": state_ids, "ant_names": ant_names, @@ -227,7 +227,7 @@ def process_extract_holog_chunk(extract_holog_params, holog_mds): holog_name = extract_holog_params["holog_name"] pnt_mds = extract_holog_params["pnt_mds"] - inp_data_dict = extract_holog_params["data_dict"] + inp_data_dict = extract_holog_params["dic_data"] ddi_key = extract_holog_params["this_ddi"] map_key = extract_holog_params["this_map"] @@ -236,7 +236,7 @@ def process_extract_holog_chunk(extract_holog_params, holog_mds): scans = inp_data_dict["scans"] ant_names = inp_data_dict["ant_names"] - ant_stations = inp_data_dict["ref_ant_stations"] + ant_stations = inp_data_dict["ant_stations"] ref_ant_per_map_ant_tuple = inp_data_dict["ref_ant_per_map_ant_tuple"] map_ant_tuple = inp_data_dict["map_ant_tuple"] map_ant_name_tuple = inp_data_dict["map_ant_name_tuple"] diff --git a/src/astrohack/core/extract_pointing_2.py b/src/astrohack/core/extract_pointing_2.py index c43242ad..2cc43afb 100644 --- a/src/astrohack/core/extract_pointing_2.py +++ b/src/astrohack/core/extract_pointing_2.py @@ -140,7 +140,7 @@ def extract_pointing_chunk(pnt_params, output_mds): pnt_params(dict): extract_pointing parameters output_mds: Output AstrohackPointFile """ - data_dict = pnt_params["data_dict"] + data_dict = pnt_params["dic_data"] ms_name = pnt_params["ms_name"] scan_time_dict = pnt_params["scan_time_dict"] From dd82c372a9184e1b4a1bb5ea55f267b6f8f63888 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Mon, 9 Feb 2026 14:43:34 -0700 Subject: [PATCH 109/295] Introduced a simple tuple analysis tool. --- src/astrohack/utils/text.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/astrohack/utils/text.py b/src/astrohack/utils/text.py index 19247b85..8df74024 100644 --- a/src/astrohack/utils/text.py +++ b/src/astrohack/utils/text.py @@ -2,6 +2,7 @@ import inspect import textwrap +import numba.typed.typeddict import numpy as np import xarray from astropy.time import Time @@ -76,6 +77,25 @@ def print_array(array, columns, indent=4): print(str_line) +def tuple_inspect(param_tuple): + outstr = "" + for idx, item in enumerate(param_tuple): + # print(idx, type(item)) + outstr += f"{idx:3d} => " + if isinstance(item, (list, tuple)): + outstr += f"{len(item)} = {item}" + elif isinstance(item, np.ndarray): + outstr += f"{item.shape} sum = {np.sum(item)}" + elif isinstance(item, numba.typed.typeddict.Dict): + outstr += f"dict = {dict(item).keys()}" + elif isinstance(item, dict): + outstr += f"dict = {item.keys()}" + else: + outstr += f"{item}" + outstr += lnbr + return outstr + + def approve_prefix(key): approved_prefix = ["ant_", "map_", "ddi_"] From 2071f9e759e8902ac5d9e1f698c72fa764087564 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Mon, 9 Feb 2026 14:47:26 -0700 Subject: [PATCH 110/295] Fixed issue where the code could erroneously flag all visibilities. --- src/astrohack/core/extract_holog_2.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/astrohack/core/extract_holog_2.py b/src/astrohack/core/extract_holog_2.py index 56d4934b..ac7593ee 100644 --- a/src/astrohack/core/extract_holog_2.py +++ b/src/astrohack/core/extract_holog_2.py @@ -14,6 +14,8 @@ from astrohack.antenna import get_proper_telescope from astrohack.utils import ( create_dataset_label, + print_dict_types, + tuple_inspect, ) from astrohack.utils.imaging import calculate_parallactic_angle_chunk from astrohack.utils.algorithms import calculate_optimal_grid_parameters @@ -297,7 +299,6 @@ def process_extract_holog_chunk(extract_holog_params, holog_mds): map_ref_dict = _get_map_ref_dict( map_ant_tuple, ref_ant_per_map_ant_tuple, ant_names, ant_stations ) - ( time_vis, vis_map_dict, @@ -319,7 +320,6 @@ def process_extract_holog_chunk(extract_holog_params, holog_mds): time_interval, scan_list, ) - del vis_data, weight, ant1, ant2, time_vis_row, flag, flag_row, field_ids, scan_list map_ant_name_list = list(map(str, map_ant_name_tuple)) @@ -414,7 +414,7 @@ def _get_time_intervals(time_vis_row, scan_list, time_interval): return time_samples, scan_time_ranges, unq_scans -@njit(cache=False, nogil=True) +@njit(cache=True, nogil=True) def _extract_holog_chunk_jit( vis_data, weight, @@ -484,7 +484,7 @@ def _extract_holog_chunk_jit( time_index = 0 for row in range(n_row): - if np.all(flag_row == False): + if flag_row is False: continue # Find index of time_vis_row[row] in time_samples, assumes time_vis_row is ordered in time @@ -631,7 +631,7 @@ def _create_holog_file( for map_ant_index in vis_map_dict.keys(): dataset_label = create_dataset_label( - ant_names[map_ant_index], ddi_key.split("_")[0] + ant_names[map_ant_index], ddi_key.split("_")[1] ) if map_ant_index not in flagged_mapping_antennas: map_ant_key = f"ant_{ant_names[map_ant_index]}" @@ -753,7 +753,7 @@ def _extract_pointing_chunk( return pnt_map_dict -@njit(cache=False, nogil=True) +@njit(cache=True, nogil=True) def _get_time_index(data_time, i_time, time_axis, half_int): if i_time == time_axis.shape[0]: return -1 From b044d8e30aac1f86f9d1f48b86488b5c40d75d34 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Mon, 9 Feb 2026 15:03:55 -0700 Subject: [PATCH 111/295] Ported beamcut to use an input holog_mds that is a datatree. --- src/astrohack/beamcut.py | 22 ++++++---------------- src/astrohack/core/beamcut.py | 11 +++-------- 2 files changed, 9 insertions(+), 24 deletions(-) diff --git a/src/astrohack/beamcut.py b/src/astrohack/beamcut.py index d5007236..0be8fc71 100644 --- a/src/astrohack/beamcut.py +++ b/src/astrohack/beamcut.py @@ -10,6 +10,7 @@ from astrohack.utils.file import overwrite_file, check_if_file_can_be_opened from astrohack.utils.graph import create_and_execute_graph_from_dict from astrohack.io.beamcut_mds import AstrohackBeamcutFile +from astrohack.io.dio import open_holog from astrohack.utils.validation import custom_plots_checker from typing import Union, List @@ -112,42 +113,31 @@ def beamcut( ) """ - check_if_file_can_be_opened(holog_name, "0.9.5") - if beamcut_name is None: beamcut_name = get_default_file_name( input_file=holog_name, output_type=".beamcut.zarr" ) - if destination is not None: - pathlib.Path(destination).mkdir(exist_ok=True) - beamcut_params = locals() - input_params = beamcut_params.copy() - assert pathlib.Path(beamcut_params["holog_name"]).exists() is True, logger.error( - f"File {beamcut_params['holog_name']} does not exists." - ) - - json_data = "/".join((beamcut_params["holog_name"], ".holog_json")) + if destination is not None: + pathlib.Path(destination).mkdir(exist_ok=True) - with open(json_data, "r") as json_file: - holog_json = json.load(json_file) + overwrite_file(beamcut_name, overwrite) - overwrite_file(beamcut_params["beamcut_name"], beamcut_params["overwrite"]) beamcut_mds = AstrohackBeamcutFile.create_from_input_parameters( beamcut_params["beamcut_name"], beamcut_params ) + holog_mds = open_holog(holog_name) executed_graph = create_and_execute_graph_from_dict( - looping_dict=holog_json, + looping_dict=holog_mds, chunk_function=process_beamcut_chunk, param_dict=beamcut_params, key_order=["ant", "ddi"], output_mds=beamcut_mds, parallel=parallel, ) - if executed_graph: beamcut_mds.write(mode="a") return beamcut_mds diff --git a/src/astrohack/core/beamcut.py b/src/astrohack/core/beamcut.py index 77363417..8cfbba0c 100644 --- a/src/astrohack/core/beamcut.py +++ b/src/astrohack/core/beamcut.py @@ -44,16 +44,11 @@ def process_beamcut_chunk(beamcut_chunk_params, output_mds): """ ddi = beamcut_chunk_params["this_ddi"] antenna = beamcut_chunk_params["this_ant"] + xdt_data = beamcut_chunk_params["xdt_data"] - _, ant_data_dict = load_holog_file( - beamcut_chunk_params["holog_name"], - dask_load=False, - load_pnt_dict=False, - ant_id=antenna, - ddi_id=ddi, - ) # This assumes that there will be no more than one mapping - input_xds = ant_data_dict[ddi]["map_0"] + input_xds = xdt_data["map_0"] + datalabel = create_dataset_label(antenna, ddi) logger.info(f"processing {datalabel}") From 6dce9371df759914fdafbc5176e2dbb42106255c Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Mon, 9 Feb 2026 15:16:55 -0700 Subject: [PATCH 112/295] Beamcut product summaries now have a correct spectral part. --- src/astrohack/core/beamcut.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/astrohack/core/beamcut.py b/src/astrohack/core/beamcut.py index 8cfbba0c..97883dac 100644 --- a/src/astrohack/core/beamcut.py +++ b/src/astrohack/core/beamcut.py @@ -1,3 +1,5 @@ +from copy import deepcopy + import numpy import toolviper.utils.logger as logger import numpy as np @@ -8,7 +10,6 @@ import xarray as xr from astrohack.antenna.telescope import get_proper_telescope -from astrohack.utils.file import load_holog_file from astrohack.utils import ( create_dataset_label, convert_unit, @@ -288,7 +289,10 @@ def _extract_cuts_from_visibilities(input_xds, antenna, ddi): cut_xdtree = xr.DataTree(name=f"{antenna}-{ddi}") scan_time_ranges = input_xds.attrs["scan_time_ranges"] scan_list = input_xds.attrs["scan_list"] - cut_xdtree.attrs["summary"] = input_xds.attrs["summary"] + obs_summ = deepcopy(input_xds.attrs["summary"]) + obs_summ["spectral"]["channel width"] *= obs_summ["spectral"]["number of channels"] + obs_summ["spectral"]["number of channels"] = 1 + cut_xdtree.attrs["summary"] = obs_summ lm_offsets = input_xds.DIRECTIONAL_COSINES.values time_axis = input_xds.time.values From 9ffab6942784de267d411eb6127694b3c81bd564 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Mon, 9 Feb 2026 15:22:21 -0700 Subject: [PATCH 113/295] Removed unused imports. --- src/astrohack/beamcut.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/astrohack/beamcut.py b/src/astrohack/beamcut.py index 0be8fc71..a08a86c1 100644 --- a/src/astrohack/beamcut.py +++ b/src/astrohack/beamcut.py @@ -1,13 +1,10 @@ import pathlib -import json - -import toolviper.utils.logger as logger from toolviper.utils.parameter import validate from astrohack.core.beamcut import process_beamcut_chunk from astrohack.utils import get_default_file_name -from astrohack.utils.file import overwrite_file, check_if_file_can_be_opened +from astrohack.utils.file import overwrite_file from astrohack.utils.graph import create_and_execute_graph_from_dict from astrohack.io.beamcut_mds import AstrohackBeamcutFile from astrohack.io.dio import open_holog From c0145e3f9052936be5b170e08489ef940c2c9a77 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Mon, 9 Feb 2026 15:28:21 -0700 Subject: [PATCH 114/295] Moved source plotting tool from core/extract_locit.py to locit_mds.py --- src/astrohack/core/extract_locit.py | 76 +------------------------- src/astrohack/io/locit_mds.py | 83 +++++++++++++++++++++++++++-- 2 files changed, 80 insertions(+), 79 deletions(-) diff --git a/src/astrohack/core/extract_locit.py b/src/astrohack/core/extract_locit.py index 6e505258..1453b3b8 100644 --- a/src/astrohack/core/extract_locit.py +++ b/src/astrohack/core/extract_locit.py @@ -9,12 +9,7 @@ from astropy.time import Time from astrohack.utils.conversion import convert_unit, casa_time_to_mjd -from astrohack.utils.constants import figsize, twopi -from astrohack.visualization.plot_tools import ( - create_figure_and_axes, - close_figure, - scatter_plot, -) +from astrohack.utils.constants import twopi def extract_antenna_data(extract_locit_parms, locit_mds): @@ -334,72 +329,3 @@ def extract_antenna_phase_gains(extract_locit_parms, ddi_dict, locit_mds): used_sources = np.unique(np.array(used_sources)).tolist() locit_mds.root.attrs["used_sources"] = used_sources return - - -def plot_source_table( - filename, - src_dict, - label=True, - precessed=False, - obs_midpoint=None, - display=True, - figure_size=figsize, - dpi=300, -): - """Backend function for plotting the source table - Args: - filename: Name for the png plot file - src_dict: The dictionary containing the observed sources - label: Add source labels - precessed: Plot sources with precessed coordinates - obs_midpoint: Time to which precesses the coordiantes - display: Display plots in matplotlib - figure_size: plot dimensions in inches - dpi: Dots per inch (plot resolution) - """ - - n_src = len(src_dict) - radec = np.ndarray((n_src, 2)) - name = [] - if precessed: - if obs_midpoint is None: - msg = "Observation midpoint is missing" - logger.error(msg) - raise Exception(msg) - coorkey = "precessed" - time = Time(obs_midpoint, format="mjd") - title = f"Coordinates precessed to {time.iso}" - else: - coorkey = "fk5" - title = "FK5 reference frame" - - for i_src, src in src_dict.items(): - radec[int(i_src)] = src[coorkey] - name.append(src["name"]) - - fig, ax = create_figure_and_axes(figure_size, [1, 1]) - radec[:, 0] *= convert_unit("rad", "hour", "trigonometric") - radec[:, 1] *= convert_unit("rad", "deg", "trigonometric") - - xlabel = "Right Ascension [h]" - ylabel = "Declination [\u00b0]" - if label: - labels = name - else: - labels = None - - scatter_plot( - ax, - radec[:, 0], - xlabel, - radec[:, 1], - ylabel, - title=None, - labels=labels, - xlim=[-0.5, 24.5], - ylim=[-95, 95], - add_legend=False, - ) - - close_figure(fig, title, filename, dpi, display) - return diff --git a/src/astrohack/io/locit_mds.py b/src/astrohack/io/locit_mds.py index 73fca6a3..b4b46043 100644 --- a/src/astrohack/io/locit_mds.py +++ b/src/astrohack/io/locit_mds.py @@ -1,14 +1,13 @@ import numpy as np import pathlib +from astropy.time import Time from typing import Union, Tuple, List import toolviper.utils.parameter +import toolviper.utils.logger as logger from astrohack.antenna import get_proper_telescope -from astrohack.core.extract_locit import ( - plot_source_table, -) from astrohack.io.base_mds import AstrohackBaseFile from astrohack.utils import ( create_pretty_table, @@ -16,6 +15,13 @@ rad_to_deg_str, compute_antenna_relative_off, notavail, + figsize, + convert_unit, +) +from astrohack.visualization.plot_tools import ( + create_figure_and_axes, + scatter_plot, + close_figure, ) from astrohack.utils.tools import get_telescope_lat_lon_rad from astrohack.utils.validation import custom_unit_checker @@ -192,7 +198,7 @@ def plot_source_positions( ) obs_midpoint = None - plot_source_table( + _plot_source_positions_sub( filename, self.root.attrs["source_dict"], precessed=precessed, @@ -251,3 +257,72 @@ def plot_array_configuration( pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) plot_array_configuration(param_dict, self.root, "locit") return + + +def _plot_source_positions_sub( + filename, + src_dict, + label=True, + precessed=False, + obs_midpoint=None, + display=True, + figure_size=figsize, + dpi=300, +): + """Backend function for plotting the source table + Args: + filename: Name for the png plot file + src_dict: The dictionary containing the observed sources + label: Add source labels + precessed: Plot sources with precessed coordinates + obs_midpoint: Time to which precesses the coordiantes + display: Display plots in matplotlib + figure_size: plot dimensions in inches + dpi: Dots per inch (plot resolution) + """ + + n_src = len(src_dict) + radec = np.ndarray((n_src, 2)) + name = [] + if precessed: + if obs_midpoint is None: + msg = "Observation midpoint is missing" + logger.error(msg) + raise Exception(msg) + coorkey = "precessed" + time = Time(obs_midpoint, format="mjd") + title = f"Coordinates precessed to {time.iso}" + else: + coorkey = "fk5" + title = "FK5 reference frame" + + for i_src, src in src_dict.items(): + radec[int(i_src)] = src[coorkey] + name.append(src["name"]) + + fig, ax = create_figure_and_axes(figure_size, [1, 1]) + radec[:, 0] *= convert_unit("rad", "hour", "trigonometric") + radec[:, 1] *= convert_unit("rad", "deg", "trigonometric") + + xlabel = "Right Ascension [h]" + ylabel = "Declination [\u00b0]" + if label: + labels = name + else: + labels = None + + scatter_plot( + ax, + radec[:, 0], + xlabel, + radec[:, 1], + ylabel, + title=None, + labels=labels, + xlim=[-0.5, 24.5], + ylim=[-95, 95], + add_legend=False, + ) + + close_figure(fig, title, filename, dpi, display) + return From 36fc85b6be970c10eb9ea875a204a58173aa6ca3 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Mon, 9 Feb 2026 15:40:02 -0700 Subject: [PATCH 115/295] Moved plotting and exporting related functions from core/locit.py to position_mds.py --- src/astrohack/core/locit.py | 467 --------------------------- src/astrohack/io/position_mds.py | 531 ++++++++++++++++++++++++++++--- 2 files changed, 495 insertions(+), 503 deletions(-) diff --git a/src/astrohack/core/locit.py b/src/astrohack/core/locit.py index f7558311..14f6948b 100644 --- a/src/astrohack/core/locit.py +++ b/src/astrohack/core/locit.py @@ -9,22 +9,11 @@ from astrohack.utils import ( get_data_name, create_dataset_label, - fixed_format_error, - rotate_to_gmt, - compute_antenna_relative_off, ) -from astrohack.visualization.diagnostics import plot_one_antenna_position from astrohack.utils.conversion import convert_unit, hadec_to_elevation from astrohack.utils.algorithms import least_squares, phase_wrapping from astrohack.utils.constants import * -from astrohack.utils.tools import get_telescope_lat_lon_rad -from astrohack.visualization import ( - create_figure_and_axes, - scatter_plot, - close_figure, - plot_boxes_limits_and_labels, -) def locit_separated_chunk(locit_parms, output_mds): @@ -221,205 +210,6 @@ def locit_difference_chunk(locit_parms, output_mds): ) -def plot_sky_coverage_chunk(parm_dict): - """ - Plot the sky coverage for a XDS - Args: - parm_dict: Parameter dictionary from the caller function enriched with the XDS data - - Returns: - PNG file with the sky coverage - """ - - ant_xdt = parm_dict["xdt_data"] - combined = parm_dict["combined"] - antenna = parm_dict["this_ant"] - destination = parm_dict["destination"] - - if combined: - export_name = f"{destination}/position_sky_coverage_{antenna}.png" - suptitle = f'Sky coverage for antenna {antenna.split("_")[1]}' - else: - ddi = parm_dict["this_ddi"] - export_name = f"{destination}/position_sky_coverage_{antenna}_{ddi}.png" - suptitle = ( - f'Sky coverage for antenna {antenna.split("_")[1]}, DDI {ddi.split("_")[1]}' - ) - - figuresize = parm_dict["figure_size"] - angle_unit = parm_dict["angle_unit"] - time_unit = parm_dict["time_unit"] - display = parm_dict["display"] - dpi = parm_dict["dpi"] - antenna_info = ant_xdt.attrs["antenna_info"] - - time = ant_xdt.time.values * convert_unit("day", time_unit, "time") - angle_fact = convert_unit("rad", angle_unit, "trigonometric") - ha = ant_xdt["HOUR_ANGLE"] * angle_fact - dec = ant_xdt["DECLINATION"] * angle_fact - ele = ant_xdt["ELEVATION"] * angle_fact - - fig, axes = create_figure_and_axes(figuresize, [2, 2]) - - elelim, elelines, declim, declines, halim = _compute_plot_borders( - angle_fact, antenna_info["latitude"], ant_xdt.attrs["elevation_limit"] - ) - timelabel = f"Time from observation start [{time_unit}]" - halabel = f"Hour Angle [{angle_unit}]" - declabel = f"Declination [{angle_unit}]" - scatter_plot( - axes[0, 0], - time, - timelabel, - ele, - f"Elevation [{angle_unit}]", - "Time vs Elevation", - ylim=elelim, - hlines=elelines, - add_legend=False, - ) - scatter_plot( - axes[0, 1], - time, - timelabel, - ha, - halabel, - "Time vs Hour angle", - ylim=halim, - add_legend=False, - ) - scatter_plot( - axes[1, 0], - time, - timelabel, - dec, - declabel, - "Time vs Declination", - ylim=declim, - hlines=declines, - add_legend=False, - ) - scatter_plot( - axes[1, 1], - ha, - halabel, - dec, - declabel, - "Hour angle vs Declination", - ylim=declim, - xlim=halim, - hlines=declines, - add_legend=False, - ) - - close_figure(fig, suptitle, export_name, dpi, display) - return - - -def plot_delays_chunk(parm_dict): - """ - Plot the delays and optionally the delay model for a XDS - Args: - parm_dict: Parameter dictionary from the caller function enriched with the XDS data - - Returns: - PNG file with the delay plots - """ - combined = parm_dict["combined"] - plot_model = parm_dict["plot_model"] - antenna = parm_dict["this_ant"] - destination = parm_dict["destination"] - if combined: - export_name = f'{destination}/position_delays_{antenna}_combined_{parm_dict["comb_type"]}.png' - suptitle = f'Delays for antenna {antenna.split("_")[1]}' - else: - ddi = parm_dict["this_ddi"] - export_name = f"{destination}/position_delays_{antenna}_separated_{ddi}.png" - suptitle = ( - f'Delays for antenna {antenna.split("_")[1]}, DDI {ddi.split("_")[1]}' - ) - - ant_xdt = parm_dict["xdt_data"] - figuresize = parm_dict["figure_size"] - angle_unit = parm_dict["angle_unit"] - time_unit = parm_dict["time_unit"] - delay_unit = parm_dict["delay_unit"] - display = parm_dict["display"] - dpi = parm_dict["dpi"] - antenna_info = ant_xdt.attrs["antenna_info"] - - time = ant_xdt.time.values * convert_unit("day", time_unit, "time") - angle_fact = convert_unit("rad", angle_unit, "trigonometric") - delay_fact = convert_unit("sec", delay_unit, kind="time") - ha = ant_xdt["HOUR_ANGLE"] * angle_fact - dec = ant_xdt["DECLINATION"] * angle_fact - ele = ant_xdt["ELEVATION"] * angle_fact - delays = ant_xdt["DELAYS"].values * delay_fact - - elelim, elelines, declim, declines, halim = _compute_plot_borders( - angle_fact, antenna_info["latitude"], ant_xdt.attrs["elevation_limit"] - ) - delay_minmax = [np.min(delays), np.max(delays)] - delay_border = 0.05 * (delay_minmax[1] - delay_minmax[0]) - delaylim = [delay_minmax[0] - delay_border, delay_minmax[1] + delay_border] - - fig, axes = create_figure_and_axes(figuresize, [2, 2]) - - ylabel = f"Delays [{delay_unit}]" - if plot_model: - model = ant_xdt["MODEL"].values * delay_fact - else: - model = None - scatter_plot( - axes[0, 0], - time, - f"Time from observation start [{time_unit}]", - delays, - ylabel, - "Time vs Delays", - ylim=delaylim, - model=model, - ) - scatter_plot( - axes[0, 1], - ele, - f"Elevation [{angle_unit}]", - delays, - ylabel, - "Elevation vs Delays", - xlim=elelim, - vlines=elelines, - ylim=delaylim, - model=model, - ) - scatter_plot( - axes[1, 0], - ha, - f"Hour Angle [{angle_unit}]", - delays, - ylabel, - "Hour Angle vs Delays", - xlim=halim, - ylim=delaylim, - model=model, - ) - scatter_plot( - axes[1, 1], - dec, - f"Declination [{angle_unit}]", - delays, - ylabel, - "Declination vs Delays", - xlim=declim, - vlines=declines, - ylim=delaylim, - model=model, - ) - - close_figure(fig, suptitle, export_name, dpi, display) - return - - def _delays_from_phase_differences(ddi_0, ddi_1): """ Compute delays from the difference in phase between two DDIs of different frequencies @@ -1108,260 +898,3 @@ def _delay_model_kterm_rate(coordinates, fixed_delay, xoff, yoff, zoff, koff, ra sterm = _rate_coeff(coordinates) * rate kterm = _kterm_coeff(coordinates) * koff return xterm + yterm + zterm + fixed_delay + kterm + sterm - - -def export_position_xds_to_table_row( - row, - attributes, - del_fact, - pha_fact, - pos_fact, - slo_fact, - pos_unit, - del_unit, - kterm_present, - rate_present, -): - """ - Export the data from a single X array DataSet attributes to a table row (a list) - Args: - row: row onto which the data results are to be added - attributes: The XDS attributes dictionary - del_fact: Delay unit scaling factor - pos_fact: Position unit scaling factor - slo_fact: Delay rate unit scaling factor - kterm_present: Is the elevation axis offset term present? - rate_present: Is the delay rate term present? - pha_fact: phase unit scaling factor - pos_unit: Position unit - del_unit: Delay unit - - Returns: - The filled table row - """ - - delay_rms = np.sqrt(attributes["chi_squared"]) - mean_freq = np.nanmean(attributes["frequency"]) - phase_rms = twopi * mean_freq * delay_rms - row.append(f"{delay_rms*del_fact:4.2e}") - row.append(f"{phase_rms*pha_fact:5.1f}") - - sig_scale_pos = convert_unit("mm", pos_unit, "length") - sig_scale_del = 1e-3 * convert_unit("nsec", del_unit, "time") - - row.append( - fixed_format_error( - attributes["fixed_delay_fit"], - attributes["fixed_delay_error"], - del_fact, - sig_scale_del, - ) - ) - position, poserr = rotate_to_gmt( - np.copy(attributes["position_fit"]), - attributes["position_error"], - attributes["antenna_info"]["longitude"], - ) - - for i_pos in range(3): - row.append( - fixed_format_error(position[i_pos], poserr[i_pos], pos_fact, sig_scale_pos) - ) - if kterm_present: - row.append( - fixed_format_error( - attributes["koff_fit"], - attributes["koff_error"], - pos_fact, - sig_scale_pos, - ) - ) - if rate_present: - row.append( - fixed_format_error( - attributes["rate_fit"], - attributes["rate_error"], - slo_fact, - sig_scale_del, - ) - ) - return row - - -def export_position_xds_to_parminator(attributes, threshold, kterm_present): - """ - Export a position xds attributes to a string ingestible by VLA's parminator - :param attributes: xds attributes - :param threshold: threshold of valid corrections in meters - :param kterm_present: include K term in the parminator output - :return: string Formated for parminator output - """ - axes = ["X", "Y", "Z"] - delays, _ = rotate_to_gmt( - np.copy(attributes["position_fit"]), - attributes["position_error"], - attributes["antenna_info"]["longitude"], - ) - station = attributes["antenna_info"]["station"] - - outstr = "" - for iaxis, delay in enumerate(delays): - correction = delay * clight - if np.abs(correction) > threshold: - outstr += f"{station}, ,{axes[iaxis]},${correction: .4f}\n" - - if kterm_present: - correction = attributes["koff_fit"] * clight - if np.abs(correction) > threshold: - outstr += f"{station}, ,K,${correction: .4f}\n" - return outstr - - -def _compute_plot_borders(angle_fact, latitude, elevation_limit): - """ - Compute plot limits and position of lines to be added to the plots - Args: - angle_fact: Angle scaling unit factor - latitude: Antenna latitude - elevation_limit: The elevation limit in the data set - - Returns: - Elevation limits, elevation lines, declination limits, declination lines and hour angle limits - """ - latitude *= angle_fact - elevation_limit *= angle_fact - right_angle = pi / 2 * angle_fact - border = 0.05 * right_angle - elelim = [-border, right_angle + border] - border *= 2 - declim = [-border - right_angle + latitude, right_angle + border] - border *= 2 - halim = [-border, 4 * right_angle + border] - elelines = [0, elevation_limit] # lines at zero and elevation limit - declines = [latitude - right_angle, latitude + right_angle] - return elelim, elelines, declim, declines, halim - - -def plot_antenna_position_corrections_worker( - attributes_list, filename, telescope, ref_ant, parm_dict -): - """ - Does the actual individual position correction plots - Args: - attributes_list: List of XDS attributes - filename: Name of the PNG file to be created - telescope: Telescope object used in observations - ref_ant: Reference antenna in the data set - parm_dict: Parameter dictionary of the caller's caller - - Returns: - PNG file with the position corrections plot - """ - tel_lon, tel_lat, tel_rad = get_telescope_lat_lon_rad(telescope) - length_unit = parm_dict["unit"] - scaling = parm_dict["scaling"] - len_fac = convert_unit("m", length_unit, "length") - corr_fac = clight * scaling - figure_size = parm_dict["figure_size"] - box_size = parm_dict["box_size"] - dpi = parm_dict["dpi"] - display = parm_dict["display"] - - xlabel = f"East [{length_unit}]" - ylabel = f"North [{length_unit}]" - - fig, axes = create_figure_and_axes(figure_size, [2, 2], default_figsize=[8, 8]) - xy_whole = axes[0, 0] - xy_inner = axes[0, 1] - z_whole = axes[1, 0] - z_inner = axes[1, 1] - - for attributes in attributes_list: - antenna = attributes["antenna_info"] - ew_off, ns_off, _, _ = compute_antenna_relative_off( - antenna, tel_lon, tel_lat, tel_rad, len_fac - ) - corrections, _ = rotate_to_gmt( - np.copy(attributes["position_fit"]), - attributes["position_error"], - antenna["longitude"], - ) - corrections = np.array(corrections) * corr_fac - text = " " + antenna["name"] - if antenna["name"] == ref_ant: - text += "*" - plot_one_antenna_position( - xy_whole, xy_inner, ew_off, ns_off, text, box_size, marker="+" - ) - add_antenna_position_corrections_to_plot( - xy_whole, xy_inner, ew_off, ns_off, corrections[0], corrections[1], box_size - ) - plot_one_antenna_position( - z_whole, z_inner, ew_off, ns_off, text, box_size, marker="+" - ) - add_antenna_position_corrections_to_plot( - z_whole, z_inner, ew_off, ns_off, 0, corrections[2], box_size - ) - - plot_boxes_limits_and_labels( - xy_whole, - xy_inner, - xlabel, - ylabel, - box_size, - "X & Y, outer array", - "X & Y, inner array", - ) - plot_boxes_limits_and_labels( - z_whole, z_inner, xlabel, ylabel, box_size, "Z, outer array", "Z, inner array" - ) - close_figure(fig, "Position corrections", filename, dpi, display) - - -def add_antenna_position_corrections_to_plot( - outerax, innerax, xpos, ypos, xcorr, ycorr, box_size, color="red", linewidth=0.5 -): - """ - Plot an antenna position corrections as a vector from the antenna position - Args: - outerax: Plotting axis for the outer array box - innerax: Plotting axis for the inner array box - xpos: X antenna position (east-west) - ypos: Y antenna position (north-south) - xcorr: X axis correction (horizontal on plot) - ycorr: Y axis correction (vectical on plot) - box_size: inner array box size - color: vector color - linewidth: vector line width - """ - half_box = box_size / 2 - head_size = np.sqrt(xcorr**2 + ycorr**2) / 4 - if abs(xpos) > half_box or abs(ypos) > half_box: - outerax.arrow( - xpos, - ypos, - xcorr, - ycorr, - color=color, - linewidth=linewidth, - head_width=head_size, - ) - else: - outerax.arrow( - xpos, - ypos, - xcorr, - ycorr, - color=color, - linewidth=linewidth, - head_width=head_size, - ) - innerax.arrow( - xpos, - ypos, - xcorr, - ycorr, - color=color, - linewidth=linewidth, - head_width=head_size, - ) diff --git a/src/astrohack/io/position_mds.py b/src/astrohack/io/position_mds.py index ceb85425..c90ddf8e 100644 --- a/src/astrohack/io/position_mds.py +++ b/src/astrohack/io/position_mds.py @@ -6,15 +6,13 @@ import toolviper.utils.logger as logger import toolviper.utils.parameter +from astrohack.utils import ( + fixed_format_error, + rotate_to_gmt, + compute_antenna_relative_off, +) from astrohack.antenna import get_proper_telescope from astrohack.io.base_mds import AstrohackBaseFile -from astrohack.core.locit import ( - export_position_xds_to_table_row, - export_position_xds_to_parminator, - plot_sky_coverage_chunk, - plot_delays_chunk, - plot_antenna_position_corrections_worker, -) from astrohack.utils import ( convert_unit, clight, @@ -23,9 +21,19 @@ param_to_list, add_prefix, string_to_ascii_file, + pi, + twopi, ) from astrohack.utils.graph import create_and_execute_graph_from_dict from astrohack.utils.validation import custom_unit_checker +from astrohack.utils.tools import get_telescope_lat_lon_rad +from astrohack.visualization import ( + create_figure_and_axes, + scatter_plot, + close_figure, + plot_boxes_limits_and_labels, +) +from astrohack.visualization.diagnostics import plot_one_antenna_position class AstrohackPositionFile(AstrohackBaseFile): @@ -148,7 +156,7 @@ def export_locit_fit_results( if combined: row = [ant_name, antenna.attrs["antenna_info"]["station"]] table.add_row( - export_position_xds_to_table_row( + _export_position_xds_to_table_row( row, antenna.attrs, del_fact, @@ -170,7 +178,7 @@ def export_locit_fit_results( ddi_key.split("_")[1], ] table.add_row( - export_position_xds_to_table_row( + _export_position_xds_to_table_row( row, antenna[ddi_key].attrs, del_fact, @@ -243,7 +251,7 @@ def export_results_to_parminator( else: position_xds = self[ant_key][f"ddi_{ddi}"] - parmstr += export_position_xds_to_parminator( + parmstr += _export_position_xds_to_parminator( position_xds.attrs, threshold, kterm_present ) @@ -308,21 +316,16 @@ def plot_sky_coverage( param_dict["combined"] = self.root.attrs["combined"] if self.root.attrs["combined"]: - create_and_execute_graph_from_dict( - looping_dict=self, - chunk_function=plot_sky_coverage_chunk, - param_dict=param_dict, - key_order=["ant"], - parallel=parallel, - ) + key_order = ["ant"] else: - create_and_execute_graph_from_dict( - looping_dict=self, - chunk_function=plot_sky_coverage_chunk, - param_dict=param_dict, - key_order=["ant", "ddi"], - parallel=parallel, - ) + key_order = ["ant", "ddi"] + create_and_execute_graph_from_dict( + looping_dict=self, + chunk_function=_plot_sky_coverage_chunk, + param_dict=param_dict, + key_order=key_order, + parallel=parallel, + ) @toolviper.utils.parameter.validate(custom_checker=custom_unit_checker) def plot_delays( @@ -393,17 +396,17 @@ def plot_delays( param_dict["combined"] = self.root.attrs["combined"] param_dict["comb_type"] = self.root.attrs["input_parameters"]["combine_ddis"] if self.root.attrs["combined"]: - create_and_execute_graph_from_dict( - looping_dict=self, - chunk_function=plot_delays_chunk, - param_dict=param_dict, - key_order=["ant"], - parallel=parallel, - ) + key_order = ["ant"] else: - create_and_execute_graph_from_dict( - self, plot_delays_chunk, param_dict, ["ant", "ddi"], parallel=parallel - ) + key_order = ["ant", "ddi"] + + create_and_execute_graph_from_dict( + looping_dict=self, + chunk_function=_plot_delays_chunk, + param_dict=param_dict, + key_order=key_order, + parallel=parallel, + ) @toolviper.utils.parameter.validate(custom_checker=custom_unit_checker) def plot_position_corrections( @@ -475,7 +478,7 @@ def plot_position_corrections( attribute_list = [] for ant in ant_list: attribute_list.append(self[ant].attrs) - plot_antenna_position_corrections_worker( + _plot_antenna_position_corrections_sub( attribute_list, filename, telescope, ref_ant, param_dict ) @@ -497,6 +500,462 @@ def plot_position_corrections( for ant in ant_list: if ddi in self[ant].keys(): attribute_list.append(self[ant][ddi].attrs) - plot_antenna_position_corrections_worker( + _plot_antenna_position_corrections_sub( attribute_list, filename, telescope, ref_ant, param_dict ) + + +def _plot_sky_coverage_chunk(parm_dict): + """ + Plot the sky coverage for a XDS + Args: + parm_dict: Parameter dictionary from the caller function enriched with the XDS data + + Returns: + PNG file with the sky coverage + """ + + ant_xdt = parm_dict["xdt_data"] + combined = parm_dict["combined"] + antenna = parm_dict["this_ant"] + destination = parm_dict["destination"] + + if combined: + export_name = f"{destination}/position_sky_coverage_{antenna}.png" + suptitle = f'Sky coverage for antenna {antenna.split("_")[1]}' + else: + ddi = parm_dict["this_ddi"] + export_name = f"{destination}/position_sky_coverage_{antenna}_{ddi}.png" + suptitle = ( + f'Sky coverage for antenna {antenna.split("_")[1]}, DDI {ddi.split("_")[1]}' + ) + + figuresize = parm_dict["figure_size"] + angle_unit = parm_dict["angle_unit"] + time_unit = parm_dict["time_unit"] + display = parm_dict["display"] + dpi = parm_dict["dpi"] + antenna_info = ant_xdt.attrs["antenna_info"] + + time = ant_xdt.time.values * convert_unit("day", time_unit, "time") + angle_fact = convert_unit("rad", angle_unit, "trigonometric") + ha = ant_xdt["HOUR_ANGLE"] * angle_fact + dec = ant_xdt["DECLINATION"] * angle_fact + ele = ant_xdt["ELEVATION"] * angle_fact + + fig, axes = create_figure_and_axes(figuresize, [2, 2]) + + elelim, elelines, declim, declines, halim = _compute_plot_borders( + angle_fact, antenna_info["latitude"], ant_xdt.attrs["elevation_limit"] + ) + timelabel = f"Time from observation start [{time_unit}]" + halabel = f"Hour Angle [{angle_unit}]" + declabel = f"Declination [{angle_unit}]" + scatter_plot( + axes[0, 0], + time, + timelabel, + ele, + f"Elevation [{angle_unit}]", + "Time vs Elevation", + ylim=elelim, + hlines=elelines, + add_legend=False, + ) + scatter_plot( + axes[0, 1], + time, + timelabel, + ha, + halabel, + "Time vs Hour angle", + ylim=halim, + add_legend=False, + ) + scatter_plot( + axes[1, 0], + time, + timelabel, + dec, + declabel, + "Time vs Declination", + ylim=declim, + hlines=declines, + add_legend=False, + ) + scatter_plot( + axes[1, 1], + ha, + halabel, + dec, + declabel, + "Hour angle vs Declination", + ylim=declim, + xlim=halim, + hlines=declines, + add_legend=False, + ) + + close_figure(fig, suptitle, export_name, dpi, display) + return + + +def _plot_delays_chunk(parm_dict): + """ + Plot the delays and optionally the delay model for a XDS + Args: + parm_dict: Parameter dictionary from the caller function enriched with the XDS data + + Returns: + PNG file with the delay plots + """ + combined = parm_dict["combined"] + plot_model = parm_dict["plot_model"] + antenna = parm_dict["this_ant"] + destination = parm_dict["destination"] + if combined: + export_name = f'{destination}/position_delays_{antenna}_combined_{parm_dict["comb_type"]}.png' + suptitle = f'Delays for antenna {antenna.split("_")[1]}' + else: + ddi = parm_dict["this_ddi"] + export_name = f"{destination}/position_delays_{antenna}_separated_{ddi}.png" + suptitle = ( + f'Delays for antenna {antenna.split("_")[1]}, DDI {ddi.split("_")[1]}' + ) + + ant_xdt = parm_dict["xdt_data"] + figuresize = parm_dict["figure_size"] + angle_unit = parm_dict["angle_unit"] + time_unit = parm_dict["time_unit"] + delay_unit = parm_dict["delay_unit"] + display = parm_dict["display"] + dpi = parm_dict["dpi"] + antenna_info = ant_xdt.attrs["antenna_info"] + + time = ant_xdt.time.values * convert_unit("day", time_unit, "time") + angle_fact = convert_unit("rad", angle_unit, "trigonometric") + delay_fact = convert_unit("sec", delay_unit, kind="time") + ha = ant_xdt["HOUR_ANGLE"] * angle_fact + dec = ant_xdt["DECLINATION"] * angle_fact + ele = ant_xdt["ELEVATION"] * angle_fact + delays = ant_xdt["DELAYS"].values * delay_fact + + elelim, elelines, declim, declines, halim = _compute_plot_borders( + angle_fact, antenna_info["latitude"], ant_xdt.attrs["elevation_limit"] + ) + delay_minmax = [np.min(delays), np.max(delays)] + delay_border = 0.05 * (delay_minmax[1] - delay_minmax[0]) + delaylim = [delay_minmax[0] - delay_border, delay_minmax[1] + delay_border] + + fig, axes = create_figure_and_axes(figuresize, [2, 2]) + + ylabel = f"Delays [{delay_unit}]" + if plot_model: + model = ant_xdt["MODEL"].values * delay_fact + else: + model = None + scatter_plot( + axes[0, 0], + time, + f"Time from observation start [{time_unit}]", + delays, + ylabel, + "Time vs Delays", + ylim=delaylim, + model=model, + ) + scatter_plot( + axes[0, 1], + ele, + f"Elevation [{angle_unit}]", + delays, + ylabel, + "Elevation vs Delays", + xlim=elelim, + vlines=elelines, + ylim=delaylim, + model=model, + ) + scatter_plot( + axes[1, 0], + ha, + f"Hour Angle [{angle_unit}]", + delays, + ylabel, + "Hour Angle vs Delays", + xlim=halim, + ylim=delaylim, + model=model, + ) + scatter_plot( + axes[1, 1], + dec, + f"Declination [{angle_unit}]", + delays, + ylabel, + "Declination vs Delays", + xlim=declim, + vlines=declines, + ylim=delaylim, + model=model, + ) + + close_figure(fig, suptitle, export_name, dpi, display) + return + + +def _export_position_xds_to_table_row( + row, + attributes, + del_fact, + pha_fact, + pos_fact, + slo_fact, + pos_unit, + del_unit, + kterm_present, + rate_present, +): + """ + Export the data from a single X array DataSet attributes to a table row (a list) + Args: + row: row onto which the data results are to be added + attributes: The XDS attributes dictionary + del_fact: Delay unit scaling factor + pos_fact: Position unit scaling factor + slo_fact: Delay rate unit scaling factor + kterm_present: Is the elevation axis offset term present? + rate_present: Is the delay rate term present? + pha_fact: phase unit scaling factor + pos_unit: Position unit + del_unit: Delay unit + + Returns: + The filled table row + """ + + delay_rms = np.sqrt(attributes["chi_squared"]) + mean_freq = np.nanmean(attributes["frequency"]) + phase_rms = twopi * mean_freq * delay_rms + row.append(f"{delay_rms*del_fact:4.2e}") + row.append(f"{phase_rms*pha_fact:5.1f}") + + sig_scale_pos = convert_unit("mm", pos_unit, "length") + sig_scale_del = 1e-3 * convert_unit("nsec", del_unit, "time") + + row.append( + fixed_format_error( + attributes["fixed_delay_fit"], + attributes["fixed_delay_error"], + del_fact, + sig_scale_del, + ) + ) + position, poserr = rotate_to_gmt( + np.copy(attributes["position_fit"]), + attributes["position_error"], + attributes["antenna_info"]["longitude"], + ) + + for i_pos in range(3): + row.append( + fixed_format_error(position[i_pos], poserr[i_pos], pos_fact, sig_scale_pos) + ) + if kterm_present: + row.append( + fixed_format_error( + attributes["koff_fit"], + attributes["koff_error"], + pos_fact, + sig_scale_pos, + ) + ) + if rate_present: + row.append( + fixed_format_error( + attributes["rate_fit"], + attributes["rate_error"], + slo_fact, + sig_scale_del, + ) + ) + return row + + +def _export_position_xds_to_parminator(attributes, threshold, kterm_present): + """ + Export a position xds attributes to a string ingestible by VLA's parminator + :param attributes: xds attributes + :param threshold: threshold of valid corrections in meters + :param kterm_present: include K term in the parminator output + :return: string Formated for parminator output + """ + axes = ["X", "Y", "Z"] + delays, _ = rotate_to_gmt( + np.copy(attributes["position_fit"]), + attributes["position_error"], + attributes["antenna_info"]["longitude"], + ) + station = attributes["antenna_info"]["station"] + + outstr = "" + for iaxis, delay in enumerate(delays): + correction = delay * clight + if np.abs(correction) > threshold: + outstr += f"{station}, ,{axes[iaxis]},${correction: .4f}\n" + + if kterm_present: + correction = attributes["koff_fit"] * clight + if np.abs(correction) > threshold: + outstr += f"{station}, ,K,${correction: .4f}\n" + return outstr + + +def _plot_antenna_position_corrections_sub( + attributes_list, filename, telescope, ref_ant, parm_dict +): + """ + Does the actual individual position correction plots + Args: + attributes_list: List of XDS attributes + filename: Name of the PNG file to be created + telescope: Telescope object used in observations + ref_ant: Reference antenna in the data set + parm_dict: Parameter dictionary of the caller's caller + + Returns: + PNG file with the position corrections plot + """ + tel_lon, tel_lat, tel_rad = get_telescope_lat_lon_rad(telescope) + length_unit = parm_dict["unit"] + scaling = parm_dict["scaling"] + len_fac = convert_unit("m", length_unit, "length") + corr_fac = clight * scaling + figure_size = parm_dict["figure_size"] + box_size = parm_dict["box_size"] + dpi = parm_dict["dpi"] + display = parm_dict["display"] + + xlabel = f"East [{length_unit}]" + ylabel = f"North [{length_unit}]" + + fig, axes = create_figure_and_axes(figure_size, [2, 2], default_figsize=[8, 8]) + xy_whole = axes[0, 0] + xy_inner = axes[0, 1] + z_whole = axes[1, 0] + z_inner = axes[1, 1] + + for attributes in attributes_list: + antenna = attributes["antenna_info"] + ew_off, ns_off, _, _ = compute_antenna_relative_off( + antenna, tel_lon, tel_lat, tel_rad, len_fac + ) + corrections, _ = rotate_to_gmt( + np.copy(attributes["position_fit"]), + attributes["position_error"], + antenna["longitude"], + ) + corrections = np.array(corrections) * corr_fac + text = " " + antenna["name"] + if antenna["name"] == ref_ant: + text += "*" + plot_one_antenna_position( + xy_whole, xy_inner, ew_off, ns_off, text, box_size, marker="+" + ) + _add_antenna_position_corrections_to_plot( + xy_whole, xy_inner, ew_off, ns_off, corrections[0], corrections[1], box_size + ) + plot_one_antenna_position( + z_whole, z_inner, ew_off, ns_off, text, box_size, marker="+" + ) + _add_antenna_position_corrections_to_plot( + z_whole, z_inner, ew_off, ns_off, 0, corrections[2], box_size + ) + + plot_boxes_limits_and_labels( + xy_whole, + xy_inner, + xlabel, + ylabel, + box_size, + "X & Y, outer array", + "X & Y, inner array", + ) + plot_boxes_limits_and_labels( + z_whole, z_inner, xlabel, ylabel, box_size, "Z, outer array", "Z, inner array" + ) + close_figure(fig, "Position corrections", filename, dpi, display) + + +def _add_antenna_position_corrections_to_plot( + outerax, innerax, xpos, ypos, xcorr, ycorr, box_size, color="red", linewidth=0.5 +): + """ + Plot an antenna position corrections as a vector from the antenna position + Args: + outerax: Plotting axis for the outer array box + innerax: Plotting axis for the inner array box + xpos: X antenna position (east-west) + ypos: Y antenna position (north-south) + xcorr: X axis correction (horizontal on plot) + ycorr: Y axis correction (vectical on plot) + box_size: inner array box size + color: vector color + linewidth: vector line width + """ + half_box = box_size / 2 + head_size = np.sqrt(xcorr**2 + ycorr**2) / 4 + if abs(xpos) > half_box or abs(ypos) > half_box: + outerax.arrow( + xpos, + ypos, + xcorr, + ycorr, + color=color, + linewidth=linewidth, + head_width=head_size, + ) + else: + outerax.arrow( + xpos, + ypos, + xcorr, + ycorr, + color=color, + linewidth=linewidth, + head_width=head_size, + ) + innerax.arrow( + xpos, + ypos, + xcorr, + ycorr, + color=color, + linewidth=linewidth, + head_width=head_size, + ) + + +def _compute_plot_borders(angle_fact, latitude, elevation_limit): + """ + Compute plot limits and position of lines to be added to the plots + Args: + angle_fact: Angle scaling unit factor + latitude: Antenna latitude + elevation_limit: The elevation limit in the data set + + Returns: + Elevation limits, elevation lines, declination limits, declination lines and hour angle limits + """ + latitude *= angle_fact + elevation_limit *= angle_fact + right_angle = pi / 2 * angle_fact + border = 0.05 * right_angle + elelim = [-border, right_angle + border] + border *= 2 + declim = [-border - right_angle + latitude, right_angle + border] + border *= 2 + halim = [-border, 4 * right_angle + border] + elelines = [0, elevation_limit] # lines at zero and elevation limit + declines = [latitude - right_angle, latitude + right_angle] + return elelim, elelines, declim, declines, halim From 895cc52f435f554b70d98e52e31c3f200d46641d Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Mon, 9 Feb 2026 15:52:28 -0700 Subject: [PATCH 116/295] Added file creation time to origin_info dictionary. --- src/astrohack/utils/data.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/astrohack/utils/data.py b/src/astrohack/utils/data.py index 3028ba3c..45d919d3 100644 --- a/src/astrohack/utils/data.py +++ b/src/astrohack/utils/data.py @@ -1,6 +1,7 @@ import copy import inspect import json +import datetime from datetime import date import toolviper.utils.logger as logger @@ -43,10 +44,15 @@ def add_caller_and_version_to_dict_2(in_dict, direct_call=False): ipos = 1 else: ipos = 2 + curr_time = datetime.datetime.now() + local_tz = curr_time.astimezone().tzinfo + time_str = curr_time.strftime(f"%Y-%m-%d %H:%M:%S {local_tz}") + in_dict["origin_info"] = { "origin": "astrohack", "version": astrohack.__version__, "creator_function": inspect.stack()[ipos].function, + "creation_time": time_str, } From 749d1f82e0c5d1a96d7fe5ada57112cc6d8e174d Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Mon, 9 Feb 2026 16:13:45 -0700 Subject: [PATCH 117/295] Ported vla holography tutorial to use the new data format functions. --- docs/tutorials/vla_holography_tutorial.ipynb | 2707 +++++------------- 1 file changed, 726 insertions(+), 1981 deletions(-) diff --git a/docs/tutorials/vla_holography_tutorial.ipynb b/docs/tutorials/vla_holography_tutorial.ipynb index 432f854f..9bb20a3a 100644 --- a/docs/tutorials/vla_holography_tutorial.ipynb +++ b/docs/tutorials/vla_holography_tutorial.ipynb @@ -38,30 +38,20 @@ }, { "cell_type": "code", - "execution_count": 1, "id": "7db6f868-030c-41ee-8188-c236aa675c27", "metadata": { - "ExecuteTime": { - "end_time": "2026-01-05T22:47:48.235090526Z", - "start_time": "2026-01-05T22:47:46.159029440Z" - }, "execution": { "iopub.execute_input": "2026-01-06T18:54:54.557332Z", "iopub.status.busy": "2026-01-06T18:54:54.556652Z", "iopub.status.idle": "2026-01-06T18:54:56.799151Z", "shell.execute_reply": "2026-01-06T18:54:56.798363Z" }, - "tags": [] - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "AstroHACK version 0.10.1 already installed.\n" - ] + "tags": [], + "ExecuteTime": { + "end_time": "2026-02-09T23:02:34.054516629Z", + "start_time": "2026-02-09T23:02:31.034912598Z" } - ], + }, "source": [ "import os\n", "\n", @@ -78,7 +68,17 @@ " import astrohack\n", "\n", " print(\"astrohack version\", astrohack.__version__, \" installed.\")" - ] + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "AstroHACK version 0.10.1 already installed.\n" + ] + } + ], + "execution_count": 1 }, { "cell_type": "markdown", @@ -90,94 +90,41 @@ }, { "cell_type": "code", - "execution_count": 2, "id": "ffb79bcd", "metadata": { - "ExecuteTime": { - "end_time": "2026-01-05T22:47:48.421392416Z", - "start_time": "2026-01-05T22:47:48.236970781Z" - }, "execution": { "iopub.execute_input": "2026-01-06T18:54:56.801947Z", "iopub.status.busy": "2026-01-06T18:54:56.801400Z", "iopub.status.idle": "2026-01-06T18:54:56.952249Z", "shell.execute_reply": "2026-01-06T18:54:56.951523Z" }, - "tags": [] + "tags": [], + "ExecuteTime": { + "end_time": "2026-02-09T23:02:34.413856861Z", + "start_time": "2026-02-09T23:02:34.083964716Z" + } }, + "source": [ + "import toolviper\n", + "\n", + "toolviper.utils.data.download(file=\"ea25_cal_small_after_fixed.split.ms\", folder=\"data\")" + ], "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[\u001b[38;2;128;05;128m2026-01-06 11:54:56,802\u001b[0m] \u001b[38;2;50;50;205m INFO\u001b[0m\u001b[38;2;112;128;144m astrohack: \u001b[0m Module path: \u001b[38;2;50;50;205m/home/victor/mambaforge/envs/casadev/lib/python3.12/site-packages/toolviper\u001b[0m \n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[\u001b[38;2;128;05;128m2026-01-06 11:54:56,808\u001b[0m] \u001b[38;2;50;50;205m INFO\u001b[0m\u001b[38;2;112;128;144m astrohack: \u001b[0m Downloading from [cloudflare] .... \n" - ] - }, - { - "data": { - "text/html": [ - "
                                       \n",
-       "  Download List                        \n",
-       " ───────────────────────────────────── \n",
-       "  ea25_cal_small_after_fixed.split.ms  \n",
-       "                                       \n",
-       "
\n" - ], - "text/plain": [ - " \n", - " \u001b[1m \u001b[0m\u001b[1mDownload List \u001b[0m\u001b[1m \u001b[0m \n", - " ───────────────────────────────────── \n", - " \u001b[35mea25_cal_small_after_fixed.split.ms\u001b[0m \n", - " \n" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[\u001b[38;2;128;05;128m2026-01-06 11:54:56,814\u001b[0m] \u001b[38;2;50;50;205m INFO\u001b[0m\u001b[38;2;112;128;144m astrohack: \u001b[0m File exists: data/ea25_cal_small_after_fixed.split.ms \n" - ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "ce6e189e131e43269ec002b5880c7f8f", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Output()" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, { "data": { + "text/plain": [], "text/html": [ "
\n"
-      ],
-      "text/plain": []
+      ]
      },
      "metadata": {},
-     "output_type": "display_data"
+     "output_type": "display_data",
+     "jetTransient": {
+      "display_id": null
+     }
     }
    ],
-   "source": [
-    "import toolviper\n",
-    "\n",
-    "toolviper.utils.data.download(file=\"ea25_cal_small_after_fixed.split.ms\", folder=\"data\")"
-   ]
+   "execution_count": 2
   },
   {
    "cell_type": "markdown",
@@ -202,10 +149,10 @@
     "from astrohack import open_panel\n",
     "from astrohack import open_pointing\n",
     "\n",
-    "holog_data = open_holog(file='./data/ea25_cal_small_spw1_4_60_ea04_after.holog.zarr')\n",
-    "image_data = open_image(file='./data/ea25_cal_small_spw1_4_60_ea04_after.image.zarr')\n",
-    "panel_data = open_panel(file='./data/ea25_cal_small_spw1_4_60_ea04_after.panel.zarr')\n",
-    "pointing_data = open_pointing(file='./data/ea25_cal_small_spw1_4_60_ea04_after.point.zarr')\n",
+    "holog_data = open_holog('./data/ea25_cal_small_spw1_4_60_ea04_after.holog.zarr')\n",
+    "image_data = open_image('./data/ea25_cal_small_spw1_4_60_ea04_after.image.zarr')\n",
+    "panel_data = open_panel('./data/ea25_cal_small_spw1_4_60_ea04_after.panel.zarr')\n",
+    "pointing_data = open_pointing('./data/ea25_cal_small_spw1_4_60_ea04_after.point.zarr')\n",
     "```"
    ]
   },
@@ -216,889 +163,84 @@
     "tags": []
    },
    "source": [
-    "## Setup Dask Local Cluster\n",
-    "\n",
-    "The local Dask client handles scheduling and worker managment for the parallelization. The user has the option of choosing the number of cores and memory allocations for each worker howerver, we recommend a minimum of 8Gb per core with standard settings.\n",
-    "\n",
-    "\n",
-    "A significant amount of information related to the client and scheduling can be found using the [Dask Dashboard](https://docs.dask.org/en/stable/dashboard.html). This is a built in dashboard native to Dask and allows the user to monitor the workers during processing. This is especially useful for profilling. For those that are interested in working soley within Jupyterlab a dashboard extension is availabe for [Jupyterlab](https://github.com/dask/dask-labextension#dask-jupyterlab-extension).\n",
-    "\n",
-    "![dashboard](../_media/dashboard.png)"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "id": "4c68374a-905f-4183-80e6-04eb326cfcbc",
-   "metadata": {},
-   "source": [
-    "## Estimating Memory Requirements\n",
-    "A new functionality, currently being refined, is a function to estimate the amount of memory per core max that would be required to process a given file. The estimation is given as the suggested memory per core need to not spilling over into swap memory. If the user has already computed the holog_obs_dict, it can be added as a parameter to speed up the estitmate as this is a serial function currently.\n",
-    "\n",
-    "In the resulting table the following definitions are important:\n",
-    "\n",
-    "- **Available memory**: The available memory on the system currently, ie. the total not currently in use.\n",
-    "- **Total memory**: The total system memory\n",
-    "- **Suggested memory per core**: Memory allocation per core estimated to not spill ove rinto swap memory.\n",
-    "\n",
-    "  Reference: *https://psutil.readthedocs.io/en/latest/#psutil.virtual_memory*"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 3,
-   "id": "7fb0902f-f274-4d47-a48e-61c0c2561ca7",
-   "metadata": {
-    "ExecuteTime": {
-     "end_time": "2026-01-05T22:47:52.610264979Z",
-     "start_time": "2026-01-05T22:47:48.425359013Z"
-    },
-    "execution": {
-     "iopub.execute_input": "2026-01-06T18:54:56.955190Z",
-     "iopub.status.busy": "2026-01-06T18:54:56.954965Z",
-     "iopub.status.idle": "2026-01-06T18:55:00.909468Z",
-     "shell.execute_reply": "2026-01-06T18:55:00.908739Z"
-    }
-   },
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "[\u001b[38;2;128;05;128m2026-01-06 11:54:56,956\u001b[0m] \u001b[38;2;50;50;205m    INFO\u001b[0m\u001b[38;2;112;128;144m   astrohack: \u001b[0m Module path: \u001b[38;2;50;50;205m/home/victor/mambaforge/envs/casadev/lib/python3.12/site-packages/toolviper\u001b[0m \n"
-     ]
-    },
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "[\u001b[38;2;128;05;128m2026-01-06 11:54:56,957\u001b[0m] \u001b[38;2;50;50;205m    INFO\u001b[0m\u001b[38;2;112;128;144m   astrohack: \u001b[0m Downloading from [cloudflare] .... \n"
-     ]
-    },
-    {
-     "data": {
-      "text/html": [
-       "
                   \n",
-       "  Download List    \n",
-       " ───────────────── \n",
-       "  heuristic_model  \n",
-       "                   \n",
-       "
\n" - ], - "text/plain": [ - " \n", - " \u001b[1m \u001b[0m\u001b[1mDownload List \u001b[0m\u001b[1m \u001b[0m \n", - " ───────────────── \n", - " \u001b[35mheuristic_model\u001b[0m \n", - " \n" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "8a76f8d8befe4f7394e3c72e86a9c8ef", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Output()" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
\n"
-      ],
-      "text/plain": []
-     },
-     "metadata": {},
-     "output_type": "display_data"
-    },
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "[\u001b[38;2;128;05;128m2026-01-06 11:54:57,332\u001b[0m] \u001b[38;2;50;50;205m    INFO\u001b[0m\u001b[38;2;112;128;144m   astrohack: \u001b[0m Module path: \u001b[38;2;50;50;205m/home/victor/work/Holography-1022/astrohack/src/astrohack\u001b[0m \n"
-     ]
-    },
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "[\u001b[38;2;128;05;128m2026-01-06 11:55:00,481\u001b[0m] \u001b[38;2;50;50;205m    INFO\u001b[0m\u001b[38;2;112;128;144m   astrohack: \u001b[0m Finished processing \n"
-     ]
-    },
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "[\u001b[38;2;128;05;128m2026-01-06 11:55:00,522\u001b[0m] \u001b[38;2;50;50;205m    INFO\u001b[0m\u001b[38;2;112;128;144m   astrohack: \u001b[0m Writing distance matrix to /home/victor/work/Holography-1022/astrohack/docs/tutorials/.baseline_distance_matrix.csv ... \n"
-     ]
-    },
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "[\u001b[38;2;128;05;128m2026-01-06 11:55:00,529\u001b[0m] \u001b[38;2;50;50;205m    INFO\u001b[0m\u001b[38;2;112;128;144m   astrohack: \u001b[0m Module path: \u001b[38;2;50;50;205m/home/victor/mambaforge/envs/casadev/lib/python3.12/site-packages/toolviper\u001b[0m \n"
-     ]
-    },
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "[\u001b[38;2;128;05;128m2026-01-06 11:55:00,530\u001b[0m] \u001b[38;2;50;50;205m    INFO\u001b[0m\u001b[38;2;112;128;144m   astrohack: \u001b[0m Downloading from [cloudflare] .... \n"
-     ]
-    },
-    {
-     "data": {
-      "text/html": [
-       "
                   \n",
-       "  Download List    \n",
-       " ───────────────── \n",
-       "  heuristic_model  \n",
-       "                   \n",
-       "
\n" - ], - "text/plain": [ - " \n", - " \u001b[1m \u001b[0m\u001b[1mDownload List \u001b[0m\u001b[1m \u001b[0m \n", - " ───────────────── \n", - " \u001b[35mheuristic_model\u001b[0m \n", - " \n" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "41c93000971a4b0a9557df0aa00c878d", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Output()" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
\n"
-      ],
-      "text/plain": []
-     },
-     "metadata": {},
-     "output_type": "display_data"
-    },
-    {
-     "name": "stderr",
-     "output_type": "stream",
-     "text": [
-      "/home/victor/mambaforge/envs/casadev/lib/python3.12/site-packages/sklearn/base.py:380: InconsistentVersionWarning: Trying to unpickle estimator ElasticNet from version 1.3.2 when using version 1.6.1. This might lead to breaking code or invalid results. Use at your own risk. For more info please refer to:\n",
-      "https://scikit-learn.org/stable/model_persistence.html#security-maintainability-limitations\n",
-      "  warnings.warn(\n"
-     ]
-    },
-    {
-     "data": {
-      "text/html": [
-       "
                                      System Info                                       \n",
-       "┏━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n",
-       "┃ N-cores  Available memory (MB)  Total memory (MB)  Suggested memory per core (MB) ┃\n",
-       "┡━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩\n",
-       "│       8  23748                  31977                                       11911 │\n",
-       "└─────────┴───────────────────────┴───────────────────┴────────────────────────────────┘\n",
-       "    Available memory: represents the system memory available without going into swap    \n",
-       "
\n" - ], - "text/plain": [ - "\u001b[3m System Info \u001b[0m\n", - "┏━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n", - "┃\u001b[1m \u001b[0m\u001b[1mN-cores\u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mAvailable memory (MB)\u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mTotal memory (MB)\u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mSuggested memory per core (MB)\u001b[0m\u001b[1m \u001b[0m┃\n", - "┡━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩\n", - "│\u001b[34m \u001b[0m\u001b[34m 8\u001b[0m\u001b[34m \u001b[0m│\u001b[35m \u001b[0m\u001b[35m23748 \u001b[0m\u001b[35m \u001b[0m│\u001b[36m \u001b[0m\u001b[36m31977 \u001b[0m\u001b[36m \u001b[0m│\u001b[32m \u001b[0m\u001b[32m 11911\u001b[0m\u001b[32m \u001b[0m│\n", - "└─────────┴───────────────────────┴───────────────────┴────────────────────────────────┘\n", - "\u001b[2;3m Available memory: represents the system memory available without going into swap \u001b[0m\n" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "11911" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from astrohack.extract_holog import model_memory_usage\n", - "\n", - "toolviper.utils.data.download(\"heuristic_model\", folder=\"./\")\n", - "# the elastic model download needs to be fixed\n", - "model_memory_usage(\n", - " ms_name=\"data/ea25_cal_small_after_fixed.split.ms\", holog_obs_dict=None\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "10dffc63-1907-497f-b025-392b5813eac9", - "metadata": { - "ExecuteTime": { - "end_time": "2026-01-05T22:47:53.623610719Z", - "start_time": "2026-01-05T22:47:52.613642729Z" - }, - "execution": { - "iopub.execute_input": "2026-01-06T18:55:00.925689Z", - "iopub.status.busy": "2026-01-06T18:55:00.925541Z", - "iopub.status.idle": "2026-01-06T18:55:02.096721Z", - "shell.execute_reply": "2026-01-06T18:55:02.096075Z" - }, - "tags": [] - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[\u001b[38;2;128;05;128m2026-01-06 11:55:00,944\u001b[0m] \u001b[38;2;50;50;205m INFO\u001b[0m\u001b[38;2;112;128;144m astrohack: \u001b[0m Module path: \u001b[38;2;50;50;205m/home/victor/mambaforge/envs/casadev/lib/python3.12/site-packages/toolviper\u001b[0m \n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[\u001b[38;2;128;05;128m2026-01-06 11:55:00,949\u001b[0m] \u001b[38;2;255;160;0m WARNING\u001b[0m\u001b[38;2;112;128;144m astrohack: \u001b[0m It is recommended that the local cache directory be set using the \u001b[38;2;50;50;205mdask_local_dir\u001b[0m parameter. \n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[\u001b[38;2;128;05;128m2026-01-06 11:55:02,075\u001b[0m] \u001b[38;2;50;50;205m INFO\u001b[0m\u001b[38;2;112;128;144m astrohack: \u001b[0m Client \n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "
\n", - "
\n", - "

Client

\n", - "

MenrvaClient-34c09f1c-eb31-11f0-8d77-40a3ccc2bb2c

\n", - " \n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "\n", - "
Connection method: Cluster objectCluster type: distributed.LocalCluster
\n", - " Dashboard: http://127.0.0.1:8787/status\n", - "
\n", - "\n", - " \n", - "\n", - " \n", - "
\n", - "

Cluster Info

\n", - "
\n", - "
\n", - "
\n", - "
\n", - "

LocalCluster

\n", - "

74a7428e

\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "\n", - "\n", - " \n", - "
\n", - " Dashboard: http://127.0.0.1:8787/status\n", - " \n", - " Workers: 4\n", - "
\n", - " Total threads: 4\n", - " \n", - " Total memory: 14.90 GiB\n", - "
Status: runningUsing processes: True
\n", - "\n", - "
\n", - " \n", - "

Scheduler Info

\n", - "
\n", - "\n", - "
\n", - "
\n", - "
\n", - "
\n", - "

Scheduler

\n", - "

Scheduler-4f848e66-9009-41ae-b51f-53cc2aea0a7a

\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
\n", - " Comm: tcp://127.0.0.1:38557\n", - " \n", - " Workers: 4 \n", - "
\n", - " Dashboard: http://127.0.0.1:8787/status\n", - " \n", - " Total threads: 4\n", - "
\n", - " Started: Just now\n", - " \n", - " Total memory: 14.90 GiB\n", - "
\n", - "
\n", - "
\n", - "\n", - "
\n", - " \n", - "

Workers

\n", - "
\n", - "\n", - " \n", - "
\n", - "
\n", - "
\n", - "
\n", - " \n", - "

Worker: 0

\n", - "
\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "\n", - " \n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "\n", - "
\n", - " Comm: tcp://127.0.0.1:42623\n", - " \n", - " Total threads: 1\n", - "
\n", - " Dashboard: http://127.0.0.1:46207/status\n", - " \n", - " Memory: 3.73 GiB\n", - "
\n", - " Nanny: tcp://127.0.0.1:39241\n", - "
\n", - " Local directory: /tmp/dask-scratch-space/worker-wdmm0ydo\n", - "
\n", - " Tasks executing: \n", - " \n", - " Tasks in memory: \n", - "
\n", - " Tasks ready: \n", - " \n", - " Tasks in flight: \n", - "
\n", - " CPU usage: 0.0%\n", - " \n", - " Last seen: Just now\n", - "
\n", - " Memory usage: 62.69 MiB\n", - " \n", - " Spilled bytes: 0 B\n", - "
\n", - " Read bytes: 0.0 B\n", - " \n", - " Write bytes: 0.0 B\n", - "
\n", - "
\n", - "
\n", - "
\n", - " \n", - "
\n", - "
\n", - "
\n", - "
\n", - " \n", - "

Worker: 1

\n", - "
\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "\n", - " \n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "\n", - "
\n", - " Comm: tcp://127.0.0.1:44045\n", - " \n", - " Total threads: 1\n", - "
\n", - " Dashboard: http://127.0.0.1:43439/status\n", - " \n", - " Memory: 3.73 GiB\n", - "
\n", - " Nanny: tcp://127.0.0.1:33249\n", - "
\n", - " Local directory: /tmp/dask-scratch-space/worker-zgmbgm2c\n", - "
\n", - " Tasks executing: \n", - " \n", - " Tasks in memory: \n", - "
\n", - " Tasks ready: \n", - " \n", - " Tasks in flight: \n", - "
\n", - " CPU usage: 0.0%\n", - " \n", - " Last seen: Just now\n", - "
\n", - " Memory usage: 62.94 MiB\n", - " \n", - " Spilled bytes: 0 B\n", - "
\n", - " Read bytes: 0.0 B\n", - " \n", - " Write bytes: 0.0 B\n", - "
\n", - "
\n", - "
\n", - "
\n", - " \n", - "
\n", - "
\n", - "
\n", - "
\n", - " \n", - "

Worker: 2

\n", - "
\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "\n", - " \n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "\n", - "
\n", - " Comm: tcp://127.0.0.1:39715\n", - " \n", - " Total threads: 1\n", - "
\n", - " Dashboard: http://127.0.0.1:41885/status\n", - " \n", - " Memory: 3.73 GiB\n", - "
\n", - " Nanny: tcp://127.0.0.1:44377\n", - "
\n", - " Local directory: /tmp/dask-scratch-space/worker-5nkdmxq1\n", - "
\n", - " Tasks executing: \n", - " \n", - " Tasks in memory: \n", - "
\n", - " Tasks ready: \n", - " \n", - " Tasks in flight: \n", - "
\n", - " CPU usage: 0.0%\n", - " \n", - " Last seen: Just now\n", - "
\n", - " Memory usage: 62.68 MiB\n", - " \n", - " Spilled bytes: 0 B\n", - "
\n", - " Read bytes: 0.0 B\n", - " \n", - " Write bytes: 0.0 B\n", - "
\n", - "
\n", - "
\n", - "
\n", - " \n", - "
\n", - "
\n", - "
\n", - "
\n", - " \n", - "

Worker: 3

\n", - "
\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "\n", - " \n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "\n", - "
\n", - " Comm: tcp://127.0.0.1:34741\n", - " \n", - " Total threads: 1\n", - "
\n", - " Dashboard: http://127.0.0.1:42825/status\n", - " \n", - " Memory: 3.73 GiB\n", - "
\n", - " Nanny: tcp://127.0.0.1:34049\n", - "
\n", - " Local directory: /tmp/dask-scratch-space/worker-1iurjkui\n", - "
\n", - " Tasks executing: \n", - " \n", - " Tasks in memory: \n", - "
\n", - " Tasks ready: \n", - " \n", - " Tasks in flight: \n", - "
\n", - " CPU usage: 0.0%\n", - " \n", - " Last seen: Just now\n", - "
\n", - " Memory usage: 62.94 MiB\n", - " \n", - " Spilled bytes: 0 B\n", - "
\n", - " Read bytes: 244.98 kiB\n", - " \n", - " Write bytes: 244.98 kiB\n", - "
\n", - "
\n", - "
\n", - "
\n", - " \n", - "\n", - "
\n", - "
\n", - "\n", - "
\n", - "
\n", - "
\n", - "
\n", - " \n", - "\n", - "
\n", - "
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from toolviper.dask import menrva\n", - "from toolviper.dask.client import local_client\n", - "\n", - "client = local_client(cores=4, memory_limit=\"4GB\")\n", - "client" - ] - }, - { - "cell_type": "markdown", - "id": "a65ccd34-aa93-4689-bc5e-68d4775f759d", - "metadata": {}, - "source": [ - "### Holography processing\n", - "## Extract Holog\n", - "\n", - "The extraction and restructuring of the holography data is done using the `extract_holog` function. This function is similar in function to the `UVHOL` task in AIPS. \n", - "The holography data that is extracted can be set using the compound dictionary *holog_obs_description*: *mapping*, *scan*, and *antenna* id. A detailed description of the structure of the *holog_obs_description* dictionary can be found in the documentation [here](https://astrohack.readthedocs.io/en/latest/_api/autoapi/astrohack/extract_holog/index.html). The `extract_holog` can automatically generate the *holog_obs_description* by inspecting the pointing table. \n", - "\n", - "Inline information on the input parameters can also be gotten using `help(extract_holog)` in the cell." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "0f6f37a0-4994-48b6-b930-7fb61e0d8db7", - "metadata": { - "ExecuteTime": { - "end_time": "2026-01-05T22:48:11.330846420Z", - "start_time": "2026-01-05T22:47:53.624963380Z" - }, - "execution": { - "iopub.execute_input": "2026-01-06T18:55:02.098466Z", - "iopub.status.busy": "2026-01-06T18:55:02.098252Z", - "iopub.status.idle": "2026-01-06T18:55:18.963606Z", - "shell.execute_reply": "2026-01-06T18:55:18.962595Z" - }, - "tags": [] - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[\u001b[38;2;128;05;128m2026-01-06 11:55:02,099\u001b[0m] \u001b[38;2;50;50;205m INFO\u001b[0m\u001b[38;2;112;128;144m astrohack: \u001b[0m Module path: \u001b[38;2;50;50;205m/home/victor/work/Holography-1022/astrohack/src/astrohack\u001b[0m \n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[\u001b[38;2;128;05;128m2026-01-06 11:55:02,101\u001b[0m] \u001b[38;2;255;160;0m WARNING\u001b[0m\u001b[38;2;112;128;144m astrohack: \u001b[0m data/ea25_cal_small_after_fixed.split.point.zarr will be overwritten. \n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[\u001b[38;2;128;05;128m2026-01-06 11:55:05,387\u001b[0m] \u001b[38;2;50;50;205m INFO\u001b[0m\u001b[38;2;112;128;144m astrohack: \u001b[0m Finished processing \n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[\u001b[38;2;128;05;128m2026-01-06 11:55:05,422\u001b[0m] \u001b[38;2;50;50;205m INFO\u001b[0m\u001b[38;2;112;128;144m astrohack: \u001b[0m Creating output file name: data/ea25_cal_small_after_fixed.split.holog.zarr \n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[\u001b[38;2;128;05;128m2026-01-06 11:55:05,422\u001b[0m] \u001b[38;2;255;160;0m WARNING\u001b[0m\u001b[38;2;112;128;144m astrohack: \u001b[0m data/ea25_cal_small_after_fixed.split.holog.zarr will be overwritten. \n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[\u001b[38;2;128;05;128m2026-01-06 11:55:05,477\u001b[0m] \u001b[38;2;50;50;205m INFO\u001b[0m\u001b[38;2;112;128;144m astrohack: \u001b[0m Processing ddi: 0, scans: [8 ... 57] \n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[\u001b[38;2;128;05;128m2026-01-06 11:55:05,478\u001b[0m] \u001b[38;2;50;50;205m INFO\u001b[0m\u001b[38;2;112;128;144m astrohack: \u001b[0m Processing ddi: 1, scans: [8 ... 57] \n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[\u001b[38;2;128;05;128m2026-01-06 11:55:15,494\u001b[0m] \u001b[38;2;50;50;205m INFO\u001b[0m\u001b[38;2;112;128;144m worker_0: \u001b[0m EA06: DDI 1: Suggested cell size 2.20 amin, FOV: (1.11 deg, 1.11 deg) \n", - "[\u001b[38;2;128;05;128m2026-01-06 11:55:15,496\u001b[0m] \u001b[38;2;50;50;205m INFO\u001b[0m\u001b[38;2;112;128;144m worker_0: \u001b[0m EA25: DDI 1: Suggested cell size 2.20 amin, FOV: (1.11 deg, 1.11 deg) \n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[\u001b[38;2;128;05;128m2026-01-06 11:55:17,098\u001b[0m] \u001b[38;2;50;50;205m INFO\u001b[0m\u001b[38;2;112;128;144m worker_0: \u001b[0m Finished extracting holography chunk for ddi: 1 holog_map_key: map_0 \n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[\u001b[38;2;128;05;128m2026-01-06 11:55:17,366\u001b[0m] \u001b[38;2;50;50;205m INFO\u001b[0m\u001b[38;2;112;128;144m worker_1: \u001b[0m EA06: DDI 0: Suggested cell size 2.47 amin, FOV: (1.11 deg, 1.11 deg) \n", - "[\u001b[38;2;128;05;128m2026-01-06 11:55:17,367\u001b[0m] \u001b[38;2;50;50;205m INFO\u001b[0m\u001b[38;2;112;128;144m worker_1: \u001b[0m EA25: DDI 0: Suggested cell size 2.47 amin, FOV: (1.11 deg, 1.11 deg) \n" - ] + "## Setup Dask Local Cluster\n", + "\n", + "The local Dask client handles scheduling and worker managment for the parallelization. The user has the option of choosing the number of cores and memory allocations for each worker howerver, we recommend a minimum of 8Gb per core with standard settings.\n", + "\n", + "\n", + "A significant amount of information related to the client and scheduling can be found using the [Dask Dashboard](https://docs.dask.org/en/stable/dashboard.html). This is a built in dashboard native to Dask and allows the user to monitor the workers during processing. This is especially useful for profilling. For those that are interested in working soley within Jupyterlab a dashboard extension is availabe for [Jupyterlab](https://github.com/dask/dask-labextension#dask-jupyterlab-extension).\n", + "\n", + "![dashboard](../_media/dashboard.png)" + ] + }, + { + "cell_type": "code", + "id": "10dffc63-1907-497f-b025-392b5813eac9", + "metadata": { + "execution": { + "iopub.execute_input": "2026-01-06T18:55:00.925689Z", + "iopub.status.busy": "2026-01-06T18:55:00.925541Z", + "iopub.status.idle": "2026-01-06T18:55:02.096721Z", + "shell.execute_reply": "2026-01-06T18:55:02.096075Z" }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[\u001b[38;2;128;05;128m2026-01-06 11:55:18,887\u001b[0m] \u001b[38;2;50;50;205m INFO\u001b[0m\u001b[38;2;112;128;144m worker_1: \u001b[0m Finished extracting holography chunk for ddi: 0 holog_map_key: map_0 \n", - "[\u001b[38;2;128;05;128m2026-01-06 11:55:18,903\u001b[0m] \u001b[38;2;50;50;205m INFO\u001b[0m\u001b[38;2;112;128;144m astrohack: \u001b[0m Finished processing \n" - ] + "tags": [], + "ExecuteTime": { + "end_time": "2026-02-09T23:02:34.536098217Z", + "start_time": "2026-02-09T23:02:34.416392866Z" } + }, + "source": [ + "from toolviper.dask.client import local_client\n", + "\n", + "parallel = False\n", + "\n", + "if parallel:\n", + " client = local_client(cores=4, memory_limit=\"4GB\")\n", + " print(client)\n", + "else:\n", + " client = None" ], + "outputs": [], + "execution_count": 3 + }, + { + "cell_type": "markdown", + "id": "a65ccd34-aa93-4689-bc5e-68d4775f759d", + "metadata": {}, + "source": [ + "### Holography processing\n", + "## Extract Holog\n", + "\n", + "The extraction and restructuring of the holography data is done using the `extract_holog` function. This function is similar in function to the `UVHOL` task in AIPS. \n", + "The holography data that is extracted can be set using the compound dictionary *holog_obs_description*: *mapping*, *scan*, and *antenna* id. A detailed description of the structure of the *holog_obs_description* dictionary can be found in the documentation [here](https://astrohack.readthedocs.io/en/latest/_api/autoapi/astrohack/extract_holog/index.html). The `extract_holog` can automatically generate the *holog_obs_description* by inspecting the pointing table. \n", + "\n", + "Inline information on the input parameters can also be gotten using `help(extract_holog)` in the cell." + ] + }, + { + "cell_type": "code", + "id": "0f6f37a0-4994-48b6-b930-7fb61e0d8db7", + "metadata": { + "execution": { + "iopub.execute_input": "2026-01-06T18:55:02.098466Z", + "iopub.status.busy": "2026-01-06T18:55:02.098252Z", + "iopub.status.idle": "2026-01-06T18:55:18.963606Z", + "shell.execute_reply": "2026-01-06T18:55:18.962595Z" + }, + "tags": [], + "ExecuteTime": { + "end_time": "2026-02-09T23:02:49.283845099Z", + "start_time": "2026-02-09T23:02:34.548093808Z" + } + }, "source": [ - "from astrohack.extract_pointing import extract_pointing\n", - "from astrohack.extract_holog import extract_holog\n", + "from astrohack.extract_pointing_2 import extract_pointing\n", + "from astrohack.extract_holog_2 import extract_holog\n", "\n", "extract_pointing(\n", " ms_name=\"data/ea25_cal_small_after_fixed.split.ms\",\n", " point_name=\"data/ea25_cal_small_after_fixed.split.point.zarr\",\n", - " parallel=True,\n", + " parallel=parallel,\n", " overwrite=True,\n", ")\n", "\n", @@ -1106,20 +248,29 @@ " ms_name=\"data/ea25_cal_small_after_fixed.split.ms\",\n", " point_name=\"data/ea25_cal_small_after_fixed.split.point.zarr\",\n", " data_column=\"CORRECTED_DATA\",\n", - " parallel=True,\n", + " parallel=parallel,\n", " overwrite=True,\n", ")" - ] + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[\u001B[38;2;128;05;128m2026-02-09 16:02:48,528\u001B[0m] \u001B[38;2;50;50;205m INFO\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m Finished extracting holography chunk for DDI 1, map_0. \n" + ] + } + ], + "execution_count": 4 }, { "cell_type": "markdown", "id": "108c9f23-1091-4dca-bb4e-b6fbf5f1abd3", "metadata": {}, "source": [ - "Two files are created by `extract_holog`: The extracted pointing information in the form of `.point.zarr` and the extracted holography data as `.holog.zarr`. In addition, a holography data object is returned. This is the same holography data object returned by the hologrphy data API above. The `holog_mds` object is a python dict containing the extracted holography data found in `.holog.zarr` but with extended functionality such as providing a summary of the run infomation in table form. Below for each `DDI` we can see the available `scan` and `antenna` information.\n", - "\n", + "`extract_pointing` creates a file for holding the extracted pointing information in the form of `.point.zarr`.\n", "\n", - "___point_name.point.zarr:___ The pointing zarr file contains position and pointing information extracted from the pointing table of the input measurement set. In addition, the antenna and mapping scan information is listed for each antenna. The pointing object is structured as a simple dictionary with `key:value` sets with the key being the antenna id and the value being the pointing dataset. \n", + "___point_name.point.zarr:___ The pointing zarr file contains position and pointing information extracted from the pointing table of the input measurement set. In addition, the antenna and mapping scan information is listed for each antenna. The pointing object is structured in a single depth data tree with the key being the antenna id and the value being the pointing dataset. \n", "\n", "```\n", "point_mds = \n", @@ -1136,22 +287,24 @@ "id": "702f7a0b-bc15-4ca9-a476-b655901cd23d", "metadata": {}, "source": [ - "___holog_name.holog.zarr:___ The holog zarr file contains ungridded data extracted from the pointing and main tables in the measurement set. The holog file includes the directional, visibility and weight information recorded on a shared time axis; the sampling is done because the native sample rates between the pointing and main tables are not the same. In addition, the meta data such as sampled parallactic data (beginning, middle and end of scan) and l(m) extent is recorded in the file attributes. The holog file structure is a compound dictionary keyed according to `ddi` -> `map` -> `ant` with values consisting of the holog dataset. \n", + "`extract_holog` creates a file for holding the extracted holography data as `.holog.zarr`. In addition, a holography data object is returned. This is the same holography data object returned by the hologrphy data API above. The `holog_mds` object is a python dict containing the extracted holography data found in `.holog.zarr` but with extended functionality such as providing a summary of the run infomation in table form. Below for each `DDI` we can see the available `scan` and `antenna` information.\n", + "\n", + "___holog_name.holog.zarr:___ The holog zarr file contains ungridded data extracted from the pointing and main tables in the measurement set. The holog file includes the directional, visibility and weight information recorded on a shared time axis; a time resampling is done because the sampling rates of the pointing and main tables are different. In addition, the meta data such as sampled parallactic data (beginning, middle and end of scan) and l and m extents is recorded in the file attributes. The holog file structure is a data tree, with `ant` -> `ddi` -> `map` as the keys for each depth. \n", "\n", "```\n", "holog_mds = \n", "{\n", - " ddi_0:{\n", - " map_0:{\n", - " ant_0: holog_ds,\n", + " ant_0:{\n", + " ddi_0:{\n", + " map_0: holog_ds,\n", " ⋮\n", - " ant_n: holog_ds\n", + " map_n: holog_ds\n", " },\n", " ⋮\n", - " map_p: …\n", + " ddi_p: …\n", " },\n", " ⋮\n", - " ddi_m: …\n", + " ant_m: …\n", "}\n", "\n", "```\n", @@ -1161,24 +314,45 @@ }, { "cell_type": "code", - "execution_count": 6, "id": "f4ce2575-5925-4822-b426-fc8b67580e9e", "metadata": { - "ExecuteTime": { - "end_time": "2026-01-05T22:48:11.429233810Z", - "start_time": "2026-01-05T22:48:11.332947124Z" - }, "execution": { "iopub.execute_input": "2026-01-06T18:55:18.966171Z", "iopub.status.busy": "2026-01-06T18:55:18.965653Z", "iopub.status.idle": "2026-01-06T18:55:18.984229Z", "shell.execute_reply": "2026-01-06T18:55:18.983689Z" }, - "tags": [] + "tags": [], + "ExecuteTime": { + "end_time": "2026-02-09T23:02:49.415924012Z", + "start_time": "2026-02-09T23:02:49.303898547Z" + } }, + "source": "holog_mds[\"ant_ea25\"][\"ddi_0\"][\"map_0\"]", "outputs": [ { "data": { + "text/plain": [ + "\n", + "Group: /ant_ea25/ddi_0/map_0\n", + " Dimensions: (time: 8914, lm: 2, chan: 64, pol: 4)\n", + " Coordinates:\n", + " * chan (chan) float64 512B 1.41e+10 ... 1.423e+10\n", + " * pol (pol) \n", + " IDEAL_DIRECTIONAL_COSINES (time, lm) float64 143kB dask.array\n", + " VIS (time, chan, pol) complex128 37MB dask.array\n", + " WEIGHT (time, chan, pol) float64 18MB dask.array\n", + " Attributes:\n", + " parallactic_samples: [5.308157433326323, 5.357028871639436, 5.502977...\n", + " scan_list: [8, 9, 10, 12, 13, 14, 16, 17, 18, 23, 24, 25, ...\n", + " scan_time_ranges: [[5170354117.500001, 5170354438.499999], [51703...\n", + " summary: {'aperture': None, 'beam': {'cell size': 0.0007...\n", + " time_smoothing_interval: 1.0" + ], "text/html": [ "
\n", "\n", @@ -1550,7 +724,7 @@ " stroke: currentColor;\n", " fill: currentColor;\n", "}\n", - "
<xarray.Dataset> Size: 55MB\n",
+       "
<xarray.DatasetView> Size: 55MB\n",
        "Dimensions:                    (time: 8914, lm: 2, chan: 64, pol: 4)\n",
        "Coordinates:\n",
        "  * chan                       (chan) float64 512B 1.41e+10 ... 1.423e+10\n",
@@ -1560,16 +734,14 @@
        "Data variables:\n",
        "    DIRECTIONAL_COSINES        (time, lm) float64 143kB dask.array<chunksize=(8914, 2), meta=np.ndarray>\n",
        "    IDEAL_DIRECTIONAL_COSINES  (time, lm) float64 143kB dask.array<chunksize=(8914, 2), meta=np.ndarray>\n",
-       "    VIS                        (time, chan, pol) complex128 37MB dask.array<chunksize=(2229, 16, 2), meta=np.ndarray>\n",
-       "    WEIGHT                     (time, chan, pol) float64 18MB dask.array<chunksize=(2229, 16, 2), meta=np.ndarray>\n",
+       "    VIS                        (time, chan, pol) complex128 37MB dask.array<chunksize=(8914, 64, 4), meta=np.ndarray>\n",
+       "    WEIGHT                     (time, chan, pol) float64 18MB dask.array<chunksize=(8914, 64, 4), meta=np.ndarray>\n",
        "Attributes:\n",
-       "    ddi:                      0\n",
-       "    holog_map_key:            map_0\n",
        "    parallactic_samples:      [5.308157433326323, 5.357028871639436, 5.502977...\n",
        "    scan_list:                [8, 9, 10, 12, 13, 14, 16, 17, 18, 23, 24, 25, ...\n",
        "    scan_time_ranges:         [[5170354117.500001, 5170354438.499999], [51703...\n",
        "    summary:                  {'aperture': None, 'beam': {'cell size': 0.0007...\n",
-       "    time_smoothing_interval:  1.0