From 91a77c955920d8b86fe07bdaef498d97df2a180d Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Fri, 12 Dec 2025 12:29:14 -0700 Subject: [PATCH 01/95] Prototype of a beamcut calibration pipeline. --- etc/beamcuts/beamcut_CASA_calibration.py | 288 +++++++++++++++++++++++ 1 file changed, 288 insertions(+) create mode 100644 etc/beamcuts/beamcut_CASA_calibration.py diff --git a/etc/beamcuts/beamcut_CASA_calibration.py b/etc/beamcuts/beamcut_CASA_calibration.py new file mode 100644 index 00000000..56a4dea7 --- /dev/null +++ b/etc/beamcuts/beamcut_CASA_calibration.py @@ -0,0 +1,288 @@ +import os +import numpy as np +import casatools +from datetime import datetime, timedelta +from pathlib import Path + +lnbr = '\n' +spc=' ' + +def search(string): + first = string.find('.') + if first == -1: + return -1, -1 + second = string.find('.',first+1) + return first+1, second+7 + + +def julian_day_to_date(jd): + # JD 2440587.5 corresponds to 1970-01-01 00:00:00 UTC + unix_epoch = datetime(1970, 1, 1) + days_since_unix_epoch = jd - 2440587 #.5 + dt = unix_epoch + timedelta(days=days_since_unix_epoch) + #return dt.strftime("%Y-%m-%d-Hh-%Mm") + return dt.strftime("%m-%d-%Y at %Hh%Mm") + +def yesno(prompt): + user_ans = input(f'{prompt} <(Y)es/(N)o>: ').lower() + if user_ans == 'y' or user_ans == 'yes': + return True + elif user_ans == 'n' or user_ans == 'no': + return False + else: + print('Use or ') + return yesno(prompt) + + +def is_asdm(filename): + file_path = Path(f"{filename}/ASDM.xml") + return file_path.exists() + + +def asdm_to_ms(asdm_name, overwrite): + msname = asdm_name + '.ms' + if os.path.exists(msname) and not overwrite: + print(msname+' already exists.') + else: + importasdm(asdm=asdm_name, + vis=msname, + createmms=False, + ocorr_mode='co', + lazy=False, + asis='Receiver CalAtmosphere', + process_caldevice=True, + process_pointing=True, + savecmds=True, + outfile=msname + '.flagonline.txt', + overwrite=False, + bdfflags=False, + with_pointing_correction=True, + applyflags =True) + return msname + + +def create_heading(heading, width=60, block_char='#', spacing=1, blocking=3): + outstr = width*block_char+lnbr + capo = blocking*block_char + spacing*spc + coda = capo[::-1]+lnbr + usable_width = width - 2*spacing - 2*blocking + heading = heading.strip() + head_wrds = heading.split() + capo_len = len(capo) + + def end_line(line, usable_width, coda): + line_len = len(line) + 1 - capo_len + spc_to_add = usable_width - line_len + return spc_to_add*spc + coda + + line = capo + for wrd in head_wrds: + wrd_len = len(wrd) + if wrd_len > usable_width: + raise ValueError(f'Word {wrd} is larger than the usable width') + line_len = len(line) + wrd_len + 1 - capo_len + if line_len > usable_width: + outstr += line + end_line(line, usable_width, coda) + line = capo + else: + line += spc + wrd + outstr += line + end_line(line, usable_width, coda) + outstr += width*block_char+lnbr + return outstr + + +class UserInteraction: + last_use_file = '.beamcut_cal.last' + user_inp_list = ['filename', 'field', 'refant', 'overwrite'] + sep = '=' + + def __init__(self): + self.filename = None + self.field = None + self.refant = None + self.overwrite = None + self.last_use_list = None + + def _find_previous_input(self): + return Path(self.last_use_file).exists() + + def _read_last_use_file(self): + self.last_use_list = [] + with open(self.last_use_file, 'r') as infile: + for line in infile: + self.last_use_list.append(line.strip()) + + def _reuse_last(self): + print('Previous inputs:') + for line in self.last_use_list: + print(f'\t{line}') + return yesno('Re-use previous input?') + + def _init_from_user(self): + self.filename = input("Enter file name: ") + self.field = input("Enter field number: ") + self.refant = input("Enter referece antenna: ") + self.overwrite = yesno('Re-do calibration if already done?') + + def save_input(self): + outstr = '' + for key in self.user_inp_list: + outstr += f'{key} {self.sep} {getattr(self, key)}\n' + with open(self.last_use_file, 'w') as outfile: + outfile.write(outstr) + + def read_input(self): + if self._find_previous_input(): + self._read_last_use_file() + if self._reuse_last(): + for line in self.last_use_list: + wrds = line.split(self.sep) + self.__setattr__(wrds[0], wrds[1]) + else: + self._init_from_user() + else: + self._init_from_user() + print(self.filename) + + @classmethod + def perform_beamcut_calibration(cls): + + print(create_heading('Welcome to the beam cut calibration pipeline')) + my_obj = cls() + my_obj.read_input() + + print(my_obj.filename) + if is_asdm(my_obj.filename): + msname = asdm_to_ms(my_obj.filename, my_obj.overwrite) + else: + msname = my_obj.filename + + print(msname) + # my_obj.save_input() + # exit() + # mycal_obj = CalObject(msname, my_obj.field, my_obj.refant, my_obj.overwrite) + # mycal_obj.calibration_pipeline() + # mycal_obj.apply_calibration() + + +class CalObject: + + def __init__(self, msname, field, refant, overwrite, calversion='01'): + self.msname = msname + self.refant = refant + self.overwrite = overwrite + self.field = field + # Supposition this will be + + base_cal_name = msname+'.'+calversion+'.' + self.delay_caltable = base_cal_name+'delay.cal' + self.bandpass_caltable = 'bandpass.bcal' + self.gain_caltable = base_cal_name+'gain.cal' + + self._initialize_metadata() + self._report_init() + + def _initialize_metadata(self): + # Fetch metadata from ms + msmd = casatools.msmetadata() + msmd.open(self.msname) + cal_scans = msmd.scansforintent('*PHASE*') + beamcut_scans = msmd.scansforintent('*MAP*ON_SOURCE') + spw_list = msmd.spwsforintent('*MAP*') + msmd.done() + + # Convert to comma-separated string + self.cal_scans = ','.join(map(str, cal_scans)) + self.beamcut_scans = ','.join(map(str, beamcut_scans)) + + self.minspw = str(np.min(spw_list)) + self.maxspw = str(np.max(spw_list)) + self.spwrange = self.minspw+'~'+self.maxspw + self.quacked_spwstr = self.spwrange+':4~60' + + f_dot, l_dot = search(self.msname) + mod_julian_date = self.msname[f_dot:l_dot] + julian_date = 2400000.+float(mod_julian_date) + self.day = julian_day_to_date(julian_date) + + def _report_init(self): + print('Scans used for calibration:') + print(self.cal_scans) + print('Scans used for beamcut:') + print(self.beamcut_scans) + print('SPWSs used for beamcuts:') + print(self.spwrange) + print('Date obtained:') + print(self.day) + + def _do_calibration(self, cal_name): + if os.path.exists(cal_name): + print(f'{cal_name} exists.') + if self.overwrite: + print('\r Overwriting it') + return True + else: + print('\r keeping it') + return False + else: + print(f'{cal_name} does not exist, creating it...') + return True + + def delay_calibration(self): + if self._do_calibration(self.delay_caltable): + gaincal(vis = self.msname, + caltable = self.delay_caltable, + refant = self.refant, + solint = 'inf', + spw = self.quacked_spwstr, + scan = self.cal_scans, + gaintype = 'K') + return + + def bandpass_calibration(self): + if self._do_calibration(self.bandpass_caltable): + bandpass(vis = self.msname, + caltable = self.bandpass_caltable, + refant = self.refant, + solint = '10s', + spw = self.quacked_spwstr, + solnorm = True, + scan = self.cal_scans, + gaintable = [self.delay_caltable]) + return + + def gain_calibration(self): + if self._do_calibration(self.gain_caltable): + gaincal(vis=self.msname, + caltable=self.gain_caltable, + refant=self.refant, + calmode='ap', + solint='inf', + spw=self.quacked_spwstr, + minsnr=2, + minblperant=2, + scan=self.cal_scans, + gaintable=[self.delay_caltable, self.bandpass_caltable]) + return + + def apply_calibration(self): + applycal(vis=self.msname, + field=self.field, + spw=self.quacked_spwstr, + applymode='calonly', + gaintable=[self.delay_caltable, + self.bandpass_caltable, + self.gain_caltable] + ) + return + + def calibration_pipeline(self): + self.delay_calibration() + self.bandpass_calibration() + self.gain_calibration() + return + + +UserInteraction.perform_beamcut_calibration() + + From 8fd0961dda82bfb208ff0f7ea8f4014d9c957ac8 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Mon, 15 Dec 2025 15:10:50 -0700 Subject: [PATCH 02/95] improvements on calibration script. --- etc/beamcuts/beamcut_CASA_calibration.py | 232 +++++++++++++---------- 1 file changed, 132 insertions(+), 100 deletions(-) diff --git a/etc/beamcuts/beamcut_CASA_calibration.py b/etc/beamcuts/beamcut_CASA_calibration.py index 56a4dea7..a6d11a81 100644 --- a/etc/beamcuts/beamcut_CASA_calibration.py +++ b/etc/beamcuts/beamcut_CASA_calibration.py @@ -1,28 +1,13 @@ import os +import shutil + import numpy as np import casatools -from datetime import datetime, timedelta from pathlib import Path lnbr = '\n' spc=' ' -def search(string): - first = string.find('.') - if first == -1: - return -1, -1 - second = string.find('.',first+1) - return first+1, second+7 - - -def julian_day_to_date(jd): - # JD 2440587.5 corresponds to 1970-01-01 00:00:00 UTC - unix_epoch = datetime(1970, 1, 1) - days_since_unix_epoch = jd - 2440587 #.5 - dt = unix_epoch + timedelta(days=days_since_unix_epoch) - #return dt.strftime("%Y-%m-%d-Hh-%Mm") - return dt.strftime("%m-%d-%Y at %Hh%Mm") - def yesno(prompt): user_ans = input(f'{prompt} <(Y)es/(N)o>: ').lower() if user_ans == 'y' or user_ans == 'yes': @@ -34,66 +19,61 @@ def yesno(prompt): return yesno(prompt) -def is_asdm(filename): - file_path = Path(f"{filename}/ASDM.xml") - return file_path.exists() +class MessageBoard: + def __init__(self, width=60, block_char='#', spacing=1, blocking=3): + self.width = width + self.block_char = block_char + self.spacing = spacing, + self.blocking = blocking -def asdm_to_ms(asdm_name, overwrite): - msname = asdm_name + '.ms' - if os.path.exists(msname) and not overwrite: - print(msname+' already exists.') - else: - importasdm(asdm=asdm_name, - vis=msname, - createmms=False, - ocorr_mode='co', - lazy=False, - asis='Receiver CalAtmosphere', - process_caldevice=True, - process_pointing=True, - savecmds=True, - outfile=msname + '.flagonline.txt', - overwrite=False, - bdfflags=False, - with_pointing_correction=True, - applyflags =True) - return msname - - -def create_heading(heading, width=60, block_char='#', spacing=1, blocking=3): - outstr = width*block_char+lnbr - capo = blocking*block_char + spacing*spc - coda = capo[::-1]+lnbr - usable_width = width - 2*spacing - 2*blocking - heading = heading.strip() - head_wrds = heading.split() - capo_len = len(capo) - - def end_line(line, usable_width, coda): - line_len = len(line) + 1 - capo_len - spc_to_add = usable_width - line_len - return spc_to_add*spc + coda - - line = capo - for wrd in head_wrds: - wrd_len = len(wrd) - if wrd_len > usable_width: - raise ValueError(f'Word {wrd} is larger than the usable width') - line_len = len(line) + wrd_len + 1 - capo_len - if line_len > usable_width: - outstr += line + end_line(line, usable_width, coda) - line = capo + self.capo = blocking*block_char + spacing*spc + self.coda = self.capo[::-1]+lnbr + self.usable_width = width - 2*spacing - 2*blocking + self.block_line = self.width*self.block_char+lnbr + self.block_len = len(self.capo) + + def end_line(self, line, centered=True): + line_len = len(line) + 1 - self.block_len + spc_to_add = self.usable_width - line_len + if centered: + out_line = self.capo + line + self.coda else: - line += spc + wrd - outstr += line + end_line(line, usable_width, coda) - outstr += width*block_char+lnbr - return outstr + out_line = self.capo + line + spc_to_add + self.coda + return out_line + + def heading(self, user_msg): + outstr = '' + outstr += self.block_line + head_wrds = user_msg.split() + + line = '' + for wrd in head_wrds: + wrd_len = len(wrd) + if wrd_len > self.usable_width: + raise ValueError(f'Word {wrd} is larger than the usable self.width') + line_len = len(line) + wrd_len + 1 + if line_len > self.usable_width: + outstr += self.end_line(line) + line = wrd + else: + line += spc + wrd + outstr += self.end_line(line) + outstr += self.block_line + return outstr + + def one_liner(self, msg): + if len(msg) > self.usable_width: + raise ValueError('Message is larger than usable width') + return self.end_line(msg) + + def done(self): + return self.one_liner('Done!') class UserInteraction: last_use_file = '.beamcut_cal.last' - user_inp_list = ['filename', 'field', 'refant', 'overwrite'] + user_inp_list = ['filename', 'field', 'refant', 'overwrite', 'confirmation_before_start'] sep = '=' def __init__(self): @@ -102,6 +82,7 @@ def __init__(self): self.refant = None self.overwrite = None self.last_use_list = None + self.confirmation_before_start = None def _find_previous_input(self): return Path(self.last_use_file).exists() @@ -116,13 +97,16 @@ def _reuse_last(self): print('Previous inputs:') for line in self.last_use_list: print(f'\t{line}') - return yesno('Re-use previous input?') + ans = yesno('Re-use previous input?') + print() + return ans def _init_from_user(self): self.filename = input("Enter file name: ") self.field = input("Enter field number: ") self.refant = input("Enter referece antenna: ") self.overwrite = yesno('Re-do calibration if already done?') + self.confirmation_before_start = yesno('Confirm info before starting calibration?') def save_input(self): outstr = '' @@ -137,51 +121,89 @@ def read_input(self): if self._reuse_last(): for line in self.last_use_list: wrds = line.split(self.sep) - self.__setattr__(wrds[0], wrds[1]) + key = wrds[0].strip() + value = wrds[1].strip() + setattr(self, key, value) else: self._init_from_user() else: self._init_from_user() - print(self.filename) @classmethod def perform_beamcut_calibration(cls): - - print(create_heading('Welcome to the beam cut calibration pipeline')) + msger = MessageBoard() + print(msger.heading('Welcome to the beam cut calibration pipeline')) my_obj = cls() my_obj.read_input() + print() - print(my_obj.filename) - if is_asdm(my_obj.filename): - msname = asdm_to_ms(my_obj.filename, my_obj.overwrite) + mycal_obj = CalObject(my_obj.filename, my_obj.field, my_obj.refant, my_obj.overwrite, msger) + if my_obj.confirmation_before_start: + proceed = yesno('Proceed with calibration?') else: - msname = my_obj.filename + proceed = True + print() - print(msname) - # my_obj.save_input() - # exit() - # mycal_obj = CalObject(msname, my_obj.field, my_obj.refant, my_obj.overwrite) - # mycal_obj.calibration_pipeline() - # mycal_obj.apply_calibration() + if proceed: + my_obj.save_input() + mycal_obj.calibration_pipeline() + mycal_obj.apply_calibration() + + print(msger.heading('All Done!')) class CalObject: - def __init__(self, msname, field, refant, overwrite, calversion='01'): + def __init__(self, msname, field, refant, overwrite, msger, first_chan=4, last_chan=60): self.msname = msname self.refant = refant - self.overwrite = overwrite + self.overwrite = bool(overwrite) self.field = field - # Supposition this will be + self.msger = msger + self.fchan = first_chan + self.lchan = last_chan - base_cal_name = msname+'.'+calversion+'.' + base_cal_name = msname+'.' self.delay_caltable = base_cal_name+'delay.cal' - self.bandpass_caltable = 'bandpass.bcal' + self.bandpass_caltable = base_cal_name+'bandpass.bcal' self.gain_caltable = base_cal_name+'gain.cal' + if self._is_asdm(): + print(self.msger.one_liner('Input is an SDM running importasdm...')) + self.asdm_to_ms() + print(self.msger.done()) + self._initialize_metadata() self._report_init() + def _is_asdm(self): + file_path = Path(f"{self.msname}/ASDM.xml") + return file_path.exists() + + def asdm_to_ms(self): + msname = self.msname + '.ms' + if os.path.exists(msname) and self.overwrite: + print(self.msger.heading('Removing old file')) + shutil.rmtree(msname) + + importasdm(asdm=self.msname, + vis=msname, + createmms=False, + ocorr_mode='co', + lazy=False, + asis='Receiver CalAtmosphere', + process_caldevice=True, + process_pointing=True, + savecmds=True, + outfile=msname + '.flagonline.txt', + bdfflags=False, + with_pointing_correction=True, + applyflags=True, + overwrite=False, + ) + self.msname = msname + return + def _initialize_metadata(self): # Fetch metadata from ms msmd = casatools.msmetadata() @@ -198,37 +220,34 @@ def _initialize_metadata(self): self.minspw = str(np.min(spw_list)) self.maxspw = str(np.max(spw_list)) self.spwrange = self.minspw+'~'+self.maxspw - self.quacked_spwstr = self.spwrange+':4~60' - - f_dot, l_dot = search(self.msname) - mod_julian_date = self.msname[f_dot:l_dot] - julian_date = 2400000.+float(mod_julian_date) - self.day = julian_day_to_date(julian_date) + self.quacked_spwstr = self.spwrange+f':{self.fchan}~{self.lchan}' def _report_init(self): print('Scans used for calibration:') print(self.cal_scans) + print() print('Scans used for beamcut:') print(self.beamcut_scans) + print() print('SPWSs used for beamcuts:') print(self.spwrange) - print('Date obtained:') - print(self.day) + print() def _do_calibration(self, cal_name): if os.path.exists(cal_name): print(f'{cal_name} exists.') if self.overwrite: - print('\r Overwriting it') + print(f'{cal_name} exists, overwriting.') return True else: - print('\r keeping it') + print(f'{cal_name} exists, keeping it.') return False else: print(f'{cal_name} does not exist, creating it...') return True def delay_calibration(self): + print(self.msger.one_liner('Delay calibration...')) if self._do_calibration(self.delay_caltable): gaincal(vis = self.msname, caltable = self.delay_caltable, @@ -237,9 +256,13 @@ def delay_calibration(self): spw = self.quacked_spwstr, scan = self.cal_scans, gaintype = 'K') + print(self.msger.done()) + else: + print(self.msger.one_liner('Skipping delay calibration...')) return def bandpass_calibration(self): + print(self.msger.one_liner('Bandpass calibration...')) if self._do_calibration(self.bandpass_caltable): bandpass(vis = self.msname, caltable = self.bandpass_caltable, @@ -249,9 +272,13 @@ def bandpass_calibration(self): solnorm = True, scan = self.cal_scans, gaintable = [self.delay_caltable]) + print(self.msger.done()) + else: + print(self.msger.one_liner('Skipping bandpass calibration...')) return def gain_calibration(self): + print(self.msger.one_liner('Gain calibration...')) if self._do_calibration(self.gain_caltable): gaincal(vis=self.msname, caltable=self.gain_caltable, @@ -263,9 +290,13 @@ def gain_calibration(self): minblperant=2, scan=self.cal_scans, gaintable=[self.delay_caltable, self.bandpass_caltable]) + print(self.msger.done()) + else: + print(self.msger.one_liner('Skipping gain calibration...')) return def apply_calibration(self): + print(self.msger.one_liner('Applying calibration...')) applycal(vis=self.msname, field=self.field, spw=self.quacked_spwstr, @@ -274,6 +305,7 @@ def apply_calibration(self): self.bandpass_caltable, self.gain_caltable] ) + print(self.msger.done()) return def calibration_pipeline(self): From 9e4ea2e61549da86b5942c96addfa501745e6c28 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Mon, 15 Dec 2025 17:05:35 -0700 Subject: [PATCH 03/95] Started work on a beamcut tool. --- src/astrohack/beamcut_tool.py | 61 +++++++++++++++++++++++++++++ src/astrohack/core/beamcut.py | 72 +++++++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 src/astrohack/beamcut_tool.py create mode 100644 src/astrohack/core/beamcut.py diff --git a/src/astrohack/beamcut_tool.py b/src/astrohack/beamcut_tool.py new file mode 100644 index 00000000..6629bb6a --- /dev/null +++ b/src/astrohack/beamcut_tool.py @@ -0,0 +1,61 @@ +import pathlib +import toolviper.utils.logger as logger +import json + +from astrohack.core.beamcut import process_beamcut_chunk +from astrohack.utils import get_default_file_name +from astrohack.utils.file import overwrite_file +from astrohack.utils.graph import compute_graph +from astrohack.utils.data import write_meta_data + +from typing import Union, List + +def beamcut_tool( + holog_name: str, + beamcut_name: str = None, + ant: Union[str, List[str]] = "all", + ddi: Union[int, List[str]] = "all", + correlations: str = "all", + parallel: bool = False, + overwrite: bool = False, +): + + if beamcut_name is None: + beamcut_name = get_default_file_name( + input_file=holog_name, output_type=".beamcut.zarr" + ) + + 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")) + + with open(json_data, "r") as json_file: + holog_json = json.load(json_file) + + overwrite_file(beamcut_params["beamcut_name"], beamcut_params['overwrite']) + + if compute_graph( + holog_json, + process_beamcut_chunk, + beamcut_params, + ["ant", "ddi"], + parallel=parallel, + ): + logger.info("Finished processing") + output_attr_file = "{name}/{ext}".format( + name=beamcut_params["beamcut_name"], ext=".beamcut_input" + ) + # write_meta_data(output_attr_file, input_params) + # beamcut_mds = AstrohackbeamcutFile(beamcut_params["beamcut_name"]) + # beamcut_mds.open() + # + # return beamcut_mds + return None + else: + logger.warning("No data to process") + return None \ No newline at end of file diff --git a/src/astrohack/core/beamcut.py b/src/astrohack/core/beamcut.py new file mode 100644 index 00000000..e3d34236 --- /dev/null +++ b/src/astrohack/core/beamcut.py @@ -0,0 +1,72 @@ +import toolviper.utils.logger as logger +import numpy as np + +from astrohack import get_proper_telescope +from astrohack.utils.file import load_holog_file +from astrohack.utils import create_dataset_label, data_statistics, statistics_to_text +from astrohack.visualization import create_figure_and_axes, scatter_plot, close_figure + + +def process_beamcut_chunk(beamcut_chunk_params): + ddi = beamcut_chunk_params["this_ddi"] + antenna = beamcut_chunk_params["this_ant"] + + _, ant_data_dict = load_holog_file( + beamcut_chunk_params["holog_name"], + dask_load=False, + load_pnt_dict=False, + ant_id=beamcut_chunk_params["this_ant"], + ddi_id=beamcut_chunk_params["this_ddi"], + ) + # This assumes that there will be no more than one mapping + this_xds = ant_data_dict[ddi]['map_0'] + logger.info(f"processing {create_dataset_label(antenna, ddi)}") + + print(this_xds) + + summary = this_xds.attrs["summary"] + telescope = get_proper_telescope( + summary["general"]["telescope name"], summary["general"]["antenna name"] + ) + + lm_offsets = this_xds.DIRECTIONAL_COSINES.values + lm_deltas = np.diff(lm_offsets, axis=0) + lm_angle = np.arctan2(lm_deltas[:, 1], lm_deltas[:, 0]) + + lm_exclusion = sigma_clip_deltas(lm_deltas) + print(lm_exclusion.shape) + lm_deltas = lm_deltas[lm_exclusion, :] + lm_angle = lm_angle[lm_exclusion] + print(lm_deltas.shape, lm_angle.shape) + + timesteps = np.arange(lm_angle.shape[0]) + timefracs = np.arange(lm_offsets.shape[0]) + fig, ax = create_figure_and_axes(None, [2, 3]) + scatter_plot(ax[0, 0], timesteps, 'time intervals', lm_angle, 'LM angle [rad]') + scatter_plot(ax[0, 1], timefracs, 'time intervals', lm_offsets[:, 0], 'L [rad]') + scatter_plot(ax[0, 2], timefracs, 'time intervals', lm_offsets[:, 1], 'M [rad]') + scatter_plot(ax[1, 1], timesteps, 'time intervals', lm_deltas[:, 0], 'delta L [rad]') + scatter_plot(ax[1, 2], timesteps, 'time intervals', lm_deltas[:, 1], 'delta M [rad]') + + close_figure(fig, 'LM study', 'lm_simple.png', 300, False) + + # vis = this_xds.VIS.values + +def sigma_clip_deltas(lm_deltas, clip=5): + l_delta_stats = data_statistics(lm_deltas[:, 0]) + m_delta_stats = data_statistics(lm_deltas[:, 1]) + print('L before:\n\t',statistics_to_text(l_delta_stats, num_format='.6f')) + print('M before:\n\t',statistics_to_text(m_delta_stats, num_format='.6f')) + + sigma_exclusion = np.logical_and(np.abs(lm_deltas[:, 0]) < clip * l_delta_stats['rms'], + np.abs(lm_deltas[:, 1]) < clip * m_delta_stats['rms']) + + l_delta_stats = data_statistics(lm_deltas[sigma_exclusion, 0]) + m_delta_stats = data_statistics(lm_deltas[sigma_exclusion, 1]) + print('L after:\n\t',statistics_to_text(l_delta_stats, num_format='.6f')) + print('M after:\n\t',statistics_to_text(m_delta_stats, num_format='.6f')) + return sigma_exclusion + + + + From a56668b7c060eabb53507a348f4896a8dc2631d3 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 16 Dec 2025 11:10:08 -0700 Subject: [PATCH 04/95] Extract holog now preserves scan information. --- src/astrohack/core/extract_holog.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/astrohack/core/extract_holog.py b/src/astrohack/core/extract_holog.py index f7cab11c..34ae6188 100644 --- a/src/astrohack/core/extract_holog.py +++ b/src/astrohack/core/extract_holog.py @@ -109,6 +109,8 @@ def process_extract_holog_chunk(extract_holog_params): weight_map_dict, flagged_mapping_antennas, used_samples_dict, + scan_time_ranges, + unq_scans ) = _extract_holog_chunk_jit( vis_data, weight, @@ -123,7 +125,7 @@ def process_extract_holog_chunk(extract_holog_params): scan_list, ) - del vis_data, weight, ant1, ant2, time_vis_row, flag, flag_row, field_ids + 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)) @@ -172,6 +174,8 @@ def process_extract_holog_chunk(extract_holog_params): time_interval, gen_info, map_ref_dict, + scan_time_ranges, + unq_scans, ) logger.info( @@ -215,7 +219,7 @@ def _get_time_intervals(time_vis_row, scan_list, time_interval): filtered_time_samples.append(time_sample) break time_samples = np.array(filtered_time_samples) - return time_samples + return time_samples, scan_time_ranges, unq_scans @njit(cache=False, nogil=True) @@ -252,7 +256,7 @@ def _extract_holog_chunk_jit( polarization) """ - time_samples = _get_time_intervals(time_vis_row, scan_list, time_interval) + 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 @@ -356,6 +360,8 @@ def _extract_holog_chunk_jit( sum_weight_map_dict, flagged_mapping_antennas, used_samples_dict, + scan_time_ranges, + unq_scans ) @@ -396,6 +402,8 @@ def _create_holog_file( time_interval, gen_info, map_ref_dict, + scan_time_ranges, + unq_scans, ): """Create holog-structured, formatted output file and save to zarr. @@ -471,6 +479,8 @@ def _create_holog_file( 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], From ede4bcb2d04612a71223eda266d4353eeb228329 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 16 Dec 2025 11:10:45 -0700 Subject: [PATCH 05/95] Added version checking constraints to beamcut_tool.py as scan information is needed. --- src/astrohack/beamcut_tool.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/astrohack/beamcut_tool.py b/src/astrohack/beamcut_tool.py index 6629bb6a..a13684cf 100644 --- a/src/astrohack/beamcut_tool.py +++ b/src/astrohack/beamcut_tool.py @@ -4,7 +4,7 @@ from astrohack.core.beamcut import process_beamcut_chunk from astrohack.utils import get_default_file_name -from astrohack.utils.file import overwrite_file +from astrohack.utils.file import overwrite_file, check_if_file_can_be_opened from astrohack.utils.graph import compute_graph from astrohack.utils.data import write_meta_data @@ -20,6 +20,8 @@ def beamcut_tool( overwrite: bool = False, ): + check_if_file_can_be_opened(holog_name, "0.9.4") + if beamcut_name is None: beamcut_name = get_default_file_name( input_file=holog_name, output_type=".beamcut.zarr" From 1d964bc13866e4b9aa2a96d946bdd419420a9b5e Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 16 Dec 2025 11:24:07 -0700 Subject: [PATCH 06/95] Full cut extraction, with: channel averaging, parallel hand extraction, direction determination and distance axis construction. --- src/astrohack/core/beamcut.py | 144 +++++++++++++++++++++++++++++----- 1 file changed, 124 insertions(+), 20 deletions(-) diff --git a/src/astrohack/core/beamcut.py b/src/astrohack/core/beamcut.py index e3d34236..72085edc 100644 --- a/src/astrohack/core/beamcut.py +++ b/src/astrohack/core/beamcut.py @@ -1,9 +1,10 @@ import toolviper.utils.logger as logger import numpy as np +from scipy.stats import linregress from astrohack import get_proper_telescope from astrohack.utils.file import load_holog_file -from astrohack.utils import create_dataset_label, data_statistics, statistics_to_text +from astrohack.utils import create_dataset_label, data_statistics, statistics_to_text, convert_unit from astrohack.visualization import create_figure_and_axes, scatter_plot, close_figure @@ -24,31 +25,43 @@ def process_beamcut_chunk(beamcut_chunk_params): print(this_xds) + scan_time_ranges = this_xds.attrs['scan_time_ranges'] + scan_list = this_xds.attrs['scan_list'] summary = this_xds.attrs["summary"] + telescope = get_proper_telescope( summary["general"]["telescope name"], summary["general"]["antenna name"] ) lm_offsets = this_xds.DIRECTIONAL_COSINES.values - lm_deltas = np.diff(lm_offsets, axis=0) - lm_angle = np.arctan2(lm_deltas[:, 1], lm_deltas[:, 0]) - - lm_exclusion = sigma_clip_deltas(lm_deltas) - print(lm_exclusion.shape) - lm_deltas = lm_deltas[lm_exclusion, :] - lm_angle = lm_angle[lm_exclusion] - print(lm_deltas.shape, lm_angle.shape) - - timesteps = np.arange(lm_angle.shape[0]) - timefracs = np.arange(lm_offsets.shape[0]) - fig, ax = create_figure_and_axes(None, [2, 3]) - scatter_plot(ax[0, 0], timesteps, 'time intervals', lm_angle, 'LM angle [rad]') - scatter_plot(ax[0, 1], timefracs, 'time intervals', lm_offsets[:, 0], 'L [rad]') - scatter_plot(ax[0, 2], timefracs, 'time intervals', lm_offsets[:, 1], 'M [rad]') - scatter_plot(ax[1, 1], timesteps, 'time intervals', lm_deltas[:, 0], 'delta L [rad]') - scatter_plot(ax[1, 2], timesteps, 'time intervals', lm_deltas[:, 1], 'delta M [rad]') - - close_figure(fig, 'LM study', 'lm_simple.png', 300, False) + time_axis = this_xds.time.values + corr_axis = this_xds.pol.values + visibilities = this_xds.VIS.values + weights = this_xds.WEIGHT.values + + cut_list = extract_cuts_from_visibilities(scan_list, scan_time_ranges, time_axis, corr_axis, lm_offsets, + visibilities, weights) + + plot_cuts(cut_list) + # lm_deltas = np.diff(lm_offsets, axis=0) + # lm_angle = np.arctan2(lm_deltas[:, 1], lm_deltas[:, 0]) + # + # lm_exclusion = sigma_clip_deltas(lm_deltas) + # print(lm_exclusion.shape) + # lm_deltas = lm_deltas[lm_exclusion, :] + # lm_angle = lm_angle[lm_exclusion] + # print(lm_deltas.shape, lm_angle.shape) + # + # timesteps = np.arange(lm_angle.shape[0]) + # timefracs = np.arange(lm_offsets.shape[0]) + # fig, ax = create_figure_and_axes(None, [2, 3]) + # scatter_plot(ax[0, 0], timesteps, 'time intervals', lm_angle, 'LM angle [rad]') + # scatter_plot(ax[0, 1], timefracs, 'time intervals', lm_offsets[:, 0], 'L [rad]') + # scatter_plot(ax[0, 2], timefracs, 'time intervals', lm_offsets[:, 1], 'M [rad]') + # scatter_plot(ax[1, 1], timesteps, 'time intervals', lm_deltas[:, 0], 'delta L [rad]') + # scatter_plot(ax[1, 2], timesteps, 'time intervals', lm_deltas[:, 1], 'delta M [rad]') + # + # close_figure(fig, 'LM study', 'lm_simple.png', 300, False) # vis = this_xds.VIS.values @@ -68,5 +81,96 @@ def sigma_clip_deltas(lm_deltas, clip=5): return sigma_exclusion +def time_scan_selection(scan_time_ranges, time_axis): + time_selections = [] + for scan_time_range in scan_time_ranges: + time_selection = np.logical_and(time_axis >= scan_time_range[0], + time_axis < scan_time_range[1]) + time_selections.append(time_selection) + return time_selections + + +def extract_cuts_from_visibilities(scan_list, scan_time_ranges, time_axis, corr_axis, lm_offsets, + visibilities, weights): + cut_list = [] + nchan = visibilities.shape[1] + fchan = 4 + lchan = int(nchan - fchan) + for iscan, scan_number in enumerate(scan_list): + scan_time_range = scan_time_ranges[iscan] + time_selection = np.logical_and(time_axis >= scan_time_range[0], + time_axis < scan_time_range[1]) + time = time_axis[time_selection] + this_lm_offsets = lm_offsets[time_selection, :] + + lm_angle, lm_dist = cut_direction_determination(this_lm_offsets) + hands_dict = get_hand_indexes(corr_axis) + + avg_vis = np.average(visibilities[time_selection, fchan:lchan, :], axis=1, + weights=weights[time_selection, fchan:lchan, :]) + avg_wei = np.average(weights[time_selection, fchan:lchan, :], axis=1) + + cut_dict = { + 'scan_number': scan_number, + 'time': time, + 'lm_offsets': this_lm_offsets, + 'lm_angle': lm_angle, + 'lm_dist': lm_dist, + 'available_corrs': hands_dict['parallel_hands'] + } + for parallel_hand in hands_dict['parallel_hands']: + icorr = hands_dict[parallel_hand] + cut_dict[f'{parallel_hand}_amplitude'] = np.abs(avg_vis[:, icorr]) + cut_dict[f'{parallel_hand}_phase'] = np.angle(avg_vis[:, icorr]) + cut_dict[f'{parallel_hand}_weight'] = np.angle(avg_vis[:, icorr]) + + cut_list.append(cut_dict) + + return cut_list + + +def cut_direction_determination(lm_offsets): + result = linregress(lm_offsets[:, 0], lm_offsets[:, 1]) + lm_angle = np.arctan(result.slope) - np.pi/2 + + lm_dist = np.sqrt(lm_offsets[:, 0]**2 + lm_offsets[:, 1]**2) + imin = np.argmin(lm_dist) + lm_dist[:imin] = -lm_dist[:imin] + return lm_angle, lm_dist + + +def get_hand_indexes(corr_axis): + if 'L' in corr_axis[0] or 'R' in corr_axis[0]: + parallel_hands = ['RR', 'LL'] + else: + parallel_hands = ['XX', 'YY'] + + hands_dict ={ + 'parallel_hands': parallel_hands + } + for icorr, corr in enumerate(corr_axis): + hands_dict[corr] = icorr + return hands_dict + + +def plot_cuts(cut_list): + n_cuts = len(cut_list) + print(n_cuts) + title = 'Scans: ' + fig, ax = create_figure_and_axes(None, [n_cuts, 3]) + for icut, cut in enumerate(cut_list): + sub_title = (f'Cut angle w.r.t. North = ' + f'{cut["lm_angle"] * convert_unit('rad', 'deg', 'trigonometric'):.1f} deg') + scatter_plot(ax[icut, 0], cut['lm_dist'], 'LM distance [rad]', cut['lm_offsets'][:, 0], 'L [rad]', + title=sub_title) + scatter_plot(ax[icut, 1], cut['lm_dist'], 'LM distance [rad]', cut['lm_offsets'][:, 1], 'M [rad]') + scatter_plot(ax[icut, 2], cut['lm_dist'], 'LM distance [rad]', cut['RR_amplitude'][:], + 'RR visibilities [Jy]') + title += f'{cut["scan_number"]}, ' + title = title[:-2] + close_figure(fig, title, 'lm_simple.png', 300, False) + + + From e48c0440f0c50a80aafddfad3266bc04f5d7304e Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 16 Dec 2025 12:29:07 -0700 Subject: [PATCH 07/95] Added full sidelobe fits, up to second sidelobe. --- src/astrohack/core/beamcut.py | 102 +++++++++++++++++++++++++++++++++- 1 file changed, 99 insertions(+), 3 deletions(-) diff --git a/src/astrohack/core/beamcut.py b/src/astrohack/core/beamcut.py index 72085edc..b722ba1c 100644 --- a/src/astrohack/core/beamcut.py +++ b/src/astrohack/core/beamcut.py @@ -1,10 +1,12 @@ import toolviper.utils.logger as logger import numpy as np +from scipy.optimize import curve_fit +from scipy.signal import find_peaks from scipy.stats import linregress from astrohack import get_proper_telescope from astrohack.utils.file import load_holog_file -from astrohack.utils import create_dataset_label, data_statistics, statistics_to_text, convert_unit +from astrohack.utils import create_dataset_label, data_statistics, statistics_to_text, convert_unit, sig_2_fwhm from astrohack.visualization import create_figure_and_axes, scatter_plot, close_figure @@ -42,6 +44,7 @@ def process_beamcut_chunk(beamcut_chunk_params): cut_list = extract_cuts_from_visibilities(scan_list, scan_time_ranges, time_axis, corr_axis, lm_offsets, visibilities, weights) + beamcut_fit(cut_list, telescope, summary) plot_cuts(cut_list) # lm_deltas = np.diff(lm_offsets, axis=0) # lm_angle = np.arctan2(lm_deltas[:, 1], lm_deltas[:, 0]) @@ -122,7 +125,7 @@ def extract_cuts_from_visibilities(scan_list, scan_time_ranges, time_axis, corr_ icorr = hands_dict[parallel_hand] cut_dict[f'{parallel_hand}_amplitude'] = np.abs(avg_vis[:, icorr]) cut_dict[f'{parallel_hand}_phase'] = np.angle(avg_vis[:, icorr]) - cut_dict[f'{parallel_hand}_weight'] = np.angle(avg_vis[:, icorr]) + cut_dict[f'{parallel_hand}_weight'] = np.angle(avg_wei[:, icorr]) cut_list.append(cut_dict) @@ -165,12 +168,105 @@ def plot_cuts(cut_list): title=sub_title) scatter_plot(ax[icut, 1], cut['lm_dist'], 'LM distance [rad]', cut['lm_offsets'][:, 1], 'M [rad]') scatter_plot(ax[icut, 2], cut['lm_dist'], 'LM distance [rad]', cut['RR_amplitude'][:], - 'RR visibilities [Jy]') + 'RR visibilities [Jy]', model=cut['RR_amp_fit'][:]) title += f'{cut["scan_number"]}, ' title = title[:-2] close_figure(fig, title, 'lm_simple.png', 300, False) +def gaussian(x_axis, x_off, amp, fwhm): + sigma = fwhm / sig_2_fwhm + return amp * np.exp(-(x_axis - x_off)**2/(2*sigma**2)) + + +def primary_and_first_side_lobes(x_axis, primary_fwhm, primary_amp, primary_center, first_sidelobe_offset, + left_sidelobe_amp, left_sidelobe_fwhm, right_sidelobe_amp, right_sidelobe_fwhm): + primary = gaussian(x_axis, primary_center, primary_fwhm, primary_amp) + left_sidelobe = gaussian(x_axis, primary_center-first_sidelobe_offset, + left_sidelobe_amp, left_sidelobe_fwhm) + right_sidelobe = gaussian(x_axis, primary_center+first_sidelobe_offset, + right_sidelobe_amp, right_sidelobe_fwhm) + full_beam = primary + left_sidelobe + right_sidelobe + return full_beam + + +def pb_fsl(x_axis, pb_off, pb_amp, pb_fwhm, lfsl_off, lfsl_amp, lfsl_fwhm, rfsl_off, rfsl_amp, rfsl_fwhm): + pb = gaussian(x_axis, pb_off, pb_amp, pb_fwhm) + lfsl = gaussian(x_axis, lfsl_off, lfsl_amp, lfsl_fwhm) + rfsl = gaussian(x_axis, rfsl_off, rfsl_amp, rfsl_fwhm) + return pb + lfsl + rfsl + + +def pb_fsl_ssl(x_axis, pb_off, pb_amp, pb_fwhm, lfsl_off, lfsl_amp, lfsl_fwhm, rfsl_off, rfsl_amp, rfsl_fwhm, + lssl_off, lssl_amp, lssl_fwhm, rssl_off, rssl_amp, rssl_fwhm): + lssl = gaussian(x_axis, lssl_off, lssl_amp, lssl_fwhm) + rssl = gaussian(x_axis, rssl_off, rssl_amp, rssl_fwhm) + pb_fsl_model = pb_fsl(x_axis, pb_off, pb_amp, pb_fwhm, lfsl_off, lfsl_amp, lfsl_fwhm, rfsl_off, rfsl_amp, rfsl_fwhm) + return pb_fsl_model + lssl + rssl + + +def beamcut_fit(cut_list, telescope, summary): + wavelength = summary["spectral"]["rep. wavelength"] + primary_fwhm = 1.2 * wavelength / telescope.diameter + for cut_dict in cut_list: + x_data = cut_dict['lm_dist'] + step = np.median(np.diff(x_data)) + min_dist = 1.5 * primary_fwhm / step + for parallel_hand in cut_dict['available_corrs']: + y_data = cut_dict[f'{parallel_hand}_amplitude'] + # ymax = np.max(y_data) + # p0 = [primary_fwhm, ymax, 0.0, 1*primary_fwhm, 0.2*ymax, primary_fwhm, 0.2*ymax, primary_fwhm] + # results = curve_fit(primary_and_first_side_lobes, x_data, y_data, p0=p0) + # fit_pars = results[0] + # fit = primary_and_first_side_lobes(x_data, *fit_pars) + # p0 = [0.0, ymax, primary_fwhm] + # results = curve_fit(gaussian, x_data, y_data, p0=p0) + # fit_pars = results[0] + # fit = gaussian(x_data, *fit_pars) + peaks, _ = find_peaks(y_data, distance=min_dist) + n_peaks = len(peaks) + if n_peaks == 1: + fit_func = gaussian + i_peak = peaks[0] + p0 = [x_data[i_peak], y_data[i_peak], primary_fwhm] + elif n_peaks == 3: + fit_func = pb_fsl + i_lfsl_peak = peaks[0] + i_pb_peak = peaks[1] + i_rfsl_peak = peaks[2] + p0 = [x_data[i_pb_peak], y_data[i_pb_peak], primary_fwhm, + x_data[i_lfsl_peak], y_data[i_lfsl_peak], primary_fwhm, + x_data[i_rfsl_peak], y_data[i_rfsl_peak], primary_fwhm,] + elif n_peaks == 5: + fit_func = pb_fsl_ssl + i_lssl_peak = peaks[0] + i_lfsl_peak = peaks[1] + i_pb_peak = peaks[2] + i_rfsl_peak = peaks[3] + i_rssl_peak = peaks[4] + + p0 = [x_data[i_pb_peak], y_data[i_pb_peak], primary_fwhm, + x_data[i_lfsl_peak], y_data[i_lfsl_peak], primary_fwhm, + x_data[i_rfsl_peak], y_data[i_rfsl_peak], primary_fwhm, + x_data[i_lssl_peak], y_data[i_lssl_peak], primary_fwhm, + x_data[i_rssl_peak], y_data[i_rssl_peak], primary_fwhm, + ] + else: + raise RuntimeError(f"Don't know how to fit a beam cut with {n_peaks} peaks") + + print(min_dist, primary_fwhm, n_peaks) + results = curve_fit(fit_func, x_data, y_data, p0=p0) + fit_pars = results[0] + fit = fit_func(x_data, *fit_pars) + + print(fit_pars) + cut_dict[f'{parallel_hand}_amp_fit_pars'] = fit_pars + cut_dict[f'{parallel_hand}_amp_fit'] = fit + + return cut_list + + + From 689c6fd1b81d4d15ba4b092a1aa969ee92da83b1 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 16 Dec 2025 15:28:42 -0700 Subject: [PATCH 08/95] arbitrary number of gaussians and mostly complete plotting. --- src/astrohack/core/beamcut.py | 133 +++++++++++++++++----------------- 1 file changed, 67 insertions(+), 66 deletions(-) diff --git a/src/astrohack/core/beamcut.py b/src/astrohack/core/beamcut.py index b722ba1c..3dc0f6ab 100644 --- a/src/astrohack/core/beamcut.py +++ b/src/astrohack/core/beamcut.py @@ -18,8 +18,8 @@ def process_beamcut_chunk(beamcut_chunk_params): beamcut_chunk_params["holog_name"], dask_load=False, load_pnt_dict=False, - ant_id=beamcut_chunk_params["this_ant"], - ddi_id=beamcut_chunk_params["this_ddi"], + ant_id=antenna, + ddi_id=ddi, ) # This assumes that there will be no more than one mapping this_xds = ant_data_dict[ddi]['map_0'] @@ -45,7 +45,7 @@ def process_beamcut_chunk(beamcut_chunk_params): visibilities, weights) beamcut_fit(cut_list, telescope, summary) - plot_cuts(cut_list) + plot_cuts(cut_list, 'amin', 'GHz', antenna, ddi, summary) # lm_deltas = np.diff(lm_offsets, axis=0) # lm_angle = np.arctan2(lm_deltas[:, 1], lm_deltas[:, 0]) # @@ -121,12 +121,17 @@ def extract_cuts_from_visibilities(scan_list, scan_time_ranges, time_axis, corr_ 'lm_dist': lm_dist, 'available_corrs': hands_dict['parallel_hands'] } + all_corr_ymax = 1e-34 for parallel_hand in hands_dict['parallel_hands']: icorr = hands_dict[parallel_hand] - cut_dict[f'{parallel_hand}_amplitude'] = np.abs(avg_vis[:, icorr]) + amp = np.abs(avg_vis[:, icorr]) + maxamp = np.max(amp) + if maxamp > all_corr_ymax: + all_corr_ymax = maxamp + cut_dict[f'{parallel_hand}_amplitude'] = amp cut_dict[f'{parallel_hand}_phase'] = np.angle(avg_vis[:, icorr]) cut_dict[f'{parallel_hand}_weight'] = np.angle(avg_wei[:, icorr]) - + cut_dict['max_amp'] = all_corr_ymax cut_list.append(cut_dict) return cut_list @@ -156,22 +161,40 @@ def get_hand_indexes(corr_axis): return hands_dict -def plot_cuts(cut_list): +def plot_cuts(cut_list, lm_unit, freq_unit, antenna, ddi, summary, y_scale=None, dpi=300, display=False): n_cuts = len(cut_list) - print(n_cuts) - title = 'Scans: ' - fig, ax = create_figure_and_axes(None, [n_cuts, 3]) - for icut, cut in enumerate(cut_list): - sub_title = (f'Cut angle w.r.t. North = ' - f'{cut["lm_angle"] * convert_unit('rad', 'deg', 'trigonometric'):.1f} deg') - scatter_plot(ax[icut, 0], cut['lm_dist'], 'LM distance [rad]', cut['lm_offsets'][:, 0], 'L [rad]', - title=sub_title) - scatter_plot(ax[icut, 1], cut['lm_dist'], 'LM distance [rad]', cut['lm_offsets'][:, 1], 'M [rad]') - scatter_plot(ax[icut, 2], cut['lm_dist'], 'LM distance [rad]', cut['RR_amplitude'][:], - 'RR visibilities [Jy]', model=cut['RR_amp_fit'][:]) - title += f'{cut["scan_number"]}, ' - title = title[:-2] - close_figure(fig, title, 'lm_simple.png', 300, False) + freq = summary['spectral']['rep. frequency'] + mean_az = + mean_el = "az el info" "mean" + title = f'Beam cut for {create_dataset_label(antenna, ddi)}, $\nu$ = {}' + lm_fac = convert_unit('rad', lm_unit, 'trigonometric') + fig, ax = create_figure_and_axes(None, [n_cuts, 2]) + for icut, cut_dict in enumerate(cut_list): + sub_title = (f'Scan = {cut_dict["scan_number"]}, ' + r'$\theta$(N) = ' + + f'{cut_dict["lm_angle"] * convert_unit('rad', 'deg', 'trigonometric'):.1f} deg') + max_amp = cut_dict['max_amp'] + for i_corr, parallel_hand in enumerate(cut_dict['available_corrs']): + x_data = lm_fac * cut_dict['lm_dist'] + y_data = cut_dict[f'{parallel_hand}_amplitude'] + fit_data = cut_dict[f'{parallel_hand}_amp_fit'] + this_ax = ax[icut, i_corr] + scatter_plot(this_ax, x_data, f'LM distance [{lm_unit}]', + y_data, f'{parallel_hand} Amplitude [ ]', + model=fit_data, model_marker='', + title=sub_title+f', corr = {parallel_hand}', + data_marker='+', residuals_marker='.', model_linestyle='-', + data_label=f'{parallel_hand} data', model_label=f'{parallel_hand} fit',) + xrange = x_data[-1] - x_data[0] + for i_peak in range(cut_dict[f'{parallel_hand}_n_peaks']): + x_cen = lm_fac*cut_dict[f'{parallel_hand}_amp_fit_pars'][i_peak*3] + this_ax.axvline(x=x_cen, linestyle='--', color='black') + this_ax.text(x_cen+0.01*xrange, 0.5, i_peak+1, ha='left', va='center') + if y_scale is None: + this_ax.set_ylim((-0.025*max_amp, 1.05*max_amp)) + else: + this_ax.set_ylim(y_scale) + filename = f'beamcut_{antenna}_{ddi}.png' + close_figure(fig, title, filename, dpi, display) def gaussian(x_axis, x_off, amp, fwhm): @@ -205,63 +228,41 @@ def pb_fsl_ssl(x_axis, pb_off, pb_amp, pb_fwhm, lfsl_off, lfsl_amp, lfsl_fwhm, r return pb_fsl_model + lssl + rssl +def build_multi_gaussian_initial_guesses(x_data, y_data, pb_fwhm, min_dist_fraction=1.3): + p0 = [] + step = float(np.median(np.diff(x_data))) + min_dist = min_dist_fraction * pb_fwhm / step + peaks, _ = find_peaks(y_data, distance=min_dist) + for ipeak in peaks: + p0.extend([x_data[ipeak], y_data[ipeak], pb_fwhm]) + return p0, len(peaks) + +def multi_gaussian(xdata, *args): + nargs = len(args) + if nargs%3 != 0: + raise ValueError('Number of arguments should be multiple of 3') + y_values = np.zeros_like(xdata) + for iarg in range(0, nargs, 3): + y_values += gaussian(xdata, args[iarg], args[iarg+1], args[iarg+2]) + return y_values + + + def beamcut_fit(cut_list, telescope, summary): wavelength = summary["spectral"]["rep. wavelength"] primary_fwhm = 1.2 * wavelength / telescope.diameter for cut_dict in cut_list: x_data = cut_dict['lm_dist'] - step = np.median(np.diff(x_data)) - min_dist = 1.5 * primary_fwhm / step for parallel_hand in cut_dict['available_corrs']: y_data = cut_dict[f'{parallel_hand}_amplitude'] - # ymax = np.max(y_data) - # p0 = [primary_fwhm, ymax, 0.0, 1*primary_fwhm, 0.2*ymax, primary_fwhm, 0.2*ymax, primary_fwhm] - # results = curve_fit(primary_and_first_side_lobes, x_data, y_data, p0=p0) - # fit_pars = results[0] - # fit = primary_and_first_side_lobes(x_data, *fit_pars) - # p0 = [0.0, ymax, primary_fwhm] - # results = curve_fit(gaussian, x_data, y_data, p0=p0) - # fit_pars = results[0] - # fit = gaussian(x_data, *fit_pars) - peaks, _ = find_peaks(y_data, distance=min_dist) - n_peaks = len(peaks) - if n_peaks == 1: - fit_func = gaussian - i_peak = peaks[0] - p0 = [x_data[i_peak], y_data[i_peak], primary_fwhm] - elif n_peaks == 3: - fit_func = pb_fsl - i_lfsl_peak = peaks[0] - i_pb_peak = peaks[1] - i_rfsl_peak = peaks[2] - p0 = [x_data[i_pb_peak], y_data[i_pb_peak], primary_fwhm, - x_data[i_lfsl_peak], y_data[i_lfsl_peak], primary_fwhm, - x_data[i_rfsl_peak], y_data[i_rfsl_peak], primary_fwhm,] - elif n_peaks == 5: - fit_func = pb_fsl_ssl - i_lssl_peak = peaks[0] - i_lfsl_peak = peaks[1] - i_pb_peak = peaks[2] - i_rfsl_peak = peaks[3] - i_rssl_peak = peaks[4] - - p0 = [x_data[i_pb_peak], y_data[i_pb_peak], primary_fwhm, - x_data[i_lfsl_peak], y_data[i_lfsl_peak], primary_fwhm, - x_data[i_rfsl_peak], y_data[i_rfsl_peak], primary_fwhm, - x_data[i_lssl_peak], y_data[i_lssl_peak], primary_fwhm, - x_data[i_rssl_peak], y_data[i_rssl_peak], primary_fwhm, - ] - else: - raise RuntimeError(f"Don't know how to fit a beam cut with {n_peaks} peaks") - - print(min_dist, primary_fwhm, n_peaks) - results = curve_fit(fit_func, x_data, y_data, p0=p0) + p0, n_peaks = build_multi_gaussian_initial_guesses(x_data, y_data, primary_fwhm) + results = curve_fit(multi_gaussian, x_data, y_data, p0=p0) fit_pars = results[0] - fit = fit_func(x_data, *fit_pars) + fit = multi_gaussian(x_data, *fit_pars) - print(fit_pars) cut_dict[f'{parallel_hand}_amp_fit_pars'] = fit_pars cut_dict[f'{parallel_hand}_amp_fit'] = fit + cut_dict[f'{parallel_hand}_n_peaks'] = n_peaks return cut_list From 453918230ef96efaf79326779bc7a345c5de3aac Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Wed, 17 Dec 2025 09:46:57 -0700 Subject: [PATCH 09/95] Fixed issue where xlabel on the bottom disapeared when plotting residuals. --- src/astrohack/visualization/plot_tools.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/astrohack/visualization/plot_tools.py b/src/astrohack/visualization/plot_tools.py index d002e20c..9a2abbb7 100644 --- a/src/astrohack/visualization/plot_tools.py +++ b/src/astrohack/visualization/plot_tools.py @@ -366,6 +366,7 @@ def scatter_plot( ax_res.axhline(0, color=hv_color, ls=hv_linestyle) ax_res.set_ylabel("Residuals") + ax_res.set_xlabel(xlabel) if title is not None: ax.set_title(title) From d729e647318ee59062ed0ec97c315ac9818b8e1e Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Wed, 17 Dec 2025 11:10:18 -0700 Subject: [PATCH 10/95] Cosmetics --- src/astrohack/core/extract_pointing.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/astrohack/core/extract_pointing.py b/src/astrohack/core/extract_pointing.py index c503f88f..1d8a623d 100644 --- a/src/astrohack/core/extract_pointing.py +++ b/src/astrohack/core/extract_pointing.py @@ -189,9 +189,8 @@ 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]) + 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") From 2617bf5c924f465936a4410fe098d374e5d2c6d9 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Wed, 17 Dec 2025 11:11:34 -0700 Subject: [PATCH 11/95] Improvements to the beamcut plotting in amplitudes --- src/astrohack/core/beamcut.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/astrohack/core/beamcut.py b/src/astrohack/core/beamcut.py index 3dc0f6ab..0268bd14 100644 --- a/src/astrohack/core/beamcut.py +++ b/src/astrohack/core/beamcut.py @@ -6,7 +6,8 @@ from astrohack import get_proper_telescope from astrohack.utils.file import load_holog_file -from astrohack.utils import create_dataset_label, data_statistics, statistics_to_text, convert_unit, sig_2_fwhm +from astrohack.utils import create_dataset_label, data_statistics, statistics_to_text, convert_unit, sig_2_fwhm, \ + format_frequency, format_value_unit from astrohack.visualization import create_figure_and_axes, scatter_plot, close_figure @@ -45,7 +46,7 @@ def process_beamcut_chunk(beamcut_chunk_params): visibilities, weights) beamcut_fit(cut_list, telescope, summary) - plot_cuts(cut_list, 'amin', 'GHz', antenna, ddi, summary) + plot_cuts_in_amplitude(cut_list, 'amin', 'deg', antenna, ddi, summary) # lm_deltas = np.diff(lm_offsets, axis=0) # lm_angle = np.arctan2(lm_deltas[:, 1], lm_deltas[:, 0]) # @@ -161,18 +162,21 @@ def get_hand_indexes(corr_axis): return hands_dict -def plot_cuts(cut_list, lm_unit, freq_unit, antenna, ddi, summary, y_scale=None, dpi=300, display=False): +def plot_cuts_in_amplitude(cut_list, lm_unit, azel_unit, antenna, ddi, summary, y_scale=None, dpi=300, display=False): n_cuts = len(cut_list) - freq = summary['spectral']['rep. frequency'] - mean_az = - mean_el = "az el info" "mean" - title = f'Beam cut for {create_dataset_label(antenna, ddi)}, $\nu$ = {}' + freq_str = format_frequency(summary['spectral']['rep. frequency'], decimal_places=4) + raw_azel = np.array(summary['general']["az el info"]["mean"]) + mean_azel = convert_unit('rad', azel_unit, 'trigonometric') * raw_azel + title = f'Beam cut for {create_dataset_label(antenna, ddi, separator=',')}, '+r'$\nu$ = ' + f'{freq_str}, ' + title += f'Az ~ {format_value_unit(mean_azel[0], 'deg', decimal_places=0)}, ' + title += f'El ~ {format_value_unit(mean_azel[1], 'deg', decimal_places=0)}' lm_fac = convert_unit('rad', lm_unit, 'trigonometric') fig, ax = create_figure_and_axes(None, [n_cuts, 2]) for icut, cut_dict in enumerate(cut_list): sub_title = (f'Scan = {cut_dict["scan_number"]}, ' + r'$\theta$(N) = ' + f'{cut_dict["lm_angle"] * convert_unit('rad', 'deg', 'trigonometric'):.1f} deg') max_amp = cut_dict['max_amp'] + y_off = 0.05*max_amp for i_corr, parallel_hand in enumerate(cut_dict['available_corrs']): x_data = lm_fac * cut_dict['lm_dist'] y_data = cut_dict[f'{parallel_hand}_amplitude'] @@ -184,13 +188,12 @@ def plot_cuts(cut_list, lm_unit, freq_unit, antenna, ddi, summary, y_scale=None, title=sub_title+f', corr = {parallel_hand}', data_marker='+', residuals_marker='.', model_linestyle='-', data_label=f'{parallel_hand} data', model_label=f'{parallel_hand} fit',) - xrange = x_data[-1] - x_data[0] for i_peak in range(cut_dict[f'{parallel_hand}_n_peaks']): x_cen = lm_fac*cut_dict[f'{parallel_hand}_amp_fit_pars'][i_peak*3] - this_ax.axvline(x=x_cen, linestyle='--', color='black') - this_ax.text(x_cen+0.01*xrange, 0.5, i_peak+1, ha='left', va='center') + y_amp = cut_dict[f'{parallel_hand}_amp_fit_pars'][i_peak*3+1] + this_ax.text(x_cen, y_amp+y_off/2, i_peak+1, ha='center', va='bottom') if y_scale is None: - this_ax.set_ylim((-0.025*max_amp, 1.05*max_amp)) + this_ax.set_ylim((-y_off, max_amp+3*y_off)) else: this_ax.set_ylim(y_scale) filename = f'beamcut_{antenna}_{ddi}.png' From 4ec82e228b330ac57a2bcb51bc0b637b7d4c6d23 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Wed, 17 Dec 2025 17:11:53 -0700 Subject: [PATCH 12/95] Further improvements on beam amplitude plots --- src/astrohack/core/beamcut.py | 80 ++++++++++++++++++++++++++++++----- 1 file changed, 69 insertions(+), 11 deletions(-) diff --git a/src/astrohack/core/beamcut.py b/src/astrohack/core/beamcut.py index 0268bd14..76521c6e 100644 --- a/src/astrohack/core/beamcut.py +++ b/src/astrohack/core/beamcut.py @@ -3,6 +3,7 @@ from scipy.optimize import curve_fit from scipy.signal import find_peaks from scipy.stats import linregress +import astropy from astrohack import get_proper_telescope from astrohack.utils.file import load_holog_file @@ -44,8 +45,8 @@ def process_beamcut_chunk(beamcut_chunk_params): cut_list = extract_cuts_from_visibilities(scan_list, scan_time_ranges, time_axis, corr_axis, lm_offsets, visibilities, weights) - beamcut_fit(cut_list, telescope, summary) + plot_cuts_in_amplitude(cut_list, 'amin', 'deg', antenna, ddi, summary) # lm_deltas = np.diff(lm_offsets, axis=0) # lm_angle = np.arctan2(lm_deltas[:, 1], lm_deltas[:, 0]) @@ -107,20 +108,26 @@ def extract_cuts_from_visibilities(scan_list, scan_time_ranges, time_axis, corr_ time = time_axis[time_selection] this_lm_offsets = lm_offsets[time_selection, :] - lm_angle, lm_dist = cut_direction_determination(this_lm_offsets) + lm_angle, lm_dist, direction = cut_direction_determination(this_lm_offsets) hands_dict = get_hand_indexes(corr_axis) avg_vis = np.average(visibilities[time_selection, fchan:lchan, :], axis=1, weights=weights[time_selection, fchan:lchan, :]) avg_wei = np.average(weights[time_selection, fchan:lchan, :], axis=1) + avg_time = np.average(time)*convert_unit('sec', 'day', 'time') + timestr = astropy.time.Time(avg_time, format='mjd').to_value('iso', subfmt='date_hm') + + cut_dict = { 'scan_number': scan_number, 'time': time, 'lm_offsets': this_lm_offsets, 'lm_angle': lm_angle, 'lm_dist': lm_dist, - 'available_corrs': hands_dict['parallel_hands'] + 'available_corrs': hands_dict['parallel_hands'], + 'direction': direction, + 'time_string': timestr } all_corr_ymax = 1e-34 for parallel_hand in hands_dict['parallel_hands']: @@ -139,13 +146,45 @@ def extract_cuts_from_visibilities(scan_list, scan_time_ranges, time_axis, corr_ def cut_direction_determination(lm_offsets): - result = linregress(lm_offsets[:, 0], lm_offsets[:, 1]) - lm_angle = np.arctan(result.slope) - np.pi/2 + + dx = lm_offsets[-1, 0] - lm_offsets[0, 0] + dy = lm_offsets[-1, 1] - lm_offsets[0, 1] + + if np.isclose(dx, dy, rtol=1e-1): # X case + result = linregress(lm_offsets[:, 0], lm_offsets[:, 1]) + lm_angle = np.arctan(result.slope) + np.pi/2 + direction = 'mixed cut (' + if dy < 0 and dx < 0: + direction += 'NE -> SW' + elif dy < 0 < dx: + direction += 'NW -> SE' + elif dy > 0 > dx: + direction += 'SE -> NW' + else: + direction += 'SW -> NE' + elif np.abs(dy) > np.abs(dx): # Elevation case + result = linregress(lm_offsets[:, 1], lm_offsets[:, 0]) + lm_angle = np.arctan(result.slope) + direction = 'El. cut (' + if dy < 0: + direction += 'N -> S' + else: + direction += 'S -> N' + else: # Azimuth case + result = linregress(lm_offsets[:, 0], lm_offsets[:, 1]) + lm_angle = np.arctan(result.slope) + np.pi/2 + direction = 'Az. cut (' + if dx < 0: + direction += 'E -> W' + else: + direction += 'W -> E' + + direction += ')' lm_dist = np.sqrt(lm_offsets[:, 0]**2 + lm_offsets[:, 1]**2) imin = np.argmin(lm_dist) lm_dist[:imin] = -lm_dist[:imin] - return lm_angle, lm_dist + return lm_angle, lm_dist, direction def get_hand_indexes(corr_axis): @@ -164,7 +203,7 @@ def get_hand_indexes(corr_axis): def plot_cuts_in_amplitude(cut_list, lm_unit, azel_unit, antenna, ddi, summary, y_scale=None, dpi=300, display=False): n_cuts = len(cut_list) - freq_str = format_frequency(summary['spectral']['rep. frequency'], decimal_places=4) + freq_str = format_frequency(summary['spectral']['rep. frequency'], decimal_places=3) raw_azel = np.array(summary['general']["az el info"]["mean"]) mean_azel = convert_unit('rad', azel_unit, 'trigonometric') * raw_azel title = f'Beam cut for {create_dataset_label(antenna, ddi, separator=',')}, '+r'$\nu$ = ' + f'{freq_str}, ' @@ -173,8 +212,7 @@ def plot_cuts_in_amplitude(cut_list, lm_unit, azel_unit, antenna, ddi, summary, lm_fac = convert_unit('rad', lm_unit, 'trigonometric') fig, ax = create_figure_and_axes(None, [n_cuts, 2]) for icut, cut_dict in enumerate(cut_list): - sub_title = (f'Scan = {cut_dict["scan_number"]}, ' + r'$\theta$(N) = ' + - f'{cut_dict["lm_angle"] * convert_unit('rad', 'deg', 'trigonometric'):.1f} deg') + sub_title = f'{cut_dict["time_string"]}, {cut_dict["direction"]}' max_amp = cut_dict['max_amp'] y_off = 0.05*max_amp for i_corr, parallel_hand in enumerate(cut_dict['available_corrs']): @@ -185,7 +223,7 @@ def plot_cuts_in_amplitude(cut_list, lm_unit, azel_unit, antenna, ddi, summary, scatter_plot(this_ax, x_data, f'LM distance [{lm_unit}]', y_data, f'{parallel_hand} Amplitude [ ]', model=fit_data, model_marker='', - title=sub_title+f', corr = {parallel_hand}', + title=sub_title, data_marker='+', residuals_marker='.', model_linestyle='-', data_label=f'{parallel_hand} data', model_label=f'{parallel_hand} fit',) for i_peak in range(cut_dict[f'{parallel_hand}_n_peaks']): @@ -196,6 +234,15 @@ def plot_cuts_in_amplitude(cut_list, lm_unit, azel_unit, antenna, ddi, summary, this_ax.set_ylim((-y_off, max_amp+3*y_off)) else: this_ax.set_ylim(y_scale) + + pars_str = f'PB off. = {cut_dict[f'{parallel_hand}_pb_center']*lm_fac:.2f} [{lm_unit}]\n' + pars_str += f'PB FWHM = {cut_dict[f'{parallel_hand}_pb_fwhm']*lm_fac:.2f} [{lm_unit}]\n' + pars_str += (f'FSL ratio = ' + f'{10*np.log10(cut_dict[f'{parallel_hand}_first_side_lobe_ratio']):.1f} dB') + bounds_box = dict(boxstyle='square', facecolor='white', alpha=0.5) + this_ax.text(0.05, 0.95, pars_str, transform=this_ax.transAxes, fontsize=7, verticalalignment='top', + bbox=bounds_box) + filename = f'beamcut_{antenna}_{ddi}.png' close_figure(fig, title, filename, dpi, display) @@ -250,7 +297,6 @@ def multi_gaussian(xdata, *args): return y_values - def beamcut_fit(cut_list, telescope, summary): wavelength = summary["spectral"]["rep. wavelength"] primary_fwhm = 1.2 * wavelength / telescope.diameter @@ -267,6 +313,18 @@ def beamcut_fit(cut_list, telescope, summary): cut_dict[f'{parallel_hand}_amp_fit'] = fit cut_dict[f'{parallel_hand}_n_peaks'] = n_peaks + centers = fit_pars[0::3] + amps = fit_pars[1::3] + fwhms = fit_pars[2::3] + + i_pb = np.argmax(amps) + cut_dict[f'{parallel_hand}_pb_fwhm'] = fwhms[i_pb] + cut_dict[f'{parallel_hand}_pb_center'] = centers[i_pb] + + left_first_sl_amp = amps[i_pb-1] + right_first_sl_amp = amps[i_pb+1] + cut_dict[f'{parallel_hand}_first_side_lobe_ratio'] = left_first_sl_amp / right_first_sl_amp + return cut_list From c3804f74146074d8df597546a2d85a2a3098f866 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Thu, 18 Dec 2025 15:19:25 -0700 Subject: [PATCH 13/95] Added simple utility to set ylims. --- src/astrohack/visualization/plot_tools.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/astrohack/visualization/plot_tools.py b/src/astrohack/visualization/plot_tools.py index 9a2abbb7..d913afb1 100644 --- a/src/astrohack/visualization/plot_tools.py +++ b/src/astrohack/visualization/plot_tools.py @@ -428,3 +428,11 @@ def simple_imshow_map_plot( ax.set_xlabel(x_label) ax.set_ylabel(y_label) return im + + +def set_y_axis_lims_from_default(ax, user_y_scale, prog_defaults): + if user_y_scale is None: + ax.set_ylim(prog_defaults) + else: + ax.set_ylim(user_y_scale) + From a1417c360a32e285630a6f281c8d206fd3996d46 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Thu, 18 Dec 2025 16:10:48 -0700 Subject: [PATCH 14/95] progress towards amplitude plot. --- src/astrohack/core/beamcut.py | 249 ++++++++++++++++++---------------- 1 file changed, 129 insertions(+), 120 deletions(-) diff --git a/src/astrohack/core/beamcut.py b/src/astrohack/core/beamcut.py index 76521c6e..b2c35e51 100644 --- a/src/astrohack/core/beamcut.py +++ b/src/astrohack/core/beamcut.py @@ -8,8 +8,10 @@ from astrohack import get_proper_telescope from astrohack.utils.file import load_holog_file from astrohack.utils import create_dataset_label, data_statistics, statistics_to_text, convert_unit, sig_2_fwhm, \ - format_frequency, format_value_unit + format_frequency, format_value_unit, to_db, fontsize from astrohack.visualization import create_figure_and_axes, scatter_plot, close_figure +from astrohack.visualization.plot_tools import set_y_axis_lims_from_default +import matplotlib.ticker as mticker def process_beamcut_chunk(beamcut_chunk_params): @@ -47,44 +49,13 @@ def process_beamcut_chunk(beamcut_chunk_params): visibilities, weights) beamcut_fit(cut_list, telescope, summary) - plot_cuts_in_amplitude(cut_list, 'amin', 'deg', antenna, ddi, summary) - # lm_deltas = np.diff(lm_offsets, axis=0) - # lm_angle = np.arctan2(lm_deltas[:, 1], lm_deltas[:, 0]) - # - # lm_exclusion = sigma_clip_deltas(lm_deltas) - # print(lm_exclusion.shape) - # lm_deltas = lm_deltas[lm_exclusion, :] - # lm_angle = lm_angle[lm_exclusion] - # print(lm_deltas.shape, lm_angle.shape) - # - # timesteps = np.arange(lm_angle.shape[0]) - # timefracs = np.arange(lm_offsets.shape[0]) - # fig, ax = create_figure_and_axes(None, [2, 3]) - # scatter_plot(ax[0, 0], timesteps, 'time intervals', lm_angle, 'LM angle [rad]') - # scatter_plot(ax[0, 1], timefracs, 'time intervals', lm_offsets[:, 0], 'L [rad]') - # scatter_plot(ax[0, 2], timefracs, 'time intervals', lm_offsets[:, 1], 'M [rad]') - # scatter_plot(ax[1, 1], timesteps, 'time intervals', lm_deltas[:, 0], 'delta L [rad]') - # scatter_plot(ax[1, 2], timesteps, 'time intervals', lm_deltas[:, 1], 'delta M [rad]') - # - # close_figure(fig, 'LM study', 'lm_simple.png', 300, False) - - # vis = this_xds.VIS.values - -def sigma_clip_deltas(lm_deltas, clip=5): - l_delta_stats = data_statistics(lm_deltas[:, 0]) - m_delta_stats = data_statistics(lm_deltas[:, 1]) - print('L before:\n\t',statistics_to_text(l_delta_stats, num_format='.6f')) - print('M before:\n\t',statistics_to_text(m_delta_stats, num_format='.6f')) - - sigma_exclusion = np.logical_and(np.abs(lm_deltas[:, 0]) < clip * l_delta_stats['rms'], - np.abs(lm_deltas[:, 1]) < clip * m_delta_stats['rms']) - - l_delta_stats = data_statistics(lm_deltas[sigma_exclusion, 0]) - m_delta_stats = data_statistics(lm_deltas[sigma_exclusion, 1]) - print('L after:\n\t',statistics_to_text(l_delta_stats, num_format='.6f')) - print('M after:\n\t',statistics_to_text(m_delta_stats, num_format='.6f')) - return sigma_exclusion + beamcut_chunk_params['lm_unit'] = 'amin' + beamcut_chunk_params['azel_unit'] = 'deg' + beamcut_chunk_params['dpi'] = 300 + beamcut_chunk_params['display'] = False + beamcut_chunk_params['y_scale'] = None + plot_cuts_in_amplitude(cut_list, summary, beamcut_chunk_params) def time_scan_selection(scan_time_ranges, time_axis): time_selections = [] @@ -108,8 +79,8 @@ def extract_cuts_from_visibilities(scan_list, scan_time_ranges, time_axis, corr_ time = time_axis[time_selection] this_lm_offsets = lm_offsets[time_selection, :] - lm_angle, lm_dist, direction = cut_direction_determination(this_lm_offsets) - hands_dict = get_hand_indexes(corr_axis) + lm_angle, lm_dist, direction, xlabel = cut_direction_determination_and_label_creation(this_lm_offsets) + hands_dict = get_parallel_hand_indexes(corr_axis) avg_vis = np.average(visibilities[time_selection, fchan:lchan, :], axis=1, weights=weights[time_selection, fchan:lchan, :]) @@ -118,7 +89,6 @@ def extract_cuts_from_visibilities(scan_list, scan_time_ranges, time_axis, corr_ avg_time = np.average(time)*convert_unit('sec', 'day', 'time') timestr = astropy.time.Time(avg_time, format='mjd').to_value('iso', subfmt='date_hm') - cut_dict = { 'scan_number': scan_number, 'time': time, @@ -127,6 +97,7 @@ def extract_cuts_from_visibilities(scan_list, scan_time_ranges, time_axis, corr_ 'lm_dist': lm_dist, 'available_corrs': hands_dict['parallel_hands'], 'direction': direction, + 'xlabel': xlabel, 'time_string': timestr } all_corr_ymax = 1e-34 @@ -145,49 +116,59 @@ def extract_cuts_from_visibilities(scan_list, scan_time_ranges, time_axis, corr_ return cut_list -def cut_direction_determination(lm_offsets): - +def cut_direction_determination_and_label_creation(lm_offsets, angle_unit='deg'): dx = lm_offsets[-1, 0] - lm_offsets[0, 0] dy = lm_offsets[-1, 1] - lm_offsets[0, 1] + lm_dist = np.sqrt(lm_offsets[:, 0] ** 2 + lm_offsets[:, 1] ** 2) + imin_lm = np.argmin(lm_dist) + lm_dist[:imin_lm] = -lm_dist[:imin_lm] - if np.isclose(dx, dy, rtol=1e-1): # X case + if np.isclose(dx, dy, rtol=3e-1): # X case result = linregress(lm_offsets[:, 0], lm_offsets[:, 1]) lm_angle = np.arctan(result.slope) + np.pi/2 - direction = 'mixed cut (' + direction = 'mixed cut(' if dy < 0 and dx < 0: - direction += 'NE -> SW' - elif dy < 0 < dx: direction += 'NW -> SE' + + elif dy < 0 < dx: + direction += 'NE -> SW' + elif dy > 0 > dx: - direction += 'SE -> NW' - else: direction += 'SW -> NE' + + else: + direction += 'SE -> NW' + + direction += (r', $\theta$ = ' + + f'{format_value_unit(convert_unit('rad', angle_unit, 'trigonometric')*lm_angle, angle_unit)}') + xlabel = 'Mixed offset' elif np.abs(dy) > np.abs(dx): # Elevation case result = linregress(lm_offsets[:, 1], lm_offsets[:, 0]) lm_angle = np.arctan(result.slope) direction = 'El. cut (' if dy < 0: direction += 'N -> S' + lm_dist *= -1 # Flip as sense is negative else: direction += 'S -> N' + xlabel = 'Elevation offset' else: # Azimuth case result = linregress(lm_offsets[:, 0], lm_offsets[:, 1]) lm_angle = np.arctan(result.slope) + np.pi/2 direction = 'Az. cut (' - if dx < 0: + if dx > 0: direction += 'E -> W' else: direction += 'W -> E' + lm_dist *= -1 # Flip as sense is negative + xlabel = 'Azimuth offset' direction += ')' - lm_dist = np.sqrt(lm_offsets[:, 0]**2 + lm_offsets[:, 1]**2) - imin = np.argmin(lm_dist) - lm_dist[:imin] = -lm_dist[:imin] - return lm_angle, lm_dist, direction + return lm_angle, lm_dist, direction, xlabel -def get_hand_indexes(corr_axis): +def get_parallel_hand_indexes(corr_axis): if 'L' in corr_axis[0] or 'R' in corr_axis[0]: parallel_hands = ['RR', 'LL'] else: @@ -201,50 +182,103 @@ def get_hand_indexes(corr_axis): return hands_dict -def plot_cuts_in_amplitude(cut_list, lm_unit, azel_unit, antenna, ddi, summary, y_scale=None, dpi=300, display=False): +def add_secondary_beam_hpbw_x_axis_to_plot(pb_fwhm, ax): + with np.errstate(divide='ignore'): + to_fwhm = lambda x: x/pb_fwhm + from_fwhm = lambda xb: pb_fwhm/xb + print('to fwhm', to_fwhm(pb_fwhm)) + print('from fwhm', from_fwhm(1.0)) + sec_x_axis = ax.secondary_xaxis('top', functions=(to_fwhm, from_fwhm)) + sec_x_axis.set_xlabel('Offset in Primary Beam HPBWs\n') + old_xticks = sec_x_axis.get_xticks() + for itk in np.arange(-4, 5, 1): + ax.axvline(itk*pb_fwhm, color='k', linestyle='--', linewidth=0.5) + ax.text(itk*pb_fwhm, 1.4, f'{itk+1}', ) + print(old_xticks, to_fwhm(old_xticks), pb_fwhm) + new_ticks = np.arange(0.05, 0.2, 0.05) + new_ticks = [0, 0.2, 0.4, 0.6, 0.8, 1.0] + print(new_ticks) + sec_x_axis.set_xticks([]) + + +def add_lobe_identification_to_plot(ax, centers, peaks, y_off, attenunation_plot=False): + if attenunation_plot: + plot_peaks = to_db(peaks/np.max(peaks)) # maximum of peaks is always the PB + else: + plot_peaks = peaks + + for i_peak, peak in enumerate(plot_peaks): + ax.text(centers[i_peak], peak+y_off/2, i_peak+1, ha='center', va='bottom') + + +def make_parallel_hand_subplot_title(direction, time_string): + return f'{direction}, {time_string} UTC' + + +def plot_single_cut_parallel_corrs(cut_dict, axes, par_dict): + # Init + sub_title = make_parallel_hand_subplot_title(cut_dict["direction"], cut_dict["time_string"]) + max_amp = cut_dict['max_amp'] + y_off = 0.05*max_amp + lm_unit = par_dict['lm_unit'] + lm_fac = convert_unit('rad', lm_unit, 'trigonometric') + + # Loop over correlations + for i_corr, parallel_hand in enumerate(cut_dict['available_corrs']): + # Init labels + this_ax = axes[i_corr] + x_data = lm_fac * cut_dict['lm_dist'] + y_data = cut_dict[f'{parallel_hand}_amplitude'] + fit_data = cut_dict[f'{parallel_hand}_amp_fit'] + xlabel = f'{cut_dict['xlabel']} [{lm_unit}]' + ylabel = f'{parallel_hand} Amplitude [ ]' + + # Call plotting tool + scatter_plot(this_ax, x_data, xlabel, y_data, ylabel, model=fit_data, model_marker='', title=sub_title, + data_marker='+', residuals_marker='.', model_linestyle='-', data_label=f'{parallel_hand} data', + model_label=f'{parallel_hand} fit', data_color='red', model_color='blue', residuals_color='black',) + + # Add fit peak identifiers + add_lobe_identification_to_plot(this_ax, lm_fac * cut_dict[f'{parallel_hand}_amp_fit_pars'][0::3], + cut_dict[f'{parallel_hand}_amp_fit_pars'][1::3], y_off, attenunation_plot=False) + + # equalize Y scale between correlations + set_y_axis_lims_from_default(this_ax, par_dict['y_scale'], (-y_off, max_amp+3*y_off)) + + add_secondary_beam_hpbw_x_axis_to_plot(cut_dict[f'{parallel_hand}_pb_fwhm']*lm_fac, this_ax) + + # Add bounded box with Beam parameters + pars_str = f'PB off. = {cut_dict[f'{parallel_hand}_pb_center']*lm_fac:.2f} [{lm_unit}]\n' + pars_str += f'PB FWHM = {cut_dict[f'{parallel_hand}_pb_fwhm']*lm_fac:.2f} [{lm_unit}]\n' + pars_str += (f'FSL ratio = ' + f'{to_db(cut_dict[f'{parallel_hand}_first_side_lobe_ratio']):.1f} dB') + bounds_box = dict(boxstyle='square', facecolor='white', alpha=0.5) + this_ax.text(0.05, 0.95, pars_str, transform=this_ax.transAxes, verticalalignment='top', + bbox=bounds_box) + + +def plot_cuts_in_amplitude(cut_list, summary, par_dict): + # Init n_cuts = len(cut_list) + antenna = par_dict['this_ant'] + ddi = par_dict['this_ddi'] + azel_unit = par_dict['azel_unit'] + + # Loop over cuts + fig, axes = create_figure_and_axes([12, 1+n_cuts*4], [n_cuts, 2]) + for icut, cut_dict in enumerate(cut_list): + plot_single_cut_parallel_corrs(cut_dict, axes[icut, :], par_dict) + + # Header creation freq_str = format_frequency(summary['spectral']['rep. frequency'], decimal_places=3) raw_azel = np.array(summary['general']["az el info"]["mean"]) mean_azel = convert_unit('rad', azel_unit, 'trigonometric') * raw_azel - title = f'Beam cut for {create_dataset_label(antenna, ddi, separator=',')}, '+r'$\nu$ = ' + f'{freq_str}, ' + title = f'Beam cut for {create_dataset_label(antenna, ddi, separator=',')}, ' + r'$\nu$ = ' + f'{freq_str}, ' title += f'Az ~ {format_value_unit(mean_azel[0], 'deg', decimal_places=0)}, ' title += f'El ~ {format_value_unit(mean_azel[1], 'deg', decimal_places=0)}' - lm_fac = convert_unit('rad', lm_unit, 'trigonometric') - fig, ax = create_figure_and_axes(None, [n_cuts, 2]) - for icut, cut_dict in enumerate(cut_list): - sub_title = f'{cut_dict["time_string"]}, {cut_dict["direction"]}' - max_amp = cut_dict['max_amp'] - y_off = 0.05*max_amp - for i_corr, parallel_hand in enumerate(cut_dict['available_corrs']): - x_data = lm_fac * cut_dict['lm_dist'] - y_data = cut_dict[f'{parallel_hand}_amplitude'] - fit_data = cut_dict[f'{parallel_hand}_amp_fit'] - this_ax = ax[icut, i_corr] - scatter_plot(this_ax, x_data, f'LM distance [{lm_unit}]', - y_data, f'{parallel_hand} Amplitude [ ]', - model=fit_data, model_marker='', - title=sub_title, - data_marker='+', residuals_marker='.', model_linestyle='-', - data_label=f'{parallel_hand} data', model_label=f'{parallel_hand} fit',) - for i_peak in range(cut_dict[f'{parallel_hand}_n_peaks']): - x_cen = lm_fac*cut_dict[f'{parallel_hand}_amp_fit_pars'][i_peak*3] - y_amp = cut_dict[f'{parallel_hand}_amp_fit_pars'][i_peak*3+1] - this_ax.text(x_cen, y_amp+y_off/2, i_peak+1, ha='center', va='bottom') - if y_scale is None: - this_ax.set_ylim((-y_off, max_amp+3*y_off)) - else: - this_ax.set_ylim(y_scale) - - pars_str = f'PB off. = {cut_dict[f'{parallel_hand}_pb_center']*lm_fac:.2f} [{lm_unit}]\n' - pars_str += f'PB FWHM = {cut_dict[f'{parallel_hand}_pb_fwhm']*lm_fac:.2f} [{lm_unit}]\n' - pars_str += (f'FSL ratio = ' - f'{10*np.log10(cut_dict[f'{parallel_hand}_first_side_lobe_ratio']):.1f} dB') - bounds_box = dict(boxstyle='square', facecolor='white', alpha=0.5) - this_ax.text(0.05, 0.95, pars_str, transform=this_ax.transAxes, fontsize=7, verticalalignment='top', - bbox=bounds_box) filename = f'beamcut_{antenna}_{ddi}.png' - close_figure(fig, title, filename, dpi, display) + close_figure(fig, title, filename, par_dict['dpi'], par_dict['display']) def gaussian(x_axis, x_off, amp, fwhm): @@ -252,36 +286,10 @@ def gaussian(x_axis, x_off, amp, fwhm): return amp * np.exp(-(x_axis - x_off)**2/(2*sigma**2)) -def primary_and_first_side_lobes(x_axis, primary_fwhm, primary_amp, primary_center, first_sidelobe_offset, - left_sidelobe_amp, left_sidelobe_fwhm, right_sidelobe_amp, right_sidelobe_fwhm): - primary = gaussian(x_axis, primary_center, primary_fwhm, primary_amp) - left_sidelobe = gaussian(x_axis, primary_center-first_sidelobe_offset, - left_sidelobe_amp, left_sidelobe_fwhm) - right_sidelobe = gaussian(x_axis, primary_center+first_sidelobe_offset, - right_sidelobe_amp, right_sidelobe_fwhm) - full_beam = primary + left_sidelobe + right_sidelobe - return full_beam - - -def pb_fsl(x_axis, pb_off, pb_amp, pb_fwhm, lfsl_off, lfsl_amp, lfsl_fwhm, rfsl_off, rfsl_amp, rfsl_fwhm): - pb = gaussian(x_axis, pb_off, pb_amp, pb_fwhm) - lfsl = gaussian(x_axis, lfsl_off, lfsl_amp, lfsl_fwhm) - rfsl = gaussian(x_axis, rfsl_off, rfsl_amp, rfsl_fwhm) - return pb + lfsl + rfsl - - -def pb_fsl_ssl(x_axis, pb_off, pb_amp, pb_fwhm, lfsl_off, lfsl_amp, lfsl_fwhm, rfsl_off, rfsl_amp, rfsl_fwhm, - lssl_off, lssl_amp, lssl_fwhm, rssl_off, rssl_amp, rssl_fwhm): - lssl = gaussian(x_axis, lssl_off, lssl_amp, lssl_fwhm) - rssl = gaussian(x_axis, rssl_off, rssl_amp, rssl_fwhm) - pb_fsl_model = pb_fsl(x_axis, pb_off, pb_amp, pb_fwhm, lfsl_off, lfsl_amp, lfsl_fwhm, rfsl_off, rfsl_amp, rfsl_fwhm) - return pb_fsl_model + lssl + rssl - - def build_multi_gaussian_initial_guesses(x_data, y_data, pb_fwhm, min_dist_fraction=1.3): p0 = [] step = float(np.median(np.diff(x_data))) - min_dist = min_dist_fraction * pb_fwhm / step + min_dist = np.abs(min_dist_fraction * pb_fwhm / step) peaks, _ = find_peaks(y_data, distance=min_dist) for ipeak in peaks: p0.extend([x_data[ipeak], y_data[ipeak], pb_fwhm]) @@ -300,6 +308,7 @@ def multi_gaussian(xdata, *args): def beamcut_fit(cut_list, telescope, summary): wavelength = summary["spectral"]["rep. wavelength"] primary_fwhm = 1.2 * wavelength / telescope.diameter + for cut_dict in cut_list: x_data = cut_dict['lm_dist'] for parallel_hand in cut_dict['available_corrs']: @@ -315,10 +324,10 @@ def beamcut_fit(cut_list, telescope, summary): centers = fit_pars[0::3] amps = fit_pars[1::3] - fwhms = fit_pars[2::3] + sigmas = fit_pars[2::3] i_pb = np.argmax(amps) - cut_dict[f'{parallel_hand}_pb_fwhm'] = fwhms[i_pb] + cut_dict[f'{parallel_hand}_pb_fwhm'] = sigmas[i_pb] cut_dict[f'{parallel_hand}_pb_center'] = centers[i_pb] left_first_sl_amp = amps[i_pb-1] From e63579c17efcd891621b1af8051e61f36e6d2b02 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Fri, 19 Dec 2025 11:41:23 -0700 Subject: [PATCH 15/95] Amplitude plot is factorized. --- src/astrohack/core/beamcut.py | 69 ++++++++++++++++++++--------------- 1 file changed, 39 insertions(+), 30 deletions(-) diff --git a/src/astrohack/core/beamcut.py b/src/astrohack/core/beamcut.py index b2c35e51..0f857a52 100644 --- a/src/astrohack/core/beamcut.py +++ b/src/astrohack/core/beamcut.py @@ -183,22 +183,16 @@ def get_parallel_hand_indexes(corr_axis): def add_secondary_beam_hpbw_x_axis_to_plot(pb_fwhm, ax): - with np.errstate(divide='ignore'): - to_fwhm = lambda x: x/pb_fwhm - from_fwhm = lambda xb: pb_fwhm/xb - print('to fwhm', to_fwhm(pb_fwhm)) - print('from fwhm', from_fwhm(1.0)) - sec_x_axis = ax.secondary_xaxis('top', functions=(to_fwhm, from_fwhm)) - sec_x_axis.set_xlabel('Offset in Primary Beam HPBWs\n') - old_xticks = sec_x_axis.get_xticks() - for itk in np.arange(-4, 5, 1): - ax.axvline(itk*pb_fwhm, color='k', linestyle='--', linewidth=0.5) - ax.text(itk*pb_fwhm, 1.4, f'{itk+1}', ) - print(old_xticks, to_fwhm(old_xticks), pb_fwhm) - new_ticks = np.arange(0.05, 0.2, 0.05) - new_ticks = [0, 0.2, 0.4, 0.6, 0.8, 1.0] - print(new_ticks) - sec_x_axis.set_xticks([]) + sec_x_axis = ax.secondary_xaxis('top', functions=(lambda x: x*1.0, lambda xb: 1*xb)) + sec_x_axis.set_xlabel('Offset in Primary Beam HPBWs\n') + sec_x_axis.set_xticks([]) + y_min, y_max = ax.get_ylim() + x_lims = ax.get_xlim() + pb_min, pb_max = np.ceil(x_lims/pb_fwhm) + + for itk in np.arange(pb_min, pb_max, 1): + ax.axvline(itk*pb_fwhm, color='k', linestyle='--', linewidth=0.5) + ax.text(itk*pb_fwhm, y_max, f'{itk+1}', va='bottom', ha='center') def add_lobe_identification_to_plot(ax, centers, peaks, y_off, attenunation_plot=False): @@ -208,7 +202,7 @@ def add_lobe_identification_to_plot(ax, centers, peaks, y_off, attenunation_plot plot_peaks = peaks for i_peak, peak in enumerate(plot_peaks): - ax.text(centers[i_peak], peak+y_off/2, i_peak+1, ha='center', va='bottom') + ax.text(centers[i_peak], peak+y_off, f'{i_peak+1})', ha='center', va='bottom') def make_parallel_hand_subplot_title(direction, time_string): @@ -248,21 +242,38 @@ def plot_single_cut_parallel_corrs(cut_dict, axes, par_dict): add_secondary_beam_hpbw_x_axis_to_plot(cut_dict[f'{parallel_hand}_pb_fwhm']*lm_fac, this_ax) # Add bounded box with Beam parameters - pars_str = f'PB off. = {cut_dict[f'{parallel_hand}_pb_center']*lm_fac:.2f} [{lm_unit}]\n' - pars_str += f'PB FWHM = {cut_dict[f'{parallel_hand}_pb_fwhm']*lm_fac:.2f} [{lm_unit}]\n' - pars_str += (f'FSL ratio = ' - f'{to_db(cut_dict[f'{parallel_hand}_first_side_lobe_ratio']):.1f} dB') - bounds_box = dict(boxstyle='square', facecolor='white', alpha=0.5) - this_ax.text(0.05, 0.95, pars_str, transform=this_ax.transAxes, verticalalignment='top', + add_beam_parameters_box(this_ax, cut_dict[f'{parallel_hand}_pb_center']*lm_fac, + cut_dict[f'{parallel_hand}_pb_fwhm']*lm_fac, + cut_dict[f'{parallel_hand}_first_side_lobe_ratio'], + lm_unit) + + +def add_beam_parameters_box(ax, pb_center, pb_fwhm, sidelobe_ratio, lm_unit, alpha=0.8, x_pos=0.05, y_pos=0.95): + pars_str = f'PB off. = {format_value_unit(pb_center, lm_unit, 3)}\n' + pars_str += f'PB FWHM = {format_value_unit(pb_fwhm, lm_unit, 3)}\n' + pars_str += f'FSLR = {format_value_unit(to_db(sidelobe_ratio), 'dB', 2)}' + bounds_box = dict(boxstyle='square', facecolor='white', alpha=alpha) + ax.text(0.05, 0.95, pars_str, transform=ax.transAxes, verticalalignment='top', bbox=bounds_box) +def create_beamcut_header(summary, par_dict): + azel_unit = par_dict['azel_unit'] + antenna = par_dict['this_ant'] + ddi = par_dict['this_ddi'] + freq_str = format_frequency(summary['spectral']['rep. frequency'], decimal_places=3) + raw_azel = np.array(summary['general']["az el info"]["mean"]) + mean_azel = convert_unit('rad', azel_unit, 'trigonometric') * raw_azel + title = f'Beam cut for {create_dataset_label(antenna, ddi, separator=',')}, ' + r'$\nu$ = ' + f'{freq_str}, ' + title += f'Az ~ {format_value_unit(mean_azel[0], 'deg', decimal_places=0)}, ' + title += f'El ~ {format_value_unit(mean_azel[1], 'deg', decimal_places=0)}' + return title + def plot_cuts_in_amplitude(cut_list, summary, par_dict): # Init n_cuts = len(cut_list) antenna = par_dict['this_ant'] ddi = par_dict['this_ddi'] - azel_unit = par_dict['azel_unit'] # Loop over cuts fig, axes = create_figure_and_axes([12, 1+n_cuts*4], [n_cuts, 2]) @@ -270,12 +281,7 @@ def plot_cuts_in_amplitude(cut_list, summary, par_dict): plot_single_cut_parallel_corrs(cut_dict, axes[icut, :], par_dict) # Header creation - freq_str = format_frequency(summary['spectral']['rep. frequency'], decimal_places=3) - raw_azel = np.array(summary['general']["az el info"]["mean"]) - mean_azel = convert_unit('rad', azel_unit, 'trigonometric') * raw_azel - title = f'Beam cut for {create_dataset_label(antenna, ddi, separator=',')}, ' + r'$\nu$ = ' + f'{freq_str}, ' - title += f'Az ~ {format_value_unit(mean_azel[0], 'deg', decimal_places=0)}, ' - title += f'El ~ {format_value_unit(mean_azel[1], 'deg', decimal_places=0)}' + title = create_beamcut_header(summary, par_dict) filename = f'beamcut_{antenna}_{ddi}.png' close_figure(fig, title, filename, par_dict['dpi'], par_dict['display']) @@ -291,6 +297,9 @@ def build_multi_gaussian_initial_guesses(x_data, y_data, pb_fwhm, min_dist_fract step = float(np.median(np.diff(x_data))) min_dist = np.abs(min_dist_fraction * pb_fwhm / step) peaks, _ = find_peaks(y_data, distance=min_dist) + dx = x_data[-1] - x_data[0] + if dx < 0: + peaks = peaks[::-1] for ipeak in peaks: p0.extend([x_data[ipeak], y_data[ipeak], pb_fwhm]) return p0, len(peaks) From c7fa4ce8c2f97e039a684acbe3bca584c9422b65 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Fri, 19 Dec 2025 14:44:28 -0700 Subject: [PATCH 16/95] Added a parameter to control the presence of the legend in scatter_plot plot tool. --- src/astrohack/core/extract_locit.py | 1 + src/astrohack/visualization/diagnostics.py | 12 +++++++++++- src/astrohack/visualization/plot_tools.py | 10 ++++++++-- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/astrohack/core/extract_locit.py b/src/astrohack/core/extract_locit.py index e4ecfe40..8aee0a63 100644 --- a/src/astrohack/core/extract_locit.py +++ b/src/astrohack/core/extract_locit.py @@ -420,6 +420,7 @@ def plot_source_table( labels=labels, xlim=[-0.5, 24.5], ylim=[-95, 95], + add_legend=False, ) close_figure(fig, title, filename, dpi, display) diff --git a/src/astrohack/visualization/diagnostics.py b/src/astrohack/visualization/diagnostics.py index e7301874..a84f52dc 100644 --- a/src/astrohack/visualization/diagnostics.py +++ b/src/astrohack/visualization/diagnostics.py @@ -312,6 +312,7 @@ def _plot_lm_coverage_sub(time, real_lm, ideal_lm, param_dict): data_marker=param_dict["marker"], data_linestyle=param_dict["linestyle"], data_color=param_dict["color"], + add_legend=False, ) scatter_plot( ax[0, 1], @@ -323,6 +324,7 @@ def _plot_lm_coverage_sub(time, real_lm, ideal_lm, param_dict): data_marker=param_dict["marker"], data_linestyle=param_dict["linestyle"], data_color=param_dict["color"], + add_legend=False ) scatter_plot( ax[1, 0], @@ -334,6 +336,7 @@ def _plot_lm_coverage_sub(time, real_lm, ideal_lm, param_dict): data_marker=param_dict["marker"], data_linestyle=param_dict["linestyle"], data_color=param_dict["color"], + add_legend=False ) scatter_plot( ax[1, 1], @@ -345,6 +348,7 @@ def _plot_lm_coverage_sub(time, real_lm, ideal_lm, param_dict): 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_{param_dict["this_map"]}_' @@ -394,6 +398,7 @@ def plot_correlation(visi, weights, correlation, pol_axis, time, lm, param_dict) data_marker=param_dict["marker"], data_linestyle=param_dict["linestyle"], data_color=param_dict["color"], + add_legend=False ) scatter_plot( ax[isplit, 1], @@ -405,6 +410,7 @@ def plot_correlation(visi, weights, correlation, pol_axis, time, lm, param_dict) data_marker=param_dict["marker"], data_linestyle=param_dict["linestyle"], data_color=param_dict["color"], + add_legend=False, ) scatter_plot( ax[isplit, 2], @@ -416,6 +422,7 @@ def plot_correlation(visi, weights, correlation, pol_axis, time, lm, param_dict) data_marker=param_dict["marker"], data_linestyle=param_dict["linestyle"], data_color=param_dict["color"], + add_legend=False ) plotfile = ( @@ -493,9 +500,10 @@ def plot_sky_coverage_chunk(parm_dict): "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 + axes[0, 1], time, timelabel, ha, halabel, "Time vs Hour angle", ylim=halim, add_legend=False ) scatter_plot( axes[1, 0], @@ -506,6 +514,7 @@ def plot_sky_coverage_chunk(parm_dict): "Time vs Declination", ylim=declim, hlines=declines, + add_legend=False ) scatter_plot( axes[1, 1], @@ -517,6 +526,7 @@ def plot_sky_coverage_chunk(parm_dict): ylim=declim, xlim=halim, hlines=declines, + add_legend=False ) close_figure(fig, suptitle, export_name, dpi, display) diff --git a/src/astrohack/visualization/plot_tools.py b/src/astrohack/visualization/plot_tools.py index d913afb1..cf132278 100644 --- a/src/astrohack/visualization/plot_tools.py +++ b/src/astrohack/visualization/plot_tools.py @@ -248,6 +248,8 @@ def scatter_plot( add_regression=False, regression_linestyle="-", regression_color="black", + add_legend=True, + legend_location='best', ): """ Do scatter simple scatter plots of data to a plotting axis @@ -282,6 +284,8 @@ def scatter_plot( add_regression: Add a linear regression between X and y data regression_linestyle: Line style for the regression plot regression_color: Color for the regression plot + add_legend: add legend to the plot + legend_location: Location of the legend in the plot """ ax.plot( xdata, @@ -329,7 +333,7 @@ def scatter_plot( label=regression_label, lw=2, ) - ax.legend() + if model is not None: ax.plot( @@ -340,7 +344,6 @@ def scatter_plot( color=model_color, label=model_label, ) - ax.legend() if plot_residuals: divider = make_axes_locatable(ax) ax_res = divider.append_axes("bottom", size="20%", pad=0) @@ -371,6 +374,9 @@ def scatter_plot( if title is not None: ax.set_title(title) + if add_legend: + ax.legend(loc=legend_location) + return From c6d1a9525520fda664cb5f1c36c0535b91a5ee76 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Fri, 19 Dec 2025 15:20:10 -0700 Subject: [PATCH 17/95] Added attenuation plot. --- src/astrohack/core/beamcut.py | 100 ++++++++++++++++++++++++++++++---- 1 file changed, 88 insertions(+), 12 deletions(-) diff --git a/src/astrohack/core/beamcut.py b/src/astrohack/core/beamcut.py index 0f857a52..db71fc3d 100644 --- a/src/astrohack/core/beamcut.py +++ b/src/astrohack/core/beamcut.py @@ -56,6 +56,7 @@ def process_beamcut_chunk(beamcut_chunk_params): beamcut_chunk_params['y_scale'] = None plot_cuts_in_amplitude(cut_list, summary, beamcut_chunk_params) + plot_cuts_in_attenuation(cut_list, summary, beamcut_chunk_params) def time_scan_selection(scan_time_ranges, time_axis): time_selections = [] @@ -189,10 +190,11 @@ def add_secondary_beam_hpbw_x_axis_to_plot(pb_fwhm, ax): y_min, y_max = ax.get_ylim() x_lims = ax.get_xlim() pb_min, pb_max = np.ceil(x_lims/pb_fwhm) + beam_offsets = np.arange(pb_min, pb_max, 1, dtype=int) - for itk in np.arange(pb_min, pb_max, 1): - ax.axvline(itk*pb_fwhm, color='k', linestyle='--', linewidth=0.5) - ax.text(itk*pb_fwhm, y_max, f'{itk+1}', va='bottom', ha='center') + for itk in beam_offsets: + ax.axvline(itk * pb_fwhm, color='k', linestyle='--', linewidth=0.5) + ax.text(itk*pb_fwhm, y_max, f'{itk:d}', va='bottom', ha='center') def add_lobe_identification_to_plot(ax, centers, peaks, y_off, attenunation_plot=False): @@ -209,7 +211,7 @@ def make_parallel_hand_subplot_title(direction, time_string): return f'{direction}, {time_string} UTC' -def plot_single_cut_parallel_corrs(cut_dict, axes, par_dict): +def plot_single_cut_amp_parallel_corrs(cut_dict, axes, par_dict): # Init sub_title = make_parallel_hand_subplot_title(cut_dict["direction"], cut_dict["time_string"]) max_amp = cut_dict['max_amp'] @@ -230,7 +232,8 @@ def plot_single_cut_parallel_corrs(cut_dict, axes, par_dict): # Call plotting tool scatter_plot(this_ax, x_data, xlabel, y_data, ylabel, model=fit_data, model_marker='', title=sub_title, data_marker='+', residuals_marker='.', model_linestyle='-', data_label=f'{parallel_hand} data', - model_label=f'{parallel_hand} fit', data_color='red', model_color='blue', residuals_color='black',) + model_label=f'{parallel_hand} fit', data_color='red', model_color='blue', residuals_color='black', + legend_location='upper right') # Add fit peak identifiers add_lobe_identification_to_plot(this_ax, lm_fac * cut_dict[f'{parallel_hand}_amp_fit_pars'][0::3], @@ -248,12 +251,17 @@ def plot_single_cut_parallel_corrs(cut_dict, axes, par_dict): lm_unit) -def add_beam_parameters_box(ax, pb_center, pb_fwhm, sidelobe_ratio, lm_unit, alpha=0.8, x_pos=0.05, y_pos=0.95): - pars_str = f'PB off. = {format_value_unit(pb_center, lm_unit, 3)}\n' - pars_str += f'PB FWHM = {format_value_unit(pb_fwhm, lm_unit, 3)}\n' - pars_str += f'FSLR = {format_value_unit(to_db(sidelobe_ratio), 'dB', 2)}' +def add_beam_parameters_box(ax, pb_center, pb_fwhm, sidelobe_ratio, lm_unit, alpha=0.8, x_pos=0.05, y_pos=0.95, + attenuation_plot=False): + if attenuation_plot: + head = 'avg ' + else: + head = '' + pars_str = f'{head}PB off. = {format_value_unit(pb_center, lm_unit, 3)}\n' + pars_str += f'{head}PB FWHM = {format_value_unit(pb_fwhm, lm_unit, 3)}\n' + pars_str += f'{head}FSLR = {format_value_unit(to_db(sidelobe_ratio), 'dB', 2)}' bounds_box = dict(boxstyle='square', facecolor='white', alpha=alpha) - ax.text(0.05, 0.95, pars_str, transform=ax.transAxes, verticalalignment='top', + ax.text(x_pos, y_pos, pars_str, transform=ax.transAxes, verticalalignment='top', bbox=bounds_box) @@ -278,12 +286,12 @@ def plot_cuts_in_amplitude(cut_list, summary, par_dict): # Loop over cuts fig, axes = create_figure_and_axes([12, 1+n_cuts*4], [n_cuts, 2]) for icut, cut_dict in enumerate(cut_list): - plot_single_cut_parallel_corrs(cut_dict, axes[icut, :], par_dict) + plot_single_cut_amp_parallel_corrs(cut_dict, axes[icut, :], par_dict) # Header creation title = create_beamcut_header(summary, par_dict) - filename = f'beamcut_{antenna}_{ddi}.png' + filename = f'beamcut_amplitude_{antenna}_{ddi}.png' close_figure(fig, title, filename, par_dict['dpi'], par_dict['display']) @@ -346,7 +354,75 @@ def beamcut_fit(cut_list, telescope, summary): return cut_list +def plot_single_cut_attenuation_parallel_corrs(cut_dict, ax, par_dict): + sub_title = make_parallel_hand_subplot_title(cut_dict["direction"], cut_dict["time_string"]) + lm_unit = par_dict['lm_unit'] + lm_fac = convert_unit('rad', lm_unit, 'trigonometric') + corr_colors = ['blue', 'red'] + + min_attenuation = 1e34 + pb_center = 0.0 + pb_fwhm = 0.0 + fsl_ratio = 0.0 + xlabel = f'{cut_dict['xlabel']} [{lm_unit}]' + ylabel = f'Attenuation [dB]' + + # Loop over correlations + for i_corr, parallel_hand in enumerate(cut_dict['available_corrs']): + # Init labels + x_data = lm_fac * cut_dict['lm_dist'] + amps = cut_dict[f'{parallel_hand}_amplitude'] + max_amp = np.max(amps) + y_data = to_db(amps/max_amp) + y_min = np.min(y_data) + if y_min < min_attenuation: + min_attenuation = y_min + + ax.plot(x_data, y_data, label=parallel_hand, color=corr_colors[i_corr], marker='.', ls='') + + pb_center += cut_dict[f'{parallel_hand}_pb_center'] + pb_fwhm += cut_dict[f'{parallel_hand}_pb_fwhm'] + fsl_ratio += cut_dict[f'{parallel_hand}_first_side_lobe_ratio'] + + ax.set_xlabel(xlabel) + ax.set_ylabel(ylabel) + ax.set_title(sub_title) + ax.legend(loc='upper right') + + n_corrs = len(cut_dict['available_corrs']) + pb_center /= n_corrs + pb_fwhm /= n_corrs + fsl_ratio /= n_corrs + # equalize Y scale between correlations + y_off = 0.1 *np.abs(min_attenuation) + set_y_axis_lims_from_default(ax, par_dict['y_scale'], (min_attenuation-y_off, y_off )) + + # Add fit peak identifiers + first_corr = cut_dict['available_corrs'][0] + add_secondary_beam_hpbw_x_axis_to_plot(cut_dict[f'{first_corr}_pb_fwhm']*lm_fac, ax) + + # Add bounded box with Beam parameters + add_beam_parameters_box(ax, pb_center*lm_fac, pb_fwhm*lm_fac, fsl_ratio, lm_unit, attenuation_plot=True) + return + + +def plot_cuts_in_attenuation(cut_list, summary, par_dict): + # Init + n_cuts = len(cut_list) + antenna = par_dict['this_ant'] + ddi = par_dict['this_ddi'] + + # Loop over cuts + fig, axes = create_figure_and_axes([6, 1+n_cuts*4], [n_cuts, 1]) + for icut, cut_dict in enumerate(cut_list): + plot_single_cut_attenuation_parallel_corrs(cut_dict, axes[icut], par_dict) + + # Header creation + title = create_beamcut_header(summary, par_dict) + + filename = f'beamcut_attenuation_{antenna}_{ddi}.png' + close_figure(fig, title, filename, par_dict['dpi'], par_dict['display']) From 7ef4f795b2c877625af23f4415b2e759effa2e3c Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Fri, 19 Dec 2025 15:29:38 -0700 Subject: [PATCH 18/95] Skeleton of a report routine. --- src/astrohack/core/beamcut.py | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/src/astrohack/core/beamcut.py b/src/astrohack/core/beamcut.py index db71fc3d..26588bf0 100644 --- a/src/astrohack/core/beamcut.py +++ b/src/astrohack/core/beamcut.py @@ -7,11 +7,12 @@ from astrohack import get_proper_telescope from astrohack.utils.file import load_holog_file -from astrohack.utils import create_dataset_label, data_statistics, statistics_to_text, convert_unit, sig_2_fwhm, \ - format_frequency, format_value_unit, to_db, fontsize +from astrohack.utils import create_dataset_label, convert_unit, sig_2_fwhm, \ + format_frequency, format_value_unit, to_db from astrohack.visualization import create_figure_and_axes, scatter_plot, close_figure from astrohack.visualization.plot_tools import set_y_axis_lims_from_default -import matplotlib.ticker as mticker + +lnbr = '\n' def process_beamcut_chunk(beamcut_chunk_params): @@ -57,6 +58,7 @@ def process_beamcut_chunk(beamcut_chunk_params): plot_cuts_in_amplitude(cut_list, summary, beamcut_chunk_params) plot_cuts_in_attenuation(cut_list, summary, beamcut_chunk_params) + create_report_chunk(cut_list, summary, beamcut_chunk_params) def time_scan_selection(scan_time_ranges, time_axis): time_selections = [] @@ -207,13 +209,13 @@ def add_lobe_identification_to_plot(ax, centers, peaks, y_off, attenunation_plot ax.text(centers[i_peak], peak+y_off, f'{i_peak+1})', ha='center', va='bottom') -def make_parallel_hand_subplot_title(direction, time_string): +def make_parallel_hand_sub_title(direction, time_string): return f'{direction}, {time_string} UTC' def plot_single_cut_amp_parallel_corrs(cut_dict, axes, par_dict): # Init - sub_title = make_parallel_hand_subplot_title(cut_dict["direction"], cut_dict["time_string"]) + sub_title = make_parallel_hand_sub_title(cut_dict["direction"], cut_dict["time_string"]) max_amp = cut_dict['max_amp'] y_off = 0.05*max_amp lm_unit = par_dict['lm_unit'] @@ -277,6 +279,7 @@ def create_beamcut_header(summary, par_dict): title += f'El ~ {format_value_unit(mean_azel[1], 'deg', decimal_places=0)}' return title + def plot_cuts_in_amplitude(cut_list, summary, par_dict): # Init n_cuts = len(cut_list) @@ -355,7 +358,7 @@ def beamcut_fit(cut_list, telescope, summary): def plot_single_cut_attenuation_parallel_corrs(cut_dict, ax, par_dict): - sub_title = make_parallel_hand_subplot_title(cut_dict["direction"], cut_dict["time_string"]) + sub_title = make_parallel_hand_sub_title(cut_dict["direction"], cut_dict["time_string"]) lm_unit = par_dict['lm_unit'] lm_fac = convert_unit('rad', lm_unit, 'trigonometric') corr_colors = ['blue', 'red'] @@ -425,4 +428,22 @@ def plot_cuts_in_attenuation(cut_list, summary, par_dict): close_figure(fig, title, filename, par_dict['dpi'], par_dict['display']) +def create_report_chunk(cut_list, summary, par_dict): + outstr = '' + + outstr += create_beamcut_header(summary, par_dict)+2*lnbr + for icut, cut_dict in enumerate(cut_list): + sub_title = make_parallel_hand_sub_title(cut_dict["direction"], cut_dict["time_string"]) + for i_corr, parallel_hand in enumerate(cut_dict['available_corrs']): + outstr += f'{parallel_hand} {sub_title}{lnbr}' + + outstr += lnbr + + outstr += lnbr + + print(outstr) + + + + From c22fd64ba16e7ac15d70a608ddd41b858142c183 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Mon, 22 Dec 2025 10:13:33 -0700 Subject: [PATCH 19/95] Added beam fit report routine. --- src/astrohack/core/beamcut.py | 39 ++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/src/astrohack/core/beamcut.py b/src/astrohack/core/beamcut.py index 26588bf0..1644720e 100644 --- a/src/astrohack/core/beamcut.py +++ b/src/astrohack/core/beamcut.py @@ -8,11 +8,12 @@ from astrohack import get_proper_telescope from astrohack.utils.file import load_holog_file from astrohack.utils import create_dataset_label, convert_unit, sig_2_fwhm, \ - format_frequency, format_value_unit, to_db + format_frequency, format_value_unit, to_db, create_pretty_table from astrohack.visualization import create_figure_and_axes, scatter_plot, close_figure from astrohack.visualization.plot_tools import set_y_axis_lims_from_default lnbr = '\n' +spc = ' ' def process_beamcut_chunk(beamcut_chunk_params): @@ -344,12 +345,13 @@ def beamcut_fit(cut_list, telescope, summary): centers = fit_pars[0::3] amps = fit_pars[1::3] - sigmas = fit_pars[2::3] + fwhms = fit_pars[2::3] i_pb = np.argmax(amps) - cut_dict[f'{parallel_hand}_pb_fwhm'] = sigmas[i_pb] + cut_dict[f'{parallel_hand}_pb_fwhm'] = fwhms[i_pb] cut_dict[f'{parallel_hand}_pb_center'] = centers[i_pb] + left_first_sl_amp = amps[i_pb-1] right_first_sl_amp = amps[i_pb+1] cut_dict[f'{parallel_hand}_first_side_lobe_ratio'] = left_first_sl_amp / right_first_sl_amp @@ -428,20 +430,37 @@ def plot_cuts_in_attenuation(cut_list, summary, par_dict): close_figure(fig, title, filename, par_dict['dpi'], par_dict['display']) -def create_report_chunk(cut_list, summary, par_dict): - outstr = '' +def create_report_chunk(cut_list, summary, par_dict, spacing=2, item_marker='-', precision=3): + outstr = f'{item_marker}{spc}' + lm_unit = par_dict['lm_unit'] + lm_fac = convert_unit('rad', lm_unit, 'trigonometric') + items = ['Id', f'Center [{lm_unit}]', 'Amplitude [ ]', f'FWHM [{lm_unit}]', 'Attenuation [dB]'] outstr += create_beamcut_header(summary, par_dict)+2*lnbr for icut, cut_dict in enumerate(cut_list): sub_title = make_parallel_hand_sub_title(cut_dict["direction"], cut_dict["time_string"]) for i_corr, parallel_hand in enumerate(cut_dict['available_corrs']): - outstr += f'{parallel_hand} {sub_title}{lnbr}' - + outstr += f'{spacing*spc}{item_marker}{spc}{parallel_hand} {sub_title}, Beam fit results:{lnbr}' + table = create_pretty_table(items, 'c') + fit_pars = cut_dict[f'{parallel_hand}_amp_fit_pars'] + centers = fit_pars[0::3] + amps = fit_pars[1::3] + fwhms = fit_pars[2::3] + max_amp = np.max(cut_dict[f'{parallel_hand}_amplitude']) + + for i_peak in range(cut_dict[f'{parallel_hand}_n_peaks']): + + table.add_row([f'{i_peak+1})', # Id + f'{lm_fac*centers[i_peak]:.{precision}f}', # center + f'{amps[i_peak]:.{precision}f}', # Amp + f'{lm_fac*fwhms[i_peak]:.{precision}f}', # FWHM + f'{to_db(amps[i_peak]/max_amp):.{precision}f}', # Attenuation + ]) + for line in table.get_string().splitlines(): + outstr += 2*spacing*spc+line+lnbr outstr += lnbr - outstr += lnbr - - print(outstr) + return outstr From a3c364b0b093e92f398db4f83720ad2f8a14e36b Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Mon, 22 Dec 2025 11:27:26 -0700 Subject: [PATCH 20/95] Implemented output file for beamcuts as a datatree. --- src/astrohack/{beamcut_tool.py => beamcut.py} | 31 ++++++++---- src/astrohack/core/beamcut.py | 47 +++++++++++++++++-- 2 files changed, 64 insertions(+), 14 deletions(-) rename src/astrohack/{beamcut_tool.py => beamcut.py} (64%) diff --git a/src/astrohack/beamcut_tool.py b/src/astrohack/beamcut.py similarity index 64% rename from src/astrohack/beamcut_tool.py rename to src/astrohack/beamcut.py index a13684cf..4922cbdb 100644 --- a/src/astrohack/beamcut_tool.py +++ b/src/astrohack/beamcut.py @@ -3,14 +3,15 @@ import json from astrohack.core.beamcut import process_beamcut_chunk -from astrohack.utils import get_default_file_name +from astrohack.utils import get_default_file_name, add_caller_and_version_to_dict from astrohack.utils.file import overwrite_file, check_if_file_can_be_opened from astrohack.utils.graph import compute_graph +import xarray as xr from astrohack.utils.data import write_meta_data from typing import Union, List -def beamcut_tool( +def beamcut( holog_name: str, beamcut_name: str = None, ant: Union[str, List[str]] = "all", @@ -41,18 +42,28 @@ def beamcut_tool( overwrite_file(beamcut_params["beamcut_name"], beamcut_params['overwrite']) - if compute_graph( - holog_json, - process_beamcut_chunk, - beamcut_params, - ["ant", "ddi"], - parallel=parallel, - ): + executed_graph, graph_results = compute_graph(holog_json, process_beamcut_chunk, beamcut_params, + ["ant", "ddi"], parallel=parallel, fetch_returns=True) + + if executed_graph: logger.info("Finished processing") output_attr_file = "{name}/{ext}".format( name=beamcut_params["beamcut_name"], ext=".beamcut_input" ) - # write_meta_data(output_attr_file, input_params) + root = xr.DataTree(name='root') + root.attrs.update(beamcut_params) + add_caller_and_version_to_dict(root.attrs, direct_call=True) + + for xdtree in graph_results: + ant , ddi = xdtree.name.split('-') + if ant in root.children.keys(): + ant = root.children[ant].assign({ddi: xdtree}) + else: + ant_tree = xr.DataTree(name=ant, children={ddi: xdtree}) + root = root.assign({ant: ant_tree}) + + root.to_zarr(beamcut_params["beamcut_name"], mode="w", consolidated=True) + # beamcut_mds = AstrohackbeamcutFile(beamcut_params["beamcut_name"]) # beamcut_mds.open() # diff --git a/src/astrohack/core/beamcut.py b/src/astrohack/core/beamcut.py index 1644720e..f6de3f23 100644 --- a/src/astrohack/core/beamcut.py +++ b/src/astrohack/core/beamcut.py @@ -4,6 +4,7 @@ from scipy.signal import find_peaks from scipy.stats import linregress import astropy +import xarray as xr from astrohack import get_proper_telescope from astrohack.utils.file import load_holog_file @@ -31,8 +32,6 @@ def process_beamcut_chunk(beamcut_chunk_params): this_xds = ant_data_dict[ddi]['map_0'] logger.info(f"processing {create_dataset_label(antenna, ddi)}") - print(this_xds) - scan_time_ranges = this_xds.attrs['scan_time_ranges'] scan_list = this_xds.attrs['scan_list'] summary = this_xds.attrs["summary"] @@ -51,6 +50,10 @@ def process_beamcut_chunk(beamcut_chunk_params): visibilities, weights) beamcut_fit(cut_list, telescope, summary) + + + return create_output_datatree(cut_list, antenna, ddi, summary) + # This is stuff that should not be dealt here, but is here for testing/development purposes . beamcut_chunk_params['lm_unit'] = 'amin' beamcut_chunk_params['azel_unit'] = 'deg' beamcut_chunk_params['dpi'] = 300 @@ -61,6 +64,42 @@ def process_beamcut_chunk(beamcut_chunk_params): plot_cuts_in_attenuation(cut_list, summary, beamcut_chunk_params) create_report_chunk(cut_list, summary, beamcut_chunk_params) + +def create_output_datatree(cut_list, antenna, ddi, summary): + this_branch = xr.DataTree(name=f'{antenna}-{ddi}') + for icut, cut_dict in enumerate(cut_list): + xds = xr.Dataset() + xds.attrs['scan_number'] = cut_dict['scan_number'] + xds.attrs['lm_angle'] = cut_dict['lm_angle'] + xds.attrs['available_corrs'] = cut_dict['available_corrs'] + xds.attrs['direction'] = cut_dict['direction'] + xds.attrs['xlabel'] = cut_dict['xlabel'] + xds.attrs['time_string'] = cut_dict['time_string'] + xds.attrs['all_corr_ymax'] = cut_dict['all_corr_ymax'] + xds.attrs['summary'] = summary + + coords = {"time": cut_dict['time'], "lm_dist": cut_dict['lm_dist']} + + xds['lm_offsets'] = xr.DataArray(cut_dict['lm_offsets'], dims=["time", "lm"]) + + for parallel_hand in cut_dict['available_corrs']: + xds.attrs[f'{parallel_hand}_n_peaks'] = cut_dict[f'{parallel_hand}_n_peaks'] + xds.attrs[f'{parallel_hand}_amp_fit_pars'] = cut_dict[f'{parallel_hand}_amp_fit_pars'] + xds.attrs[f'{parallel_hand}_pb_fwhm'] = cut_dict[f'{parallel_hand}_pb_fwhm'] + xds.attrs[f'{parallel_hand}_pb_center'] = cut_dict[f'{parallel_hand}_pb_center'] + xds.attrs[f'{parallel_hand}_first_side_lobe_ratio'] = cut_dict[f'{parallel_hand}_first_side_lobe_ratio'] + + xds[f'{parallel_hand}_amplitude'] = xr.DataArray(cut_dict[f'{parallel_hand}_amplitude'], dims='lm_dist') + xds[f'{parallel_hand}_phase'] = xr.DataArray(cut_dict[f'{parallel_hand}_phase'], dims='lm_dist') + xds[f'{parallel_hand}_weight'] = xr.DataArray(cut_dict[f'{parallel_hand}_weight'], dims='lm_dist') + xds[f'{parallel_hand}_amp_fit'] = xr.DataArray(cut_dict[f'{parallel_hand}_amp_fit'], dims='lm_dist') + + xds = xds.assign_coords(coords) + this_branch = this_branch.assign({f'cut_{icut}': xr.DataTree(dataset=xds, name=f'cut_{icut}')}) + return this_branch + + + def time_scan_selection(scan_time_ranges, time_axis): time_selections = [] for scan_time_range in scan_time_ranges: @@ -113,8 +152,8 @@ def extract_cuts_from_visibilities(scan_list, scan_time_ranges, time_axis, corr_ all_corr_ymax = maxamp cut_dict[f'{parallel_hand}_amplitude'] = amp cut_dict[f'{parallel_hand}_phase'] = np.angle(avg_vis[:, icorr]) - cut_dict[f'{parallel_hand}_weight'] = np.angle(avg_wei[:, icorr]) - cut_dict['max_amp'] = all_corr_ymax + cut_dict[f'{parallel_hand}_weight'] = avg_wei[:, icorr] + cut_dict['all_corr_ymax'] = all_corr_ymax cut_list.append(cut_dict) return cut_list From ee4ccb70755afcf9ba93e1f9c4517e357abdf362 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Mon, 22 Dec 2025 11:44:12 -0700 Subject: [PATCH 21/95] Added beamcut plotting and reporting to regular execution. --- src/astrohack/beamcut.py | 10 +++++++- src/astrohack/core/beamcut.py | 48 ++++++++++++++++++++--------------- 2 files changed, 37 insertions(+), 21 deletions(-) diff --git a/src/astrohack/beamcut.py b/src/astrohack/beamcut.py index 4922cbdb..050e14a1 100644 --- a/src/astrohack/beamcut.py +++ b/src/astrohack/beamcut.py @@ -7,7 +7,6 @@ from astrohack.utils.file import overwrite_file, check_if_file_can_be_opened from astrohack.utils.graph import compute_graph import xarray as xr -from astrohack.utils.data import write_meta_data from typing import Union, List @@ -17,6 +16,12 @@ def beamcut( ant: Union[str, List[str]] = "all", ddi: Union[int, List[str]] = "all", correlations: str = "all", + destination: str = None, + lm_unit: str = 'amin', + azel_unit: str = 'deg', + dpi: int = 300, + display: bool = False, + y_scale: str = None, parallel: bool = False, overwrite: bool = False, ): @@ -28,6 +33,9 @@ def beamcut( 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() diff --git a/src/astrohack/core/beamcut.py b/src/astrohack/core/beamcut.py index f6de3f23..86195f11 100644 --- a/src/astrohack/core/beamcut.py +++ b/src/astrohack/core/beamcut.py @@ -30,7 +30,8 @@ def process_beamcut_chunk(beamcut_chunk_params): ) # This assumes that there will be no more than one mapping this_xds = ant_data_dict[ddi]['map_0'] - logger.info(f"processing {create_dataset_label(antenna, ddi)}") + datalabel = create_dataset_label(antenna, ddi) + logger.info(f"processing {datalabel}") scan_time_ranges = this_xds.attrs['scan_time_ranges'] scan_list = this_xds.attrs['scan_list'] @@ -50,19 +51,16 @@ def process_beamcut_chunk(beamcut_chunk_params): visibilities, weights) beamcut_fit(cut_list, telescope, summary) + destination = beamcut_chunk_params['destination'] + if destination is not None: + logger.info(f'Producing plots for {datalabel}') + plot_cuts_in_amplitude(cut_list, summary, beamcut_chunk_params) + plot_cuts_in_attenuation(cut_list, summary, beamcut_chunk_params) + create_report_chunk(cut_list, summary, beamcut_chunk_params) + logger.info(f'Completed plots for {datalabel}') return create_output_datatree(cut_list, antenna, ddi, summary) - # This is stuff that should not be dealt here, but is here for testing/development purposes . - beamcut_chunk_params['lm_unit'] = 'amin' - beamcut_chunk_params['azel_unit'] = 'deg' - beamcut_chunk_params['dpi'] = 300 - beamcut_chunk_params['display'] = False - beamcut_chunk_params['y_scale'] = None - - plot_cuts_in_amplitude(cut_list, summary, beamcut_chunk_params) - plot_cuts_in_attenuation(cut_list, summary, beamcut_chunk_params) - create_report_chunk(cut_list, summary, beamcut_chunk_params) def create_output_datatree(cut_list, antenna, ddi, summary): @@ -99,7 +97,6 @@ def create_output_datatree(cut_list, antenna, ddi, summary): return this_branch - def time_scan_selection(scan_time_ranges, time_axis): time_selections = [] for scan_time_range in scan_time_ranges: @@ -256,7 +253,7 @@ def make_parallel_hand_sub_title(direction, time_string): def plot_single_cut_amp_parallel_corrs(cut_dict, axes, par_dict): # Init sub_title = make_parallel_hand_sub_title(cut_dict["direction"], cut_dict["time_string"]) - max_amp = cut_dict['max_amp'] + max_amp = cut_dict['all_corr_ymax'] y_off = 0.05*max_amp lm_unit = par_dict['lm_unit'] lm_fac = convert_unit('rad', lm_unit, 'trigonometric') @@ -323,8 +320,6 @@ def create_beamcut_header(summary, par_dict): def plot_cuts_in_amplitude(cut_list, summary, par_dict): # Init n_cuts = len(cut_list) - antenna = par_dict['this_ant'] - ddi = par_dict['this_ddi'] # Loop over cuts fig, axes = create_figure_and_axes([12, 1+n_cuts*4], [n_cuts, 2]) @@ -334,7 +329,7 @@ def plot_cuts_in_amplitude(cut_list, summary, par_dict): # Header creation title = create_beamcut_header(summary, par_dict) - filename = f'beamcut_amplitude_{antenna}_{ddi}.png' + filename = file_name_factory('amplitude', par_dict) close_figure(fig, title, filename, par_dict['dpi'], par_dict['display']) @@ -355,6 +350,7 @@ def build_multi_gaussian_initial_guesses(x_data, y_data, pb_fwhm, min_dist_fract p0.extend([x_data[ipeak], y_data[ipeak], pb_fwhm]) return p0, len(peaks) + def multi_gaussian(xdata, *args): nargs = len(args) if nargs%3 != 0: @@ -454,8 +450,6 @@ def plot_single_cut_attenuation_parallel_corrs(cut_dict, ax, par_dict): def plot_cuts_in_attenuation(cut_list, summary, par_dict): # Init n_cuts = len(cut_list) - antenna = par_dict['this_ant'] - ddi = par_dict['this_ddi'] # Loop over cuts fig, axes = create_figure_and_axes([6, 1+n_cuts*4], [n_cuts, 1]) @@ -465,7 +459,7 @@ def plot_cuts_in_attenuation(cut_list, summary, par_dict): # Header creation title = create_beamcut_header(summary, par_dict) - filename = f'beamcut_attenuation_{antenna}_{ddi}.png' + filename = file_name_factory('attenuation', par_dict) close_figure(fig, title, filename, par_dict['dpi'], par_dict['display']) @@ -499,7 +493,21 @@ def create_report_chunk(cut_list, summary, par_dict, spacing=2, item_marker='-', outstr += 2*spacing*spc+line+lnbr outstr += lnbr - return outstr + with open(file_name_factory('report', par_dict), 'w') as outfile: + outfile.write(outstr) + + +def file_name_factory(file_type, par_dict): + destination = par_dict['destination'] + antenna = par_dict['this_ant'] + ddi = par_dict['this_ddi'] + if file_type in ['attenuation', 'amplitude']: + ext = 'png' + elif file_type == 'report': + ext = 'txt' + else: + raise ValueError('Invalid file type') + return f'{destination}/beamcut_{file_type}_{antenna}_{ddi}.{ext}' From 7edcaa5d5860c7086b4b855796f25e19813eebe1 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Mon, 22 Dec 2025 13:54:13 -0700 Subject: [PATCH 22/95] Added exclusion of gaussians outside of x_data range, and added a test to check that primary beam selection is robust. --- src/astrohack/core/beamcut.py | 65 ++++++++++++++++++++++++++--------- 1 file changed, 48 insertions(+), 17 deletions(-) diff --git a/src/astrohack/core/beamcut.py b/src/astrohack/core/beamcut.py index 86195f11..1c032439 100644 --- a/src/astrohack/core/beamcut.py +++ b/src/astrohack/core/beamcut.py @@ -49,7 +49,7 @@ def process_beamcut_chunk(beamcut_chunk_params): cut_list = extract_cuts_from_visibilities(scan_list, scan_time_ranges, time_axis, corr_axis, lm_offsets, visibilities, weights) - beamcut_fit(cut_list, telescope, summary) + beamcut_fit(cut_list, telescope, summary, datalabel) destination = beamcut_chunk_params['destination'] if destination is not None: @@ -223,6 +223,8 @@ def get_parallel_hand_indexes(corr_axis): def add_secondary_beam_hpbw_x_axis_to_plot(pb_fwhm, ax): + if np.isnan(pb_fwhm): + return sec_x_axis = ax.secondary_xaxis('top', functions=(lambda x: x*1.0, lambda xb: 1*xb)) sec_x_axis.set_xlabel('Offset in Primary Beam HPBWs\n') sec_x_axis.set_xticks([]) @@ -361,7 +363,7 @@ def multi_gaussian(xdata, *args): return y_values -def beamcut_fit(cut_list, telescope, summary): +def beamcut_fit(cut_list, telescope, summary, datalabel): wavelength = summary["spectral"]["rep. wavelength"] primary_fwhm = 1.2 * wavelength / telescope.diameter @@ -374,22 +376,48 @@ def beamcut_fit(cut_list, telescope, summary): fit_pars = results[0] fit = multi_gaussian(x_data, *fit_pars) - cut_dict[f'{parallel_hand}_amp_fit_pars'] = fit_pars - cut_dict[f'{parallel_hand}_amp_fit'] = fit - cut_dict[f'{parallel_hand}_n_peaks'] = n_peaks - centers = fit_pars[0::3] amps = fit_pars[1::3] fwhms = fit_pars[2::3] + # select fits that are within x_data + x_min = np.min(x_data) + x_max = np.max(x_data) + selection = ~ np.logical_or(centers < x_min, centers > x_max) + + centers = centers[selection] + amps = amps[selection] + fwhms = fwhms[selection] + + n_peaks = centers.shape[0] + fit_pars = np.zeros((3*n_peaks)) + fit_pars[0::3] = centers + fit_pars[1::3] = amps + fit_pars[2::3] = fwhms + + cut_dict[f'{parallel_hand}_amp_fit_pars'] = fit_pars + cut_dict[f'{parallel_hand}_amp_fit'] = fit + cut_dict[f'{parallel_hand}_n_peaks'] = n_peaks + i_pb = np.argmax(amps) - cut_dict[f'{parallel_hand}_pb_fwhm'] = fwhms[i_pb] - cut_dict[f'{parallel_hand}_pb_center'] = centers[i_pb] + if n_peaks%2 == 0: + pb_problem = i_pb not in [n_peaks//2, n_peaks//2 - 1] + else: + pb_problem = i_pb != n_peaks//2 + + if pb_problem: + logger.warning(f'Cannot reliably identify primary beam for {datalabel}.') + cut_dict[f'{parallel_hand}_pb_fwhm'] = np.nan + cut_dict[f'{parallel_hand}_pb_center'] = np.nan + cut_dict[f'{parallel_hand}_first_side_lobe_ratio'] = np.nan + else: + cut_dict[f'{parallel_hand}_pb_fwhm'] = fwhms[i_pb] + cut_dict[f'{parallel_hand}_pb_center'] = centers[i_pb] - left_first_sl_amp = amps[i_pb-1] - right_first_sl_amp = amps[i_pb+1] - cut_dict[f'{parallel_hand}_first_side_lobe_ratio'] = left_first_sl_amp / right_first_sl_amp + left_first_sl_amp = amps[i_pb-1] + right_first_sl_amp = amps[i_pb+1] + cut_dict[f'{parallel_hand}_first_side_lobe_ratio'] = left_first_sl_amp / right_first_sl_amp return cut_list @@ -408,6 +436,7 @@ def plot_single_cut_attenuation_parallel_corrs(cut_dict, ax, par_dict): ylabel = f'Attenuation [dB]' # Loop over correlations + n_data = 0 for i_corr, parallel_hand in enumerate(cut_dict['available_corrs']): # Init labels x_data = lm_fac * cut_dict['lm_dist'] @@ -420,9 +449,11 @@ def plot_single_cut_attenuation_parallel_corrs(cut_dict, ax, par_dict): ax.plot(x_data, y_data, label=parallel_hand, color=corr_colors[i_corr], marker='.', ls='') - pb_center += cut_dict[f'{parallel_hand}_pb_center'] - pb_fwhm += cut_dict[f'{parallel_hand}_pb_fwhm'] - fsl_ratio += cut_dict[f'{parallel_hand}_first_side_lobe_ratio'] + if not np.isnan(pb_center): + pb_center += cut_dict[f'{parallel_hand}_pb_center'] + pb_fwhm += cut_dict[f'{parallel_hand}_pb_fwhm'] + fsl_ratio += cut_dict[f'{parallel_hand}_first_side_lobe_ratio'] + n_data += 1 ax.set_xlabel(xlabel) ax.set_ylabel(ylabel) @@ -430,9 +461,9 @@ def plot_single_cut_attenuation_parallel_corrs(cut_dict, ax, par_dict): ax.legend(loc='upper right') n_corrs = len(cut_dict['available_corrs']) - pb_center /= n_corrs - pb_fwhm /= n_corrs - fsl_ratio /= n_corrs + pb_center /= n_data + pb_fwhm /= n_data + fsl_ratio /= n_data # equalize Y scale between correlations y_off = 0.1 *np.abs(min_attenuation) set_y_axis_lims_from_default(ax, par_dict['y_scale'], (min_attenuation-y_off, y_off )) From 6b825f111370e9cefbab05619e021e1c3ea89f57 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Mon, 22 Dec 2025 14:15:49 -0700 Subject: [PATCH 23/95] Protected plots against failed fits. --- src/astrohack/core/beamcut.py | 118 ++++++++++++++++++++-------------- 1 file changed, 70 insertions(+), 48 deletions(-) diff --git a/src/astrohack/core/beamcut.py b/src/astrohack/core/beamcut.py index 1c032439..aeb64103 100644 --- a/src/astrohack/core/beamcut.py +++ b/src/astrohack/core/beamcut.py @@ -271,14 +271,19 @@ def plot_single_cut_amp_parallel_corrs(cut_dict, axes, par_dict): ylabel = f'{parallel_hand} Amplitude [ ]' # Call plotting tool - scatter_plot(this_ax, x_data, xlabel, y_data, ylabel, model=fit_data, model_marker='', title=sub_title, - data_marker='+', residuals_marker='.', model_linestyle='-', data_label=f'{parallel_hand} data', - model_label=f'{parallel_hand} fit', data_color='red', model_color='blue', residuals_color='black', - legend_location='upper right') - - # Add fit peak identifiers - add_lobe_identification_to_plot(this_ax, lm_fac * cut_dict[f'{parallel_hand}_amp_fit_pars'][0::3], - cut_dict[f'{parallel_hand}_amp_fit_pars'][1::3], y_off, attenunation_plot=False) + if cut_dict[f'{parallel_hand}_fit_succeeded']: + scatter_plot(this_ax, x_data, xlabel, y_data, ylabel, model=fit_data, model_marker='', title=sub_title, + data_marker='+', residuals_marker='.', model_linestyle='-', data_label=f'{parallel_hand} data', + model_label=f'{parallel_hand} fit', data_color='red', model_color='blue', + residuals_color='black', legend_location='upper right') + + # Add fit peak identifiers + add_lobe_identification_to_plot(this_ax, lm_fac * cut_dict[f'{parallel_hand}_amp_fit_pars'][0::3], + cut_dict[f'{parallel_hand}_amp_fit_pars'][1::3], y_off, + attenunation_plot=False) + else: + scatter_plot(this_ax, x_data, xlabel, y_data, ylabel, title=sub_title, data_marker='+', + data_label=f'{parallel_hand} data', data_color='red', legend_location='upper right') # equalize Y scale between correlations set_y_axis_lims_from_default(this_ax, par_dict['y_scale'], (-y_off, max_amp+3*y_off)) @@ -372,52 +377,69 @@ def beamcut_fit(cut_list, telescope, summary, datalabel): for parallel_hand in cut_dict['available_corrs']: y_data = cut_dict[f'{parallel_hand}_amplitude'] p0, n_peaks = build_multi_gaussian_initial_guesses(x_data, y_data, primary_fwhm) - results = curve_fit(multi_gaussian, x_data, y_data, p0=p0) - fit_pars = results[0] - fit = multi_gaussian(x_data, *fit_pars) - - centers = fit_pars[0::3] - amps = fit_pars[1::3] - fwhms = fit_pars[2::3] - - # select fits that are within x_data - x_min = np.min(x_data) - x_max = np.max(x_data) - selection = ~ np.logical_or(centers < x_min, centers > x_max) - centers = centers[selection] - amps = amps[selection] - fwhms = fwhms[selection] - - n_peaks = centers.shape[0] - fit_pars = np.zeros((3*n_peaks)) - fit_pars[0::3] = centers - fit_pars[1::3] = amps - fit_pars[2::3] = fwhms - - cut_dict[f'{parallel_hand}_amp_fit_pars'] = fit_pars - cut_dict[f'{parallel_hand}_amp_fit'] = fit - cut_dict[f'{parallel_hand}_n_peaks'] = n_peaks - - i_pb = np.argmax(amps) - - if n_peaks%2 == 0: - pb_problem = i_pb not in [n_peaks//2, n_peaks//2 - 1] + try: + results = curve_fit(multi_gaussian, x_data, y_data, p0=p0, maxfev=int(5e4)) + fit_pars = results[0] + fit_succeeded = True + except RuntimeError: + logger.warning(f'Gaussian fit to lobes failed for {datalabel}, corr = {parallel_hand}.') + fit_succeeded = False + + if fit_succeeded: + fit = multi_gaussian(x_data, *fit_pars) + + centers = fit_pars[0::3] + amps = fit_pars[1::3] + fwhms = fit_pars[2::3] + + # select fits that are within x_data + x_min = np.min(x_data) + x_max = np.max(x_data) + selection = ~ np.logical_or(centers < x_min, centers > x_max) + + centers = centers[selection] + amps = amps[selection] + fwhms = fwhms[selection] + + n_peaks = centers.shape[0] + fit_pars = np.zeros((3*n_peaks)) + fit_pars[0::3] = centers + fit_pars[1::3] = amps + fit_pars[2::3] = fwhms + + cut_dict[f'{parallel_hand}_amp_fit_pars'] = fit_pars + cut_dict[f'{parallel_hand}_amp_fit'] = fit + cut_dict[f'{parallel_hand}_n_peaks'] = n_peaks + + i_pb = np.argmax(amps) + + if n_peaks%2 == 0: + pb_problem = i_pb not in [n_peaks//2, n_peaks//2 - 1] + else: + pb_problem = i_pb != n_peaks//2 + + if pb_problem: + logger.warning(f'Cannot reliably identify primary beam for {datalabel}.') + cut_dict[f'{parallel_hand}_pb_fwhm'] = np.nan + cut_dict[f'{parallel_hand}_pb_center'] = np.nan + cut_dict[f'{parallel_hand}_first_side_lobe_ratio'] = np.nan + else: + cut_dict[f'{parallel_hand}_pb_fwhm'] = fwhms[i_pb] + cut_dict[f'{parallel_hand}_pb_center'] = centers[i_pb] + + left_first_sl_amp = amps[i_pb - 1] + right_first_sl_amp = amps[i_pb + 1] + cut_dict[f'{parallel_hand}_first_side_lobe_ratio'] = left_first_sl_amp / right_first_sl_amp else: - pb_problem = i_pb != n_peaks//2 - - if pb_problem: - logger.warning(f'Cannot reliably identify primary beam for {datalabel}.') + cut_dict[f'{parallel_hand}_amp_fit_pars'] = np.full(3, np.nan) + cut_dict[f'{parallel_hand}_amp_fit'] = np.full_like(y_data, np.nan) + cut_dict[f'{parallel_hand}_n_peaks'] = 1 cut_dict[f'{parallel_hand}_pb_fwhm'] = np.nan cut_dict[f'{parallel_hand}_pb_center'] = np.nan cut_dict[f'{parallel_hand}_first_side_lobe_ratio'] = np.nan - else: - cut_dict[f'{parallel_hand}_pb_fwhm'] = fwhms[i_pb] - cut_dict[f'{parallel_hand}_pb_center'] = centers[i_pb] - left_first_sl_amp = amps[i_pb-1] - right_first_sl_amp = amps[i_pb+1] - cut_dict[f'{parallel_hand}_first_side_lobe_ratio'] = left_first_sl_amp / right_first_sl_amp + cut_dict[f'{parallel_hand}_fit_succeeded'] = fit_succeeded return cut_list From 09c43bf6b81fa22d744adfcfde084b387b395cee Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Mon, 22 Dec 2025 16:29:54 -0700 Subject: [PATCH 24/95] Reordered and renamed functions for clearer reading. --- src/astrohack/core/beamcut.py | 429 ++++++++++++++++++---------------- 1 file changed, 222 insertions(+), 207 deletions(-) diff --git a/src/astrohack/core/beamcut.py b/src/astrohack/core/beamcut.py index aeb64103..9291ec6b 100644 --- a/src/astrohack/core/beamcut.py +++ b/src/astrohack/core/beamcut.py @@ -16,7 +16,9 @@ lnbr = '\n' spc = ' ' - +########################################################### +### Processing Chunks +########################################################### def process_beamcut_chunk(beamcut_chunk_params): ddi = beamcut_chunk_params["this_ddi"] antenna = beamcut_chunk_params["this_ant"] @@ -47,23 +49,92 @@ def process_beamcut_chunk(beamcut_chunk_params): visibilities = this_xds.VIS.values weights = this_xds.WEIGHT.values - cut_list = extract_cuts_from_visibilities(scan_list, scan_time_ranges, time_axis, corr_axis, lm_offsets, - visibilities, weights) - beamcut_fit(cut_list, telescope, summary, datalabel) + cut_list = _extract_cuts_from_visibilities(scan_list, scan_time_ranges, time_axis, corr_axis, lm_offsets, + visibilities, weights) + _beamcut_multi_lobes_gaussian_fit(cut_list, telescope, summary, datalabel) destination = beamcut_chunk_params['destination'] if destination is not None: logger.info(f'Producing plots for {datalabel}') - plot_cuts_in_amplitude(cut_list, summary, beamcut_chunk_params) - plot_cuts_in_attenuation(cut_list, summary, beamcut_chunk_params) + plot_beamcut_in_amplitude_chunk(cut_list, summary, beamcut_chunk_params) + plot_beamcut_in_attenuation_chunk(cut_list, summary, beamcut_chunk_params) create_report_chunk(cut_list, summary, beamcut_chunk_params) logger.info(f'Completed plots for {datalabel}') - return create_output_datatree(cut_list, antenna, ddi, summary) + return _create_output_datatree(cut_list, antenna, ddi, summary) + + +def plot_beamcut_in_amplitude_chunk(cut_list, summary, par_dict): + # Init + n_cuts = len(cut_list) + + # Loop over cuts + fig, axes = create_figure_and_axes([12, 1+n_cuts*4], [n_cuts, 2]) + for icut, cut_dict in enumerate(cut_list): + _plot_single_cut_in_amplitude(cut_dict, axes[icut, :], par_dict) + + # Header creation + title = _create_beamcut_header(summary, par_dict) + + filename = _file_name_factory('amplitude', par_dict) + close_figure(fig, title, filename, par_dict['dpi'], par_dict['display']) + + +def plot_beamcut_in_attenuation_chunk(cut_list, summary, par_dict): + # Init + n_cuts = len(cut_list) + + # Loop over cuts + fig, axes = create_figure_and_axes([6, 1+n_cuts*4], [n_cuts, 1]) + for icut, cut_dict in enumerate(cut_list): + _plot_single_cut_in_attenuation(cut_dict, axes[icut], par_dict) + + # Header creation + title = _create_beamcut_header(summary, par_dict) + + filename = _file_name_factory('attenuation', par_dict) + close_figure(fig, title, filename, par_dict['dpi'], par_dict['display']) -def create_output_datatree(cut_list, antenna, ddi, summary): +def create_report_chunk(cut_list, summary, par_dict, spacing=2, item_marker='-', precision=3): + outstr = f'{item_marker}{spc}' + lm_unit = par_dict['lm_unit'] + lm_fac = convert_unit('rad', lm_unit, 'trigonometric') + + items = ['Id', f'Center [{lm_unit}]', 'Amplitude [ ]', f'FWHM [{lm_unit}]', 'Attenuation [dB]'] + outstr += _create_beamcut_header(summary, par_dict) + 2 * lnbr + for icut, cut_dict in enumerate(cut_list): + sub_title = _make_parallel_hand_sub_title(cut_dict["direction"], cut_dict["time_string"]) + for i_corr, parallel_hand in enumerate(cut_dict['available_corrs']): + outstr += f'{spacing*spc}{item_marker}{spc}{parallel_hand} {sub_title}, Beam fit results:{lnbr}' + table = create_pretty_table(items, 'c') + fit_pars = cut_dict[f'{parallel_hand}_amp_fit_pars'] + centers = fit_pars[0::3] + amps = fit_pars[1::3] + fwhms = fit_pars[2::3] + max_amp = np.max(cut_dict[f'{parallel_hand}_amplitude']) + + for i_peak in range(cut_dict[f'{parallel_hand}_n_peaks']): + + table.add_row([f'{i_peak+1})', # Id + f'{lm_fac*centers[i_peak]:.{precision}f}', # center + f'{amps[i_peak]:.{precision}f}', # Amp + f'{lm_fac*fwhms[i_peak]:.{precision}f}', # FWHM + f'{to_db(amps[i_peak]/max_amp):.{precision}f}', # Attenuation + ]) + for line in table.get_string().splitlines(): + outstr += 2*spacing*spc+line+lnbr + outstr += lnbr + + with open(_file_name_factory('report', par_dict), 'w') as outfile: + outfile.write(outstr) + + +########################################################### +### Data IO +########################################################### +def _create_output_datatree(cut_list, antenna, ddi, summary): this_branch = xr.DataTree(name=f'{antenna}-{ddi}') for icut, cut_dict in enumerate(cut_list): xds = xr.Dataset() @@ -97,7 +168,23 @@ def create_output_datatree(cut_list, antenna, ddi, summary): return this_branch -def time_scan_selection(scan_time_ranges, time_axis): +def _file_name_factory(file_type, par_dict): + destination = par_dict['destination'] + antenna = par_dict['this_ant'] + ddi = par_dict['this_ddi'] + if file_type in ['attenuation', 'amplitude']: + ext = 'png' + elif file_type == 'report': + ext = 'txt' + else: + raise ValueError('Invalid file type') + return f'{destination}/beamcut_{file_type}_{antenna}_{ddi}.{ext}' + + +########################################################### +### Data extraction +########################################################### +def _time_scan_selection(scan_time_ranges, time_axis): time_selections = [] for scan_time_range in scan_time_ranges: time_selection = np.logical_and(time_axis >= scan_time_range[0], @@ -106,8 +193,8 @@ def time_scan_selection(scan_time_ranges, time_axis): return time_selections -def extract_cuts_from_visibilities(scan_list, scan_time_ranges, time_axis, corr_axis, lm_offsets, - visibilities, weights): +def _extract_cuts_from_visibilities(scan_list, scan_time_ranges, time_axis, corr_axis, lm_offsets, + visibilities, weights): cut_list = [] nchan = visibilities.shape[1] fchan = 4 @@ -119,8 +206,8 @@ def extract_cuts_from_visibilities(scan_list, scan_time_ranges, time_axis, corr_ time = time_axis[time_selection] this_lm_offsets = lm_offsets[time_selection, :] - lm_angle, lm_dist, direction, xlabel = cut_direction_determination_and_label_creation(this_lm_offsets) - hands_dict = get_parallel_hand_indexes(corr_axis) + lm_angle, lm_dist, direction, xlabel = _cut_direction_determination_and_label_creation(this_lm_offsets) + hands_dict = _get_parallel_hand_indexes(corr_axis) avg_vis = np.average(visibilities[time_selection, fchan:lchan, :], axis=1, weights=weights[time_selection, fchan:lchan, :]) @@ -156,7 +243,7 @@ def extract_cuts_from_visibilities(scan_list, scan_time_ranges, time_axis, corr_ return cut_list -def cut_direction_determination_and_label_creation(lm_offsets, angle_unit='deg'): +def _cut_direction_determination_and_label_creation(lm_offsets, angle_unit='deg'): dx = lm_offsets[-1, 0] - lm_offsets[0, 0] dy = lm_offsets[-1, 1] - lm_offsets[0, 1] lm_dist = np.sqrt(lm_offsets[:, 0] ** 2 + lm_offsets[:, 1] ** 2) @@ -208,7 +295,7 @@ def cut_direction_determination_and_label_creation(lm_offsets, angle_unit='deg') return lm_angle, lm_dist, direction, xlabel -def get_parallel_hand_indexes(corr_axis): +def _get_parallel_hand_indexes(corr_axis): if 'L' in corr_axis[0] or 'R' in corr_axis[0]: parallel_hands = ['RR', 'LL'] else: @@ -222,130 +309,15 @@ def get_parallel_hand_indexes(corr_axis): return hands_dict -def add_secondary_beam_hpbw_x_axis_to_plot(pb_fwhm, ax): - if np.isnan(pb_fwhm): - return - sec_x_axis = ax.secondary_xaxis('top', functions=(lambda x: x*1.0, lambda xb: 1*xb)) - sec_x_axis.set_xlabel('Offset in Primary Beam HPBWs\n') - sec_x_axis.set_xticks([]) - y_min, y_max = ax.get_ylim() - x_lims = ax.get_xlim() - pb_min, pb_max = np.ceil(x_lims/pb_fwhm) - beam_offsets = np.arange(pb_min, pb_max, 1, dtype=int) - - for itk in beam_offsets: - ax.axvline(itk * pb_fwhm, color='k', linestyle='--', linewidth=0.5) - ax.text(itk*pb_fwhm, y_max, f'{itk:d}', va='bottom', ha='center') - - -def add_lobe_identification_to_plot(ax, centers, peaks, y_off, attenunation_plot=False): - if attenunation_plot: - plot_peaks = to_db(peaks/np.max(peaks)) # maximum of peaks is always the PB - else: - plot_peaks = peaks - - for i_peak, peak in enumerate(plot_peaks): - ax.text(centers[i_peak], peak+y_off, f'{i_peak+1})', ha='center', va='bottom') - - -def make_parallel_hand_sub_title(direction, time_string): - return f'{direction}, {time_string} UTC' - - -def plot_single_cut_amp_parallel_corrs(cut_dict, axes, par_dict): - # Init - sub_title = make_parallel_hand_sub_title(cut_dict["direction"], cut_dict["time_string"]) - max_amp = cut_dict['all_corr_ymax'] - y_off = 0.05*max_amp - lm_unit = par_dict['lm_unit'] - lm_fac = convert_unit('rad', lm_unit, 'trigonometric') - - # Loop over correlations - for i_corr, parallel_hand in enumerate(cut_dict['available_corrs']): - # Init labels - this_ax = axes[i_corr] - x_data = lm_fac * cut_dict['lm_dist'] - y_data = cut_dict[f'{parallel_hand}_amplitude'] - fit_data = cut_dict[f'{parallel_hand}_amp_fit'] - xlabel = f'{cut_dict['xlabel']} [{lm_unit}]' - ylabel = f'{parallel_hand} Amplitude [ ]' - - # Call plotting tool - if cut_dict[f'{parallel_hand}_fit_succeeded']: - scatter_plot(this_ax, x_data, xlabel, y_data, ylabel, model=fit_data, model_marker='', title=sub_title, - data_marker='+', residuals_marker='.', model_linestyle='-', data_label=f'{parallel_hand} data', - model_label=f'{parallel_hand} fit', data_color='red', model_color='blue', - residuals_color='black', legend_location='upper right') - - # Add fit peak identifiers - add_lobe_identification_to_plot(this_ax, lm_fac * cut_dict[f'{parallel_hand}_amp_fit_pars'][0::3], - cut_dict[f'{parallel_hand}_amp_fit_pars'][1::3], y_off, - attenunation_plot=False) - else: - scatter_plot(this_ax, x_data, xlabel, y_data, ylabel, title=sub_title, data_marker='+', - data_label=f'{parallel_hand} data', data_color='red', legend_location='upper right') - - # equalize Y scale between correlations - set_y_axis_lims_from_default(this_ax, par_dict['y_scale'], (-y_off, max_amp+3*y_off)) - - add_secondary_beam_hpbw_x_axis_to_plot(cut_dict[f'{parallel_hand}_pb_fwhm']*lm_fac, this_ax) - - # Add bounded box with Beam parameters - add_beam_parameters_box(this_ax, cut_dict[f'{parallel_hand}_pb_center']*lm_fac, - cut_dict[f'{parallel_hand}_pb_fwhm']*lm_fac, - cut_dict[f'{parallel_hand}_first_side_lobe_ratio'], - lm_unit) - - -def add_beam_parameters_box(ax, pb_center, pb_fwhm, sidelobe_ratio, lm_unit, alpha=0.8, x_pos=0.05, y_pos=0.95, - attenuation_plot=False): - if attenuation_plot: - head = 'avg ' - else: - head = '' - pars_str = f'{head}PB off. = {format_value_unit(pb_center, lm_unit, 3)}\n' - pars_str += f'{head}PB FWHM = {format_value_unit(pb_fwhm, lm_unit, 3)}\n' - pars_str += f'{head}FSLR = {format_value_unit(to_db(sidelobe_ratio), 'dB', 2)}' - bounds_box = dict(boxstyle='square', facecolor='white', alpha=alpha) - ax.text(x_pos, y_pos, pars_str, transform=ax.transAxes, verticalalignment='top', - bbox=bounds_box) - - -def create_beamcut_header(summary, par_dict): - azel_unit = par_dict['azel_unit'] - antenna = par_dict['this_ant'] - ddi = par_dict['this_ddi'] - freq_str = format_frequency(summary['spectral']['rep. frequency'], decimal_places=3) - raw_azel = np.array(summary['general']["az el info"]["mean"]) - mean_azel = convert_unit('rad', azel_unit, 'trigonometric') * raw_azel - title = f'Beam cut for {create_dataset_label(antenna, ddi, separator=',')}, ' + r'$\nu$ = ' + f'{freq_str}, ' - title += f'Az ~ {format_value_unit(mean_azel[0], 'deg', decimal_places=0)}, ' - title += f'El ~ {format_value_unit(mean_azel[1], 'deg', decimal_places=0)}' - return title - - -def plot_cuts_in_amplitude(cut_list, summary, par_dict): - # Init - n_cuts = len(cut_list) - - # Loop over cuts - fig, axes = create_figure_and_axes([12, 1+n_cuts*4], [n_cuts, 2]) - for icut, cut_dict in enumerate(cut_list): - plot_single_cut_amp_parallel_corrs(cut_dict, axes[icut, :], par_dict) - - # Header creation - title = create_beamcut_header(summary, par_dict) - - filename = file_name_factory('amplitude', par_dict) - close_figure(fig, title, filename, par_dict['dpi'], par_dict['display']) - - -def gaussian(x_axis, x_off, amp, fwhm): +########################################################### +### Multiple side lobe Gaussian fitting +########################################################### +def _fwhm_gaussian(x_axis, x_off, amp, fwhm): sigma = fwhm / sig_2_fwhm return amp * np.exp(-(x_axis - x_off)**2/(2*sigma**2)) -def build_multi_gaussian_initial_guesses(x_data, y_data, pb_fwhm, min_dist_fraction=1.3): +def _build_multi_gaussian_initial_guesses(x_data, y_data, pb_fwhm, min_dist_fraction=1.3): p0 = [] step = float(np.median(np.diff(x_data))) min_dist = np.abs(min_dist_fraction * pb_fwhm / step) @@ -358,17 +330,17 @@ def build_multi_gaussian_initial_guesses(x_data, y_data, pb_fwhm, min_dist_fract return p0, len(peaks) -def multi_gaussian(xdata, *args): +def _multi_gaussian(xdata, *args): nargs = len(args) if nargs%3 != 0: raise ValueError('Number of arguments should be multiple of 3') y_values = np.zeros_like(xdata) for iarg in range(0, nargs, 3): - y_values += gaussian(xdata, args[iarg], args[iarg+1], args[iarg+2]) + y_values += _fwhm_gaussian(xdata, args[iarg], args[iarg + 1], args[iarg + 2]) return y_values -def beamcut_fit(cut_list, telescope, summary, datalabel): +def _beamcut_multi_lobes_gaussian_fit(cut_list, telescope, summary, datalabel): wavelength = summary["spectral"]["rep. wavelength"] primary_fwhm = 1.2 * wavelength / telescope.diameter @@ -376,10 +348,10 @@ def beamcut_fit(cut_list, telescope, summary, datalabel): x_data = cut_dict['lm_dist'] for parallel_hand in cut_dict['available_corrs']: y_data = cut_dict[f'{parallel_hand}_amplitude'] - p0, n_peaks = build_multi_gaussian_initial_guesses(x_data, y_data, primary_fwhm) + p0, n_peaks = _build_multi_gaussian_initial_guesses(x_data, y_data, primary_fwhm) try: - results = curve_fit(multi_gaussian, x_data, y_data, p0=p0, maxfev=int(5e4)) + results = curve_fit(_multi_gaussian, x_data, y_data, p0=p0, maxfev=int(5e4)) fit_pars = results[0] fit_succeeded = True except RuntimeError: @@ -387,7 +359,7 @@ def beamcut_fit(cut_list, telescope, summary, datalabel): fit_succeeded = False if fit_succeeded: - fit = multi_gaussian(x_data, *fit_pars) + fit = _multi_gaussian(x_data, *fit_pars) centers = fit_pars[0::3] amps = fit_pars[1::3] @@ -444,8 +416,99 @@ def beamcut_fit(cut_list, telescope, summary, datalabel): return cut_list -def plot_single_cut_attenuation_parallel_corrs(cut_dict, ax, par_dict): - sub_title = make_parallel_hand_sub_title(cut_dict["direction"], cut_dict["time_string"]) +########################################################### +### Plot utilities +########################################################### +def _add_secondary_beam_hpbw_x_axis_to_plot(pb_fwhm, ax): + if np.isnan(pb_fwhm): + return + sec_x_axis = ax.secondary_xaxis('top', functions=(lambda x: x*1.0, lambda xb: 1*xb)) + sec_x_axis.set_xlabel('Offset in Primary Beam HPBWs\n') + sec_x_axis.set_xticks([]) + y_min, y_max = ax.get_ylim() + x_lims = ax.get_xlim() + pb_min, pb_max = np.ceil(x_lims/pb_fwhm) + beam_offsets = np.arange(pb_min, pb_max, 1, dtype=int) + + for itk in beam_offsets: + ax.axvline(itk * pb_fwhm, color='k', linestyle='--', linewidth=0.5) + ax.text(itk*pb_fwhm, y_max, f'{itk:d}', va='bottom', ha='center') + + +def _add_lobe_identification_to_plot(ax, centers, peaks, y_off, attenunation_plot=False): + if attenunation_plot: + plot_peaks = to_db(peaks/np.max(peaks)) # maximum of peaks is always the PB + else: + plot_peaks = peaks + + for i_peak, peak in enumerate(plot_peaks): + ax.text(centers[i_peak], peak+y_off, f'{i_peak+1})', ha='center', va='bottom') + + +def _add_beam_parameters_box(ax, pb_center, pb_fwhm, sidelobe_ratio, lm_unit, alpha=0.8, x_pos=0.05, y_pos=0.95, + attenuation_plot=False): + if attenuation_plot: + head = 'avg ' + else: + head = '' + pars_str = f'{head}PB off. = {format_value_unit(pb_center, lm_unit, 3)}\n' + pars_str += f'{head}PB FWHM = {format_value_unit(pb_fwhm, lm_unit, 3)}\n' + pars_str += f'{head}FSLR = {format_value_unit(to_db(sidelobe_ratio), 'dB', 2)}' + bounds_box = dict(boxstyle='square', facecolor='white', alpha=alpha) + ax.text(x_pos, y_pos, pars_str, transform=ax.transAxes, verticalalignment='top', + bbox=bounds_box) + + +########################################################### +### Plot correlation subroutines +########################################################### +def _plot_single_cut_in_amplitude(cut_dict, axes, par_dict): + # Init + sub_title = _make_parallel_hand_sub_title(cut_dict["direction"], cut_dict["time_string"]) + max_amp = cut_dict['all_corr_ymax'] + y_off = 0.05*max_amp + lm_unit = par_dict['lm_unit'] + lm_fac = convert_unit('rad', lm_unit, 'trigonometric') + + # Loop over correlations + for i_corr, parallel_hand in enumerate(cut_dict['available_corrs']): + # Init labels + this_ax = axes[i_corr] + x_data = lm_fac * cut_dict['lm_dist'] + y_data = cut_dict[f'{parallel_hand}_amplitude'] + fit_data = cut_dict[f'{parallel_hand}_amp_fit'] + xlabel = f'{cut_dict['xlabel']} [{lm_unit}]' + ylabel = f'{parallel_hand} Amplitude [ ]' + + # Call plotting tool + if cut_dict[f'{parallel_hand}_fit_succeeded']: + scatter_plot(this_ax, x_data, xlabel, y_data, ylabel, model=fit_data, model_marker='', title=sub_title, + data_marker='+', residuals_marker='.', model_linestyle='-', data_label=f'{parallel_hand} data', + model_label=f'{parallel_hand} fit', data_color='red', model_color='blue', + residuals_color='black', legend_location='upper right') + + # Add fit peak identifiers + _add_lobe_identification_to_plot(this_ax, lm_fac * cut_dict[f'{parallel_hand}_amp_fit_pars'][0::3], + cut_dict[f'{parallel_hand}_amp_fit_pars'][1::3], y_off, + attenunation_plot=False) + else: + scatter_plot(this_ax, x_data, xlabel, y_data, ylabel, title=sub_title, data_marker='+', + data_label=f'{parallel_hand} data', data_color='red', legend_location='upper right') + + # equalize Y scale between correlations + set_y_axis_lims_from_default(this_ax, par_dict['y_scale'], (-y_off, max_amp+3*y_off)) + + _add_secondary_beam_hpbw_x_axis_to_plot(cut_dict[f'{parallel_hand}_pb_fwhm'] * lm_fac, this_ax) + + # Add bounded box with Beam parameters + _add_beam_parameters_box(this_ax, cut_dict[f'{parallel_hand}_pb_center'] * lm_fac, + cut_dict[f'{parallel_hand}_pb_fwhm'] * lm_fac, + cut_dict[f'{parallel_hand}_first_side_lobe_ratio'], + lm_unit) + + +def _plot_single_cut_in_attenuation(cut_dict, ax, par_dict): + sub_title = _make_parallel_hand_sub_title(cut_dict["direction"], cut_dict["time_string"]) lm_unit = par_dict['lm_unit'] lm_fac = convert_unit('rad', lm_unit, 'trigonometric') corr_colors = ['blue', 'red'] @@ -493,76 +556,28 @@ def plot_single_cut_attenuation_parallel_corrs(cut_dict, ax, par_dict): # Add fit peak identifiers first_corr = cut_dict['available_corrs'][0] - add_secondary_beam_hpbw_x_axis_to_plot(cut_dict[f'{first_corr}_pb_fwhm']*lm_fac, ax) + _add_secondary_beam_hpbw_x_axis_to_plot(cut_dict[f'{first_corr}_pb_fwhm'] * lm_fac, ax) # Add bounded box with Beam parameters - add_beam_parameters_box(ax, pb_center*lm_fac, pb_fwhm*lm_fac, fsl_ratio, lm_unit, attenuation_plot=True) + _add_beam_parameters_box(ax, pb_center * lm_fac, pb_fwhm * lm_fac, fsl_ratio, lm_unit, attenuation_plot=True) return -def plot_cuts_in_attenuation(cut_list, summary, par_dict): - # Init - n_cuts = len(cut_list) - - # Loop over cuts - fig, axes = create_figure_and_axes([6, 1+n_cuts*4], [n_cuts, 1]) - for icut, cut_dict in enumerate(cut_list): - plot_single_cut_attenuation_parallel_corrs(cut_dict, axes[icut], par_dict) - - # Header creation - title = create_beamcut_header(summary, par_dict) - - filename = file_name_factory('attenuation', par_dict) - close_figure(fig, title, filename, par_dict['dpi'], par_dict['display']) - - -def create_report_chunk(cut_list, summary, par_dict, spacing=2, item_marker='-', precision=3): - outstr = f'{item_marker}{spc}' - lm_unit = par_dict['lm_unit'] - lm_fac = convert_unit('rad', lm_unit, 'trigonometric') - - items = ['Id', f'Center [{lm_unit}]', 'Amplitude [ ]', f'FWHM [{lm_unit}]', 'Attenuation [dB]'] - outstr += create_beamcut_header(summary, par_dict)+2*lnbr - for icut, cut_dict in enumerate(cut_list): - sub_title = make_parallel_hand_sub_title(cut_dict["direction"], cut_dict["time_string"]) - for i_corr, parallel_hand in enumerate(cut_dict['available_corrs']): - outstr += f'{spacing*spc}{item_marker}{spc}{parallel_hand} {sub_title}, Beam fit results:{lnbr}' - table = create_pretty_table(items, 'c') - fit_pars = cut_dict[f'{parallel_hand}_amp_fit_pars'] - centers = fit_pars[0::3] - amps = fit_pars[1::3] - fwhms = fit_pars[2::3] - max_amp = np.max(cut_dict[f'{parallel_hand}_amplitude']) - - for i_peak in range(cut_dict[f'{parallel_hand}_n_peaks']): - - table.add_row([f'{i_peak+1})', # Id - f'{lm_fac*centers[i_peak]:.{precision}f}', # center - f'{amps[i_peak]:.{precision}f}', # Amp - f'{lm_fac*fwhms[i_peak]:.{precision}f}', # FWHM - f'{to_db(amps[i_peak]/max_amp):.{precision}f}', # Attenuation - ]) - for line in table.get_string().splitlines(): - outstr += 2*spacing*spc+line+lnbr - outstr += lnbr - - with open(file_name_factory('report', par_dict), 'w') as outfile: - outfile.write(outstr) +########################################################### +### Data labeling +########################################################### +def _make_parallel_hand_sub_title(direction, time_string): + return f'{direction}, {time_string} UTC' -def file_name_factory(file_type, par_dict): - destination = par_dict['destination'] +def _create_beamcut_header(summary, par_dict): + azel_unit = par_dict['azel_unit'] antenna = par_dict['this_ant'] ddi = par_dict['this_ddi'] - if file_type in ['attenuation', 'amplitude']: - ext = 'png' - elif file_type == 'report': - ext = 'txt' - else: - raise ValueError('Invalid file type') - return f'{destination}/beamcut_{file_type}_{antenna}_{ddi}.{ext}' - - - - - + freq_str = format_frequency(summary['spectral']['rep. frequency'], decimal_places=3) + raw_azel = np.array(summary['general']["az el info"]["mean"]) + mean_azel = convert_unit('rad', azel_unit, 'trigonometric') * raw_azel + title = f'Beam cut for {create_dataset_label(antenna, ddi, separator=',')}, ' + r'$\nu$ = ' + f'{freq_str}, ' + title += f'Az ~ {format_value_unit(mean_azel[0], 'deg', decimal_places=0)}, ' + title += f'El ~ {format_value_unit(mean_azel[1], 'deg', decimal_places=0)}' + return title From 4de0a32105d808657119a0b50930c74df6dab542 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 23 Dec 2025 09:47:00 -0700 Subject: [PATCH 25/95] Processing chunk pieces have now been cloned to use xarray datatrees. --- src/astrohack/core/beamcut.py | 196 +++++++++++++++++++++++++++++++--- 1 file changed, 183 insertions(+), 13 deletions(-) diff --git a/src/astrohack/core/beamcut.py b/src/astrohack/core/beamcut.py index 9291ec6b..d69270f3 100644 --- a/src/astrohack/core/beamcut.py +++ b/src/astrohack/core/beamcut.py @@ -15,6 +15,7 @@ lnbr = '\n' spc = ' ' +quack_chans = 4 ########################################################### ### Processing Chunks @@ -31,28 +32,31 @@ def process_beamcut_chunk(beamcut_chunk_params): ddi_id=ddi, ) # This assumes that there will be no more than one mapping - this_xds = ant_data_dict[ddi]['map_0'] + input_xds = ant_data_dict[ddi]['map_0'] datalabel = create_dataset_label(antenna, ddi) logger.info(f"processing {datalabel}") - scan_time_ranges = this_xds.attrs['scan_time_ranges'] - scan_list = this_xds.attrs['scan_list'] - summary = this_xds.attrs["summary"] + scan_time_ranges = input_xds.attrs['scan_time_ranges'] + scan_list = input_xds.attrs['scan_list'] + summary = input_xds.attrs["summary"] telescope = get_proper_telescope( summary["general"]["telescope name"], summary["general"]["antenna name"] ) - lm_offsets = this_xds.DIRECTIONAL_COSINES.values - time_axis = this_xds.time.values - corr_axis = this_xds.pol.values - visibilities = this_xds.VIS.values - weights = this_xds.WEIGHT.values + lm_offsets = input_xds.DIRECTIONAL_COSINES.values + time_axis = input_xds.time.values + corr_axis = input_xds.pol.values + visibilities = input_xds.VIS.values + weights = input_xds.WEIGHT.values - cut_list = _extract_cuts_from_visibilities(scan_list, scan_time_ranges, time_axis, corr_axis, lm_offsets, - visibilities, weights) + cut_list = _extract_cuts_from_visibilities_orig(scan_list, scan_time_ranges, time_axis, corr_axis, lm_offsets, + visibilities, weights) + cut_xdtree = _extract_cuts_from_visibilities_xr(input_xds, antenna, ddi) _beamcut_multi_lobes_gaussian_fit(cut_list, telescope, summary, datalabel) + _beamcut_multi_lobes_gaussian_fit_xr(cut_xdtree, datalabel) + destination = beamcut_chunk_params['destination'] if destination is not None: logger.info(f'Producing plots for {datalabel}') @@ -193,8 +197,8 @@ def _time_scan_selection(scan_time_ranges, time_axis): return time_selections -def _extract_cuts_from_visibilities(scan_list, scan_time_ranges, time_axis, corr_axis, lm_offsets, - visibilities, weights): +def _extract_cuts_from_visibilities_orig(scan_list, scan_time_ranges, time_axis, corr_axis, lm_offsets, + visibilities, weights): cut_list = [] nchan = visibilities.shape[1] fchan = 4 @@ -239,10 +243,74 @@ def _extract_cuts_from_visibilities(scan_list, scan_time_ranges, time_axis, corr cut_dict[f'{parallel_hand}_weight'] = avg_wei[:, icorr] cut_dict['all_corr_ymax'] = all_corr_ymax cut_list.append(cut_dict) + #this_branch = this_branch.assign({f'cut_{icut}': xr.DataTree(dataset=xds, name=f'cut_{icut}')}) return cut_list +def _extract_cuts_from_visibilities_xr(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'] + summary = input_xds.attrs["summary"] + + lm_offsets = input_xds.DIRECTIONAL_COSINES.values + time_axis = input_xds.time.values + corr_axis = input_xds.pol.values + visibilities = input_xds.VIS.values + weights = input_xds.WEIGHT.values + + nchan = visibilities.shape[1] + fchan = 4 + lchan = int(nchan - fchan) + for iscan, scan_number in enumerate(scan_list): + scan_time_range = scan_time_ranges[iscan] + time_selection = np.logical_and(time_axis >= scan_time_range[0], + time_axis < scan_time_range[1]) + time = time_axis[time_selection] + this_lm_offsets = lm_offsets[time_selection, :] + + lm_angle, lm_dist, direction, xlabel = _cut_direction_determination_and_label_creation(this_lm_offsets) + hands_dict = _get_parallel_hand_indexes(corr_axis) + + avg_vis = np.average(visibilities[time_selection, fchan:lchan, :], axis=1, + weights=weights[time_selection, fchan:lchan, :]) + avg_wei = np.average(weights[time_selection, fchan:lchan, :], axis=1) + + avg_time = np.average(time)*convert_unit('sec', 'day', 'time') + timestr = astropy.time.Time(avg_time, format='mjd').to_value('iso', subfmt='date_hm') + + xds = xr.Dataset() + coords = {'lm_dist': lm_dist, "time": time} + + xds.attrs.update({ + 'scan_number': scan_number, + 'lm_angle': lm_angle, + 'available_corrs': hands_dict['parallel_hands'], + 'direction': direction, + 'xlabel': xlabel, + 'time_string': timestr, + 'summary': summary, + }) + + xds['lm_offsets'] = xr.DataArray(this_lm_offsets, dims=["time", "lm"]) + all_corr_ymax = 1e-34 + for parallel_hand in hands_dict['parallel_hands']: + icorr = hands_dict[parallel_hand] + amp = np.abs(avg_vis[:, icorr]) + maxamp = np.max(amp) + if maxamp > all_corr_ymax: + all_corr_ymax = maxamp + xds[f'{parallel_hand}_amplitude'] = xr.DataArray(amp, dims='lm_dist') + xds[f'{parallel_hand}_phase'] = xr.DataArray(np.angle(avg_vis[:, icorr]), dims='lm_dist') + xds[f'{parallel_hand}_weight'] = xr.DataArray(avg_wei[:, icorr], dims='lm_dist') + xds.attrs.update({'all_corr_ymax': all_corr_ymax}) + cut_xdtree = cut_xdtree.assign({f'cut_{iscan}': xr.DataTree(dataset=xds.assign_coords(coords), + name=f'cut_{iscan}')}) + + return cut_xdtree + + def _cut_direction_determination_and_label_creation(lm_offsets, angle_unit='deg'): dx = lm_offsets[-1, 0] - lm_offsets[0, 0] dy = lm_offsets[-1, 1] - lm_offsets[0, 1] @@ -416,6 +484,108 @@ def _beamcut_multi_lobes_gaussian_fit(cut_list, telescope, summary, datalabel): return cut_list +def _perform_curvefit_with_given_functions(x_data, y_data, initial_guesses, fit_func, datalabel, maxit=50000): + try: + results = curve_fit(fit_func, x_data, y_data, p0=initial_guesses, maxfev=int(maxit)) + fit_pars = results[0] + return True, fit_pars + except RuntimeError: + logger.warning(f'{fit_func.__name__} fit to lobes failed for {datalabel}.') + return False, np.full_like(initial_guesses, np.nan) + + +def _identify_pb_and_sidelobes_in_fit(datalabel, x_data, fit_pars): + centers = fit_pars[0::3] + amps = fit_pars[1::3] + fwhms = fit_pars[2::3] + + # select fits that are within x_data + x_min = np.min(x_data) + x_max = np.max(x_data) + selection = ~ np.logical_or(centers < x_min, centers > x_max) + + # apply selection + centers = centers[selection] + amps = amps[selection] + fwhms = fwhms[selection] + + # Reconstruct fit metadata + n_peaks = centers.shape[0] + fit_pars = np.zeros((3*n_peaks)) + fit_pars[0::3] = centers + fit_pars[1::3] = amps + fit_pars[2::3] = fwhms + + # This assumes the primary beam is the closest to the center, which is expected + i_pb_cen = np.argmin(np.abs(centers)) + # This assumes the primary beam is the strongest + i_pb_amp = np.argmax(amps) + pb_problem = i_pb_cen != i_pb_amp + + if pb_problem: + logger.warning(f'Cannot reliably identify primary beam for {datalabel}.') + pb_center, pb_fwhm, first_side_lobe_ratio = np.nan, np.nan, np.nan + + else: + pb_fwhm = fwhms[i_pb_cen] + pb_center = centers[i_pb_cen] + + pb_cen = centers[i_pb_cen] + i_closest_to_center = np.argsort(np.abs(centers - pb_cen)) + if centers[i_closest_to_center[1]] < 0: + i_lsl = i_closest_to_center[1] + i_rsl = i_closest_to_center[2] + else: + i_lsl = i_closest_to_center[2] + i_rsl = i_closest_to_center[1] + left_first_sl_amp = amps[i_lsl] + right_first_sl_amp = amps[i_rsl] + first_side_lobe_ratio = left_first_sl_amp / right_first_sl_amp + + return n_peaks, fit_pars, pb_center, pb_fwhm, first_side_lobe_ratio + + + +def _beamcut_multi_lobes_gaussian_fit_xr(cut_xdtree, datalabel): + # Get the summary from the first cut, but it should be equal anyway + summary = cut_xdtree.children['cut_0'].dataset.attrs['summary'] + wavelength = summary["spectral"]["rep. wavelength"] + telescope = get_proper_telescope( + summary["general"]["telescope name"], summary["general"]["antenna name"] + ) + primary_fwhm = 1.2 * wavelength / telescope.diameter + + for cut_xds in cut_xdtree.children.values(): + x_data = cut_xds['lm_dist'].values + for parallel_hand in cut_xds.attrs['available_corrs']: + y_data = cut_xds[f'{parallel_hand}_amplitude'] + + p0, n_peaks = _build_multi_gaussian_initial_guesses(x_data, y_data, primary_fwhm) + fit_succeeded, fit_pars = _perform_curvefit_with_given_functions(x_data, + y_data, + p0, + _multi_gaussian, + f'{datalabel}, corr = {parallel_hand}') + + if fit_succeeded: + fit = _multi_gaussian(x_data, *fit_pars) + n_peaks, fit_pars, pb_center, pb_fwhm, first_side_lobe_ratio = \ + _identify_pb_and_sidelobes_in_fit(datalabel, x_data, fit_pars) + else: + pb_center, pb_fwhm, first_side_lobe_ratio = np.nan, np.nan, np.nan + fit = np.full_like(y_data, np.nan) + + cut_xds.attrs[f'{parallel_hand}_amp_fit_pars'] = fit_pars + cut_xds.attrs[f'{parallel_hand}_n_peaks'] = n_peaks + cut_xds.attrs[f'{parallel_hand}_pb_fwhm'] = pb_fwhm + cut_xds.attrs[f'{parallel_hand}_pb_center'] = pb_center + cut_xds.attrs[f'{parallel_hand}_first_side_lobe_ratio'] = first_side_lobe_ratio + cut_xds.attrs[f'{parallel_hand}_fit_succeeded'] = fit_succeeded + + cut_xds[f'{parallel_hand}_amp_fit'] = xr.DataArray(fit, dims='lm_dist') + return + + ########################################################### ### Plot utilities ########################################################### From 83a4bbead7b496c785ff2f8ce7bf05f11bab7b07 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 23 Dec 2025 09:55:26 -0700 Subject: [PATCH 26/95] Amplitude plots now use xdses directly --- src/astrohack/core/beamcut.py | 73 ++++++++++++++--------------------- 1 file changed, 28 insertions(+), 45 deletions(-) diff --git a/src/astrohack/core/beamcut.py b/src/astrohack/core/beamcut.py index d69270f3..fb61312e 100644 --- a/src/astrohack/core/beamcut.py +++ b/src/astrohack/core/beamcut.py @@ -36,49 +36,31 @@ def process_beamcut_chunk(beamcut_chunk_params): datalabel = create_dataset_label(antenna, ddi) logger.info(f"processing {datalabel}") - scan_time_ranges = input_xds.attrs['scan_time_ranges'] - scan_list = input_xds.attrs['scan_list'] - summary = input_xds.attrs["summary"] - - telescope = get_proper_telescope( - summary["general"]["telescope name"], summary["general"]["antenna name"] - ) - - lm_offsets = input_xds.DIRECTIONAL_COSINES.values - time_axis = input_xds.time.values - corr_axis = input_xds.pol.values - visibilities = input_xds.VIS.values - weights = input_xds.WEIGHT.values - - cut_list = _extract_cuts_from_visibilities_orig(scan_list, scan_time_ranges, time_axis, corr_axis, lm_offsets, - visibilities, weights) cut_xdtree = _extract_cuts_from_visibilities_xr(input_xds, antenna, ddi) - _beamcut_multi_lobes_gaussian_fit(cut_list, telescope, summary, datalabel) _beamcut_multi_lobes_gaussian_fit_xr(cut_xdtree, datalabel) destination = beamcut_chunk_params['destination'] if destination is not None: logger.info(f'Producing plots for {datalabel}') - plot_beamcut_in_amplitude_chunk(cut_list, summary, beamcut_chunk_params) - plot_beamcut_in_attenuation_chunk(cut_list, summary, beamcut_chunk_params) - create_report_chunk(cut_list, summary, beamcut_chunk_params) + plot_beamcut_in_amplitude_chunk(cut_xdtree, beamcut_chunk_params) + #plot_beamcut_in_attenuation_chunk(cut_list, summary, beamcut_chunk_params) + #create_report_chunk(cut_list, summary, beamcut_chunk_params) logger.info(f'Completed plots for {datalabel}') - return _create_output_datatree(cut_list, antenna, ddi, summary) - + return cut_xdtree -def plot_beamcut_in_amplitude_chunk(cut_list, summary, par_dict): - # Init - n_cuts = len(cut_list) +def plot_beamcut_in_amplitude_chunk(cut_xdtree, par_dict): + n_cuts = len(cut_xdtree.children.values()) # Loop over cuts fig, axes = create_figure_and_axes([12, 1+n_cuts*4], [n_cuts, 2]) - for icut, cut_dict in enumerate(cut_list): - _plot_single_cut_in_amplitude(cut_dict, axes[icut, :], par_dict) + for icut, cut_xds in enumerate(cut_xdtree.children.values()): + _plot_single_cut_in_amplitude(cut_xds, axes[icut, :], par_dict) # Header creation + summary = cut_xdtree.children['cut_0'].attrs['summary'] title = _create_beamcut_header(summary, par_dict) filename = _file_name_factory('amplitude', par_dict) @@ -545,10 +527,9 @@ def _identify_pb_and_sidelobes_in_fit(datalabel, x_data, fit_pars): return n_peaks, fit_pars, pb_center, pb_fwhm, first_side_lobe_ratio - def _beamcut_multi_lobes_gaussian_fit_xr(cut_xdtree, datalabel): # Get the summary from the first cut, but it should be equal anyway - summary = cut_xdtree.children['cut_0'].dataset.attrs['summary'] + summary = cut_xdtree.children['cut_0'].attrs['summary'] wavelength = summary["spectral"]["rep. wavelength"] telescope = get_proper_telescope( summary["general"]["telescope name"], summary["general"]["antenna name"] @@ -632,34 +613,34 @@ def _add_beam_parameters_box(ax, pb_center, pb_fwhm, sidelobe_ratio, lm_unit, al ########################################################### ### Plot correlation subroutines ########################################################### -def _plot_single_cut_in_amplitude(cut_dict, axes, par_dict): +def _plot_single_cut_in_amplitude(cut_xds, axes, par_dict): # Init - sub_title = _make_parallel_hand_sub_title(cut_dict["direction"], cut_dict["time_string"]) - max_amp = cut_dict['all_corr_ymax'] + sub_title = _make_parallel_hand_sub_title(cut_xds.attrs) + max_amp = cut_xds.attrs['all_corr_ymax'] y_off = 0.05*max_amp lm_unit = par_dict['lm_unit'] lm_fac = convert_unit('rad', lm_unit, 'trigonometric') # Loop over correlations - for i_corr, parallel_hand in enumerate(cut_dict['available_corrs']): + for i_corr, parallel_hand in enumerate(cut_xds.attrs['available_corrs']): # Init labels this_ax = axes[i_corr] - x_data = lm_fac * cut_dict['lm_dist'] - y_data = cut_dict[f'{parallel_hand}_amplitude'] - fit_data = cut_dict[f'{parallel_hand}_amp_fit'] - xlabel = f'{cut_dict['xlabel']} [{lm_unit}]' + x_data = lm_fac * cut_xds['lm_dist'].values + y_data = cut_xds[f'{parallel_hand}_amplitude'].values + fit_data = cut_xds[f'{parallel_hand}_amp_fit'].values + xlabel = f'{cut_xds.attrs['xlabel']} [{lm_unit}]' ylabel = f'{parallel_hand} Amplitude [ ]' # Call plotting tool - if cut_dict[f'{parallel_hand}_fit_succeeded']: + if cut_xds.attrs[f'{parallel_hand}_fit_succeeded']: scatter_plot(this_ax, x_data, xlabel, y_data, ylabel, model=fit_data, model_marker='', title=sub_title, data_marker='+', residuals_marker='.', model_linestyle='-', data_label=f'{parallel_hand} data', model_label=f'{parallel_hand} fit', data_color='red', model_color='blue', residuals_color='black', legend_location='upper right') # Add fit peak identifiers - _add_lobe_identification_to_plot(this_ax, lm_fac * cut_dict[f'{parallel_hand}_amp_fit_pars'][0::3], - cut_dict[f'{parallel_hand}_amp_fit_pars'][1::3], y_off, + _add_lobe_identification_to_plot(this_ax, lm_fac * cut_xds.attrs[f'{parallel_hand}_amp_fit_pars'][0::3], + cut_xds.attrs[f'{parallel_hand}_amp_fit_pars'][1::3], y_off, attenunation_plot=False) else: scatter_plot(this_ax, x_data, xlabel, y_data, ylabel, title=sub_title, data_marker='+', @@ -668,12 +649,12 @@ def _plot_single_cut_in_amplitude(cut_dict, axes, par_dict): # equalize Y scale between correlations set_y_axis_lims_from_default(this_ax, par_dict['y_scale'], (-y_off, max_amp+3*y_off)) - _add_secondary_beam_hpbw_x_axis_to_plot(cut_dict[f'{parallel_hand}_pb_fwhm'] * lm_fac, this_ax) + _add_secondary_beam_hpbw_x_axis_to_plot(cut_xds.attrs[f'{parallel_hand}_pb_fwhm'] * lm_fac, this_ax) # Add bounded box with Beam parameters - _add_beam_parameters_box(this_ax, cut_dict[f'{parallel_hand}_pb_center'] * lm_fac, - cut_dict[f'{parallel_hand}_pb_fwhm'] * lm_fac, - cut_dict[f'{parallel_hand}_first_side_lobe_ratio'], + _add_beam_parameters_box(this_ax, cut_xds.attrs[f'{parallel_hand}_pb_center'] * lm_fac, + cut_xds.attrs[f'{parallel_hand}_pb_fwhm'] * lm_fac, + cut_xds.attrs[f'{parallel_hand}_first_side_lobe_ratio'], lm_unit) @@ -736,7 +717,9 @@ def _plot_single_cut_in_attenuation(cut_dict, ax, par_dict): ########################################################### ### Data labeling ########################################################### -def _make_parallel_hand_sub_title(direction, time_string): +def _make_parallel_hand_sub_title(attributes): + direction = attributes['direction'] + time_string = attributes['time_string'] return f'{direction}, {time_string} UTC' From 98f8100d7f18f8611a062884b66263299d0cc663 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 23 Dec 2025 10:08:36 -0700 Subject: [PATCH 27/95] Attenuation plots and reports now use xdses directly, removed old code using lists of dicts. --- src/astrohack/core/beamcut.py | 189 ++++++---------------------------- 1 file changed, 30 insertions(+), 159 deletions(-) diff --git a/src/astrohack/core/beamcut.py b/src/astrohack/core/beamcut.py index fb61312e..b4efa8cf 100644 --- a/src/astrohack/core/beamcut.py +++ b/src/astrohack/core/beamcut.py @@ -36,19 +36,18 @@ def process_beamcut_chunk(beamcut_chunk_params): datalabel = create_dataset_label(antenna, ddi) logger.info(f"processing {datalabel}") - cut_xdtree = _extract_cuts_from_visibilities_xr(input_xds, antenna, ddi) + cut_xdtree = _extract_cuts_from_visibilities(input_xds, antenna, ddi) - _beamcut_multi_lobes_gaussian_fit_xr(cut_xdtree, datalabel) + _beamcut_multi_lobes_gaussian_fit(cut_xdtree, datalabel) destination = beamcut_chunk_params['destination'] if destination is not None: logger.info(f'Producing plots for {datalabel}') plot_beamcut_in_amplitude_chunk(cut_xdtree, beamcut_chunk_params) - #plot_beamcut_in_attenuation_chunk(cut_list, summary, beamcut_chunk_params) - #create_report_chunk(cut_list, summary, beamcut_chunk_params) + plot_beamcut_in_attenuation_chunk(cut_xdtree, beamcut_chunk_params) + create_report_chunk(cut_xdtree, beamcut_chunk_params) logger.info(f'Completed plots for {datalabel}') - return cut_xdtree @@ -67,41 +66,41 @@ def plot_beamcut_in_amplitude_chunk(cut_xdtree, par_dict): close_figure(fig, title, filename, par_dict['dpi'], par_dict['display']) -def plot_beamcut_in_attenuation_chunk(cut_list, summary, par_dict): - # Init - n_cuts = len(cut_list) - +def plot_beamcut_in_attenuation_chunk(cut_xdtree, par_dict): + n_cuts = len(cut_xdtree.children.values()) # Loop over cuts fig, axes = create_figure_and_axes([6, 1+n_cuts*4], [n_cuts, 1]) - for icut, cut_dict in enumerate(cut_list): - _plot_single_cut_in_attenuation(cut_dict, axes[icut], par_dict) + for icut, cut_xds in enumerate(cut_xdtree.children.values()): + _plot_single_cut_in_attenuation(cut_xds, axes[icut], par_dict) # Header creation + summary = cut_xdtree.children['cut_0'].attrs['summary'] title = _create_beamcut_header(summary, par_dict) filename = _file_name_factory('attenuation', par_dict) close_figure(fig, title, filename, par_dict['dpi'], par_dict['display']) -def create_report_chunk(cut_list, summary, par_dict, spacing=2, item_marker='-', precision=3): +def create_report_chunk(cut_xdtree, par_dict, spacing=2, item_marker='-', precision=3): outstr = f'{item_marker}{spc}' lm_unit = par_dict['lm_unit'] lm_fac = convert_unit('rad', lm_unit, 'trigonometric') + summary = cut_xdtree.children['cut_0'].attrs['summary'] items = ['Id', f'Center [{lm_unit}]', 'Amplitude [ ]', f'FWHM [{lm_unit}]', 'Attenuation [dB]'] outstr += _create_beamcut_header(summary, par_dict) + 2 * lnbr - for icut, cut_dict in enumerate(cut_list): - sub_title = _make_parallel_hand_sub_title(cut_dict["direction"], cut_dict["time_string"]) - for i_corr, parallel_hand in enumerate(cut_dict['available_corrs']): + for icut, cut_xds in enumerate(cut_xdtree.children.values()): + sub_title = _make_parallel_hand_sub_title(cut_xds.attrs) + for i_corr, parallel_hand in enumerate(cut_xds.attrs['available_corrs']): outstr += f'{spacing*spc}{item_marker}{spc}{parallel_hand} {sub_title}, Beam fit results:{lnbr}' table = create_pretty_table(items, 'c') - fit_pars = cut_dict[f'{parallel_hand}_amp_fit_pars'] + fit_pars = cut_xds.attrs[f'{parallel_hand}_amp_fit_pars'] centers = fit_pars[0::3] amps = fit_pars[1::3] fwhms = fit_pars[2::3] - max_amp = np.max(cut_dict[f'{parallel_hand}_amplitude']) + max_amp = np.max(cut_xds[f'{parallel_hand}_amplitude'].values) - for i_peak in range(cut_dict[f'{parallel_hand}_n_peaks']): + for i_peak in range(cut_xds.attrs[f'{parallel_hand}_n_peaks']): table.add_row([f'{i_peak+1})', # Id f'{lm_fac*centers[i_peak]:.{precision}f}', # center @@ -179,58 +178,7 @@ def _time_scan_selection(scan_time_ranges, time_axis): return time_selections -def _extract_cuts_from_visibilities_orig(scan_list, scan_time_ranges, time_axis, corr_axis, lm_offsets, - visibilities, weights): - cut_list = [] - nchan = visibilities.shape[1] - fchan = 4 - lchan = int(nchan - fchan) - for iscan, scan_number in enumerate(scan_list): - scan_time_range = scan_time_ranges[iscan] - time_selection = np.logical_and(time_axis >= scan_time_range[0], - time_axis < scan_time_range[1]) - time = time_axis[time_selection] - this_lm_offsets = lm_offsets[time_selection, :] - - lm_angle, lm_dist, direction, xlabel = _cut_direction_determination_and_label_creation(this_lm_offsets) - hands_dict = _get_parallel_hand_indexes(corr_axis) - - avg_vis = np.average(visibilities[time_selection, fchan:lchan, :], axis=1, - weights=weights[time_selection, fchan:lchan, :]) - avg_wei = np.average(weights[time_selection, fchan:lchan, :], axis=1) - - avg_time = np.average(time)*convert_unit('sec', 'day', 'time') - timestr = astropy.time.Time(avg_time, format='mjd').to_value('iso', subfmt='date_hm') - - cut_dict = { - 'scan_number': scan_number, - 'time': time, - 'lm_offsets': this_lm_offsets, - 'lm_angle': lm_angle, - 'lm_dist': lm_dist, - 'available_corrs': hands_dict['parallel_hands'], - 'direction': direction, - 'xlabel': xlabel, - 'time_string': timestr - } - all_corr_ymax = 1e-34 - for parallel_hand in hands_dict['parallel_hands']: - icorr = hands_dict[parallel_hand] - amp = np.abs(avg_vis[:, icorr]) - maxamp = np.max(amp) - if maxamp > all_corr_ymax: - all_corr_ymax = maxamp - cut_dict[f'{parallel_hand}_amplitude'] = amp - cut_dict[f'{parallel_hand}_phase'] = np.angle(avg_vis[:, icorr]) - cut_dict[f'{parallel_hand}_weight'] = avg_wei[:, icorr] - cut_dict['all_corr_ymax'] = all_corr_ymax - cut_list.append(cut_dict) - #this_branch = this_branch.assign({f'cut_{icut}': xr.DataTree(dataset=xds, name=f'cut_{icut}')}) - - return cut_list - - -def _extract_cuts_from_visibilities_xr(input_xds, antenna, ddi): +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'] @@ -390,82 +338,6 @@ def _multi_gaussian(xdata, *args): return y_values -def _beamcut_multi_lobes_gaussian_fit(cut_list, telescope, summary, datalabel): - wavelength = summary["spectral"]["rep. wavelength"] - primary_fwhm = 1.2 * wavelength / telescope.diameter - - for cut_dict in cut_list: - x_data = cut_dict['lm_dist'] - for parallel_hand in cut_dict['available_corrs']: - y_data = cut_dict[f'{parallel_hand}_amplitude'] - p0, n_peaks = _build_multi_gaussian_initial_guesses(x_data, y_data, primary_fwhm) - - try: - results = curve_fit(_multi_gaussian, x_data, y_data, p0=p0, maxfev=int(5e4)) - fit_pars = results[0] - fit_succeeded = True - except RuntimeError: - logger.warning(f'Gaussian fit to lobes failed for {datalabel}, corr = {parallel_hand}.') - fit_succeeded = False - - if fit_succeeded: - fit = _multi_gaussian(x_data, *fit_pars) - - centers = fit_pars[0::3] - amps = fit_pars[1::3] - fwhms = fit_pars[2::3] - - # select fits that are within x_data - x_min = np.min(x_data) - x_max = np.max(x_data) - selection = ~ np.logical_or(centers < x_min, centers > x_max) - - centers = centers[selection] - amps = amps[selection] - fwhms = fwhms[selection] - - n_peaks = centers.shape[0] - fit_pars = np.zeros((3*n_peaks)) - fit_pars[0::3] = centers - fit_pars[1::3] = amps - fit_pars[2::3] = fwhms - - cut_dict[f'{parallel_hand}_amp_fit_pars'] = fit_pars - cut_dict[f'{parallel_hand}_amp_fit'] = fit - cut_dict[f'{parallel_hand}_n_peaks'] = n_peaks - - i_pb = np.argmax(amps) - - if n_peaks%2 == 0: - pb_problem = i_pb not in [n_peaks//2, n_peaks//2 - 1] - else: - pb_problem = i_pb != n_peaks//2 - - if pb_problem: - logger.warning(f'Cannot reliably identify primary beam for {datalabel}.') - cut_dict[f'{parallel_hand}_pb_fwhm'] = np.nan - cut_dict[f'{parallel_hand}_pb_center'] = np.nan - cut_dict[f'{parallel_hand}_first_side_lobe_ratio'] = np.nan - else: - cut_dict[f'{parallel_hand}_pb_fwhm'] = fwhms[i_pb] - cut_dict[f'{parallel_hand}_pb_center'] = centers[i_pb] - - left_first_sl_amp = amps[i_pb - 1] - right_first_sl_amp = amps[i_pb + 1] - cut_dict[f'{parallel_hand}_first_side_lobe_ratio'] = left_first_sl_amp / right_first_sl_amp - else: - cut_dict[f'{parallel_hand}_amp_fit_pars'] = np.full(3, np.nan) - cut_dict[f'{parallel_hand}_amp_fit'] = np.full_like(y_data, np.nan) - cut_dict[f'{parallel_hand}_n_peaks'] = 1 - cut_dict[f'{parallel_hand}_pb_fwhm'] = np.nan - cut_dict[f'{parallel_hand}_pb_center'] = np.nan - cut_dict[f'{parallel_hand}_first_side_lobe_ratio'] = np.nan - - cut_dict[f'{parallel_hand}_fit_succeeded'] = fit_succeeded - - return cut_list - - def _perform_curvefit_with_given_functions(x_data, y_data, initial_guesses, fit_func, datalabel, maxit=50000): try: results = curve_fit(fit_func, x_data, y_data, p0=initial_guesses, maxfev=int(maxit)) @@ -527,7 +399,7 @@ def _identify_pb_and_sidelobes_in_fit(datalabel, x_data, fit_pars): return n_peaks, fit_pars, pb_center, pb_fwhm, first_side_lobe_ratio -def _beamcut_multi_lobes_gaussian_fit_xr(cut_xdtree, datalabel): +def _beamcut_multi_lobes_gaussian_fit(cut_xdtree, datalabel): # Get the summary from the first cut, but it should be equal anyway summary = cut_xdtree.children['cut_0'].attrs['summary'] wavelength = summary["spectral"]["rep. wavelength"] @@ -658,8 +530,8 @@ def _plot_single_cut_in_amplitude(cut_xds, axes, par_dict): lm_unit) -def _plot_single_cut_in_attenuation(cut_dict, ax, par_dict): - sub_title = _make_parallel_hand_sub_title(cut_dict["direction"], cut_dict["time_string"]) +def _plot_single_cut_in_attenuation(cut_xds, ax, par_dict): + sub_title = _make_parallel_hand_sub_title(cut_xds.attrs) lm_unit = par_dict['lm_unit'] lm_fac = convert_unit('rad', lm_unit, 'trigonometric') corr_colors = ['blue', 'red'] @@ -668,15 +540,15 @@ def _plot_single_cut_in_attenuation(cut_dict, ax, par_dict): pb_center = 0.0 pb_fwhm = 0.0 fsl_ratio = 0.0 - xlabel = f'{cut_dict['xlabel']} [{lm_unit}]' + xlabel = f'{cut_xds.attrs['xlabel']} [{lm_unit}]' ylabel = f'Attenuation [dB]' # Loop over correlations n_data = 0 - for i_corr, parallel_hand in enumerate(cut_dict['available_corrs']): + for i_corr, parallel_hand in enumerate(cut_xds.attrs['available_corrs']): # Init labels - x_data = lm_fac * cut_dict['lm_dist'] - amps = cut_dict[f'{parallel_hand}_amplitude'] + x_data = lm_fac * cut_xds['lm_dist'].values + amps = cut_xds[f'{parallel_hand}_amplitude'].values max_amp = np.max(amps) y_data = to_db(amps/max_amp) y_min = np.min(y_data) @@ -686,9 +558,9 @@ def _plot_single_cut_in_attenuation(cut_dict, ax, par_dict): ax.plot(x_data, y_data, label=parallel_hand, color=corr_colors[i_corr], marker='.', ls='') if not np.isnan(pb_center): - pb_center += cut_dict[f'{parallel_hand}_pb_center'] - pb_fwhm += cut_dict[f'{parallel_hand}_pb_fwhm'] - fsl_ratio += cut_dict[f'{parallel_hand}_first_side_lobe_ratio'] + pb_center += cut_xds.attrs[f'{parallel_hand}_pb_center'] + pb_fwhm += cut_xds.attrs[f'{parallel_hand}_pb_fwhm'] + fsl_ratio += cut_xds.attrs[f'{parallel_hand}_first_side_lobe_ratio'] n_data += 1 ax.set_xlabel(xlabel) @@ -696,7 +568,6 @@ def _plot_single_cut_in_attenuation(cut_dict, ax, par_dict): ax.set_title(sub_title) ax.legend(loc='upper right') - n_corrs = len(cut_dict['available_corrs']) pb_center /= n_data pb_fwhm /= n_data fsl_ratio /= n_data @@ -705,9 +576,9 @@ def _plot_single_cut_in_attenuation(cut_dict, ax, par_dict): set_y_axis_lims_from_default(ax, par_dict['y_scale'], (min_attenuation-y_off, y_off )) # Add fit peak identifiers - first_corr = cut_dict['available_corrs'][0] + first_corr = cut_xds.attrs['available_corrs'][0] - _add_secondary_beam_hpbw_x_axis_to_plot(cut_dict[f'{first_corr}_pb_fwhm'] * lm_fac, ax) + _add_secondary_beam_hpbw_x_axis_to_plot(cut_xds.attrs[f'{first_corr}_pb_fwhm'] * lm_fac, ax) # Add bounded box with Beam parameters _add_beam_parameters_box(ax, pb_center * lm_fac, pb_fwhm * lm_fac, fsl_ratio, lm_unit, attenuation_plot=True) From c1971ffb5401684c5edabbc367856c7e0bf5ddea Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 23 Dec 2025 14:47:10 -0700 Subject: [PATCH 28/95] Implemented skeleton of AstrohackBeamcutFile using datatrees as the backbone. --- src/astrohack/beamcut.py | 13 ++-- src/astrohack/beamcut_mds.py | 145 +++++++++++++++++++++++++++++++++++ 2 files changed, 151 insertions(+), 7 deletions(-) create mode 100644 src/astrohack/beamcut_mds.py diff --git a/src/astrohack/beamcut.py b/src/astrohack/beamcut.py index 050e14a1..832f7d29 100644 --- a/src/astrohack/beamcut.py +++ b/src/astrohack/beamcut.py @@ -6,6 +6,7 @@ from astrohack.utils import get_default_file_name, add_caller_and_version_to_dict from astrohack.utils.file import overwrite_file, check_if_file_can_be_opened from astrohack.utils.graph import compute_graph +from astrohack.beamcut_mds import AstrohackBeamcutFile import xarray as xr from typing import Union, List @@ -64,19 +65,17 @@ def beamcut( for xdtree in graph_results: ant , ddi = xdtree.name.split('-') - if ant in root.children.keys(): - ant = root.children[ant].assign({ddi: xdtree}) + if ant in root.keys(): + ant = root.children[ant].update({ddi: xdtree}) else: ant_tree = xr.DataTree(name=ant, children={ddi: xdtree}) root = root.assign({ant: ant_tree}) root.to_zarr(beamcut_params["beamcut_name"], mode="w", consolidated=True) - # beamcut_mds = AstrohackbeamcutFile(beamcut_params["beamcut_name"]) - # beamcut_mds.open() - # - # return beamcut_mds - return None + beamcut_mds = AstrohackBeamcutFile(beamcut_params["beamcut_name"]) + beamcut_mds.open() + return beamcut_mds else: logger.warning("No data to process") return None \ No newline at end of file diff --git a/src/astrohack/beamcut_mds.py b/src/astrohack/beamcut_mds.py new file mode 100644 index 00000000..58e69a17 --- /dev/null +++ b/src/astrohack/beamcut_mds.py @@ -0,0 +1,145 @@ +import xarray as xr + +from typing import Any, List, Union, Tuple + +import toolviper.utils.logger as logger + +from astrohack.utils.text import print_summary_header, print_dict_table, print_method_list, print_data_contents + +class AstrohackBeamcutFile: + + 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 + """ + self.file = file + self._file_is_open = False + self._input_pars = None + self.xdt=None + + def __getitem__(self, key: str): + return self.xdt[key] + + def __setitem__(self, key: str, value: Any): + self.xdt[key] = value + return + + @property + def is_open(self) -> bool: + """Check whether the object has opened the corresponding hack file. + + :return: True if open, else False. + :rtype: bool + """ + return self._file_is_open + + def keys(self, *args, **kwargs): + return self.xdt.children.keys(*args, **kwargs) + + def open(self, file: str = None) -> bool: + """Open panel holography file. + :param file: File to be opened, if None defaults to the previously defined file + :type file: str, optional + + :return: True if file is properly opened, else returns False + :rtype: bool + """ + + if file is None: + file = self.file + + try: + # Chunks='auto' means lazy dask loading with automatic choice of chunk size + # chunks=None is direct opening. + self.xdt = xr.open_datatree(file, engine='zarr', chunks='auto') + self._input_pars = self.xdt.attrs + + self._file_is_open = True + + except Exception as error: + logger.error(f"There was an exception opening the file: {error}") + self._file_is_open = False + + return self._file_is_open + + def summary(self) -> None: + """Prints summary of the AstrohackPanelFile object, with available data, attributes and available methods""" + print_summary_header(self.file) + print_dict_table(self._input_pars) + print_data_contents(self, ["Antenna", "DDI", "Cut"]) + # print_method_list( + # [ + # self.summary, + # self.get_antenna, + # self.export_screws, + # self.export_to_fits, + # self.plot_antennas, + # self.export_gain_tables, + # self.observation_summary, + # ] + # ) + # + # def observation_summary( + # self, + # summary_file: str, + # ant: Union[str, List[str]] = "all", + # ddi: 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 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) \ No newline at end of file From c5d38450a0fd88b7cc3744a8446a95da786f35c8 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 23 Dec 2025 14:59:09 -0700 Subject: [PATCH 29/95] Added bounds to limit fitting space for gaussians (e.g. avoid negative amps) --- src/astrohack/core/beamcut.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/astrohack/core/beamcut.py b/src/astrohack/core/beamcut.py index b4efa8cf..9c9e65d2 100644 --- a/src/astrohack/core/beamcut.py +++ b/src/astrohack/core/beamcut.py @@ -316,7 +316,9 @@ def _fwhm_gaussian(x_axis, x_off, amp, fwhm): def _build_multi_gaussian_initial_guesses(x_data, y_data, pb_fwhm, min_dist_fraction=1.3): - p0 = [] + initial_guesses = [] + lower_bounds = [] + upper_bounds = [] step = float(np.median(np.diff(x_data))) min_dist = np.abs(min_dist_fraction * pb_fwhm / step) peaks, _ = find_peaks(y_data, distance=min_dist) @@ -324,8 +326,11 @@ def _build_multi_gaussian_initial_guesses(x_data, y_data, pb_fwhm, min_dist_frac if dx < 0: peaks = peaks[::-1] for ipeak in peaks: - p0.extend([x_data[ipeak], y_data[ipeak], pb_fwhm]) - return p0, len(peaks) + initial_guesses.extend([x_data[ipeak], y_data[ipeak], pb_fwhm]) + lower_bounds.extend([-np.inf, 0, 0]) + upper_bounds.extend([np.inf, np.inf, np.inf]) + bounds = (lower_bounds, upper_bounds) + return initial_guesses, bounds, len(peaks) def _multi_gaussian(xdata, *args): @@ -338,9 +343,9 @@ def _multi_gaussian(xdata, *args): return y_values -def _perform_curvefit_with_given_functions(x_data, y_data, initial_guesses, fit_func, datalabel, maxit=50000): +def _perform_curvefit_with_given_functions(x_data, y_data, initial_guesses, bounds, fit_func, datalabel, maxit=50000): try: - results = curve_fit(fit_func, x_data, y_data, p0=initial_guesses, maxfev=int(maxit)) + results = curve_fit(fit_func, x_data, y_data, p0=initial_guesses, bounds=bounds, maxfev=int(maxit)) fit_pars = results[0] return True, fit_pars except RuntimeError: @@ -412,18 +417,19 @@ def _beamcut_multi_lobes_gaussian_fit(cut_xdtree, datalabel): x_data = cut_xds['lm_dist'].values for parallel_hand in cut_xds.attrs['available_corrs']: y_data = cut_xds[f'{parallel_hand}_amplitude'] - - p0, n_peaks = _build_multi_gaussian_initial_guesses(x_data, y_data, primary_fwhm) + this_corr_data_label = f'{datalabel}, {cut_xds.attrs["direction"]}, corr = {parallel_hand}' + initial_guesses, bounds, n_peaks = _build_multi_gaussian_initial_guesses(x_data, y_data, primary_fwhm) fit_succeeded, fit_pars = _perform_curvefit_with_given_functions(x_data, y_data, - p0, + initial_guesses, + bounds, _multi_gaussian, - f'{datalabel}, corr = {parallel_hand}') + this_corr_data_label) if fit_succeeded: fit = _multi_gaussian(x_data, *fit_pars) n_peaks, fit_pars, pb_center, pb_fwhm, first_side_lobe_ratio = \ - _identify_pb_and_sidelobes_in_fit(datalabel, x_data, fit_pars) + _identify_pb_and_sidelobes_in_fit(this_corr_data_label, x_data, fit_pars) else: pb_center, pb_fwhm, first_side_lobe_ratio = np.nan, np.nan, np.nan fit = np.full_like(y_data, np.nan) From ef0d5adc66a40d9b3d4f00233a8fd9e5b0e58727 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 23 Dec 2025 15:37:31 -0700 Subject: [PATCH 30/95] Added function to open beamcut file. --- src/astrohack/dio.py | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/astrohack/dio.py b/src/astrohack/dio.py index 682da3ef..a93e42a1 100644 --- a/src/astrohack/dio.py +++ b/src/astrohack/dio.py @@ -7,6 +7,7 @@ from casacore import tables from rich.console import Console +from astrohack.beamcut_mds import AstrohackBeamcutFile from astrohack.utils.file import check_if_file_can_be_opened from astrohack.mds import AstrohackImageFile from astrohack.mds import AstrohackHologFile @@ -22,6 +23,46 @@ JSON = NewType("JSON", Dict[str, Any]) +def open_beamcut(file:str) -> Union[AstrohackBeamcutFile, None]: + """ Open beamcut file and return instance of the beamcut data object. Object includes summary function to list\ + available nodes. + + :param file: Path to beamcut file. + :type file: str + + :return: beamcut object; None if file not found. + :rtype: AstrohackBeamcutFile + + .. _Description: + **AstrohackBeamcutFile** + Beamcu object allows the user to access beam cut data via a xarray data tree, in order of depth, `ant` -> `ddi` \ + -> `cut`. The beamcut object also provides a `summary()` helper function to list available nodes for each file. \ + An outline of the beam object structure is show below: + + .. parsed-literal:: + beamcut_mds = + { + ant_0:{ + ddi_0:{ + cut_0: beamcut_ds, + ⋮ + cut_n: beamcut_ds + }, + ⋮ + ddi_n: … + }, + ⋮ + ant_n: … + } + """ + _data_file = AstrohackBeamcutFile(file=file) + + if _data_file.open(): + return _data_file + else: + return None + + def open_holog(file: str) -> Union[AstrohackHologFile, None]: """ Open holog file and return instance of the holog data object. Object includes summary function to list\ available dictionary keys. From 1993fc5f459a60b2b757427ae5c03fa1bdffadaf Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 23 Dec 2025 15:38:05 -0700 Subject: [PATCH 31/95] Added beamcut functions and object types to astrohack init. --- src/astrohack/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/astrohack/__init__.py b/src/astrohack/__init__.py index d7bfeb1d..c978be7e 100644 --- a/src/astrohack/__init__.py +++ b/src/astrohack/__init__.py @@ -10,6 +10,9 @@ model_memory_usage, ) +from .beamcut import beamcut +from .beamcut_mds import AstrohackBeamcutFile + from .extract_pointing import extract_pointing from .holog import holog from .dio import ( @@ -19,6 +22,7 @@ open_panel, open_locit, open_position, + open_beamcut ) from .panel import panel from .combine import combine From 4e631fac4ea4793f5005d05f22f053266105a01c Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 23 Dec 2025 15:38:50 -0700 Subject: [PATCH 32/95] Added a recursive way to create a graph from a xarray based mds. --- src/astrohack/utils/graph.py | 70 +++++++++++++++++++++++++++++++----- 1 file changed, 62 insertions(+), 8 deletions(-) diff --git a/src/astrohack/utils/graph.py b/src/astrohack/utils/graph.py index 54ed97c0..9ab690de 100644 --- a/src/astrohack/utils/graph.py +++ b/src/astrohack/utils/graph.py @@ -7,6 +7,50 @@ from astrohack.utils.text import param_to_list +def _construct_xdtree_graph_recursively( + xr_datatree, + chunk_function, + param_dict, + delayed_list, + key_order, + parallel=False, + oneup=None, +): + if len(key_order) == 0: + param_dict["xdt_data"] = xr_datatree + if parallel: + delayed_list.append(dask.delayed(chunk_function)(dask.delayed(param_dict))) + else: + delayed_list.append((chunk_function, param_dict)) + 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, + 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, @@ -80,14 +124,24 @@ def compute_graph( """ delayed_list = [] - _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, - ) + if hasattr(looping_dict, "xdt"): + _construct_xdtree_graph_recursively( + xr_datatree=looping_dict.xdt, + chunk_function=chunk_function, + param_dict=param_dict, + delayed_list=delayed_list, + key_order=key_order[1:], + 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, + ) if len(delayed_list) == 0: logger.warning(f"List of delayed processing jobs is empty: No data to process") From 147c8dcb895204e01ba251c2e9a3609fcf35a776 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 23 Dec 2025 15:39:21 -0700 Subject: [PATCH 33/95] Fixed import. --- src/astrohack/core/beamcut.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/astrohack/core/beamcut.py b/src/astrohack/core/beamcut.py index 9c9e65d2..09d7bbd6 100644 --- a/src/astrohack/core/beamcut.py +++ b/src/astrohack/core/beamcut.py @@ -6,7 +6,7 @@ import astropy import xarray as xr -from astrohack import get_proper_telescope +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, sig_2_fwhm, \ format_frequency, format_value_unit, to_db, create_pretty_table From 5164cde9b1e04405f7a7d6d003bdaea0477c748b Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 23 Dec 2025 15:40:33 -0700 Subject: [PATCH 34/95] REmoved unused datatree creation function. --- src/astrohack/core/beamcut.py | 34 ---------------------------------- 1 file changed, 34 deletions(-) diff --git a/src/astrohack/core/beamcut.py b/src/astrohack/core/beamcut.py index 09d7bbd6..bc046f22 100644 --- a/src/astrohack/core/beamcut.py +++ b/src/astrohack/core/beamcut.py @@ -119,40 +119,6 @@ def create_report_chunk(cut_xdtree, par_dict, spacing=2, item_marker='-', precis ########################################################### ### Data IO ########################################################### -def _create_output_datatree(cut_list, antenna, ddi, summary): - this_branch = xr.DataTree(name=f'{antenna}-{ddi}') - for icut, cut_dict in enumerate(cut_list): - xds = xr.Dataset() - xds.attrs['scan_number'] = cut_dict['scan_number'] - xds.attrs['lm_angle'] = cut_dict['lm_angle'] - xds.attrs['available_corrs'] = cut_dict['available_corrs'] - xds.attrs['direction'] = cut_dict['direction'] - xds.attrs['xlabel'] = cut_dict['xlabel'] - xds.attrs['time_string'] = cut_dict['time_string'] - xds.attrs['all_corr_ymax'] = cut_dict['all_corr_ymax'] - xds.attrs['summary'] = summary - - coords = {"time": cut_dict['time'], "lm_dist": cut_dict['lm_dist']} - - xds['lm_offsets'] = xr.DataArray(cut_dict['lm_offsets'], dims=["time", "lm"]) - - for parallel_hand in cut_dict['available_corrs']: - xds.attrs[f'{parallel_hand}_n_peaks'] = cut_dict[f'{parallel_hand}_n_peaks'] - xds.attrs[f'{parallel_hand}_amp_fit_pars'] = cut_dict[f'{parallel_hand}_amp_fit_pars'] - xds.attrs[f'{parallel_hand}_pb_fwhm'] = cut_dict[f'{parallel_hand}_pb_fwhm'] - xds.attrs[f'{parallel_hand}_pb_center'] = cut_dict[f'{parallel_hand}_pb_center'] - xds.attrs[f'{parallel_hand}_first_side_lobe_ratio'] = cut_dict[f'{parallel_hand}_first_side_lobe_ratio'] - - xds[f'{parallel_hand}_amplitude'] = xr.DataArray(cut_dict[f'{parallel_hand}_amplitude'], dims='lm_dist') - xds[f'{parallel_hand}_phase'] = xr.DataArray(cut_dict[f'{parallel_hand}_phase'], dims='lm_dist') - xds[f'{parallel_hand}_weight'] = xr.DataArray(cut_dict[f'{parallel_hand}_weight'], dims='lm_dist') - xds[f'{parallel_hand}_amp_fit'] = xr.DataArray(cut_dict[f'{parallel_hand}_amp_fit'], dims='lm_dist') - - xds = xds.assign_coords(coords) - this_branch = this_branch.assign({f'cut_{icut}': xr.DataTree(dataset=xds, name=f'cut_{icut}')}) - return this_branch - - def _file_name_factory(file_type, par_dict): destination = par_dict['destination'] antenna = par_dict['this_ant'] From 9288d5e3fe8711db283d5fe7137f9633fda76faa Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 23 Dec 2025 15:50:34 -0700 Subject: [PATCH 35/95] Moved summary from a copy in each cut to a higher level (ant-ddi level) --- src/astrohack/core/beamcut.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/astrohack/core/beamcut.py b/src/astrohack/core/beamcut.py index bc046f22..b2b06164 100644 --- a/src/astrohack/core/beamcut.py +++ b/src/astrohack/core/beamcut.py @@ -59,7 +59,7 @@ def plot_beamcut_in_amplitude_chunk(cut_xdtree, par_dict): _plot_single_cut_in_amplitude(cut_xds, axes[icut, :], par_dict) # Header creation - summary = cut_xdtree.children['cut_0'].attrs['summary'] + summary = cut_xdtree.attrs['summary'] title = _create_beamcut_header(summary, par_dict) filename = _file_name_factory('amplitude', par_dict) @@ -74,7 +74,7 @@ def plot_beamcut_in_attenuation_chunk(cut_xdtree, par_dict): _plot_single_cut_in_attenuation(cut_xds, axes[icut], par_dict) # Header creation - summary = cut_xdtree.children['cut_0'].attrs['summary'] + summary = cut_xdtree.attrs['summary'] title = _create_beamcut_header(summary, par_dict) filename = _file_name_factory('attenuation', par_dict) @@ -85,7 +85,7 @@ def create_report_chunk(cut_xdtree, par_dict, spacing=2, item_marker='-', precis outstr = f'{item_marker}{spc}' lm_unit = par_dict['lm_unit'] lm_fac = convert_unit('rad', lm_unit, 'trigonometric') - summary = cut_xdtree.children['cut_0'].attrs['summary'] + summary = cut_xdtree.attrs['summary'] items = ['Id', f'Center [{lm_unit}]', 'Amplitude [ ]', f'FWHM [{lm_unit}]', 'Attenuation [dB]'] outstr += _create_beamcut_header(summary, par_dict) + 2 * lnbr @@ -148,7 +148,7 @@ 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'] - summary = input_xds.attrs["summary"] + cut_xdtree.attrs["summary"] = input_xds.attrs["summary"] lm_offsets = input_xds.DIRECTIONAL_COSINES.values time_axis = input_xds.time.values @@ -186,7 +186,6 @@ def _extract_cuts_from_visibilities(input_xds, antenna, ddi): 'direction': direction, 'xlabel': xlabel, 'time_string': timestr, - 'summary': summary, }) xds['lm_offsets'] = xr.DataArray(this_lm_offsets, dims=["time", "lm"]) @@ -372,7 +371,7 @@ def _identify_pb_and_sidelobes_in_fit(datalabel, x_data, fit_pars): def _beamcut_multi_lobes_gaussian_fit(cut_xdtree, datalabel): # Get the summary from the first cut, but it should be equal anyway - summary = cut_xdtree.children['cut_0'].attrs['summary'] + summary = cut_xdtree.attrs['summary'] wavelength = summary["spectral"]["rep. wavelength"] telescope = get_proper_telescope( summary["general"]["telescope name"], summary["general"]["antenna name"] From bd8a24c0174070dabbd55f48a6685d6c8ad60ed4 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 23 Dec 2025 15:56:21 -0700 Subject: [PATCH 36/95] Fixed typo in call to _construct_xdtree_graph_recursively --- src/astrohack/utils/graph.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/astrohack/utils/graph.py b/src/astrohack/utils/graph.py index 9ab690de..a3baf1e8 100644 --- a/src/astrohack/utils/graph.py +++ b/src/astrohack/utils/graph.py @@ -130,7 +130,7 @@ def compute_graph( chunk_function=chunk_function, param_dict=param_dict, delayed_list=delayed_list, - key_order=key_order[1:], + key_order=key_order, parallel=parallel, ) else: From f1af85f802b6dfc46b78403e293edad403e31821 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 23 Dec 2025 16:05:45 -0700 Subject: [PATCH 37/95] Added observation summary to beamcut_mds --- src/astrohack/beamcut_mds.py | 137 ++++++++++---------- src/astrohack/visualization/textual_data.py | 31 +++++ 2 files changed, 100 insertions(+), 68 deletions(-) diff --git a/src/astrohack/beamcut_mds.py b/src/astrohack/beamcut_mds.py index 58e69a17..92605663 100644 --- a/src/astrohack/beamcut_mds.py +++ b/src/astrohack/beamcut_mds.py @@ -5,6 +5,8 @@ import toolviper.utils.logger as logger from astrohack.utils.text import print_summary_header, print_dict_table, print_method_list, print_data_contents +from astrohack.visualization.textual_data import generate_observation_summary_for_beamcut +from astrohack.utils.graph import compute_graph class AstrohackBeamcutFile: @@ -67,79 +69,78 @@ def open(self, file: str = None) -> bool: return self._file_is_open def summary(self) -> None: - """Prints summary of the AstrohackPanelFile object, with available data, attributes and available methods""" + """Prints summary of the AstrohackBeamcutFile object, with available data, attributes and available methods""" print_summary_header(self.file) print_dict_table(self._input_pars) print_data_contents(self, ["Antenna", "DDI", "Cut"]) - # print_method_list( - # [ - # self.summary, + print_method_list([ + self.summary, # self.get_antenna, # self.export_screws, # self.export_to_fits, # self.plot_antennas, # self.export_gain_tables, - # self.observation_summary, - # ] - # ) - # - # def observation_summary( - # self, - # summary_file: str, - # ant: Union[str, List[str]] = "all", - # ddi: 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 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) \ No newline at end of file + self.observation_summary, + ]) + + def observation_summary( + self, + summary_file: str, + ant: Union[str, List[str]] = "all", + ddi: 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 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 AstrohackBeamcutFile displaying general information, + spectral information, beam image characteristics and aperture image characteristics. + """ + + + param_dict = locals() + key_order = ["ant", "ddi"] + execution, summary_list = compute_graph( + self, + generate_observation_summary_for_beamcut, + param_dict, + key_order, + parallel, + fetch_returns=True, + ) + full_summary = "".join(summary_list) + with open(summary_file, "w") as output_file: + output_file.write(full_summary) + if print_summary: + print(full_summary) \ No newline at end of file diff --git a/src/astrohack/visualization/textual_data.py b/src/astrohack/visualization/textual_data.py index 72425fc3..8679533e 100644 --- a/src/astrohack/visualization/textual_data.py +++ b/src/astrohack/visualization/textual_data.py @@ -603,3 +603,34 @@ def generate_observation_summary(parm_dict): ) 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' + + return outstr From 735a31550dbe7cc7b8c43b3a0a7e598bff7550b3 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 23 Dec 2025 16:33:12 -0700 Subject: [PATCH 38/95] Small changes in parameter order to make export routines callable as chunks, made some data formatting robust to data type changes due to saving to disk (e.g. np.array -> Tuple). --- src/astrohack/core/beamcut.py | 36 ++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/src/astrohack/core/beamcut.py b/src/astrohack/core/beamcut.py index b2b06164..dfedfe9c 100644 --- a/src/astrohack/core/beamcut.py +++ b/src/astrohack/core/beamcut.py @@ -43,15 +43,17 @@ def process_beamcut_chunk(beamcut_chunk_params): destination = beamcut_chunk_params['destination'] if destination is not None: logger.info(f'Producing plots for {datalabel}') - plot_beamcut_in_amplitude_chunk(cut_xdtree, beamcut_chunk_params) - plot_beamcut_in_attenuation_chunk(cut_xdtree, beamcut_chunk_params) - create_report_chunk(cut_xdtree, beamcut_chunk_params) + plot_beamcut_in_amplitude_chunk(beamcut_chunk_params, cut_xdtree) + plot_beamcut_in_attenuation_chunk(beamcut_chunk_params, cut_xdtree) + create_report_chunk(beamcut_chunk_params, cut_xdtree) logger.info(f'Completed plots for {datalabel}') return cut_xdtree -def plot_beamcut_in_amplitude_chunk(cut_xdtree, par_dict): +def plot_beamcut_in_amplitude_chunk(par_dict, cut_xdtree=None): + if cut_xdtree is None: + cut_xdtree = par_dict['xdt_data'] n_cuts = len(cut_xdtree.children.values()) # Loop over cuts fig, axes = create_figure_and_axes([12, 1+n_cuts*4], [n_cuts, 2]) @@ -66,7 +68,9 @@ def plot_beamcut_in_amplitude_chunk(cut_xdtree, par_dict): close_figure(fig, title, filename, par_dict['dpi'], par_dict['display']) -def plot_beamcut_in_attenuation_chunk(cut_xdtree, par_dict): +def plot_beamcut_in_attenuation_chunk(par_dict, cut_xdtree=None): + if cut_xdtree is None: + cut_xdtree = par_dict['xdt_data'] n_cuts = len(cut_xdtree.children.values()) # Loop over cuts fig, axes = create_figure_and_axes([6, 1+n_cuts*4], [n_cuts, 1]) @@ -81,7 +85,9 @@ def plot_beamcut_in_attenuation_chunk(cut_xdtree, par_dict): close_figure(fig, title, filename, par_dict['dpi'], par_dict['display']) -def create_report_chunk(cut_xdtree, par_dict, spacing=2, item_marker='-', precision=3): +def create_report_chunk(par_dict, cut_xdtree=None, spacing=2, item_marker='-', precision=3): + if cut_xdtree is None: + cut_xdtree = par_dict['xdt_data'] outstr = f'{item_marker}{spc}' lm_unit = par_dict['lm_unit'] lm_fac = convert_unit('rad', lm_unit, 'trigonometric') @@ -420,7 +426,7 @@ def _add_secondary_beam_hpbw_x_axis_to_plot(pb_fwhm, ax): sec_x_axis.set_xlabel('Offset in Primary Beam HPBWs\n') sec_x_axis.set_xticks([]) y_min, y_max = ax.get_ylim() - x_lims = ax.get_xlim() + x_lims = np.array(ax.get_xlim()) pb_min, pb_max = np.ceil(x_lims/pb_fwhm) beam_offsets = np.arange(pb_min, pb_max, 1, dtype=int) @@ -429,13 +435,8 @@ def _add_secondary_beam_hpbw_x_axis_to_plot(pb_fwhm, ax): ax.text(itk*pb_fwhm, y_max, f'{itk:d}', va='bottom', ha='center') -def _add_lobe_identification_to_plot(ax, centers, peaks, y_off, attenunation_plot=False): - if attenunation_plot: - plot_peaks = to_db(peaks/np.max(peaks)) # maximum of peaks is always the PB - else: - plot_peaks = peaks - - for i_peak, peak in enumerate(plot_peaks): +def _add_lobe_identification_to_plot(ax, centers, peaks, y_off): + for i_peak, peak in enumerate(peaks): ax.text(centers[i_peak], peak+y_off, f'{i_peak+1})', ha='center', va='bottom') @@ -482,9 +483,10 @@ def _plot_single_cut_in_amplitude(cut_xds, axes, par_dict): residuals_color='black', legend_location='upper right') # Add fit peak identifiers - _add_lobe_identification_to_plot(this_ax, lm_fac * cut_xds.attrs[f'{parallel_hand}_amp_fit_pars'][0::3], - cut_xds.attrs[f'{parallel_hand}_amp_fit_pars'][1::3], y_off, - attenunation_plot=False) + centers = lm_fac * np.array(cut_xds.attrs[f'{parallel_hand}_amp_fit_pars'][0::3]) + amps = np.array(cut_xds.attrs[f'{parallel_hand}_amp_fit_pars'][1::3]) + + _add_lobe_identification_to_plot(this_ax, centers, amps, y_off, attenunation_plot=False) else: scatter_plot(this_ax, x_data, xlabel, y_data, ylabel, title=sub_title, data_marker='+', data_label=f'{parallel_hand} data', data_color='red', legend_location='upper right') From 598f45f42350f8dbd874ae17bf0c8c5b42dec0ed Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 23 Dec 2025 16:33:37 -0700 Subject: [PATCH 39/95] Added data exporting routines. --- src/astrohack/beamcut_mds.py | 64 ++++++++++++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 2 deletions(-) diff --git a/src/astrohack/beamcut_mds.py b/src/astrohack/beamcut_mds.py index 92605663..c911b137 100644 --- a/src/astrohack/beamcut_mds.py +++ b/src/astrohack/beamcut_mds.py @@ -1,9 +1,12 @@ import xarray as xr +import pathlib from typing import Any, List, Union, Tuple import toolviper.utils.logger as logger +from astrohack.core.beamcut import plot_beamcut_in_amplitude_chunk, plot_beamcut_in_attenuation_chunk, \ + create_report_chunk from astrohack.utils.text import print_summary_header, print_dict_table, print_method_list, print_data_contents from astrohack.visualization.textual_data import generate_observation_summary_for_beamcut from astrohack.utils.graph import compute_graph @@ -75,7 +78,7 @@ def summary(self) -> None: print_data_contents(self, ["Antenna", "DDI", "Cut"]) print_method_list([ self.summary, - # self.get_antenna, + self.plot_beamcut_in_amplitude, # self.export_screws, # self.export_to_fits, # self.plot_antennas, @@ -143,4 +146,61 @@ def observation_summary( with open(summary_file, "w") as output_file: output_file.write(full_summary) if print_summary: - print(full_summary) \ No newline at end of file + print(full_summary) + + def plot_beamcut_in_amplitude(self, + destination: str, + ant: Union[str, List[str]] = "all", + ddi: Union[int, List[int]] = "all", + lm_unit: str = 'amin', + azel_unit: str = 'deg', + y_scale: str = None, + display: bool = False, + dpi: int = 300, + parallel: bool = False, + ) -> None: + + param_dict = locals() + + pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) + compute_graph( + self, plot_beamcut_in_amplitude_chunk, param_dict, ["ant", "ddi"], parallel=parallel + ) + return + + def plot_beamcut_in_attenuation(self, + destination: str, + ant: Union[str, List[str]] = "all", + ddi: Union[int, List[int]] = "all", + lm_unit: str = 'amin', + azel_unit: str = 'deg', + y_scale: str = None, + display: bool = False, + dpi: int = 300, + parallel: bool = False, + ) -> None: + + param_dict = locals() + + pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) + compute_graph( + self, plot_beamcut_in_attenuation_chunk, param_dict, ["ant", "ddi"], parallel=parallel + ) + return + + def create_beam_fit_report(self, + destination: str, + ant: Union[str, List[str]] = "all", + ddi: Union[int, List[int]] = "all", + lm_unit: str = 'amin', + azel_unit: str = 'deg', + parallel: bool = False, + ) -> None: + + param_dict = locals() + + pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) + compute_graph( + self, create_report_chunk, param_dict, ["ant", "ddi"], parallel=parallel + ) + return \ No newline at end of file From d64934f6f575845c7c95f1559e189542697ea6c3 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 23 Dec 2025 16:36:53 -0700 Subject: [PATCH 40/95] Black compliance --- docs/tutorial_vla.ipynb | 4 +- etc/beamcuts/beamcut_CASA_calibration.py | 241 ++++---- src/astrohack/__init__.py | 2 +- src/astrohack/beamcut.py | 45 +- src/astrohack/beamcut_mds.py | 145 ++--- src/astrohack/core/beamcut.py | 578 ++++++++++++-------- src/astrohack/core/extract_holog.py | 8 +- src/astrohack/core/extract_pointing.py | 5 +- src/astrohack/dio.py | 2 +- src/astrohack/utils/graph.py | 29 +- src/astrohack/visualization/diagnostics.py | 25 +- src/astrohack/visualization/plot_tools.py | 4 +- src/astrohack/visualization/textual_data.py | 4 +- 13 files changed, 641 insertions(+), 451 deletions(-) diff --git a/docs/tutorial_vla.ipynb b/docs/tutorial_vla.ipynb index eda68a6a..4a85d5f6 100644 --- a/docs/tutorial_vla.ipynb +++ b/docs/tutorial_vla.ipynb @@ -245,10 +245,10 @@ "from astrohack.extract_holog import model_memory_usage\n", "\n", "# the elastic model download needs to be fixed\n", - "#model_memory_usage(\n", + "# model_memory_usage(\n", "# ms_name=\"data/ea25_cal_small_after_fixed.split.ms\",\n", "# holog_obs_dict=None\n", - "#)\n" + "# )" ] }, { diff --git a/etc/beamcuts/beamcut_CASA_calibration.py b/etc/beamcuts/beamcut_CASA_calibration.py index a6d11a81..8b359677 100644 --- a/etc/beamcuts/beamcut_CASA_calibration.py +++ b/etc/beamcuts/beamcut_CASA_calibration.py @@ -5,32 +5,33 @@ import casatools from pathlib import Path -lnbr = '\n' -spc=' ' +lnbr = "\n" +spc = " " + def yesno(prompt): - user_ans = input(f'{prompt} <(Y)es/(N)o>: ').lower() - if user_ans == 'y' or user_ans == 'yes': + user_ans = input(f"{prompt} <(Y)es/(N)o>: ").lower() + if user_ans == "y" or user_ans == "yes": return True - elif user_ans == 'n' or user_ans == 'no': + elif user_ans == "n" or user_ans == "no": return False else: - print('Use or ') + print("Use or ") return yesno(prompt) class MessageBoard: - def __init__(self, width=60, block_char='#', spacing=1, blocking=3): + def __init__(self, width=60, block_char="#", spacing=1, blocking=3): self.width = width self.block_char = block_char - self.spacing = spacing, + self.spacing = (spacing,) self.blocking = blocking - self.capo = blocking*block_char + spacing*spc - self.coda = self.capo[::-1]+lnbr - self.usable_width = width - 2*spacing - 2*blocking - self.block_line = self.width*self.block_char+lnbr + self.capo = blocking * block_char + spacing * spc + self.coda = self.capo[::-1] + lnbr + self.usable_width = width - 2 * spacing - 2 * blocking + self.block_line = self.width * self.block_char + lnbr self.block_len = len(self.capo) def end_line(self, line, centered=True): @@ -43,15 +44,15 @@ def end_line(self, line, centered=True): return out_line def heading(self, user_msg): - outstr = '' + outstr = "" outstr += self.block_line head_wrds = user_msg.split() - line = '' + line = "" for wrd in head_wrds: wrd_len = len(wrd) if wrd_len > self.usable_width: - raise ValueError(f'Word {wrd} is larger than the usable self.width') + raise ValueError(f"Word {wrd} is larger than the usable self.width") line_len = len(line) + wrd_len + 1 if line_len > self.usable_width: outstr += self.end_line(line) @@ -64,17 +65,23 @@ def heading(self, user_msg): def one_liner(self, msg): if len(msg) > self.usable_width: - raise ValueError('Message is larger than usable width') + raise ValueError("Message is larger than usable width") return self.end_line(msg) def done(self): - return self.one_liner('Done!') + return self.one_liner("Done!") class UserInteraction: - last_use_file = '.beamcut_cal.last' - user_inp_list = ['filename', 'field', 'refant', 'overwrite', 'confirmation_before_start'] - sep = '=' + last_use_file = ".beamcut_cal.last" + user_inp_list = [ + "filename", + "field", + "refant", + "overwrite", + "confirmation_before_start", + ] + sep = "=" def __init__(self): self.filename = None @@ -89,15 +96,15 @@ def _find_previous_input(self): def _read_last_use_file(self): self.last_use_list = [] - with open(self.last_use_file, 'r') as infile: + with open(self.last_use_file, "r") as infile: for line in infile: self.last_use_list.append(line.strip()) def _reuse_last(self): - print('Previous inputs:') + print("Previous inputs:") for line in self.last_use_list: - print(f'\t{line}') - ans = yesno('Re-use previous input?') + print(f"\t{line}") + ans = yesno("Re-use previous input?") print() return ans @@ -105,14 +112,16 @@ def _init_from_user(self): self.filename = input("Enter file name: ") self.field = input("Enter field number: ") self.refant = input("Enter referece antenna: ") - self.overwrite = yesno('Re-do calibration if already done?') - self.confirmation_before_start = yesno('Confirm info before starting calibration?') + self.overwrite = yesno("Re-do calibration if already done?") + self.confirmation_before_start = yesno( + "Confirm info before starting calibration?" + ) def save_input(self): - outstr = '' + outstr = "" for key in self.user_inp_list: - outstr += f'{key} {self.sep} {getattr(self, key)}\n' - with open(self.last_use_file, 'w') as outfile: + outstr += f"{key} {self.sep} {getattr(self, key)}\n" + with open(self.last_use_file, "w") as outfile: outfile.write(outstr) def read_input(self): @@ -132,14 +141,16 @@ def read_input(self): @classmethod def perform_beamcut_calibration(cls): msger = MessageBoard() - print(msger.heading('Welcome to the beam cut calibration pipeline')) + print(msger.heading("Welcome to the beam cut calibration pipeline")) my_obj = cls() my_obj.read_input() print() - mycal_obj = CalObject(my_obj.filename, my_obj.field, my_obj.refant, my_obj.overwrite, msger) + mycal_obj = CalObject( + my_obj.filename, my_obj.field, my_obj.refant, my_obj.overwrite, msger + ) if my_obj.confirmation_before_start: - proceed = yesno('Proceed with calibration?') + proceed = yesno("Proceed with calibration?") else: proceed = True print() @@ -149,12 +160,14 @@ def perform_beamcut_calibration(cls): mycal_obj.calibration_pipeline() mycal_obj.apply_calibration() - print(msger.heading('All Done!')) + print(msger.heading("All Done!")) class CalObject: - def __init__(self, msname, field, refant, overwrite, msger, first_chan=4, last_chan=60): + def __init__( + self, msname, field, refant, overwrite, msger, first_chan=4, last_chan=60 + ): self.msname = msname self.refant = refant self.overwrite = bool(overwrite) @@ -163,13 +176,13 @@ def __init__(self, msname, field, refant, overwrite, msger, first_chan=4, last_c self.fchan = first_chan self.lchan = last_chan - base_cal_name = msname+'.' - self.delay_caltable = base_cal_name+'delay.cal' - self.bandpass_caltable = base_cal_name+'bandpass.bcal' - self.gain_caltable = base_cal_name+'gain.cal' + base_cal_name = msname + "." + self.delay_caltable = base_cal_name + "delay.cal" + self.bandpass_caltable = base_cal_name + "bandpass.bcal" + self.gain_caltable = base_cal_name + "gain.cal" if self._is_asdm(): - print(self.msger.one_liner('Input is an SDM running importasdm...')) + print(self.msger.one_liner("Input is an SDM running importasdm...")) self.asdm_to_ms() print(self.msger.done()) @@ -181,26 +194,27 @@ def _is_asdm(self): return file_path.exists() def asdm_to_ms(self): - msname = self.msname + '.ms' + msname = self.msname + ".ms" if os.path.exists(msname) and self.overwrite: - print(self.msger.heading('Removing old file')) + print(self.msger.heading("Removing old file")) shutil.rmtree(msname) - importasdm(asdm=self.msname, - vis=msname, - createmms=False, - ocorr_mode='co', - lazy=False, - asis='Receiver CalAtmosphere', - process_caldevice=True, - process_pointing=True, - savecmds=True, - outfile=msname + '.flagonline.txt', - bdfflags=False, - with_pointing_correction=True, - applyflags=True, - overwrite=False, - ) + importasdm( + asdm=self.msname, + vis=msname, + createmms=False, + ocorr_mode="co", + lazy=False, + asis="Receiver CalAtmosphere", + process_caldevice=True, + process_pointing=True, + savecmds=True, + outfile=msname + ".flagonline.txt", + bdfflags=False, + with_pointing_correction=True, + applyflags=True, + overwrite=False, + ) self.msname = msname return @@ -208,103 +222,108 @@ def _initialize_metadata(self): # Fetch metadata from ms msmd = casatools.msmetadata() msmd.open(self.msname) - cal_scans = msmd.scansforintent('*PHASE*') - beamcut_scans = msmd.scansforintent('*MAP*ON_SOURCE') - spw_list = msmd.spwsforintent('*MAP*') + cal_scans = msmd.scansforintent("*PHASE*") + beamcut_scans = msmd.scansforintent("*MAP*ON_SOURCE") + spw_list = msmd.spwsforintent("*MAP*") msmd.done() # Convert to comma-separated string - self.cal_scans = ','.join(map(str, cal_scans)) - self.beamcut_scans = ','.join(map(str, beamcut_scans)) + self.cal_scans = ",".join(map(str, cal_scans)) + self.beamcut_scans = ",".join(map(str, beamcut_scans)) self.minspw = str(np.min(spw_list)) self.maxspw = str(np.max(spw_list)) - self.spwrange = self.minspw+'~'+self.maxspw - self.quacked_spwstr = self.spwrange+f':{self.fchan}~{self.lchan}' + self.spwrange = self.minspw + "~" + self.maxspw + self.quacked_spwstr = self.spwrange + f":{self.fchan}~{self.lchan}" def _report_init(self): - print('Scans used for calibration:') + print("Scans used for calibration:") print(self.cal_scans) print() - print('Scans used for beamcut:') + print("Scans used for beamcut:") print(self.beamcut_scans) print() - print('SPWSs used for beamcuts:') + print("SPWSs used for beamcuts:") print(self.spwrange) print() def _do_calibration(self, cal_name): if os.path.exists(cal_name): - print(f'{cal_name} exists.') + print(f"{cal_name} exists.") if self.overwrite: - print(f'{cal_name} exists, overwriting.') + print(f"{cal_name} exists, overwriting.") return True else: - print(f'{cal_name} exists, keeping it.') + print(f"{cal_name} exists, keeping it.") return False else: - print(f'{cal_name} does not exist, creating it...') + print(f"{cal_name} does not exist, creating it...") return True def delay_calibration(self): - print(self.msger.one_liner('Delay calibration...')) + print(self.msger.one_liner("Delay calibration...")) if self._do_calibration(self.delay_caltable): - gaincal(vis = self.msname, - caltable = self.delay_caltable, - refant = self.refant, - solint = 'inf', - spw = self.quacked_spwstr, - scan = self.cal_scans, - gaintype = 'K') + gaincal( + vis=self.msname, + caltable=self.delay_caltable, + refant=self.refant, + solint="inf", + spw=self.quacked_spwstr, + scan=self.cal_scans, + gaintype="K", + ) print(self.msger.done()) else: - print(self.msger.one_liner('Skipping delay calibration...')) + print(self.msger.one_liner("Skipping delay calibration...")) return def bandpass_calibration(self): - print(self.msger.one_liner('Bandpass calibration...')) + print(self.msger.one_liner("Bandpass calibration...")) if self._do_calibration(self.bandpass_caltable): - bandpass(vis = self.msname, - caltable = self.bandpass_caltable, - refant = self.refant, - solint = '10s', - spw = self.quacked_spwstr, - solnorm = True, - scan = self.cal_scans, - gaintable = [self.delay_caltable]) + bandpass( + vis=self.msname, + caltable=self.bandpass_caltable, + refant=self.refant, + solint="10s", + spw=self.quacked_spwstr, + solnorm=True, + scan=self.cal_scans, + gaintable=[self.delay_caltable], + ) print(self.msger.done()) else: - print(self.msger.one_liner('Skipping bandpass calibration...')) + print(self.msger.one_liner("Skipping bandpass calibration...")) return def gain_calibration(self): - print(self.msger.one_liner('Gain calibration...')) + print(self.msger.one_liner("Gain calibration...")) if self._do_calibration(self.gain_caltable): - gaincal(vis=self.msname, - caltable=self.gain_caltable, - refant=self.refant, - calmode='ap', - solint='inf', - spw=self.quacked_spwstr, - minsnr=2, - minblperant=2, - scan=self.cal_scans, - gaintable=[self.delay_caltable, self.bandpass_caltable]) + gaincal( + vis=self.msname, + caltable=self.gain_caltable, + refant=self.refant, + calmode="ap", + solint="inf", + spw=self.quacked_spwstr, + minsnr=2, + minblperant=2, + scan=self.cal_scans, + gaintable=[self.delay_caltable, self.bandpass_caltable], + ) print(self.msger.done()) else: - print(self.msger.one_liner('Skipping gain calibration...')) + print(self.msger.one_liner("Skipping gain calibration...")) return def apply_calibration(self): - print(self.msger.one_liner('Applying calibration...')) - applycal(vis=self.msname, - field=self.field, - spw=self.quacked_spwstr, - applymode='calonly', - gaintable=[self.delay_caltable, - self.bandpass_caltable, - self.gain_caltable] - ) + print(self.msger.one_liner("Applying calibration...")) + applycal( + vis=self.msname, + field=self.field, + spw=self.quacked_spwstr, + applymode="calonly", + gaintable=[self.delay_caltable, self.bandpass_caltable, self.gain_caltable], + ) print(self.msger.done()) return @@ -316,5 +335,3 @@ def calibration_pipeline(self): UserInteraction.perform_beamcut_calibration() - - diff --git a/src/astrohack/__init__.py b/src/astrohack/__init__.py index c978be7e..9ef29586 100644 --- a/src/astrohack/__init__.py +++ b/src/astrohack/__init__.py @@ -22,7 +22,7 @@ open_panel, open_locit, open_position, - open_beamcut + open_beamcut, ) from .panel import panel from .combine import combine diff --git a/src/astrohack/beamcut.py b/src/astrohack/beamcut.py index 832f7d29..6c0ad852 100644 --- a/src/astrohack/beamcut.py +++ b/src/astrohack/beamcut.py @@ -11,20 +11,21 @@ from typing import Union, List + def beamcut( - holog_name: str, - beamcut_name: str = None, - ant: Union[str, List[str]] = "all", - ddi: Union[int, List[str]] = "all", - correlations: str = "all", - destination: str = None, - lm_unit: str = 'amin', - azel_unit: str = 'deg', - dpi: int = 300, - display: bool = False, - y_scale: str = None, - parallel: bool = False, - overwrite: bool = False, + holog_name: str, + beamcut_name: str = None, + ant: Union[str, List[str]] = "all", + ddi: Union[int, List[str]] = "all", + correlations: str = "all", + destination: str = None, + lm_unit: str = "amin", + azel_unit: str = "deg", + dpi: int = 300, + display: bool = False, + y_scale: str = None, + parallel: bool = False, + overwrite: bool = False, ): check_if_file_can_be_opened(holog_name, "0.9.4") @@ -49,22 +50,28 @@ def beamcut( with open(json_data, "r") as json_file: holog_json = json.load(json_file) - overwrite_file(beamcut_params["beamcut_name"], beamcut_params['overwrite']) + overwrite_file(beamcut_params["beamcut_name"], beamcut_params["overwrite"]) - executed_graph, graph_results = compute_graph(holog_json, process_beamcut_chunk, beamcut_params, - ["ant", "ddi"], parallel=parallel, fetch_returns=True) + executed_graph, graph_results = compute_graph( + holog_json, + process_beamcut_chunk, + beamcut_params, + ["ant", "ddi"], + parallel=parallel, + fetch_returns=True, + ) if executed_graph: logger.info("Finished processing") output_attr_file = "{name}/{ext}".format( name=beamcut_params["beamcut_name"], ext=".beamcut_input" ) - root = xr.DataTree(name='root') + root = xr.DataTree(name="root") root.attrs.update(beamcut_params) add_caller_and_version_to_dict(root.attrs, direct_call=True) for xdtree in graph_results: - ant , ddi = xdtree.name.split('-') + ant, ddi = xdtree.name.split("-") if ant in root.keys(): ant = root.children[ant].update({ddi: xdtree}) else: @@ -78,4 +85,4 @@ def beamcut( return beamcut_mds else: logger.warning("No data to process") - return None \ No newline at end of file + return None diff --git a/src/astrohack/beamcut_mds.py b/src/astrohack/beamcut_mds.py index c911b137..119284fa 100644 --- a/src/astrohack/beamcut_mds.py +++ b/src/astrohack/beamcut_mds.py @@ -5,12 +5,23 @@ import toolviper.utils.logger as logger -from astrohack.core.beamcut import plot_beamcut_in_amplitude_chunk, plot_beamcut_in_attenuation_chunk, \ - create_report_chunk -from astrohack.utils.text import print_summary_header, print_dict_table, print_method_list, print_data_contents -from astrohack.visualization.textual_data import generate_observation_summary_for_beamcut +from astrohack.core.beamcut import ( + plot_beamcut_in_amplitude_chunk, + plot_beamcut_in_attenuation_chunk, + create_report_chunk, +) +from astrohack.utils.text import ( + print_summary_header, + print_dict_table, + print_method_list, + print_data_contents, +) +from astrohack.visualization.textual_data import ( + generate_observation_summary_for_beamcut, +) from astrohack.utils.graph import compute_graph + class AstrohackBeamcutFile: def __init__(self, file: str): @@ -24,7 +35,7 @@ def __init__(self, file: str): self.file = file self._file_is_open = False self._input_pars = None - self.xdt=None + self.xdt = None def __getitem__(self, key: str): return self.xdt[key] @@ -60,7 +71,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.xdt = xr.open_datatree(file, engine='zarr', chunks='auto') + self.xdt = xr.open_datatree(file, engine="zarr", chunks="auto") self._input_pars = self.xdt.attrs self._file_is_open = True @@ -76,28 +87,30 @@ def summary(self) -> None: print_summary_header(self.file) print_dict_table(self._input_pars) print_data_contents(self, ["Antenna", "DDI", "Cut"]) - print_method_list([ - self.summary, - self.plot_beamcut_in_amplitude, - # self.export_screws, - # self.export_to_fits, - # self.plot_antennas, - # self.export_gain_tables, - self.observation_summary, - ]) + print_method_list( + [ + self.summary, + self.plot_beamcut_in_amplitude, + # self.export_screws, + # self.export_to_fits, + # self.plot_antennas, + # self.export_gain_tables, + self.observation_summary, + ] + ) def observation_summary( - self, - summary_file: str, - ant: Union[str, List[str]] = "all", - ddi: 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, + self, + summary_file: str, + ant: Union[str, List[str]] = "all", + ddi: 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 @@ -131,7 +144,6 @@ def observation_summary( spectral information, beam image characteristics and aperture image characteristics. """ - param_dict = locals() key_order = ["ant", "ddi"] execution, summary_list = compute_graph( @@ -148,54 +160,65 @@ def observation_summary( if print_summary: print(full_summary) - def plot_beamcut_in_amplitude(self, - destination: str, - ant: Union[str, List[str]] = "all", - ddi: Union[int, List[int]] = "all", - lm_unit: str = 'amin', - azel_unit: str = 'deg', - y_scale: str = None, - display: bool = False, - dpi: int = 300, - parallel: bool = False, - ) -> None: + def plot_beamcut_in_amplitude( + self, + destination: str, + ant: Union[str, List[str]] = "all", + ddi: Union[int, List[int]] = "all", + lm_unit: str = "amin", + azel_unit: str = "deg", + y_scale: str = None, + display: bool = False, + dpi: int = 300, + parallel: bool = False, + ) -> None: param_dict = locals() pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) compute_graph( - self, plot_beamcut_in_amplitude_chunk, param_dict, ["ant", "ddi"], parallel=parallel + self, + plot_beamcut_in_amplitude_chunk, + param_dict, + ["ant", "ddi"], + parallel=parallel, ) return - - def plot_beamcut_in_attenuation(self, - destination: str, - ant: Union[str, List[str]] = "all", - ddi: Union[int, List[int]] = "all", - lm_unit: str = 'amin', - azel_unit: str = 'deg', - y_scale: str = None, - display: bool = False, - dpi: int = 300, - parallel: bool = False, - ) -> None: + + def plot_beamcut_in_attenuation( + self, + destination: str, + ant: Union[str, List[str]] = "all", + ddi: Union[int, List[int]] = "all", + lm_unit: str = "amin", + azel_unit: str = "deg", + y_scale: str = None, + display: bool = False, + dpi: int = 300, + parallel: bool = False, + ) -> None: param_dict = locals() pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) compute_graph( - self, plot_beamcut_in_attenuation_chunk, param_dict, ["ant", "ddi"], parallel=parallel + self, + plot_beamcut_in_attenuation_chunk, + param_dict, + ["ant", "ddi"], + parallel=parallel, ) return - def create_beam_fit_report(self, - destination: str, - ant: Union[str, List[str]] = "all", - ddi: Union[int, List[int]] = "all", - lm_unit: str = 'amin', - azel_unit: str = 'deg', - parallel: bool = False, - ) -> None: + def create_beam_fit_report( + self, + destination: str, + ant: Union[str, List[str]] = "all", + ddi: Union[int, List[int]] = "all", + lm_unit: str = "amin", + azel_unit: str = "deg", + parallel: bool = False, + ) -> None: param_dict = locals() @@ -203,4 +226,4 @@ def create_beam_fit_report(self, compute_graph( self, create_report_chunk, param_dict, ["ant", "ddi"], parallel=parallel ) - return \ No newline at end of file + return diff --git a/src/astrohack/core/beamcut.py b/src/astrohack/core/beamcut.py index dfedfe9c..b60e755b 100644 --- a/src/astrohack/core/beamcut.py +++ b/src/astrohack/core/beamcut.py @@ -8,15 +8,23 @@ 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, sig_2_fwhm, \ - format_frequency, format_value_unit, to_db, create_pretty_table +from astrohack.utils import ( + create_dataset_label, + convert_unit, + sig_2_fwhm, + format_frequency, + format_value_unit, + to_db, + create_pretty_table, +) from astrohack.visualization import create_figure_and_axes, scatter_plot, close_figure from astrohack.visualization.plot_tools import set_y_axis_lims_from_default -lnbr = '\n' -spc = ' ' +lnbr = "\n" +spc = " " quack_chans = 4 + ########################################################### ### Processing Chunks ########################################################### @@ -32,7 +40,7 @@ def process_beamcut_chunk(beamcut_chunk_params): ddi_id=ddi, ) # This assumes that there will be no more than one mapping - input_xds = ant_data_dict[ddi]['map_0'] + input_xds = ant_data_dict[ddi]["map_0"] datalabel = create_dataset_label(antenna, ddi) logger.info(f"processing {datalabel}") @@ -40,85 +48,96 @@ def process_beamcut_chunk(beamcut_chunk_params): _beamcut_multi_lobes_gaussian_fit(cut_xdtree, datalabel) - destination = beamcut_chunk_params['destination'] + destination = beamcut_chunk_params["destination"] if destination is not None: - logger.info(f'Producing plots for {datalabel}') + logger.info(f"Producing plots for {datalabel}") plot_beamcut_in_amplitude_chunk(beamcut_chunk_params, cut_xdtree) plot_beamcut_in_attenuation_chunk(beamcut_chunk_params, cut_xdtree) create_report_chunk(beamcut_chunk_params, cut_xdtree) - logger.info(f'Completed plots for {datalabel}') + logger.info(f"Completed plots for {datalabel}") return cut_xdtree def plot_beamcut_in_amplitude_chunk(par_dict, cut_xdtree=None): if cut_xdtree is None: - cut_xdtree = par_dict['xdt_data'] + cut_xdtree = par_dict["xdt_data"] n_cuts = len(cut_xdtree.children.values()) # Loop over cuts - fig, axes = create_figure_and_axes([12, 1+n_cuts*4], [n_cuts, 2]) + fig, axes = create_figure_and_axes([12, 1 + n_cuts * 4], [n_cuts, 2]) for icut, cut_xds in enumerate(cut_xdtree.children.values()): _plot_single_cut_in_amplitude(cut_xds, axes[icut, :], par_dict) # Header creation - summary = cut_xdtree.attrs['summary'] + summary = cut_xdtree.attrs["summary"] title = _create_beamcut_header(summary, par_dict) - filename = _file_name_factory('amplitude', par_dict) - close_figure(fig, title, filename, par_dict['dpi'], par_dict['display']) + filename = _file_name_factory("amplitude", par_dict) + close_figure(fig, title, filename, par_dict["dpi"], par_dict["display"]) def plot_beamcut_in_attenuation_chunk(par_dict, cut_xdtree=None): if cut_xdtree is None: - cut_xdtree = par_dict['xdt_data'] + cut_xdtree = par_dict["xdt_data"] n_cuts = len(cut_xdtree.children.values()) # Loop over cuts - fig, axes = create_figure_and_axes([6, 1+n_cuts*4], [n_cuts, 1]) + fig, axes = create_figure_and_axes([6, 1 + n_cuts * 4], [n_cuts, 1]) for icut, cut_xds in enumerate(cut_xdtree.children.values()): _plot_single_cut_in_attenuation(cut_xds, axes[icut], par_dict) # Header creation - summary = cut_xdtree.attrs['summary'] + summary = cut_xdtree.attrs["summary"] title = _create_beamcut_header(summary, par_dict) - filename = _file_name_factory('attenuation', par_dict) - close_figure(fig, title, filename, par_dict['dpi'], par_dict['display']) + filename = _file_name_factory("attenuation", par_dict) + close_figure(fig, title, filename, par_dict["dpi"], par_dict["display"]) -def create_report_chunk(par_dict, cut_xdtree=None, spacing=2, item_marker='-', precision=3): +def create_report_chunk( + par_dict, cut_xdtree=None, spacing=2, item_marker="-", precision=3 +): if cut_xdtree is None: - cut_xdtree = par_dict['xdt_data'] - outstr = f'{item_marker}{spc}' - lm_unit = par_dict['lm_unit'] - lm_fac = convert_unit('rad', lm_unit, 'trigonometric') - summary = cut_xdtree.attrs['summary'] - - items = ['Id', f'Center [{lm_unit}]', 'Amplitude [ ]', f'FWHM [{lm_unit}]', 'Attenuation [dB]'] + cut_xdtree = par_dict["xdt_data"] + outstr = f"{item_marker}{spc}" + lm_unit = par_dict["lm_unit"] + lm_fac = convert_unit("rad", lm_unit, "trigonometric") + summary = cut_xdtree.attrs["summary"] + + items = [ + "Id", + f"Center [{lm_unit}]", + "Amplitude [ ]", + f"FWHM [{lm_unit}]", + "Attenuation [dB]", + ] outstr += _create_beamcut_header(summary, par_dict) + 2 * lnbr for icut, cut_xds in enumerate(cut_xdtree.children.values()): sub_title = _make_parallel_hand_sub_title(cut_xds.attrs) - for i_corr, parallel_hand in enumerate(cut_xds.attrs['available_corrs']): - outstr += f'{spacing*spc}{item_marker}{spc}{parallel_hand} {sub_title}, Beam fit results:{lnbr}' - table = create_pretty_table(items, 'c') - fit_pars = cut_xds.attrs[f'{parallel_hand}_amp_fit_pars'] + for i_corr, parallel_hand in enumerate(cut_xds.attrs["available_corrs"]): + outstr += f"{spacing*spc}{item_marker}{spc}{parallel_hand} {sub_title}, Beam fit results:{lnbr}" + table = create_pretty_table(items, "c") + fit_pars = cut_xds.attrs[f"{parallel_hand}_amp_fit_pars"] centers = fit_pars[0::3] amps = fit_pars[1::3] fwhms = fit_pars[2::3] - max_amp = np.max(cut_xds[f'{parallel_hand}_amplitude'].values) - - for i_peak in range(cut_xds.attrs[f'{parallel_hand}_n_peaks']): - - table.add_row([f'{i_peak+1})', # Id - f'{lm_fac*centers[i_peak]:.{precision}f}', # center - f'{amps[i_peak]:.{precision}f}', # Amp - f'{lm_fac*fwhms[i_peak]:.{precision}f}', # FWHM - f'{to_db(amps[i_peak]/max_amp):.{precision}f}', # Attenuation - ]) + max_amp = np.max(cut_xds[f"{parallel_hand}_amplitude"].values) + + for i_peak in range(cut_xds.attrs[f"{parallel_hand}_n_peaks"]): + + table.add_row( + [ + f"{i_peak+1})", # Id + f"{lm_fac*centers[i_peak]:.{precision}f}", # center + f"{amps[i_peak]:.{precision}f}", # Amp + f"{lm_fac*fwhms[i_peak]:.{precision}f}", # FWHM + f"{to_db(amps[i_peak]/max_amp):.{precision}f}", # Attenuation + ] + ) for line in table.get_string().splitlines(): - outstr += 2*spacing*spc+line+lnbr + outstr += 2 * spacing * spc + line + lnbr outstr += lnbr - with open(_file_name_factory('report', par_dict), 'w') as outfile: + with open(_file_name_factory("report", par_dict), "w") as outfile: outfile.write(outstr) @@ -126,16 +145,16 @@ def create_report_chunk(par_dict, cut_xdtree=None, spacing=2, item_marker='-', p ### Data IO ########################################################### def _file_name_factory(file_type, par_dict): - destination = par_dict['destination'] - antenna = par_dict['this_ant'] - ddi = par_dict['this_ddi'] - if file_type in ['attenuation', 'amplitude']: - ext = 'png' - elif file_type == 'report': - ext = 'txt' + destination = par_dict["destination"] + antenna = par_dict["this_ant"] + ddi = par_dict["this_ddi"] + if file_type in ["attenuation", "amplitude"]: + ext = "png" + elif file_type == "report": + ext = "txt" else: - raise ValueError('Invalid file type') - return f'{destination}/beamcut_{file_type}_{antenna}_{ddi}.{ext}' + raise ValueError("Invalid file type") + return f"{destination}/beamcut_{file_type}_{antenna}_{ddi}.{ext}" ########################################################### @@ -144,16 +163,17 @@ def _file_name_factory(file_type, par_dict): def _time_scan_selection(scan_time_ranges, time_axis): time_selections = [] for scan_time_range in scan_time_ranges: - time_selection = np.logical_and(time_axis >= scan_time_range[0], - time_axis < scan_time_range[1]) + time_selection = np.logical_and( + time_axis >= scan_time_range[0], time_axis < scan_time_range[1] + ) time_selections.append(time_selection) return time_selections 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 = 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"] lm_offsets = input_xds.DIRECTIONAL_COSINES.values @@ -167,112 +187,131 @@ def _extract_cuts_from_visibilities(input_xds, antenna, ddi): lchan = int(nchan - fchan) for iscan, scan_number in enumerate(scan_list): scan_time_range = scan_time_ranges[iscan] - time_selection = np.logical_and(time_axis >= scan_time_range[0], - time_axis < scan_time_range[1]) + time_selection = np.logical_and( + time_axis >= scan_time_range[0], time_axis < scan_time_range[1] + ) time = time_axis[time_selection] this_lm_offsets = lm_offsets[time_selection, :] - lm_angle, lm_dist, direction, xlabel = _cut_direction_determination_and_label_creation(this_lm_offsets) + lm_angle, lm_dist, direction, xlabel = ( + _cut_direction_determination_and_label_creation(this_lm_offsets) + ) hands_dict = _get_parallel_hand_indexes(corr_axis) - avg_vis = np.average(visibilities[time_selection, fchan:lchan, :], axis=1, - weights=weights[time_selection, fchan:lchan, :]) + avg_vis = np.average( + visibilities[time_selection, fchan:lchan, :], + axis=1, + weights=weights[time_selection, fchan:lchan, :], + ) avg_wei = np.average(weights[time_selection, fchan:lchan, :], axis=1) - avg_time = np.average(time)*convert_unit('sec', 'day', 'time') - timestr = astropy.time.Time(avg_time, format='mjd').to_value('iso', subfmt='date_hm') + avg_time = np.average(time) * convert_unit("sec", "day", "time") + timestr = astropy.time.Time(avg_time, format="mjd").to_value( + "iso", subfmt="date_hm" + ) xds = xr.Dataset() - coords = {'lm_dist': lm_dist, "time": time} - - xds.attrs.update({ - 'scan_number': scan_number, - 'lm_angle': lm_angle, - 'available_corrs': hands_dict['parallel_hands'], - 'direction': direction, - 'xlabel': xlabel, - 'time_string': timestr, - }) - - xds['lm_offsets'] = xr.DataArray(this_lm_offsets, dims=["time", "lm"]) + coords = {"lm_dist": lm_dist, "time": time} + + xds.attrs.update( + { + "scan_number": scan_number, + "lm_angle": lm_angle, + "available_corrs": hands_dict["parallel_hands"], + "direction": direction, + "xlabel": xlabel, + "time_string": timestr, + } + ) + + xds["lm_offsets"] = xr.DataArray(this_lm_offsets, dims=["time", "lm"]) all_corr_ymax = 1e-34 - for parallel_hand in hands_dict['parallel_hands']: + for parallel_hand in hands_dict["parallel_hands"]: icorr = hands_dict[parallel_hand] amp = np.abs(avg_vis[:, icorr]) maxamp = np.max(amp) if maxamp > all_corr_ymax: all_corr_ymax = maxamp - xds[f'{parallel_hand}_amplitude'] = xr.DataArray(amp, dims='lm_dist') - xds[f'{parallel_hand}_phase'] = xr.DataArray(np.angle(avg_vis[:, icorr]), dims='lm_dist') - xds[f'{parallel_hand}_weight'] = xr.DataArray(avg_wei[:, icorr], dims='lm_dist') - xds.attrs.update({'all_corr_ymax': all_corr_ymax}) - cut_xdtree = cut_xdtree.assign({f'cut_{iscan}': xr.DataTree(dataset=xds.assign_coords(coords), - name=f'cut_{iscan}')}) + xds[f"{parallel_hand}_amplitude"] = xr.DataArray(amp, dims="lm_dist") + xds[f"{parallel_hand}_phase"] = xr.DataArray( + np.angle(avg_vis[:, icorr]), dims="lm_dist" + ) + xds[f"{parallel_hand}_weight"] = xr.DataArray( + avg_wei[:, icorr], dims="lm_dist" + ) + xds.attrs.update({"all_corr_ymax": all_corr_ymax}) + cut_xdtree = cut_xdtree.assign( + { + f"cut_{iscan}": xr.DataTree( + dataset=xds.assign_coords(coords), name=f"cut_{iscan}" + ) + } + ) return cut_xdtree -def _cut_direction_determination_and_label_creation(lm_offsets, angle_unit='deg'): +def _cut_direction_determination_and_label_creation(lm_offsets, angle_unit="deg"): dx = lm_offsets[-1, 0] - lm_offsets[0, 0] dy = lm_offsets[-1, 1] - lm_offsets[0, 1] lm_dist = np.sqrt(lm_offsets[:, 0] ** 2 + lm_offsets[:, 1] ** 2) imin_lm = np.argmin(lm_dist) lm_dist[:imin_lm] = -lm_dist[:imin_lm] - if np.isclose(dx, dy, rtol=3e-1): # X case + if np.isclose(dx, dy, rtol=3e-1): # X case result = linregress(lm_offsets[:, 0], lm_offsets[:, 1]) - lm_angle = np.arctan(result.slope) + np.pi/2 - direction = 'mixed cut(' + lm_angle = np.arctan(result.slope) + np.pi / 2 + direction = "mixed cut(" if dy < 0 and dx < 0: - direction += 'NW -> SE' + direction += "NW -> SE" elif dy < 0 < dx: - direction += 'NE -> SW' + direction += "NE -> SW" elif dy > 0 > dx: - direction += 'SW -> NE' + direction += "SW -> NE" else: - direction += 'SE -> NW' - - direction += (r', $\theta$ = ' + - f'{format_value_unit(convert_unit('rad', angle_unit, 'trigonometric')*lm_angle, angle_unit)}') - xlabel = 'Mixed offset' - elif np.abs(dy) > np.abs(dx): # Elevation case + direction += "SE -> NW" + + direction += ( + r", $\theta$ = " + + f"{format_value_unit(convert_unit('rad', angle_unit, 'trigonometric')*lm_angle, angle_unit)}" + ) + xlabel = "Mixed offset" + elif np.abs(dy) > np.abs(dx): # Elevation case result = linregress(lm_offsets[:, 1], lm_offsets[:, 0]) lm_angle = np.arctan(result.slope) - direction = 'El. cut (' + direction = "El. cut (" if dy < 0: - direction += 'N -> S' - lm_dist *= -1 # Flip as sense is negative + direction += "N -> S" + lm_dist *= -1 # Flip as sense is negative else: - direction += 'S -> N' - xlabel = 'Elevation offset' - else: # Azimuth case + direction += "S -> N" + xlabel = "Elevation offset" + else: # Azimuth case result = linregress(lm_offsets[:, 0], lm_offsets[:, 1]) - lm_angle = np.arctan(result.slope) + np.pi/2 - direction = 'Az. cut (' + lm_angle = np.arctan(result.slope) + np.pi / 2 + direction = "Az. cut (" if dx > 0: - direction += 'E -> W' + direction += "E -> W" else: - direction += 'W -> E' - lm_dist *= -1 # Flip as sense is negative - xlabel = 'Azimuth offset' + direction += "W -> E" + lm_dist *= -1 # Flip as sense is negative + xlabel = "Azimuth offset" - direction += ')' + direction += ")" return lm_angle, lm_dist, direction, xlabel def _get_parallel_hand_indexes(corr_axis): - if 'L' in corr_axis[0] or 'R' in corr_axis[0]: - parallel_hands = ['RR', 'LL'] + if "L" in corr_axis[0] or "R" in corr_axis[0]: + parallel_hands = ["RR", "LL"] else: - parallel_hands = ['XX', 'YY'] + parallel_hands = ["XX", "YY"] - hands_dict ={ - 'parallel_hands': parallel_hands - } + hands_dict = {"parallel_hands": parallel_hands} for icorr, corr in enumerate(corr_axis): hands_dict[corr] = icorr return hands_dict @@ -283,10 +322,12 @@ def _get_parallel_hand_indexes(corr_axis): ########################################################### def _fwhm_gaussian(x_axis, x_off, amp, fwhm): sigma = fwhm / sig_2_fwhm - return amp * np.exp(-(x_axis - x_off)**2/(2*sigma**2)) + return amp * np.exp(-((x_axis - x_off) ** 2) / (2 * sigma**2)) -def _build_multi_gaussian_initial_guesses(x_data, y_data, pb_fwhm, min_dist_fraction=1.3): +def _build_multi_gaussian_initial_guesses( + x_data, y_data, pb_fwhm, min_dist_fraction=1.3 +): initial_guesses = [] lower_bounds = [] upper_bounds = [] @@ -306,21 +347,30 @@ def _build_multi_gaussian_initial_guesses(x_data, y_data, pb_fwhm, min_dist_frac def _multi_gaussian(xdata, *args): nargs = len(args) - if nargs%3 != 0: - raise ValueError('Number of arguments should be multiple of 3') + if nargs % 3 != 0: + raise ValueError("Number of arguments should be multiple of 3") y_values = np.zeros_like(xdata) for iarg in range(0, nargs, 3): y_values += _fwhm_gaussian(xdata, args[iarg], args[iarg + 1], args[iarg + 2]) return y_values -def _perform_curvefit_with_given_functions(x_data, y_data, initial_guesses, bounds, fit_func, datalabel, maxit=50000): +def _perform_curvefit_with_given_functions( + x_data, y_data, initial_guesses, bounds, fit_func, datalabel, maxit=50000 +): try: - results = curve_fit(fit_func, x_data, y_data, p0=initial_guesses, bounds=bounds, maxfev=int(maxit)) + results = curve_fit( + fit_func, + x_data, + y_data, + p0=initial_guesses, + bounds=bounds, + maxfev=int(maxit), + ) fit_pars = results[0] return True, fit_pars except RuntimeError: - logger.warning(f'{fit_func.__name__} fit to lobes failed for {datalabel}.') + logger.warning(f"{fit_func.__name__} fit to lobes failed for {datalabel}.") return False, np.full_like(initial_guesses, np.nan) @@ -332,7 +382,7 @@ def _identify_pb_and_sidelobes_in_fit(datalabel, x_data, fit_pars): # select fits that are within x_data x_min = np.min(x_data) x_max = np.max(x_data) - selection = ~ np.logical_or(centers < x_min, centers > x_max) + selection = ~np.logical_or(centers < x_min, centers > x_max) # apply selection centers = centers[selection] @@ -341,7 +391,7 @@ def _identify_pb_and_sidelobes_in_fit(datalabel, x_data, fit_pars): # Reconstruct fit metadata n_peaks = centers.shape[0] - fit_pars = np.zeros((3*n_peaks)) + fit_pars = np.zeros((3 * n_peaks)) fit_pars[0::3] = centers fit_pars[1::3] = amps fit_pars[2::3] = fwhms @@ -353,7 +403,7 @@ def _identify_pb_and_sidelobes_in_fit(datalabel, x_data, fit_pars): pb_problem = i_pb_cen != i_pb_amp if pb_problem: - logger.warning(f'Cannot reliably identify primary beam for {datalabel}.') + logger.warning(f"Cannot reliably identify primary beam for {datalabel}.") pb_center, pb_fwhm, first_side_lobe_ratio = np.nan, np.nan, np.nan else: @@ -377,7 +427,7 @@ def _identify_pb_and_sidelobes_in_fit(datalabel, x_data, fit_pars): def _beamcut_multi_lobes_gaussian_fit(cut_xdtree, datalabel): # Get the summary from the first cut, but it should be equal anyway - summary = cut_xdtree.attrs['summary'] + summary = cut_xdtree.attrs["summary"] wavelength = summary["spectral"]["rep. wavelength"] telescope = get_proper_telescope( summary["general"]["telescope name"], summary["general"]["antenna name"] @@ -385,34 +435,45 @@ def _beamcut_multi_lobes_gaussian_fit(cut_xdtree, datalabel): primary_fwhm = 1.2 * wavelength / telescope.diameter for cut_xds in cut_xdtree.children.values(): - x_data = cut_xds['lm_dist'].values - for parallel_hand in cut_xds.attrs['available_corrs']: - y_data = cut_xds[f'{parallel_hand}_amplitude'] - this_corr_data_label = f'{datalabel}, {cut_xds.attrs["direction"]}, corr = {parallel_hand}' - initial_guesses, bounds, n_peaks = _build_multi_gaussian_initial_guesses(x_data, y_data, primary_fwhm) - fit_succeeded, fit_pars = _perform_curvefit_with_given_functions(x_data, - y_data, - initial_guesses, - bounds, - _multi_gaussian, - this_corr_data_label) + x_data = cut_xds["lm_dist"].values + for parallel_hand in cut_xds.attrs["available_corrs"]: + y_data = cut_xds[f"{parallel_hand}_amplitude"] + this_corr_data_label = ( + f'{datalabel}, {cut_xds.attrs["direction"]}, corr = {parallel_hand}' + ) + initial_guesses, bounds, n_peaks = _build_multi_gaussian_initial_guesses( + x_data, y_data, primary_fwhm + ) + fit_succeeded, fit_pars = _perform_curvefit_with_given_functions( + x_data, + y_data, + initial_guesses, + bounds, + _multi_gaussian, + this_corr_data_label, + ) if fit_succeeded: fit = _multi_gaussian(x_data, *fit_pars) - n_peaks, fit_pars, pb_center, pb_fwhm, first_side_lobe_ratio = \ - _identify_pb_and_sidelobes_in_fit(this_corr_data_label, x_data, fit_pars) + n_peaks, fit_pars, pb_center, pb_fwhm, first_side_lobe_ratio = ( + _identify_pb_and_sidelobes_in_fit( + this_corr_data_label, x_data, fit_pars + ) + ) else: pb_center, pb_fwhm, first_side_lobe_ratio = np.nan, np.nan, np.nan fit = np.full_like(y_data, np.nan) - cut_xds.attrs[f'{parallel_hand}_amp_fit_pars'] = fit_pars - cut_xds.attrs[f'{parallel_hand}_n_peaks'] = n_peaks - cut_xds.attrs[f'{parallel_hand}_pb_fwhm'] = pb_fwhm - cut_xds.attrs[f'{parallel_hand}_pb_center'] = pb_center - cut_xds.attrs[f'{parallel_hand}_first_side_lobe_ratio'] = first_side_lobe_ratio - cut_xds.attrs[f'{parallel_hand}_fit_succeeded'] = fit_succeeded + cut_xds.attrs[f"{parallel_hand}_amp_fit_pars"] = fit_pars + cut_xds.attrs[f"{parallel_hand}_n_peaks"] = n_peaks + cut_xds.attrs[f"{parallel_hand}_pb_fwhm"] = pb_fwhm + cut_xds.attrs[f"{parallel_hand}_pb_center"] = pb_center + cut_xds.attrs[f"{parallel_hand}_first_side_lobe_ratio"] = ( + first_side_lobe_ratio + ) + cut_xds.attrs[f"{parallel_hand}_fit_succeeded"] = fit_succeeded - cut_xds[f'{parallel_hand}_amp_fit'] = xr.DataArray(fit, dims='lm_dist') + cut_xds[f"{parallel_hand}_amp_fit"] = xr.DataArray(fit, dims="lm_dist") return @@ -422,36 +483,53 @@ def _beamcut_multi_lobes_gaussian_fit(cut_xdtree, datalabel): def _add_secondary_beam_hpbw_x_axis_to_plot(pb_fwhm, ax): if np.isnan(pb_fwhm): return - sec_x_axis = ax.secondary_xaxis('top', functions=(lambda x: x*1.0, lambda xb: 1*xb)) - sec_x_axis.set_xlabel('Offset in Primary Beam HPBWs\n') + sec_x_axis = ax.secondary_xaxis( + "top", functions=(lambda x: x * 1.0, lambda xb: 1 * xb) + ) + sec_x_axis.set_xlabel("Offset in Primary Beam HPBWs\n") sec_x_axis.set_xticks([]) y_min, y_max = ax.get_ylim() x_lims = np.array(ax.get_xlim()) - pb_min, pb_max = np.ceil(x_lims/pb_fwhm) + pb_min, pb_max = np.ceil(x_lims / pb_fwhm) beam_offsets = np.arange(pb_min, pb_max, 1, dtype=int) for itk in beam_offsets: - ax.axvline(itk * pb_fwhm, color='k', linestyle='--', linewidth=0.5) - ax.text(itk*pb_fwhm, y_max, f'{itk:d}', va='bottom', ha='center') + ax.axvline(itk * pb_fwhm, color="k", linestyle="--", linewidth=0.5) + ax.text(itk * pb_fwhm, y_max, f"{itk:d}", va="bottom", ha="center") def _add_lobe_identification_to_plot(ax, centers, peaks, y_off): for i_peak, peak in enumerate(peaks): - ax.text(centers[i_peak], peak+y_off, f'{i_peak+1})', ha='center', va='bottom') - - -def _add_beam_parameters_box(ax, pb_center, pb_fwhm, sidelobe_ratio, lm_unit, alpha=0.8, x_pos=0.05, y_pos=0.95, - attenuation_plot=False): + ax.text(centers[i_peak], peak + y_off, f"{i_peak+1})", ha="center", va="bottom") + + +def _add_beam_parameters_box( + ax, + pb_center, + pb_fwhm, + sidelobe_ratio, + lm_unit, + alpha=0.8, + x_pos=0.05, + y_pos=0.95, + attenuation_plot=False, +): if attenuation_plot: - head = 'avg ' + head = "avg " else: - head = '' - pars_str = f'{head}PB off. = {format_value_unit(pb_center, lm_unit, 3)}\n' - pars_str += f'{head}PB FWHM = {format_value_unit(pb_fwhm, lm_unit, 3)}\n' - pars_str += f'{head}FSLR = {format_value_unit(to_db(sidelobe_ratio), 'dB', 2)}' - bounds_box = dict(boxstyle='square', facecolor='white', alpha=alpha) - ax.text(x_pos, y_pos, pars_str, transform=ax.transAxes, verticalalignment='top', - bbox=bounds_box) + head = "" + pars_str = f"{head}PB off. = {format_value_unit(pb_center, lm_unit, 3)}\n" + pars_str += f"{head}PB FWHM = {format_value_unit(pb_fwhm, lm_unit, 3)}\n" + pars_str += f"{head}FSLR = {format_value_unit(to_db(sidelobe_ratio), 'dB', 2)}" + bounds_box = dict(boxstyle="square", facecolor="white", alpha=alpha) + ax.text( + x_pos, + y_pos, + pars_str, + transform=ax.transAxes, + verticalalignment="top", + bbox=bounds_box, + ) ########################################################### @@ -460,101 +538,155 @@ def _add_beam_parameters_box(ax, pb_center, pb_fwhm, sidelobe_ratio, lm_unit, al def _plot_single_cut_in_amplitude(cut_xds, axes, par_dict): # Init sub_title = _make_parallel_hand_sub_title(cut_xds.attrs) - max_amp = cut_xds.attrs['all_corr_ymax'] - y_off = 0.05*max_amp - lm_unit = par_dict['lm_unit'] - lm_fac = convert_unit('rad', lm_unit, 'trigonometric') + max_amp = cut_xds.attrs["all_corr_ymax"] + y_off = 0.05 * max_amp + lm_unit = par_dict["lm_unit"] + lm_fac = convert_unit("rad", lm_unit, "trigonometric") # Loop over correlations - for i_corr, parallel_hand in enumerate(cut_xds.attrs['available_corrs']): + for i_corr, parallel_hand in enumerate(cut_xds.attrs["available_corrs"]): # Init labels this_ax = axes[i_corr] - x_data = lm_fac * cut_xds['lm_dist'].values - y_data = cut_xds[f'{parallel_hand}_amplitude'].values - fit_data = cut_xds[f'{parallel_hand}_amp_fit'].values - xlabel = f'{cut_xds.attrs['xlabel']} [{lm_unit}]' - ylabel = f'{parallel_hand} Amplitude [ ]' + x_data = lm_fac * cut_xds["lm_dist"].values + y_data = cut_xds[f"{parallel_hand}_amplitude"].values + fit_data = cut_xds[f"{parallel_hand}_amp_fit"].values + xlabel = f"{cut_xds.attrs['xlabel']} [{lm_unit}]" + ylabel = f"{parallel_hand} Amplitude [ ]" # Call plotting tool - if cut_xds.attrs[f'{parallel_hand}_fit_succeeded']: - scatter_plot(this_ax, x_data, xlabel, y_data, ylabel, model=fit_data, model_marker='', title=sub_title, - data_marker='+', residuals_marker='.', model_linestyle='-', data_label=f'{parallel_hand} data', - model_label=f'{parallel_hand} fit', data_color='red', model_color='blue', - residuals_color='black', legend_location='upper right') + if cut_xds.attrs[f"{parallel_hand}_fit_succeeded"]: + scatter_plot( + this_ax, + x_data, + xlabel, + y_data, + ylabel, + model=fit_data, + model_marker="", + title=sub_title, + data_marker="+", + residuals_marker=".", + model_linestyle="-", + data_label=f"{parallel_hand} data", + model_label=f"{parallel_hand} fit", + data_color="red", + model_color="blue", + residuals_color="black", + legend_location="upper right", + ) # Add fit peak identifiers - centers = lm_fac * np.array(cut_xds.attrs[f'{parallel_hand}_amp_fit_pars'][0::3]) - amps = np.array(cut_xds.attrs[f'{parallel_hand}_amp_fit_pars'][1::3]) - - _add_lobe_identification_to_plot(this_ax, centers, amps, y_off, attenunation_plot=False) + centers = lm_fac * np.array( + cut_xds.attrs[f"{parallel_hand}_amp_fit_pars"][0::3] + ) + amps = np.array(cut_xds.attrs[f"{parallel_hand}_amp_fit_pars"][1::3]) + + _add_lobe_identification_to_plot( + this_ax, centers, amps, y_off, attenunation_plot=False + ) else: - scatter_plot(this_ax, x_data, xlabel, y_data, ylabel, title=sub_title, data_marker='+', - data_label=f'{parallel_hand} data', data_color='red', legend_location='upper right') + scatter_plot( + this_ax, + x_data, + xlabel, + y_data, + ylabel, + title=sub_title, + data_marker="+", + data_label=f"{parallel_hand} data", + data_color="red", + legend_location="upper right", + ) # equalize Y scale between correlations - set_y_axis_lims_from_default(this_ax, par_dict['y_scale'], (-y_off, max_amp+3*y_off)) + set_y_axis_lims_from_default( + this_ax, par_dict["y_scale"], (-y_off, max_amp + 3 * y_off) + ) - _add_secondary_beam_hpbw_x_axis_to_plot(cut_xds.attrs[f'{parallel_hand}_pb_fwhm'] * lm_fac, this_ax) + _add_secondary_beam_hpbw_x_axis_to_plot( + cut_xds.attrs[f"{parallel_hand}_pb_fwhm"] * lm_fac, this_ax + ) # Add bounded box with Beam parameters - _add_beam_parameters_box(this_ax, cut_xds.attrs[f'{parallel_hand}_pb_center'] * lm_fac, - cut_xds.attrs[f'{parallel_hand}_pb_fwhm'] * lm_fac, - cut_xds.attrs[f'{parallel_hand}_first_side_lobe_ratio'], - lm_unit) + _add_beam_parameters_box( + this_ax, + cut_xds.attrs[f"{parallel_hand}_pb_center"] * lm_fac, + cut_xds.attrs[f"{parallel_hand}_pb_fwhm"] * lm_fac, + cut_xds.attrs[f"{parallel_hand}_first_side_lobe_ratio"], + lm_unit, + ) def _plot_single_cut_in_attenuation(cut_xds, ax, par_dict): sub_title = _make_parallel_hand_sub_title(cut_xds.attrs) - lm_unit = par_dict['lm_unit'] - lm_fac = convert_unit('rad', lm_unit, 'trigonometric') - corr_colors = ['blue', 'red'] + lm_unit = par_dict["lm_unit"] + lm_fac = convert_unit("rad", lm_unit, "trigonometric") + corr_colors = ["blue", "red"] min_attenuation = 1e34 pb_center = 0.0 pb_fwhm = 0.0 fsl_ratio = 0.0 - xlabel = f'{cut_xds.attrs['xlabel']} [{lm_unit}]' - ylabel = f'Attenuation [dB]' + xlabel = f"{cut_xds.attrs['xlabel']} [{lm_unit}]" + ylabel = f"Attenuation [dB]" # Loop over correlations n_data = 0 - for i_corr, parallel_hand in enumerate(cut_xds.attrs['available_corrs']): + for i_corr, parallel_hand in enumerate(cut_xds.attrs["available_corrs"]): # Init labels - x_data = lm_fac * cut_xds['lm_dist'].values - amps = cut_xds[f'{parallel_hand}_amplitude'].values + x_data = lm_fac * cut_xds["lm_dist"].values + amps = cut_xds[f"{parallel_hand}_amplitude"].values max_amp = np.max(amps) - y_data = to_db(amps/max_amp) + y_data = to_db(amps / max_amp) y_min = np.min(y_data) if y_min < min_attenuation: min_attenuation = y_min - ax.plot(x_data, y_data, label=parallel_hand, color=corr_colors[i_corr], marker='.', ls='') + ax.plot( + x_data, + y_data, + label=parallel_hand, + color=corr_colors[i_corr], + marker=".", + ls="", + ) if not np.isnan(pb_center): - pb_center += cut_xds.attrs[f'{parallel_hand}_pb_center'] - pb_fwhm += cut_xds.attrs[f'{parallel_hand}_pb_fwhm'] - fsl_ratio += cut_xds.attrs[f'{parallel_hand}_first_side_lobe_ratio'] + pb_center += cut_xds.attrs[f"{parallel_hand}_pb_center"] + pb_fwhm += cut_xds.attrs[f"{parallel_hand}_pb_fwhm"] + fsl_ratio += cut_xds.attrs[f"{parallel_hand}_first_side_lobe_ratio"] n_data += 1 ax.set_xlabel(xlabel) ax.set_ylabel(ylabel) ax.set_title(sub_title) - ax.legend(loc='upper right') + ax.legend(loc="upper right") pb_center /= n_data pb_fwhm /= n_data fsl_ratio /= n_data # equalize Y scale between correlations - y_off = 0.1 *np.abs(min_attenuation) - set_y_axis_lims_from_default(ax, par_dict['y_scale'], (min_attenuation-y_off, y_off )) + y_off = 0.1 * np.abs(min_attenuation) + set_y_axis_lims_from_default( + ax, par_dict["y_scale"], (min_attenuation - y_off, y_off) + ) # Add fit peak identifiers - first_corr = cut_xds.attrs['available_corrs'][0] + first_corr = cut_xds.attrs["available_corrs"][0] - _add_secondary_beam_hpbw_x_axis_to_plot(cut_xds.attrs[f'{first_corr}_pb_fwhm'] * lm_fac, ax) + _add_secondary_beam_hpbw_x_axis_to_plot( + cut_xds.attrs[f"{first_corr}_pb_fwhm"] * lm_fac, ax + ) # Add bounded box with Beam parameters - _add_beam_parameters_box(ax, pb_center * lm_fac, pb_fwhm * lm_fac, fsl_ratio, lm_unit, attenuation_plot=True) + _add_beam_parameters_box( + ax, + pb_center * lm_fac, + pb_fwhm * lm_fac, + fsl_ratio, + lm_unit, + attenuation_plot=True, + ) return @@ -562,19 +694,23 @@ def _plot_single_cut_in_attenuation(cut_xds, ax, par_dict): ### Data labeling ########################################################### def _make_parallel_hand_sub_title(attributes): - direction = attributes['direction'] - time_string = attributes['time_string'] - return f'{direction}, {time_string} UTC' + direction = attributes["direction"] + time_string = attributes["time_string"] + return f"{direction}, {time_string} UTC" def _create_beamcut_header(summary, par_dict): - azel_unit = par_dict['azel_unit'] - antenna = par_dict['this_ant'] - ddi = par_dict['this_ddi'] - freq_str = format_frequency(summary['spectral']['rep. frequency'], decimal_places=3) - raw_azel = np.array(summary['general']["az el info"]["mean"]) - mean_azel = convert_unit('rad', azel_unit, 'trigonometric') * raw_azel - title = f'Beam cut for {create_dataset_label(antenna, ddi, separator=',')}, ' + r'$\nu$ = ' + f'{freq_str}, ' - title += f'Az ~ {format_value_unit(mean_azel[0], 'deg', decimal_places=0)}, ' - title += f'El ~ {format_value_unit(mean_azel[1], 'deg', decimal_places=0)}' + azel_unit = par_dict["azel_unit"] + antenna = par_dict["this_ant"] + ddi = par_dict["this_ddi"] + freq_str = format_frequency(summary["spectral"]["rep. frequency"], decimal_places=3) + raw_azel = np.array(summary["general"]["az el info"]["mean"]) + mean_azel = convert_unit("rad", azel_unit, "trigonometric") * raw_azel + title = ( + f"Beam cut for {create_dataset_label(antenna, ddi, separator=',')}, " + + r"$\nu$ = " + + f"{freq_str}, " + ) + title += f"Az ~ {format_value_unit(mean_azel[0], 'deg', decimal_places=0)}, " + title += f"El ~ {format_value_unit(mean_azel[1], 'deg', decimal_places=0)}" return title diff --git a/src/astrohack/core/extract_holog.py b/src/astrohack/core/extract_holog.py index 34ae6188..610f5e9f 100644 --- a/src/astrohack/core/extract_holog.py +++ b/src/astrohack/core/extract_holog.py @@ -110,7 +110,7 @@ def process_extract_holog_chunk(extract_holog_params): flagged_mapping_antennas, used_samples_dict, scan_time_ranges, - unq_scans + unq_scans, ) = _extract_holog_chunk_jit( vis_data, weight, @@ -256,7 +256,9 @@ def _extract_holog_chunk_jit( polarization) """ - time_samples, scan_time_ranges, unq_scans = _get_time_intervals(time_vis_row, scan_list, time_interval) + 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 @@ -361,7 +363,7 @@ def _extract_holog_chunk_jit( flagged_mapping_antennas, used_samples_dict, scan_time_ranges, - unq_scans + unq_scans, ) diff --git a/src/astrohack/core/extract_pointing.py b/src/astrohack/core/extract_pointing.py index 1d8a623d..c503f88f 100644 --- a/src/astrohack/core/extract_pointing.py +++ b/src/astrohack/core/extract_pointing.py @@ -189,8 +189,9 @@ 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])) + 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") diff --git a/src/astrohack/dio.py b/src/astrohack/dio.py index a93e42a1..8572b95b 100644 --- a/src/astrohack/dio.py +++ b/src/astrohack/dio.py @@ -23,7 +23,7 @@ JSON = NewType("JSON", Dict[str, Any]) -def open_beamcut(file:str) -> Union[AstrohackBeamcutFile, None]: +def open_beamcut(file: str) -> Union[AstrohackBeamcutFile, None]: """ Open beamcut file and return instance of the beamcut data object. Object includes summary function to list\ available nodes. diff --git a/src/astrohack/utils/graph.py b/src/astrohack/utils/graph.py index a3baf1e8..717aa68c 100644 --- a/src/astrohack/utils/graph.py +++ b/src/astrohack/utils/graph.py @@ -8,13 +8,13 @@ def _construct_xdtree_graph_recursively( - xr_datatree, - chunk_function, - param_dict, - delayed_list, - key_order, - parallel=False, - oneup=None, + xr_datatree, + chunk_function, + param_dict, + delayed_list, + key_order, + parallel=False, + oneup=None, ): if len(key_order) == 0: param_dict["xdt_data"] = xr_datatree @@ -50,7 +50,6 @@ def _construct_xdtree_graph_recursively( logger.warning(f"{item} is not present for {oneup}") - def _construct_general_graph_recursively( looping_dict, chunk_function, @@ -126,13 +125,13 @@ def compute_graph( delayed_list = [] if hasattr(looping_dict, "xdt"): _construct_xdtree_graph_recursively( - xr_datatree=looping_dict.xdt, - chunk_function=chunk_function, - param_dict=param_dict, - delayed_list=delayed_list, - key_order=key_order, - parallel=parallel, - ) + xr_datatree=looping_dict.xdt, + 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, diff --git a/src/astrohack/visualization/diagnostics.py b/src/astrohack/visualization/diagnostics.py index a84f52dc..b3cb41fb 100644 --- a/src/astrohack/visualization/diagnostics.py +++ b/src/astrohack/visualization/diagnostics.py @@ -324,7 +324,7 @@ def _plot_lm_coverage_sub(time, real_lm, ideal_lm, param_dict): data_marker=param_dict["marker"], data_linestyle=param_dict["linestyle"], data_color=param_dict["color"], - add_legend=False + add_legend=False, ) scatter_plot( ax[1, 0], @@ -336,7 +336,7 @@ def _plot_lm_coverage_sub(time, real_lm, ideal_lm, param_dict): data_marker=param_dict["marker"], data_linestyle=param_dict["linestyle"], data_color=param_dict["color"], - add_legend=False + add_legend=False, ) scatter_plot( ax[1, 1], @@ -348,7 +348,7 @@ def _plot_lm_coverage_sub(time, real_lm, ideal_lm, param_dict): data_marker=param_dict["marker"], data_linestyle=param_dict["linestyle"], data_color=param_dict["color"], - add_legend=False + add_legend=False, ) plotfile = ( f'{param_dict["destination"]}/holog_directional_cosines_{param_dict["this_map"]}_' @@ -398,7 +398,7 @@ def plot_correlation(visi, weights, correlation, pol_axis, time, lm, param_dict) data_marker=param_dict["marker"], data_linestyle=param_dict["linestyle"], data_color=param_dict["color"], - add_legend=False + add_legend=False, ) scatter_plot( ax[isplit, 1], @@ -422,7 +422,7 @@ def plot_correlation(visi, weights, correlation, pol_axis, time, lm, param_dict) data_marker=param_dict["marker"], data_linestyle=param_dict["linestyle"], data_color=param_dict["color"], - add_legend=False + add_legend=False, ) plotfile = ( @@ -500,10 +500,17 @@ def plot_sky_coverage_chunk(parm_dict): "Time vs Elevation", ylim=elelim, hlines=elelines, - add_legend=False + add_legend=False, ) scatter_plot( - axes[0, 1], time, timelabel, ha, halabel, "Time vs Hour angle", ylim=halim, add_legend=False + axes[0, 1], + time, + timelabel, + ha, + halabel, + "Time vs Hour angle", + ylim=halim, + add_legend=False, ) scatter_plot( axes[1, 0], @@ -514,7 +521,7 @@ def plot_sky_coverage_chunk(parm_dict): "Time vs Declination", ylim=declim, hlines=declines, - add_legend=False + add_legend=False, ) scatter_plot( axes[1, 1], @@ -526,7 +533,7 @@ def plot_sky_coverage_chunk(parm_dict): ylim=declim, xlim=halim, hlines=declines, - add_legend=False + add_legend=False, ) close_figure(fig, suptitle, export_name, dpi, display) diff --git a/src/astrohack/visualization/plot_tools.py b/src/astrohack/visualization/plot_tools.py index cf132278..012397b4 100644 --- a/src/astrohack/visualization/plot_tools.py +++ b/src/astrohack/visualization/plot_tools.py @@ -249,7 +249,7 @@ def scatter_plot( regression_linestyle="-", regression_color="black", add_legend=True, - legend_location='best', + legend_location="best", ): """ Do scatter simple scatter plots of data to a plotting axis @@ -334,7 +334,6 @@ def scatter_plot( lw=2, ) - if model is not None: ax.plot( xdata, @@ -441,4 +440,3 @@ def set_y_axis_lims_from_default(ax, user_y_scale, prog_defaults): ax.set_ylim(prog_defaults) else: ax.set_ylim(user_y_scale) - diff --git a/src/astrohack/visualization/textual_data.py b/src/astrohack/visualization/textual_data.py index 8679533e..c5bc596e 100644 --- a/src/astrohack/visualization/textual_data.py +++ b/src/astrohack/visualization/textual_data.py @@ -615,7 +615,7 @@ def generate_observation_summary_for_beamcut(parm_dict): tab_count = 1 header = f"{antenna}, {ddi}" outstr = make_header(header, "#", 60, 3) - spc = ' ' + spc = " " outstr += ( format_observation_summary( @@ -630,7 +630,7 @@ def generate_observation_summary_for_beamcut(parm_dict): + "\n" ) for cut in xdt.children.values(): - outstr += f'{tab_count*tab_size*spc}{cut.name}:\n' + 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 32776756fd41aa9682de43800926bb5fd59948f9 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Mon, 29 Dec 2025 10:24:36 -0700 Subject: [PATCH 41/95] Added docstrings to beamcut chunk functions. --- src/astrohack/core/beamcut.py | 54 +++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/src/astrohack/core/beamcut.py b/src/astrohack/core/beamcut.py index b60e755b..b178695b 100644 --- a/src/astrohack/core/beamcut.py +++ b/src/astrohack/core/beamcut.py @@ -29,6 +29,15 @@ ### Processing Chunks ########################################################### def process_beamcut_chunk(beamcut_chunk_params): + """ + 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 + + :return: Beamcut_xdtree containing the different cuts for this antenna and DDI. + :rtype: xr.DataTree + """ ddi = beamcut_chunk_params["this_ddi"] antenna = beamcut_chunk_params["this_ant"] @@ -60,6 +69,18 @@ def process_beamcut_chunk(beamcut_chunk_params): def plot_beamcut_in_amplitude_chunk(par_dict, cut_xdtree=None): + """ + Produce Amplitude beam cut plots from a xdtree containing beam cuts. + + :param par_dict: Paremeter dictionary controlling plot aspects + :type par_dict: dict + + :param cut_xdtree: Way to deliver a xdtree when not present in par_dict + :type cut_xdtree: xr.DataTree + + :return: None + :rtype: NoneType + """ if cut_xdtree is None: cut_xdtree = par_dict["xdt_data"] n_cuts = len(cut_xdtree.children.values()) @@ -77,6 +98,18 @@ def plot_beamcut_in_amplitude_chunk(par_dict, cut_xdtree=None): def plot_beamcut_in_attenuation_chunk(par_dict, cut_xdtree=None): + """ + Produce attenuation beam cut plots from a xdtree containing beam cuts. + + :param par_dict: Paremeter dictionary controlling plot aspects + :type par_dict: dict + + :param cut_xdtree: Way to deliver a xdtree when not present in par_dict + :type cut_xdtree: xr.DataTree + + :return: None + :rtype: NoneType + """ if cut_xdtree is None: cut_xdtree = par_dict["xdt_data"] n_cuts = len(cut_xdtree.children.values()) @@ -96,6 +129,27 @@ def plot_beamcut_in_attenuation_chunk(par_dict, cut_xdtree=None): def create_report_chunk( par_dict, cut_xdtree=None, spacing=2, item_marker="-", precision=3 ): + """ + Produce a report on beamcut fit results from a xdtree containing beam cuts. + + :param par_dict: Paremeter dictionary controlling report aspects + :type par_dict: dict + + :param cut_xdtree: Way to deliver a xdtree when not present in par_dict + :type cut_xdtree: xr.DataTree + + :param spacing: Identation + :type spacing: int + + :param item_marker: Character to denote a different item in a list + :type item_marker: str + + :param precision: Number of decimal places to include in table results + :type precision: int + + :return: None + :rtype: NoneType + """ if cut_xdtree is None: cut_xdtree = par_dict["xdt_data"] outstr = f"{item_marker}{spc}" From f314db67db6696bdfb50fba4fe06ef46e272ad1e Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Mon, 29 Dec 2025 11:26:00 -0700 Subject: [PATCH 42/95] Added docstrings to beamcut data extraction functions. --- src/astrohack/core/beamcut.py | 74 ++++++++++++++++++++++++++++++++--- 1 file changed, 68 insertions(+), 6 deletions(-) diff --git a/src/astrohack/core/beamcut.py b/src/astrohack/core/beamcut.py index b178695b..6abe10c9 100644 --- a/src/astrohack/core/beamcut.py +++ b/src/astrohack/core/beamcut.py @@ -1,3 +1,4 @@ +import numpy import toolviper.utils.logger as logger import numpy as np from scipy.optimize import curve_fit @@ -26,7 +27,7 @@ ########################################################### -### Processing Chunks +### Working Chunks ########################################################### def process_beamcut_chunk(beamcut_chunk_params): """ @@ -199,6 +200,18 @@ def create_report_chunk( ### Data IO ########################################################### def _file_name_factory(file_type, par_dict): + """ + Generate filenames from file type and execution parameters + + :param file_type: File type description + :type file_type: str + + :param par_dict: Paremeter dictionary containing destination, antenna and ddi parameters + :type par_dict: dict + + :return: Filename + :rtype: str + """ destination = par_dict["destination"] antenna = par_dict["this_ant"] ddi = par_dict["this_ddi"] @@ -215,6 +228,17 @@ def _file_name_factory(file_type, par_dict): ### Data extraction ########################################################### def _time_scan_selection(scan_time_ranges, time_axis): + """ + Produce scan based time selection + :param scan_time_ranges: MS derived scan time ranges + :type scan_time_ranges: list + + :param time_axis: Visibilities time axis + :type time_axis: numpy.array + + :return: Selection in time for each scan. + :rtype: numpy.array(dtype=bool) + """ time_selections = [] for scan_time_range in scan_time_ranges: time_selection = np.logical_and( @@ -225,6 +249,21 @@ def _time_scan_selection(scan_time_ranges, time_axis): def _extract_cuts_from_visibilities(input_xds, antenna, ddi): + """ + Creates data tree containing the different cuts from a holog xds. + + :param input_xds: holog xds containing visibilities with beam cuts. + :type input_xds: xarray.Dataset + + :param antenna: Antenna key + :type antenna: str + + :param ddi: DDI key + :type ddi: str + + :return: Data tree containing the beamcut xdses. + :rtype: xarray.DataTree + """ cut_xdtree = xr.DataTree(name=f"{antenna}-{ddi}") scan_time_ranges = input_xds.attrs["scan_time_ranges"] scan_list = input_xds.attrs["scan_list"] @@ -271,7 +310,7 @@ def _extract_cuts_from_visibilities(input_xds, antenna, ddi): { "scan_number": scan_number, "lm_angle": lm_angle, - "available_corrs": hands_dict["parallel_hands"], + "available_corrs": list(hands_dict.keys()), "direction": direction, "xlabel": xlabel, "time_string": timestr, @@ -280,8 +319,7 @@ def _extract_cuts_from_visibilities(input_xds, antenna, ddi): xds["lm_offsets"] = xr.DataArray(this_lm_offsets, dims=["time", "lm"]) all_corr_ymax = 1e-34 - for parallel_hand in hands_dict["parallel_hands"]: - icorr = hands_dict[parallel_hand] + for parallel_hand, icorr in hands_dict.items(): amp = np.abs(avg_vis[:, icorr]) maxamp = np.max(amp) if maxamp > all_corr_ymax: @@ -306,6 +344,20 @@ def _extract_cuts_from_visibilities(input_xds, antenna, ddi): def _cut_direction_determination_and_label_creation(lm_offsets, angle_unit="deg"): + """ + Determines cut's direction using a linear regression between L and M offsets. + + :param lm_offsets: Array containing the cut's L and M offsets over type expected to be of shape [n_time, lm] + :type lm_offsets: numpy.ndarray + + :param angle_unit: Unit to represent cut's direction in a mixed cut. + :type angle_unit: str + + :return: Tuple containing the cuts direction angle in the sky, distance from center for each point, direction \ + label, and x-axis label for plots. + :rtype: tuple([float, numpy.array, str, str]) + + """ dx = lm_offsets[-1, 0] - lm_offsets[0, 0] dy = lm_offsets[-1, 1] - lm_offsets[0, 1] lm_dist = np.sqrt(lm_offsets[:, 0] ** 2 + lm_offsets[:, 1] ** 2) @@ -360,14 +412,24 @@ def _cut_direction_determination_and_label_creation(lm_offsets, angle_unit="deg" def _get_parallel_hand_indexes(corr_axis): + """ + Get the indices of parallel hands along the correlation axis. + + :param corr_axis: Visibilities correlation axis + :rtype: numpy.array(str) + + :return: Dictionary containing the parallel hands and their indices + :rtype: dict + """ if "L" in corr_axis[0] or "R" in corr_axis[0]: parallel_hands = ["RR", "LL"] else: parallel_hands = ["XX", "YY"] - hands_dict = {"parallel_hands": parallel_hands} + hands_dict = {} for icorr, corr in enumerate(corr_axis): - hands_dict[corr] = icorr + if corr in parallel_hands: + hands_dict[corr] = icorr return hands_dict From 7bec4675258d81d682ba735be9fa229e2966c12d Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Mon, 29 Dec 2025 12:02:06 -0700 Subject: [PATCH 43/95] Added docstrings to beamcut data fitting functions. --- src/astrohack/core/beamcut.py | 103 ++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/src/astrohack/core/beamcut.py b/src/astrohack/core/beamcut.py index 6abe10c9..4cd52d1b 100644 --- a/src/astrohack/core/beamcut.py +++ b/src/astrohack/core/beamcut.py @@ -437,6 +437,24 @@ def _get_parallel_hand_indexes(corr_axis): ### Multiple side lobe Gaussian fitting ########################################################### def _fwhm_gaussian(x_axis, x_off, amp, fwhm): + """ + Returns a gaussian of the same shape as x_axis with fwhm instead of sigma as the input parameter + + :param x_axis: X-axis of the beam cut data + :type x_axis: numpy.array + + :param x_off: X offset of the center of the gaussian + :type x_off: float + + :param amp: Amplitude of the gaussian + :type amp: float + + :param fwhm: Full width at half maximum of the gaussian + :type fwhm: float + + :return: Gaussian evaluated at x_axis points + :rtype: numpy.array + """ sigma = fwhm / sig_2_fwhm return amp * np.exp(-((x_axis - x_off) ** 2) / (2 * sigma**2)) @@ -444,6 +462,24 @@ def _fwhm_gaussian(x_axis, x_off, amp, fwhm): def _build_multi_gaussian_initial_guesses( x_data, y_data, pb_fwhm, min_dist_fraction=1.3 ): + """ + Build initial guesses array for a multi gaussian fitting from X and Y axes heuristics + + :param x_data: Fit's X-axis data. + :type x_data: numpy.array + + :param y_data: Fit's Y-axis data. + :type y_data: numpy.array + + :param pb_fwhm: Estimated FWHM of the primary beam + :type pb_fwhm: float + + :param min_dist_fraction: Fraction of pb_fwhm to use as estimate for the minimal peak distance + :type min_dist_fraction: float + + :return: Tuple containing the initial_guesses, bounds and number of peaks to fit + :rtype: tuple([list, list([list]), int]) + """ initial_guesses = [] lower_bounds = [] upper_bounds = [] @@ -462,6 +498,18 @@ def _build_multi_gaussian_initial_guesses( def _multi_gaussian(xdata, *args): + """ + Produces a multiple gaussian Y array from parameters derived fromm list of arguments + + :param xdata: X-axis data + :type xdata: numpy.array + + :param args: List of gaussian parameters [x_off_0, amp_0, fwhm_0, ..., x_off_n, amp_n, fwhm_n] + :type args: list([float]) + + :return: Multiple gaussian evaluated at x-axis points + :rtype: numpy.array + """ nargs = len(args) if nargs % 3 != 0: raise ValueError("Number of arguments should be multiple of 3") @@ -474,6 +522,33 @@ def _multi_gaussian(xdata, *args): def _perform_curvefit_with_given_functions( x_data, y_data, initial_guesses, bounds, fit_func, datalabel, maxit=50000 ): + """ + Invoke scipy optimize curve_fit with customized parameters + + :param x_data: x-axis data for the curve fit + :type x_data: numpy.array + + :param y_data: y-axis data to be fitted + :type y_data: numpy.array + + :param initial_guesses: list of initial guesses + :type initial_guesses: list + + :param bounds: List containing the lists of lower and upper bounds for each parameter + :type bounds: list([list]) + + :param fit_func: Function with which to fit the parameters. + :type fit_func: function + + :param datalabel: Data label for messaging + :type datalabel: str + + :param maxit: Maximum number of iterations + :type maxit: int + + :return: Tuple containing sucess flag and fit results (NaNs if fit failed) + :rtype: tuple([bool, numpy.array]) + """ try: results = curve_fit( fit_func, @@ -491,6 +566,22 @@ def _perform_curvefit_with_given_functions( def _identify_pb_and_sidelobes_in_fit(datalabel, x_data, fit_pars): + """ + Identify primary beam and first sidelobes in fit using expected beam shape heuristics. + + :param datalabel: Data label for messaging + :type datalabel: str + + :param x_data: X-axis data + :type x_data: numpy.array + + :param fit_pars: Fit parameters + :type fit_pars: numpy.array + + :return: Tuple containing: Number of peaks in beam, filtered fit parameters, primary beam center offset, primary \ + beam measured fwhm, ratio between left and right first sidelobes + :rtype: tuple([int, numpy.array, float, float, float]) + """ centers = fit_pars[0::3] amps = fit_pars[1::3] fwhms = fit_pars[2::3] @@ -542,6 +633,18 @@ def _identify_pb_and_sidelobes_in_fit(datalabel, x_data, fit_pars): def _beamcut_multi_lobes_gaussian_fit(cut_xdtree, datalabel): + """ + Execute multi gaussian fit to beam cut data. + + :param cut_xdtree: Datatree containing a single beam cut. + :type cut_xdtree: xarray.DataTree + + :param datalabel: Data label for messaging + :type datalabel: str + + :return: None + :rtype: NoneType + """ # Get the summary from the first cut, but it should be equal anyway summary = cut_xdtree.attrs["summary"] wavelength = summary["spectral"]["rep. wavelength"] From 46a52cbefde6a1a01a2a23206a45abcdf379623e Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Mon, 29 Dec 2025 12:21:48 -0700 Subject: [PATCH 44/95] Added docstrings to beamcut plot utility functions. --- src/astrohack/core/beamcut.py | 63 +++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/src/astrohack/core/beamcut.py b/src/astrohack/core/beamcut.py index 4cd52d1b..65dc3944 100644 --- a/src/astrohack/core/beamcut.py +++ b/src/astrohack/core/beamcut.py @@ -700,6 +700,18 @@ def _beamcut_multi_lobes_gaussian_fit(cut_xdtree, datalabel): ### Plot utilities ########################################################### def _add_secondary_beam_hpbw_x_axis_to_plot(pb_fwhm, ax): + """ + Add a secondary X axis on top of the figure representing the LM distances in primary beam FWHMs. + + :param pb_fwhm: Primary beam FWHM + :type pb_fwhm: float + + :param ax: Matplotlib Axes object + :type ax: matplotlib.axes.Axes + + :return: None + :rtype: NoneType + """ if np.isnan(pb_fwhm): return sec_x_axis = ax.secondary_xaxis( @@ -718,6 +730,24 @@ def _add_secondary_beam_hpbw_x_axis_to_plot(pb_fwhm, ax): def _add_lobe_identification_to_plot(ax, centers, peaks, y_off): + """ + Add gaussians identification to plot + + :param ax: Matplotlib Axes object + :type ax: matplotlib.axes.Axes + + :param centers: Gaussian centers + :type centers: list + + :param peaks: Gaussian peaks + :type peaks: list + + :param y_off: Y offset to add peak Ids + :type y_off: float + + :return: None + :rtype: NoneType + """ for i_peak, peak in enumerate(peaks): ax.text(centers[i_peak], peak + y_off, f"{i_peak+1})", ha="center", va="bottom") @@ -733,6 +763,39 @@ def _add_beam_parameters_box( y_pos=0.95, attenuation_plot=False, ): + """ + Add text bos with beam parameters + + :param ax: Matplotlib Axes object + :type ax: matplotlib.axes.Axes + + :param pb_center: Primary beam center offset + :type pb_center: float + + :param pb_fwhm: Primary beam FWHM + :type pb_fwhm: float + + :param sidelobe_ratio: First side lobe ratio + :type sidelobe_ratio: float + + :param lm_unit: L/M axis unit + :type lm_unit: str + + :param alpha: Opacity of text box + :type alpha: float + + :param x_pos: Relative x position of the text box + :type x_pos: float + + :param y_pos: Relative y position of the text box + :type y_pos: float + + :param attenuation_plot: Is this an attenuation plot? + :type attenuation_plot: bool + + :return: None + :rtype: NoneType + """ if attenuation_plot: head = "avg " else: From 057fe7880b258b9f991712c1685eeef2013ca8e6 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Mon, 29 Dec 2025 13:00:18 -0700 Subject: [PATCH 45/95] Added docstrings to remaining beamcut functions. --- src/astrohack/core/beamcut.py | 51 +++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/astrohack/core/beamcut.py b/src/astrohack/core/beamcut.py index 65dc3944..77d2726d 100644 --- a/src/astrohack/core/beamcut.py +++ b/src/astrohack/core/beamcut.py @@ -818,6 +818,21 @@ def _add_beam_parameters_box( ### Plot correlation subroutines ########################################################### def _plot_single_cut_in_amplitude(cut_xds, axes, par_dict): + """ + Plot a single beam cut in amplitude with each correlation in a different panel + + :param cut_xds: xarray dataset containing the beamcut + :type cut_xds: xarray.Dataset + + :param axes: numpy array with the Matplotlib Axes objects for the different panels + :type axes: numpy.array([Matplotlib.axes.Axes]) + + :param par_dict: Parameter dictionary containing plot configuration + :type par_dict: dict + + :return: None + :rtype: NoneType + """ # Init sub_title = _make_parallel_hand_sub_title(cut_xds.attrs) max_amp = cut_xds.attrs["all_corr_ymax"] @@ -900,6 +915,21 @@ def _plot_single_cut_in_amplitude(cut_xds, axes, par_dict): def _plot_single_cut_in_attenuation(cut_xds, ax, par_dict): + """ + Plot a single beam cut in attenuation with superposed correlations + + :param cut_xds: xarray dataset containing the beamcut + :type cut_xds: xarray.Dataset + + :param ax: Matplotlib Axes object + :type ax: Matplotlib.axes.Axes + + :param par_dict: Parameter dictionary containing plot configuration + :type par_dict: dict + + :return: None + :rtype: NoneType + """ sub_title = _make_parallel_hand_sub_title(cut_xds.attrs) lm_unit = par_dict["lm_unit"] lm_fac = convert_unit("rad", lm_unit, "trigonometric") @@ -976,12 +1006,33 @@ def _plot_single_cut_in_attenuation(cut_xds, ax, par_dict): ### Data labeling ########################################################### def _make_parallel_hand_sub_title(attributes): + """ + Make subtitle for data based on XDS attributes. + + :param attributes: beamcut xds attributes + :type attributes: dict + + :return: Subtitle string + :rtype: str + """ direction = attributes["direction"] time_string = attributes["time_string"] return f"{direction}, {time_string} UTC" def _create_beamcut_header(summary, par_dict): + """ + Create a data labeling header for plots and/or reports. + + :param summary: Data summary from xds attributes + :type summary: dict + + :param par_dict: Parameter dictionary containing configuration parameters + :type par_dict: dict + + :return: Data labeling header for plots and/or reports. + :rtype: str + """ azel_unit = par_dict["azel_unit"] antenna = par_dict["this_ant"] ddi = par_dict["this_ddi"] From 9b5a101893e4f8fa05cf63fb23c3afa4b53de1e7 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Mon, 29 Dec 2025 13:10:27 -0700 Subject: [PATCH 46/95] Partial beamcut.py docstring with parameters description. --- src/astrohack/beamcut.py | 80 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 78 insertions(+), 2 deletions(-) diff --git a/src/astrohack/beamcut.py b/src/astrohack/beamcut.py index 6c0ad852..07800057 100644 --- a/src/astrohack/beamcut.py +++ b/src/astrohack/beamcut.py @@ -17,16 +17,92 @@ def beamcut( beamcut_name: str = None, ant: Union[str, List[str]] = "all", ddi: Union[int, List[str]] = "all", - correlations: str = "all", destination: str = None, lm_unit: str = "amin", azel_unit: str = "deg", dpi: int = 300, display: bool = False, - y_scale: str = None, + y_scale: list[float] = None, parallel: bool = False, overwrite: bool = False, ): + """ + Process beamcut data from a .holog.zarr file to produce reports and plots. + + :param holog_name: Name of the .holog.zarr file to use as input. + :type holog_name: str + + :param beamcut_name: Name for the output .beamcut.zarr file to save data. + :type beamcut_name: str + + :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 destination: Destination directory for plots and reports if not None, defaults to None. + :type destination: str, optional + + :param lm_unit: Unit for L/M offsets in plots and report, default is "amin". + :type lm_unit: str, optional + + :param azel_unit: Unit for Az/El information in plots and report, default is "deg". + :type azel_unit: str, optional + + :param dpi: Resolution in pixels, defaults to 300. + :type dpi: int, optional + + :param display: Display plots during execution, defaults to False. + :type display: bool, optional + + :param y_scale: Define amplitude plot Y scale, defaults to None. + :type y_scale: str, optional + + :param parallel: Process beamcuts in parallel, defaults to False. + :type parallel: bool, optional + + :param overwrite: Overwrite previously existing beamcut file of same name, defaults to False. + :type overwrite: bool, optional + + :return: Beamcut mds object + :rtype: AstrohackBeamcutFile + + .. _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.9.4") From 61a82b63d50375416f9ab3c953d2b0bf148b3907 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Mon, 29 Dec 2025 14:50:45 -0700 Subject: [PATCH 47/95] Added complete docstring to beamcut user facing routine. --- src/astrohack/beamcut.py | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/astrohack/beamcut.py b/src/astrohack/beamcut.py index 07800057..d17b5bf7 100644 --- a/src/astrohack/beamcut.py +++ b/src/astrohack/beamcut.py @@ -69,19 +69,24 @@ def beamcut( :rtype: AstrohackBeamcutFile .. _Description: - **AstrohackImageFile** + **AstrohackBeamcutFile** - 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: + The beamcut mds object allows the user to access the underlying xarray datatree using compound keys, which are in \ + order of depth, `ant` -> `ddi`. This object also provides a `summary()` method to list available data and available\ + data visualization methods. + + An outline of the beamcut mds data tree is show below: .. parsed-literal:: image_mds = { ant_0:{ - ddi_0: image_ds, - ⋮ - ddi_m: image_ds + ddi_0: { + cut_0: beamcut_ds + ⋮ + cut_p: beamcut_ds + }, + ddi_m: … }, ⋮ ant_n: … @@ -90,14 +95,13 @@ def beamcut( **Example Usage** .. parsed-literal:: - from astrohack.holog import holog + from astrohack import beamcut - holog( + beamcut( holog_name="astrohack_observation.holog.zarr", - padding_factor=50, - grid_interpolation_mode='linear', - chan_average = True, - scan_average = True, + beamcut_name="astrohack_observation.beamcut.zarr", + destination="beamcut_exports", + display=False, ant='ea25', overwrite=True, parallel=True From c67dcbe3485c8e3959d06c5d380f879295852772 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Mon, 29 Dec 2025 14:52:36 -0700 Subject: [PATCH 48/95] Added beamcut API to documentation. --- docs/api.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index b1eadba3..bd7f53fe 100755 --- a/docs/api.rst +++ b/docs/api.rst @@ -11,8 +11,10 @@ Currently we only provide API reference to user facing functions. _api/autoapi/astrohack/holog/index _api/autoapi/astrohack/panel/index _api/autoapi/astrohack/combine/index + _api/autoapi/astrohack/beamcut/index _api/autoapi/astrohack/dio/index _api/autoapi/astrohack/mds/index + _api/autoapi/astrohack/beamcut_mds/index _api/autoapi/astrohack/extract_locit/index _api/autoapi/astrohack/locit/index _api/autoapi/astrohack/cassegrain_ray_tracing/index From d2e75a26a496754d9264dd101a58820812cb34e6 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Mon, 29 Dec 2025 15:14:02 -0700 Subject: [PATCH 49/95] Added docstrings to beamcut_mds.py --- src/astrohack/beamcut_mds.py | 177 +++++++++++++++++++++++++++++++---- 1 file changed, 160 insertions(+), 17 deletions(-) diff --git a/src/astrohack/beamcut_mds.py b/src/astrohack/beamcut_mds.py index 119284fa..7d79067b 100644 --- a/src/astrohack/beamcut_mds.py +++ b/src/astrohack/beamcut_mds.py @@ -1,7 +1,7 @@ import xarray as xr import pathlib -from typing import Any, List, Union, Tuple +from typing import List, Union import toolviper.utils.logger as logger @@ -25,39 +25,76 @@ class AstrohackBeamcutFile: def __init__(self, file: str): - """Initialize an AstrohackPanelFile object. + """Initialize an AstrohackBeamcutFile object. + :param file: File to be linked to this object :type file: str - :return: AstrohackPanelFile object - :rtype: AstrohackPanelFile + :return: AstrohackBeamcutFile object + :rtype: AstrohackBeamcutFile """ self.file = file self._file_is_open = False self._input_pars = None self.xdt = None - def __getitem__(self, key: str): + def __getitem__(self, key: str) -> xr.DataTree: + """ + get item implementation that gets the xdtree at key. + + :param key: Key for which to fetch a subtree + :type key: str + + :return: corresponding subtree + :rtype: xr.DataTree + """ return self.xdt[key] - def __setitem__(self, key: str, value: Any): - self.xdt[key] = value + def __setitem__(self, key: str, subtree: xr.DataTree) -> None: + """ + Set item implementation that sets the xdtree at key. + + :param key: Key for which to set a subtree + :type key: str + + :param subtree: Subtree to attach at key + :type subtree: xr.DataTree + + :return: None + :rtype: NoneType + """ + self.xdt[key] = subtree return @property def is_open(self) -> bool: - """Check whether the object has opened the corresponding hack file. + """ + Check whether the object has opened the corresponding hack file. :return: True if open, else False. :rtype: bool """ return self._file_is_open - def keys(self, *args, **kwargs): + def children_keys(self, *args, **kwargs): + """ + Get children keys + + :param args: args to deliver to dict.keys() method + :type args: list + + :param kwargs: Dict of keyword args to deliver to dict.keys() method + :type kwargs: dict + + :return: dict keys iterable + :rtype: dict_keys + """ return self.xdt.children.keys(*args, **kwargs) def open(self, file: str = None) -> bool: - """Open panel holography file. + """ + Open beamcut file. + :param file: File to be opened, if None defaults to the previously defined file :type file: str, optional @@ -83,7 +120,12 @@ def open(self, file: str = None) -> bool: return self._file_is_open def summary(self) -> None: - """Prints summary of the AstrohackBeamcutFile object, with available data, attributes and available methods""" + """ + Prints summary of the AstrohackBeamcutFile object, with available data, attributes and available methods + + :return: None + :rtype: NoneType + """ print_summary_header(self.file) print_dict_table(self._input_pars) print_data_contents(self, ["Antenna", "DDI", "Cut"]) @@ -91,10 +133,8 @@ def summary(self) -> None: [ self.summary, self.plot_beamcut_in_amplitude, - # self.export_screws, - # self.export_to_fits, - # self.plot_antennas, - # self.export_gain_tables, + self.plot_beamcut_in_attenuation, + self.create_beam_fit_report, self.observation_summary, ] ) @@ -112,32 +152,45 @@ def observation_summary( print_summary: bool = True, parallel: bool = False, ) -> None: - """ Create a Summary of observation information + """ + 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 + :return: None + :rtype: NoneType + **Additional Information** This method produces a summary of the data in the AstrohackBeamcutFile displaying general information, @@ -167,11 +220,44 @@ def plot_beamcut_in_amplitude( ddi: Union[int, List[int]] = "all", lm_unit: str = "amin", azel_unit: str = "deg", - y_scale: str = None, + y_scale: list[float] = None, display: bool = False, dpi: int = 300, parallel: bool = False, ) -> None: + """ + Plot beamcuts contained in the beamcut_mds in amplitude + + :param destination: Directory into which to save plots. + :type destination: str + + :param ant: Antenna ID to use in subselection, e.g. ea25, defaults to "all". + :type ant: list or str, optional + + :param ddi: Data description ID to use in subselection, e.g. 0, defaults to "all". + :type ddi: list or int, optional + + :param lm_unit: Unit for L/M offsets, default is "amin". + :type lm_unit: str, optional + + :param azel_unit: Unit for Az/El information, default is "deg". + :type azel_unit: str, optional + + :param y_scale: Set the y scale for the plots. + :type y_scale: str, optional + + :param display: Display plots during execution, default is False. + :type display: bool, optional + + :param dpi: Pixel resolution for plots, default is 300. + :type dpi: int, optional + + :param parallel: Run in parallel, defaults to False. + :type parallel: bool, optional + + :return: None + :rtype: NoneType + """ param_dict = locals() @@ -197,7 +283,40 @@ def plot_beamcut_in_attenuation( dpi: int = 300, parallel: bool = False, ) -> None: + """ + Plot beamcuts contained in the beamcut_mds in attenuation + + :param destination: Directory into which to save plots. + :type destination: str + + :param ant: Antenna ID to use in subselection, e.g. ea25, defaults to "all". + :type ant: list or str, optional + + :param ddi: Data description ID to use in subselection, e.g. 0, defaults to "all". + :type ddi: list or int, optional + + :param lm_unit: Unit for L/M offsets, default is "amin". + :type lm_unit: str, optional + + :param azel_unit: Unit for Az/El information, default is "deg". + :type azel_unit: str, optional + :param y_scale: Set the y scale for the plots. + :type y_scale: str, optional + + :param display: Display plots during execution, default is False. + :type display: bool, optional + + :param dpi: Pixel resolution for plots, default is 300. + :type dpi: int, optional + + :param parallel: Run in parallel, defaults to False. + :type parallel: bool, optional + + :return: None + :rtype: NoneType + """ + param_dict = locals() pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) @@ -219,6 +338,30 @@ def create_beam_fit_report( azel_unit: str = "deg", parallel: bool = False, ) -> None: + """ + Create reports on the parameters of the gaussians fitted to the beamcut. + + :param destination: Directory into which to save the reports. + :type destination: str + + :param ant: Antenna ID to use in subselection, e.g. ea25, defaults to "all". + :type ant: list or str, optional + + :param ddi: Data description ID to use in subselection, e.g. 0, defaults to "all". + :type ddi: list or int, optional + + :param lm_unit: Unit for L/M offsets, default is "amin". + :type lm_unit: str, optional + + :param azel_unit: Unit for Az/El information, default is "deg". + :type azel_unit: str, optional + + :param parallel: run in parallel, defaults to False. + :type parallel: bool, optional + + :return: None + :rtype: NoneType + """ param_dict = locals() From 5e9cfce12b009e858270bf282db6d82da2cd6daa Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Mon, 29 Dec 2025 15:30:23 -0700 Subject: [PATCH 50/95] Added parameter checking to beamcut.py --- src/astrohack/beamcut.py | 10 +++- src/astrohack/config/beamcut.param.json | 79 +++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 src/astrohack/config/beamcut.param.json diff --git a/src/astrohack/beamcut.py b/src/astrohack/beamcut.py index d17b5bf7..cff24eb2 100644 --- a/src/astrohack/beamcut.py +++ b/src/astrohack/beamcut.py @@ -1,17 +1,21 @@ import pathlib -import toolviper.utils.logger as logger 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.file import overwrite_file, check_if_file_can_be_opened from astrohack.utils.graph import compute_graph from astrohack.beamcut_mds import AstrohackBeamcutFile -import xarray as xr +from astrohack.utils.validation import custom_plots_checker from typing import Union, List - +@validate(custom_checker=custom_plots_checker) def beamcut( holog_name: str, beamcut_name: str = None, diff --git a/src/astrohack/config/beamcut.param.json b/src/astrohack/config/beamcut.param.json new file mode 100644 index 00000000..cecafc0e --- /dev/null +++ b/src/astrohack/config/beamcut.param.json @@ -0,0 +1,79 @@ +{ + "beamcut":{ + "holog_name":{ + "required": true, + "type": ["string"] + }, + "beamcut_name":{ + "required": false, + "type": ["string"], + "nullable": true + }, + "ant":{ + "nullable": false, + "required": false, + "struct_type": ["str"], + "type": ["string", "list"] + }, + "ddi":{ + "nullable": false, + "required": false, + "struct_type": ["str", "int"], + "type": ["int", "list", "string"] + }, + "destination":{ + "nullable": true, + "required": false, + "type": ["string"] + }, + "lm_unit":{ + "nullable": false, + "required": false, + "type": ["string"], + "check allowed with": "units.trig" + }, + "azel_unit":{ + "nullable": false, + "required": false, + "type": ["string"], + "check allowed with": "units.trig" + }, + "dpi":{ + "nullable": false, + "required": false, + "type": ["int"], + "min": 1, + "max": 1200 + }, + "display":{ + "nullable": false, + "required": false, + "type": ["boolean"] + }, + "y_scale":{ + "nullable": true, + "required": false, + "struct_type": [ + "float", + "int" + ], + "minlength": 2, + "maxlength": 2, + "type": [ + "list", + "tuple", + "ndarray" + ] + }, + "parallel":{ + "nullable": false, + "required": false, + "type": ["boolean"] + }, + "overwrite":{ + "nullable": false, + "required": false, + "type": ["boolean"] + } + } +} From 54300e264ce95353f25da163963968f60aea5b76 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Mon, 29 Dec 2025 17:21:14 -0700 Subject: [PATCH 51/95] Added parameter checking to beamcut_mds.py --- src/astrohack/beamcut_mds.py | 7 + src/astrohack/config/beamcut_mds.param.json | 278 ++++++++++++++++++++ 2 files changed, 285 insertions(+) create mode 100644 src/astrohack/config/beamcut_mds.param.json diff --git a/src/astrohack/beamcut_mds.py b/src/astrohack/beamcut_mds.py index 7d79067b..e48eed3f 100644 --- a/src/astrohack/beamcut_mds.py +++ b/src/astrohack/beamcut_mds.py @@ -5,6 +5,8 @@ import toolviper.utils.logger as logger +from toolviper.utils.parameter import validate + from astrohack.core.beamcut import ( plot_beamcut_in_amplitude_chunk, plot_beamcut_in_attenuation_chunk, @@ -20,6 +22,7 @@ generate_observation_summary_for_beamcut, ) from astrohack.utils.graph import compute_graph +from astrohack.utils.validation import custom_plots_checker, custom_unit_checker class AstrohackBeamcutFile: @@ -139,6 +142,7 @@ def summary(self) -> None: ] ) + @validate(custom_checker=custom_unit_checker) def observation_summary( self, summary_file: str, @@ -213,6 +217,7 @@ def observation_summary( if print_summary: print(full_summary) + @validate(custom_checker=custom_plots_checker) def plot_beamcut_in_amplitude( self, destination: str, @@ -271,6 +276,7 @@ def plot_beamcut_in_amplitude( ) return + @validate(custom_checker=custom_plots_checker) def plot_beamcut_in_attenuation( self, destination: str, @@ -329,6 +335,7 @@ def plot_beamcut_in_attenuation( ) return + @validate(custom_checker=custom_plots_checker) def create_beam_fit_report( self, destination: str, diff --git a/src/astrohack/config/beamcut_mds.param.json b/src/astrohack/config/beamcut_mds.param.json new file mode 100644 index 00000000..0de3130c --- /dev/null +++ b/src/astrohack/config/beamcut_mds.param.json @@ -0,0 +1,278 @@ +{ + "observation_summary":{ + "summary_file": { + "nullable": false, + "required": true, + "type": [ + "string" + ] + }, + "ant": { + "nullable": false, + "required": false, + "struct_type": [ + "str" + ], + "minlength": 1, + "type": [ + "string", + "list" + ] + }, + "ddi": { + "nullable": false, + "required": false, + "struct_type": [ + "int" + ], + "minlength": 1, + "type": [ + "int", + "list", + "string" + ] + }, + "az_el_key": { + "nullable": false, + "required": true, + "type": ["string"], + "allowed": ["mean", "median", "center"] + }, + "phase_center_unit": { + "nullable": false, + "required": false, + "type": ["string"], + "check allowed with": "units.radec" + }, + "az_el_unit": { + "nullable": false, + "required": false, + "type": ["string"], + "check allowed with": "units.trig" + }, + "time_format":{ + "nullable": false, + "required": false, + "type": ["string"] + }, + "tab_size": { + "nullable": false, + "required": false, + "type": ["int"], + "min": 0 + }, + "print_summary": { + "nullable": false, + "required": false, + "type": ["boolean"] + }, + "parallel": { + "nullable": false, + "required": false, + "type": [ + "boolean" + ] + } + }, + "plot_beamcut_in_amplitude":{ + "destination":{ + "nullable": false, + "required": true, + "type": ["string"] + }, + "ant": { + "nullable": false, + "required": false, + "struct_type": [ + "str" + ], + "minlength": 1, + "type": [ + "string", + "list" + ] + }, + "ddi": { + "nullable": false, + "required": false, + "struct_type": [ + "int" + ], + "minlength": 1, + "type": [ + "int", + "list", + "string" + ] + }, + "lm_unit":{ + "nullable": false, + "required": false, + "type": ["string"], + "check allowed with": "units.trig" + }, + "azel_unit":{ + "nullable": false, + "required": false, + "type": ["string"], + "check allowed with": "units.trig" + }, + "dpi":{ + "nullable": false, + "required": false, + "type": ["int"], + "min": 1, + "max": 1200 + }, + "display":{ + "nullable": false, + "required": false, + "type": ["boolean"] + }, + "y_scale":{ + "nullable": true, + "required": false, + "struct_type": [ + "float", + "int" + ], + "minlength": 2, + "maxlength": 2, + "type": [ + "list", + "tuple", + "ndarray" + ] + }, + "parallel":{ + "nullable": false, + "required": false, + "type": ["boolean"] + } + }, + "plot_beamcut_in_attenuation":{ + "destination":{ + "nullable": false, + "required": true, + "type": ["string"] + }, + "ant": { + "nullable": false, + "required": false, + "struct_type": [ + "str" + ], + "minlength": 1, + "type": [ + "string", + "list" + ] + }, + "ddi": { + "nullable": false, + "required": false, + "struct_type": [ + "int" + ], + "minlength": 1, + "type": [ + "int", + "list", + "string" + ] + }, + "lm_unit":{ + "nullable": false, + "required": false, + "type": ["string"], + "check allowed with": "units.trig" + }, + "azel_unit":{ + "nullable": false, + "required": false, + "type": ["string"], + "check allowed with": "units.trig" + }, + "dpi":{ + "nullable": false, + "required": false, + "type": ["int"], + "min": 1, + "max": 1200 + }, + "display":{ + "nullable": false, + "required": false, + "type": ["boolean"] + }, + "y_scale":{ + "nullable": true, + "required": false, + "struct_type": [ + "float", + "int" + ], + "minlength": 2, + "maxlength": 2, + "type": [ + "list", + "tuple", + "ndarray" + ] + }, + "parallel":{ + "nullable": false, + "required": false, + "type": ["boolean"] + } + }, + "create_beam_fit_report":{ + "destination":{ + "nullable": false, + "required": true, + "type": ["string"] + }, + "ant": { + "nullable": false, + "required": false, + "struct_type": [ + "str" + ], + "minlength": 1, + "type": [ + "string", + "list" + ] + }, + "ddi": { + "nullable": false, + "required": false, + "struct_type": [ + "int" + ], + "minlength": 1, + "type": [ + "int", + "list", + "string" + ] + }, + "lm_unit":{ + "nullable": false, + "required": false, + "type": ["string"], + "check allowed with": "units.trig" + }, + "azel_unit":{ + "nullable": false, + "required": false, + "type": ["string"], + "check allowed with": "units.trig" + }, + "parallel":{ + "nullable": false, + "required": false, + "type": ["boolean"] + } + } + } From dc8572e6ff535ddb099fe0042236388cee274402 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Mon, 29 Dec 2025 17:22:14 -0700 Subject: [PATCH 52/95] Black compliance --- src/astrohack/beamcut.py | 1 + src/astrohack/beamcut_mds.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/astrohack/beamcut.py b/src/astrohack/beamcut.py index cff24eb2..3cc0c947 100644 --- a/src/astrohack/beamcut.py +++ b/src/astrohack/beamcut.py @@ -15,6 +15,7 @@ from typing import Union, List + @validate(custom_checker=custom_plots_checker) def beamcut( holog_name: str, diff --git a/src/astrohack/beamcut_mds.py b/src/astrohack/beamcut_mds.py index e48eed3f..09c6e026 100644 --- a/src/astrohack/beamcut_mds.py +++ b/src/astrohack/beamcut_mds.py @@ -322,7 +322,7 @@ def plot_beamcut_in_attenuation( :return: None :rtype: NoneType """ - + param_dict = locals() pathlib.Path(param_dict["destination"]).mkdir(exist_ok=True) From cd61a4eb50a55ac8af6978e5ccec2b28d8561d18 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 30 Dec 2025 11:31:12 -0700 Subject: [PATCH 53/95] Added class name to beamcut_mds.param.json --- src/astrohack/config/beamcut_mds.param.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/astrohack/config/beamcut_mds.param.json b/src/astrohack/config/beamcut_mds.param.json index 0de3130c..e3015514 100644 --- a/src/astrohack/config/beamcut_mds.param.json +++ b/src/astrohack/config/beamcut_mds.param.json @@ -1,5 +1,5 @@ { - "observation_summary":{ + "AstrohackBeamcutFile.observation_summary":{ "summary_file": { "nullable": false, "required": true, @@ -74,7 +74,7 @@ ] } }, - "plot_beamcut_in_amplitude":{ + "AstrohackBeamcutFile.plot_beamcut_in_amplitude":{ "destination":{ "nullable": false, "required": true, @@ -150,7 +150,7 @@ "type": ["boolean"] } }, - "plot_beamcut_in_attenuation":{ + "AstrohackBeamcutFile.plot_beamcut_in_attenuation":{ "destination":{ "nullable": false, "required": true, @@ -226,7 +226,7 @@ "type": ["boolean"] } }, - "create_beam_fit_report":{ + "AstrohackBeamcutFile.create_beam_fit_report":{ "destination":{ "nullable": false, "required": true, From 7be2251fe1d19690f91f7233fb3bc82a9d77d4aa Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 30 Dec 2025 11:31:33 -0700 Subject: [PATCH 54/95] Very beginning of a beam cut tutorial. --- docs/beamcut_tutorial.ipynb | 105 ++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 docs/beamcut_tutorial.ipynb diff --git a/docs/beamcut_tutorial.ipynb b/docs/beamcut_tutorial.ipynb new file mode 100644 index 00000000..64da7757 --- /dev/null +++ b/docs/beamcut_tutorial.ipynb @@ -0,0 +1,105 @@ +{ + "cells": [ + { + "metadata": {}, + "cell_type": "markdown", + "source": "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/nrao/astrohack/blob/v0.9.4/docs/beamcut_tutorial.ipynb)", + "id": "54383333457ada66" + }, + { + "metadata": { + "collapsed": true + }, + "cell_type": "markdown", + "source": "![astrohack](_media/astrohack_logo.png)", + "id": "f4ce33ae2c4a0b61" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "# Beam cut analysis tutorial\n", + "\n", + "Bla Bla" + ], + "id": "68e9cee71e47e521" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-12-30T17:27:43.222320329Z", + "start_time": "2025-12-30T17:27:41.014694390Z" + } + }, + "cell_type": "code", + "source": [ + "import os\n", + "\n", + "try:\n", + " import astrohack\n", + "\n", + " print(\"AstroHACK version\", astrohack.__version__, \"already installed.\")\n", + "except ImportError as e:\n", + " print(e)\n", + " print(\"Installing AstroHACK\")\n", + "\n", + " os.system(\"pip install astrohack\")\n", + "\n", + " import astrohack\n", + "\n", + " print(\"astrohack version\", astrohack.__version__, \" installed.\")" + ], + "id": "cd51135c1a9c538c", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "AstroHACK version 0.9.4 already installed.\n" + ] + } + ], + "execution_count": 1 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "# Download tutorial data", + "id": "e69e12b1c4a437f2" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": [ + "# Download data.\n", + "import toolviper\n", + "\n", + "toolviper.utils.data.download(file=\"ea25_cal_small_after_fixed.split.ms\", folder=\"data\")" + ], + "id": "fe3dd386e7860dd0" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From f5e52434a088ca11a47ca625fcc1a9454b6deb70 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 30 Dec 2025 11:35:36 -0700 Subject: [PATCH 55/95] Fixed issue with keyword argument that no longer exists. --- src/astrohack/core/beamcut.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/astrohack/core/beamcut.py b/src/astrohack/core/beamcut.py index 77d2726d..20498886 100644 --- a/src/astrohack/core/beamcut.py +++ b/src/astrohack/core/beamcut.py @@ -879,7 +879,7 @@ def _plot_single_cut_in_amplitude(cut_xds, axes, par_dict): amps = np.array(cut_xds.attrs[f"{parallel_hand}_amp_fit_pars"][1::3]) _add_lobe_identification_to_plot( - this_ax, centers, amps, y_off, attenunation_plot=False + this_ax, centers, amps, y_off, ) else: scatter_plot( From e43a13867dbaddf122d35150a8f09bb128437e86 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 30 Dec 2025 11:36:30 -0700 Subject: [PATCH 56/95] Fixed issue with keyword argument that no longer exists. --- src/astrohack/core/beamcut.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/astrohack/core/beamcut.py b/src/astrohack/core/beamcut.py index 20498886..ebbdbeb6 100644 --- a/src/astrohack/core/beamcut.py +++ b/src/astrohack/core/beamcut.py @@ -737,10 +737,10 @@ def _add_lobe_identification_to_plot(ax, centers, peaks, y_off): :type ax: matplotlib.axes.Axes :param centers: Gaussian centers - :type centers: list + :type centers: list, numpy.array :param peaks: Gaussian peaks - :type peaks: list + :type peaks: list, numpy.array :param y_off: Y offset to add peak Ids :type y_off: float From d9c2761c943db9a87d7483c15edb4ad63fb9a29a Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 30 Dec 2025 12:24:21 -0700 Subject: [PATCH 57/95] Renamed children_keys to keys for API compatibility. --- src/astrohack/beamcut_mds.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/astrohack/beamcut_mds.py b/src/astrohack/beamcut_mds.py index 09c6e026..5c13b4cd 100644 --- a/src/astrohack/beamcut_mds.py +++ b/src/astrohack/beamcut_mds.py @@ -79,7 +79,7 @@ def is_open(self) -> bool: """ return self._file_is_open - def children_keys(self, *args, **kwargs): + def keys(self, *args, **kwargs): """ Get children keys From db0ef16eb0668fc7172ee6b6e03e8cd6fd05f15f Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 30 Dec 2025 12:30:06 -0700 Subject: [PATCH 58/95] Skeleton for beamcut tutorial missing plotting and reports. --- docs/beamcut_tutorial.ipynb | 2699 ++++++++++++++++++++++++++++++++++- 1 file changed, 2692 insertions(+), 7 deletions(-) diff --git a/docs/beamcut_tutorial.ipynb b/docs/beamcut_tutorial.ipynb index 64da7757..62890edb 100644 --- a/docs/beamcut_tutorial.ipynb +++ b/docs/beamcut_tutorial.ipynb @@ -27,8 +27,8 @@ { "metadata": { "ExecuteTime": { - "end_time": "2025-12-30T17:27:43.222320329Z", - "start_time": "2025-12-30T17:27:41.014694390Z" + "end_time": "2025-12-30T19:28:48.893433594Z", + "start_time": "2025-12-30T19:28:46.030643463Z" } }, "cell_type": "code", @@ -68,17 +68,2702 @@ "id": "e69e12b1c4a437f2" }, { - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2025-12-30T19:28:53.829527975Z", + "start_time": "2025-12-30T19:28:48.900165847Z" + } + }, "cell_type": "code", - "outputs": [], - "execution_count": null, "source": [ "# Download data.\n", "import toolviper\n", "\n", - "toolviper.utils.data.download(file=\"ea25_cal_small_after_fixed.split.ms\", folder=\"data\")" + "basename = \"kband_beamcut_small\"\n", + "ms_name = f\"data/{basename}.ms\"\n", + "# toolviper.utils.data.download(file=ms_name, folder=\"data\")\n", + "!rm -rf data\n", + "!mkdir data\n", + "!cp -r /home/victor/work/Holography-1022/git-shared/beam-cuts/testing/kband_beamcut_small.ms ./data/" + ], + "id": "fe3dd386e7860dd0", + "outputs": [], + "execution_count": 2 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "# Extract beam cut data to Astrohack formats\n", + "\n", + "One has to use extract_pointing and extract_holog. Ignore missing data warning because it is a split dataset." + ], + "id": "344167b8b3df6198" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-12-30T19:29:11.574320917Z", + "start_time": "2025-12-30T19:28:53.831473931Z" + } + }, + "cell_type": "code", + "source": [ + "from astrohack import extract_pointing, extract_holog\n", + "\n", + "point_name = f'{basename}.point.zarr'\n", + "holog_name = f'{basename}.holog.zarr'\n", + "\n", + "# Extract pointing data to an astrohack file format\n", + "point_mds = extract_pointing(ms_name, point_name, overwrite=True)\n", + "\n", + "# Extract visibilities to an astrohack file format\n", + "holog_mds = extract_holog(\n", + " ms_name,\n", + " point_name,\n", + " holog_name,\n", + " data_column='DATA', # This applies to this dataset only as it has been split\n", + " overwrite=True,\n", + ")" + ], + "id": "1be3727584b82d6d", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[\u001B[38;2;128;05;128m2025-12-30 12:28:53,840\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", + "[\u001B[38;2;128;05;128m2025-12-30 12:28:53,850\u001B[0m] \u001B[38;2;255;160;0m WARNING\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m kband_beamcut_small.point.zarr will be overwritten. \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:28:58,653\u001B[0m] \u001B[38;2;255;160;0m WARNING\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m ea18 pointing table has 4.3% of data with irregular time sampling \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:00,047\u001B[0m] \u001B[38;2;50;50;205m INFO\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m Finished processing \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:00,247\u001B[0m] \u001B[38;2;255;160;0m WARNING\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m kband_beamcut_small.holog.zarr will be overwritten. \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:00,408\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 ... 13] \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:08,885\u001B[0m] \u001B[38;2;50;50;205m INFO\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m EA17: DDI 0: Suggested cell size 1.64 amin, FOV: (19.73 amin, 19.76 amin) \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:08,887\u001B[0m] \u001B[38;2;50;50;205m INFO\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m EA06: DDI 0: Suggested cell size 1.64 amin, FOV: (19.73 amin, 19.76 amin) \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:08,890\u001B[0m] \u001B[38;2;50;50;205m INFO\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m EA01: DDI 0: Suggested cell size 1.64 amin, FOV: (19.73 amin, 19.76 amin) \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:08,892\u001B[0m] \u001B[38;2;50;50;205m INFO\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m EA07: DDI 0: Suggested cell size 1.64 amin, FOV: (19.72 amin, 19.76 amin) \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:08,894\u001B[0m] \u001B[38;2;50;50;205m INFO\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m EA14: DDI 0: Suggested cell size 1.64 amin, FOV: (19.72 amin, 19.76 amin) \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:08,897\u001B[0m] \u001B[38;2;50;50;205m INFO\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m EA15: DDI 0: Suggested cell size 1.64 amin, FOV: (19.72 amin, 19.76 amin) \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:08,900\u001B[0m] \u001B[38;2;50;50;205m INFO\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m EA02: DDI 0: Suggested cell size 1.64 amin, FOV: (19.72 amin, 19.76 amin) \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:08,903\u001B[0m] \u001B[38;2;50;50;205m INFO\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m EA08: DDI 0: Suggested cell size 1.64 amin, FOV: (19.72 amin, 19.76 amin) \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:08,905\u001B[0m] \u001B[38;2;50;50;205m INFO\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m EA20: DDI 0: Suggested cell size 1.64 amin, FOV: (19.72 amin, 19.76 amin) \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:08,908\u001B[0m] \u001B[38;2;50;50;205m INFO\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m EA19: DDI 0: Suggested cell size 1.64 amin, FOV: (19.72 amin, 19.76 amin) \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:08,910\u001B[0m] \u001B[38;2;50;50;205m INFO\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m EA25: DDI 0: Suggested cell size 1.64 amin, FOV: (19.73 amin, 19.76 amin) \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:08,912\u001B[0m] \u001B[38;2;50;50;205m INFO\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m EA09: DDI 0: Suggested cell size 1.64 amin, FOV: (19.72 amin, 19.76 amin) \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:10,068\u001B[0m] \u001B[38;2;255;160;0m WARNING\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m Mapping antenna ea06 has no data \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:10,068\u001B[0m] \u001B[38;2;255;160;0m WARNING\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m Mapping antenna ea01 has no data \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:10,069\u001B[0m] \u001B[38;2;255;160;0m WARNING\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m Mapping antenna ea07 has no data \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:10,069\u001B[0m] \u001B[38;2;255;160;0m WARNING\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m Mapping antenna ea14 has no data \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:10,118\u001B[0m] \u001B[38;2;255;160;0m WARNING\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m Mapping antenna ea02 has no data \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:10,119\u001B[0m] \u001B[38;2;255;160;0m WARNING\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m Mapping antenna ea08 has no data \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:10,120\u001B[0m] \u001B[38;2;255;160;0m WARNING\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m Mapping antenna ea20 has no data \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:10,120\u001B[0m] \u001B[38;2;255;160;0m WARNING\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m Mapping antenna ea19 has no data \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:10,121\u001B[0m] \u001B[38;2;255;160;0m WARNING\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m Mapping antenna ea25 has no data \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:10,121\u001B[0m] \u001B[38;2;255;160;0m WARNING\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m Mapping antenna ea09 has no data \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:10,122\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: 0 holog_map_key: map_0 \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:10,123\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 ... 13] \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,240\u001B[0m] \u001B[38;2;50;50;205m INFO\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m EA17: DDI 1: Suggested cell size 1.63 amin, FOV: (19.73 amin, 19.76 amin) \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,242\u001B[0m] \u001B[38;2;50;50;205m INFO\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m EA06: DDI 1: Suggested cell size 1.63 amin, FOV: (19.73 amin, 19.76 amin) \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,245\u001B[0m] \u001B[38;2;50;50;205m INFO\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m EA01: DDI 1: Suggested cell size 1.63 amin, FOV: (19.73 amin, 19.76 amin) \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,249\u001B[0m] \u001B[38;2;50;50;205m INFO\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m EA07: DDI 1: Suggested cell size 1.63 amin, FOV: (19.72 amin, 19.76 amin) \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,251\u001B[0m] \u001B[38;2;50;50;205m INFO\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m EA14: DDI 1: Suggested cell size 1.63 amin, FOV: (19.72 amin, 19.76 amin) \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,254\u001B[0m] \u001B[38;2;50;50;205m INFO\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m EA15: DDI 1: Suggested cell size 1.63 amin, FOV: (19.72 amin, 19.76 amin) \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,256\u001B[0m] \u001B[38;2;50;50;205m INFO\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m EA02: DDI 1: Suggested cell size 1.63 amin, FOV: (19.72 amin, 19.76 amin) \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,259\u001B[0m] \u001B[38;2;50;50;205m INFO\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m EA08: DDI 1: Suggested cell size 1.63 amin, FOV: (19.72 amin, 19.76 amin) \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,263\u001B[0m] \u001B[38;2;50;50;205m INFO\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m EA20: DDI 1: Suggested cell size 1.63 amin, FOV: (19.72 amin, 19.76 amin) \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,267\u001B[0m] \u001B[38;2;50;50;205m INFO\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m EA19: DDI 1: Suggested cell size 1.63 amin, FOV: (19.72 amin, 19.76 amin) \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,270\u001B[0m] \u001B[38;2;50;50;205m INFO\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m EA25: DDI 1: Suggested cell size 1.63 amin, FOV: (19.73 amin, 19.76 amin) \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,272\u001B[0m] \u001B[38;2;50;50;205m INFO\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m EA09: DDI 1: Suggested cell size 1.63 amin, FOV: (19.72 amin, 19.76 amin) \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,340\u001B[0m] \u001B[38;2;255;160;0m WARNING\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m Mapping antenna ea06 has no data \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,341\u001B[0m] \u001B[38;2;255;160;0m WARNING\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m Mapping antenna ea01 has no data \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,341\u001B[0m] \u001B[38;2;255;160;0m WARNING\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m Mapping antenna ea07 has no data \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,342\u001B[0m] \u001B[38;2;255;160;0m WARNING\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m Mapping antenna ea14 has no data \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,406\u001B[0m] \u001B[38;2;255;160;0m WARNING\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m Mapping antenna ea02 has no data \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,407\u001B[0m] \u001B[38;2;255;160;0m WARNING\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m Mapping antenna ea08 has no data \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,407\u001B[0m] \u001B[38;2;255;160;0m WARNING\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m Mapping antenna ea20 has no data \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,408\u001B[0m] \u001B[38;2;255;160;0m WARNING\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m Mapping antenna ea19 has no data \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,409\u001B[0m] \u001B[38;2;255;160;0m WARNING\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m Mapping antenna ea25 has no data \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,409\u001B[0m] \u001B[38;2;255;160;0m WARNING\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m Mapping antenna ea09 has no data \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,410\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 holog_map_key: map_0 \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,411\u001B[0m] \u001B[38;2;50;50;205m INFO\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m Finished processing \n" + ] + } + ], + "execution_count": 3 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "# Running beamcut\n", + "\n", + "No destination bla bla, destination bla bla" + ], + "id": "82a46ba7d5237185" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-12-30T19:29:13.888731535Z", + "start_time": "2025-12-30T19:29:11.589761961Z" + } + }, + "cell_type": "code", + "source": [ + "from astrohack import beamcut\n", + "\n", + "beamcut_name = f'{basename}.beamcut.zarr'\n", + "\n", + "beamcut_mds = beamcut(holog_name,\n", + " beamcut_name,\n", + " destination=None, # This parameter is to be filled with a destination directory for beamcut products.\n", + " overwrite=True,\n", + " )" + ], + "id": "74f116c53c0cd37c", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,593\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", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,600\u001B[0m] \u001B[38;2;255;160;0m WARNING\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m kband_beamcut_small.beamcut.zarr will be overwritten. \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,627\u001B[0m] \u001B[38;2;50;50;205m INFO\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m processing EA17: DDI 1 \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,876\u001B[0m] \u001B[38;2;50;50;205m INFO\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m processing EA17: DDI 0 \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:12,086\u001B[0m] \u001B[38;2;50;50;205m INFO\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m processing EA15: DDI 1 \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:13,252\u001B[0m] \u001B[38;2;50;50;205m INFO\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m processing EA15: DDI 0 \n", + "[\u001B[38;2;128;05;128m2025-12-30 12:29:13,561\u001B[0m] \u001B[38;2;50;50;205m INFO\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m Finished processing \n" + ] + } + ], + "execution_count": 4 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "# Interact with beamcut file", + "id": "bc8e5bc615d72136" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-12-30T19:29:14.032771572Z", + "start_time": "2025-12-30T19:29:13.891520025Z" + } + }, + "cell_type": "code", + "source": [ + "from astrohack import open_beamcut\n", + "\n", + "beamcut_mds = open_beamcut(beamcut_name)\n", + "\n", + "beamcut_mds.summary()" + ], + "id": "7ecba47fb5023883", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "####################################################################################################\n", + "### Summary for: ###\n", + "### kband_beamcut_small.beamcut.zarr ###\n", + "####################################################################################################\n", + "\n", + "Full documentation for AstrohackBeamcutFile objects' API at: \n", + "https://astrohack.readthedocs.io/en/stable/_api/autoapi/astrohack/mds/index.html#astrohack.mds.AstrohackBeamcutFile\n", + "\n", + "Input Parameters:\n", + "+--------------+----------------------------------+\n", + "| Parameter | Value |\n", + "+--------------+----------------------------------+\n", + "| ant | all |\n", + "| azel_unit | deg |\n", + "| beamcut_name | kband_beamcut_small.beamcut.zarr |\n", + "| ddi | all |\n", + "| destination | None |\n", + "| display | False |\n", + "| dpi | 300 |\n", + "| holog_name | kband_beamcut_small.holog.zarr |\n", + "| lm_unit | amin |\n", + "| origin | beamcut |\n", + "| overwrite | True |\n", + "| parallel | False |\n", + "| version | 0.9.4 |\n", + "| y_scale | None |\n", + "+--------------+----------------------------------+\n", + "\n", + "Contents:\n", + "+----------+-------+--------------------+\n", + "| Antenna | DDI | Cut |\n", + "+----------+-------+--------------------+\n", + "| ant_ea15 | ddi_0 | ['cut_0', 'cut_1'] |\n", + "| ant_ea15 | ddi_1 | ['cut_0', 'cut_1'] |\n", + "| ant_ea17 | ddi_0 | ['cut_0', 'cut_1'] |\n", + "| ant_ea17 | ddi_1 | ['cut_0', 'cut_1'] |\n", + "+----------+-------+--------------------+\n", + "\n", + "Available methods:\n", + "+-----------------------------+-------------+\n", + "| Methods | Description |\n", + "+-----------------------------+-------------+\n", + "| summary | |\n", + "| plot_beamcut_in_amplitude | |\n", + "| plot_beamcut_in_attenuation | |\n", + "| create_beam_fit_report | |\n", + "| observation_summary | |\n", + "+-----------------------------+-------------+\n", + "\n" + ] + } + ], + "execution_count": 5 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "## Observation summary", + "id": "120508c0b3af71d5" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-12-30T19:29:14.279359214Z", + "start_time": "2025-12-30T19:29:14.035178967Z" + } + }, + "cell_type": "code", + "source": "beamcut_mds.observation_summary('data/beamcut_summary.txt')", + "id": "d05b1a44071385d3", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[\u001B[38;2;128;05;128m2025-12-30 12:29:14,038\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", + "############################################################\n", + "### ant_ea15, ddi_0 ###\n", + "############################################################\n", + "\n", + " General:\n", + " Telescope name => EVLA\n", + " Antenna name => ea15\n", + " Station => N04\n", + " Reference antennas => ['ea23 @ N08']\n", + " Source => HOLORASTER\n", + " Phase center => 16h42m58.810s +39°48m36.990s [FK5]\n", + " Az el info => @ l,m = (0,0), Az, El = (294.3, 45.4) [deg]\n", + " Start time => 25 Nov 2025, 23:10:56 (UTC)\n", + " Stop time => 25 Nov 2025, 23:20:42 (UTC)\n", + " Duration => 9 min, 46.05 sec\n", + "\n", + " Spectral:\n", + " Channel width => 2.000 MHz\n", + " Frequency range => 21.315 GHz to 21.443 GHz\n", + " Number of channels => 64\n", + " Rep. frequency => 21.380 GHz\n", + " Rep. wavelength => 1.402 cm\n", + "\n", + " Beam:\n", + " Cell size => 1.64 amin\n", + " Grid size => 13 by 13 pixels\n", + " L extent => From -9.87 amin to 9.83 amin\n", + " M extent => From -9.80 amin to 9.89 amin\n", + "\n", + " cut_0:\n", + " El. cut (S -> N) at 2025-11-25 23:16 UTC\n", + "\n", + " cut_1:\n", + " Az. cut (W -> E) at 2025-11-25 23:19 UTC\n", + "\n", + "############################################################\n", + "### ant_ea15, ddi_1 ###\n", + "############################################################\n", + "\n", + " General:\n", + " Telescope name => EVLA\n", + " Antenna name => ea15\n", + " Station => N04\n", + " Reference antennas => ['ea23 @ N08']\n", + " Source => HOLORASTER\n", + " Phase center => 16h42m58.810s +39°48m36.990s [FK5]\n", + " Az el info => @ l,m = (0,0), Az, El = (294.3, 45.4) [deg]\n", + " Start time => 25 Nov 2025, 23:10:56 (UTC)\n", + " Stop time => 25 Nov 2025, 23:20:42 (UTC)\n", + " Duration => 9 min, 46.05 sec\n", + "\n", + " Spectral:\n", + " Channel width => 2.000 MHz\n", + " Frequency range => 21.443 GHz to 21.571 GHz\n", + " Number of channels => 64\n", + " Rep. frequency => 21.508 GHz\n", + " Rep. wavelength => 1.394 cm\n", + "\n", + " Beam:\n", + " Cell size => 1.63 amin\n", + " Grid size => 13 by 13 pixels\n", + " L extent => From -9.87 amin to 9.83 amin\n", + " M extent => From -9.80 amin to 9.89 amin\n", + "\n", + " cut_0:\n", + " El. cut (S -> N) at 2025-11-25 23:16 UTC\n", + "\n", + " cut_1:\n", + " Az. cut (W -> E) at 2025-11-25 23:19 UTC\n", + "\n", + "############################################################\n", + "### ant_ea17, ddi_0 ###\n", + "############################################################\n", + "\n", + " General:\n", + " Telescope name => EVLA\n", + " Antenna name => ea17\n", + " Station => W04\n", + " Reference antennas => ['ea11 @ W08']\n", + " Source => HOLORASTER\n", + " Phase center => 16h42m58.810s +39°48m36.990s [FK5]\n", + " Az el info => @ l,m = (0,0), Az, El = (294.2, 45.4) [deg]\n", + " Start time => 25 Nov 2025, 23:10:56 (UTC)\n", + " Stop time => 25 Nov 2025, 23:20:42 (UTC)\n", + " Duration => 9 min, 46.05 sec\n", + "\n", + " Spectral:\n", + " Channel width => 2.000 MHz\n", + " Frequency range => 21.315 GHz to 21.443 GHz\n", + " Number of channels => 64\n", + " Rep. frequency => 21.380 GHz\n", + " Rep. wavelength => 1.402 cm\n", + "\n", + " Beam:\n", + " Cell size => 1.64 amin\n", + " Grid size => 13 by 13 pixels\n", + " L extent => From -9.88 amin to 9.85 amin\n", + " M extent => From -9.88 amin to 9.87 amin\n", + "\n", + " cut_0:\n", + " El. cut (S -> N) at 2025-11-25 23:16 UTC\n", + "\n", + " cut_1:\n", + " Az. cut (W -> E) at 2025-11-25 23:19 UTC\n", + "\n", + "############################################################\n", + "### ant_ea17, ddi_1 ###\n", + "############################################################\n", + "\n", + " General:\n", + " Telescope name => EVLA\n", + " Antenna name => ea17\n", + " Station => W04\n", + " Reference antennas => ['ea11 @ W08']\n", + " Source => HOLORASTER\n", + " Phase center => 16h42m58.810s +39°48m36.990s [FK5]\n", + " Az el info => @ l,m = (0,0), Az, El = (294.2, 45.4) [deg]\n", + " Start time => 25 Nov 2025, 23:10:56 (UTC)\n", + " Stop time => 25 Nov 2025, 23:20:42 (UTC)\n", + " Duration => 9 min, 46.05 sec\n", + "\n", + " Spectral:\n", + " Channel width => 2.000 MHz\n", + " Frequency range => 21.443 GHz to 21.571 GHz\n", + " Number of channels => 64\n", + " Rep. frequency => 21.508 GHz\n", + " Rep. wavelength => 1.394 cm\n", + "\n", + " Beam:\n", + " Cell size => 1.63 amin\n", + " Grid size => 13 by 13 pixels\n", + " L extent => From -9.88 amin to 9.85 amin\n", + " M extent => From -9.88 amin to 9.87 amin\n", + "\n", + " cut_0:\n", + " El. cut (S -> N) at 2025-11-25 23:16 UTC\n", + "\n", + " cut_1:\n", + " Az. cut (W -> E) at 2025-11-25 23:19 UTC\n", + "\n", + "\n" + ] + } + ], + "execution_count": 6 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "## interacting with datatree", + "id": "16966dd52055f8d7" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-12-30T19:29:14.392297512Z", + "start_time": "2025-12-30T19:29:14.281185408Z" + } + }, + "cell_type": "code", + "source": [ + "ea17_ddi_0 = beamcut_mds['ant_ea17']['ddi_0']\n", + "\n", + "ea17_ddi_0" + ], + "id": "51d76e1fc74d6487", + "outputs": [ + { + "data": { + "text/plain": [ + "\n", + "Group: /ant_ea17/ddi_0\n", + "│ Attributes:\n", + "│ summary: {'aperture': None, 'beam': {'cell size': 0.0004767736386022435,...\n", + "├── Group: /ant_ea17/ddi_0/cut_0\n", + "│ Dimensions: (lm_dist: 487, time: 487, lm: 2)\n", + "│ Coordinates:\n", + "│ * lm_dist (lm_dist) float64 4kB -0.002873 -0.002863 ... 0.002871\n", + "│ * time (time) float64 4kB 5.271e+09 5.271e+09 ... 5.271e+09 5.271e+09\n", + "│ Dimensions without coordinates: lm\n", + "│ Data variables:\n", + "│ LL_amp_fit (lm_dist) float64 4kB dask.array\n", + "│ LL_amplitude (lm_dist) float64 4kB dask.array\n", + "│ LL_phase (lm_dist) float64 4kB dask.array\n", + "│ LL_weight (lm_dist) float64 4kB dask.array\n", + "│ RR_amp_fit (lm_dist) float64 4kB dask.array\n", + "│ RR_amplitude (lm_dist) float64 4kB dask.array\n", + "│ RR_phase (lm_dist) float64 4kB dask.array\n", + "│ RR_weight (lm_dist) float64 4kB dask.array\n", + "│ lm_offsets (time, lm) float64 8kB dask.array\n", + "│ Attributes: (12/19)\n", + "│ LL_amp_fit_pars: [-0.002077188719134862, 0.16536964764709944, 0...\n", + "│ LL_first_side_lobe_ratio: 0.9506970019203174\n", + "│ LL_fit_succeeded: True\n", + "│ LL_n_peaks: 5\n", + "│ LL_pb_center: 2.0062998598862443e-05\n", + "│ LL_pb_fwhm: 0.000709009354625047\n", + "│ ... ...\n", + "│ available_corrs: ['RR', 'LL']\n", + "│ direction: El. cut (S -> N)\n", + "│ lm_angle: 8.043802534404685e-06\n", + "│ scan_number: 8\n", + "│ time_string: 2025-11-25 23:16\n", + "│ xlabel: Elevation offset\n", + "└── Group: /ant_ea17/ddi_0/cut_1\n", + " Dimensions: (lm_dist: 485, time: 485, lm: 2)\n", + " Coordinates:\n", + " * lm_dist (lm_dist) float64 4kB 0.002271 0.002865 ... -0.00286 -0.002875\n", + " * time (time) float64 4kB 5.271e+09 5.271e+09 ... 5.271e+09 5.271e+09\n", + " Dimensions without coordinates: lm\n", + " Data variables:\n", + " LL_amp_fit (lm_dist) float64 4kB dask.array\n", + " LL_amplitude (lm_dist) float64 4kB dask.array\n", + " LL_phase (lm_dist) float64 4kB dask.array\n", + " LL_weight (lm_dist) float64 4kB dask.array\n", + " RR_amp_fit (lm_dist) float64 4kB dask.array\n", + " RR_amplitude (lm_dist) float64 4kB dask.array\n", + " RR_phase (lm_dist) float64 4kB dask.array\n", + " RR_weight (lm_dist) float64 4kB dask.array\n", + " lm_offsets (time, lm) float64 8kB dask.array\n", + " Attributes: (12/19)\n", + " LL_amp_fit_pars: [-0.0021472318786903435, 0.07025333233659152, ...\n", + " LL_first_side_lobe_ratio: 0.938663012205753\n", + " LL_fit_succeeded: True\n", + " LL_n_peaks: 5\n", + " LL_pb_center: 7.306736296828256e-06\n", + " LL_pb_fwhm: 0.0007120542454906039\n", + " ... ...\n", + " available_corrs: ['RR', 'LL']\n", + " direction: Az. cut (W -> E)\n", + " lm_angle: 1.5720178287317457\n", + " scan_number: 13\n", + " time_string: 2025-11-25 23:19\n", + " xlabel: Azimuth offset" + ], + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
<xarray.DatasetView> Size: 0B\n",
+       "Dimensions:  ()\n",
+       "Data variables:\n",
+       "    *empty*\n",
+       "Attributes:\n",
+       "    summary:  {'aperture': None, 'beam': {'cell size': 0.0004767736386022435,...
" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } ], - "id": "fe3dd386e7860dd0" + "execution_count": 7 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-12-30T19:29:14.538870513Z", + "start_time": "2025-12-30T19:29:14.395954201Z" + } + }, + "cell_type": "code", + "source": "", + "id": "44798944baca7195", + "outputs": [], + "execution_count": 7 } ], "metadata": { From 47cf0cd34233c4a92d8b42c1b4485b24df95bc85 Mon Sep 17 00:00:00 2001 From: Victor de Souza Magalhaes Date: Tue, 30 Dec 2025 15:45:39 -0700 Subject: [PATCH 59/95] Implemented all routines in beamcut_tutorial.ipynb, text still missing. --- docs/beamcut_tutorial.ipynb | 509 ++++++++++++++++++++++++++---------- 1 file changed, 371 insertions(+), 138 deletions(-) diff --git a/docs/beamcut_tutorial.ipynb b/docs/beamcut_tutorial.ipynb index 62890edb..e3e179e6 100644 --- a/docs/beamcut_tutorial.ipynb +++ b/docs/beamcut_tutorial.ipynb @@ -27,8 +27,8 @@ { "metadata": { "ExecuteTime": { - "end_time": "2025-12-30T19:28:48.893433594Z", - "start_time": "2025-12-30T19:28:46.030643463Z" + "end_time": "2025-12-30T21:46:30.092760292Z", + "start_time": "2025-12-30T21:46:27.793015526Z" } }, "cell_type": "code", @@ -70,8 +70,8 @@ { "metadata": { "ExecuteTime": { - "end_time": "2025-12-30T19:28:53.829527975Z", - "start_time": "2025-12-30T19:28:48.900165847Z" + "end_time": "2025-12-30T21:46:30.695479431Z", + "start_time": "2025-12-30T21:46:30.095360112Z" } }, "cell_type": "code", @@ -81,13 +81,119 @@ "\n", "basename = \"kband_beamcut_small\"\n", "ms_name = f\"data/{basename}.ms\"\n", - "# toolviper.utils.data.download(file=ms_name, folder=\"data\")\n", - "!rm -rf data\n", - "!mkdir data\n", - "!cp -r /home/victor/work/Holography-1022/git-shared/beam-cuts/testing/kband_beamcut_small.ms ./data/" + "\n", + "toolviper.utils.data.update()\n", + "toolviper.utils.data.download(file=f\"{basename}.ms\", folder=\"data\")" ], "id": "fe3dd386e7860dd0", - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[\u001B[38;2;128;05;128m2025-12-30 14:46:30,099\u001B[0m] \u001B[38;2;50;50;205m INFO\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m Updating file metadata information ... \n" + ] + }, + { + "data": { + "text/plain": [ + "Output()" + ], + "application/vnd.jupyter.widget-view+json": { + "version_major": 2, + "version_minor": 0, + "model_id": "812df0ff7dca4e0a96385fa1bc34f5e0" + } + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [], + "text/html": [ + "
\n"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data",
+     "jetTransient": {
+      "display_id": null
+     }
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "[\u001B[38;2;128;05;128m2025-12-30 14:46:30,536\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",
+      "[\u001B[38;2;128;05;128m2025-12-30 14:46:30,557\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/plain": [
+       "                          \n",
+       " \u001B[1m \u001B[0m\u001B[1mDownload List         \u001B[0m\u001B[1m \u001B[0m \n",
+       " ──────────────────────── \n",
+       "  \u001B[35mkband_beamcut_small.ms\u001B[0m  \n",
+       "                          \n"
+      ],
+      "text/html": [
+       "
                          \n",
+       "  Download List           \n",
+       " ──────────────────────── \n",
+       "  kband_beamcut_small.ms  \n",
+       "                          \n",
+       "
\n" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[\u001B[38;2;128;05;128m2025-12-30 14:46:30,567\u001B[0m] \u001B[38;2;50;50;205m INFO\u001B[0m\u001B[38;2;112;128;144m astrohack: \u001B[0m File exists: data/kband_beamcut_small.ms \n" + ] + }, + { + "data": { + "text/plain": [ + "Output()" + ], + "application/vnd.jupyter.widget-view+json": { + "version_major": 2, + "version_minor": 0, + "model_id": "9e81b310f9f64fd8972fd165c9de78d0" + } + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [], + "text/html": [ + "
\n"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data",
+     "jetTransient": {
+      "display_id": null
+     }
+    }
+   ],
    "execution_count": 2
   },
   {
@@ -103,16 +209,16 @@
   {
    "metadata": {
     "ExecuteTime": {
-     "end_time": "2025-12-30T19:29:11.574320917Z",
-     "start_time": "2025-12-30T19:28:53.831473931Z"
+     "end_time": "2025-12-30T21:46:41.604585348Z",
+     "start_time": "2025-12-30T21:46:30.697277741Z"
     }
    },
    "cell_type": "code",
    "source": [
     "from astrohack import extract_pointing, extract_holog\n",
     "\n",
-    "point_name = f'{basename}.point.zarr'\n",
-    "holog_name = f'{basename}.holog.zarr'\n",
+    "point_name = f'data/{basename}.point.zarr'\n",
+    "holog_name = f'data/{basename}.holog.zarr'\n",
     "\n",
     "# Extract pointing data to an astrohack file format\n",
     "point_mds = extract_pointing(ms_name, point_name, overwrite=True)\n",
@@ -132,60 +238,17 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "[\u001B[38;2;128;05;128m2025-12-30 12:28:53,840\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",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:28:53,850\u001B[0m] \u001B[38;2;255;160;0m WARNING\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m kband_beamcut_small.point.zarr will be overwritten. \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:28:58,653\u001B[0m] \u001B[38;2;255;160;0m WARNING\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m ea18 pointing table has 4.3% of data with irregular time sampling \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:00,047\u001B[0m] \u001B[38;2;50;50;205m    INFO\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m Finished processing \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:00,247\u001B[0m] \u001B[38;2;255;160;0m WARNING\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m kband_beamcut_small.holog.zarr will be overwritten. \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:00,408\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 ... 13] \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:08,885\u001B[0m] \u001B[38;2;50;50;205m    INFO\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m EA17: DDI 0: Suggested cell size 1.64 amin, FOV: (19.73 amin, 19.76 amin) \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:08,887\u001B[0m] \u001B[38;2;50;50;205m    INFO\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m EA06: DDI 0: Suggested cell size 1.64 amin, FOV: (19.73 amin, 19.76 amin) \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:08,890\u001B[0m] \u001B[38;2;50;50;205m    INFO\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m EA01: DDI 0: Suggested cell size 1.64 amin, FOV: (19.73 amin, 19.76 amin) \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:08,892\u001B[0m] \u001B[38;2;50;50;205m    INFO\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m EA07: DDI 0: Suggested cell size 1.64 amin, FOV: (19.72 amin, 19.76 amin) \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:08,894\u001B[0m] \u001B[38;2;50;50;205m    INFO\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m EA14: DDI 0: Suggested cell size 1.64 amin, FOV: (19.72 amin, 19.76 amin) \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:08,897\u001B[0m] \u001B[38;2;50;50;205m    INFO\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m EA15: DDI 0: Suggested cell size 1.64 amin, FOV: (19.72 amin, 19.76 amin) \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:08,900\u001B[0m] \u001B[38;2;50;50;205m    INFO\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m EA02: DDI 0: Suggested cell size 1.64 amin, FOV: (19.72 amin, 19.76 amin) \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:08,903\u001B[0m] \u001B[38;2;50;50;205m    INFO\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m EA08: DDI 0: Suggested cell size 1.64 amin, FOV: (19.72 amin, 19.76 amin) \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:08,905\u001B[0m] \u001B[38;2;50;50;205m    INFO\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m EA20: DDI 0: Suggested cell size 1.64 amin, FOV: (19.72 amin, 19.76 amin) \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:08,908\u001B[0m] \u001B[38;2;50;50;205m    INFO\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m EA19: DDI 0: Suggested cell size 1.64 amin, FOV: (19.72 amin, 19.76 amin) \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:08,910\u001B[0m] \u001B[38;2;50;50;205m    INFO\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m EA25: DDI 0: Suggested cell size 1.64 amin, FOV: (19.73 amin, 19.76 amin) \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:08,912\u001B[0m] \u001B[38;2;50;50;205m    INFO\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m EA09: DDI 0: Suggested cell size 1.64 amin, FOV: (19.72 amin, 19.76 amin) \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:10,068\u001B[0m] \u001B[38;2;255;160;0m WARNING\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m Mapping antenna ea06 has no data \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:10,068\u001B[0m] \u001B[38;2;255;160;0m WARNING\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m Mapping antenna ea01 has no data \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:10,069\u001B[0m] \u001B[38;2;255;160;0m WARNING\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m Mapping antenna ea07 has no data \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:10,069\u001B[0m] \u001B[38;2;255;160;0m WARNING\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m Mapping antenna ea14 has no data \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:10,118\u001B[0m] \u001B[38;2;255;160;0m WARNING\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m Mapping antenna ea02 has no data \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:10,119\u001B[0m] \u001B[38;2;255;160;0m WARNING\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m Mapping antenna ea08 has no data \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:10,120\u001B[0m] \u001B[38;2;255;160;0m WARNING\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m Mapping antenna ea20 has no data \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:10,120\u001B[0m] \u001B[38;2;255;160;0m WARNING\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m Mapping antenna ea19 has no data \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:10,121\u001B[0m] \u001B[38;2;255;160;0m WARNING\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m Mapping antenna ea25 has no data \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:10,121\u001B[0m] \u001B[38;2;255;160;0m WARNING\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m Mapping antenna ea09 has no data \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:10,122\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: 0 holog_map_key: map_0 \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:10,123\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 ... 13] \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,240\u001B[0m] \u001B[38;2;50;50;205m    INFO\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m EA17: DDI 1: Suggested cell size 1.63 amin, FOV: (19.73 amin, 19.76 amin) \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,242\u001B[0m] \u001B[38;2;50;50;205m    INFO\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m EA06: DDI 1: Suggested cell size 1.63 amin, FOV: (19.73 amin, 19.76 amin) \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,245\u001B[0m] \u001B[38;2;50;50;205m    INFO\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m EA01: DDI 1: Suggested cell size 1.63 amin, FOV: (19.73 amin, 19.76 amin) \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,249\u001B[0m] \u001B[38;2;50;50;205m    INFO\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m EA07: DDI 1: Suggested cell size 1.63 amin, FOV: (19.72 amin, 19.76 amin) \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,251\u001B[0m] \u001B[38;2;50;50;205m    INFO\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m EA14: DDI 1: Suggested cell size 1.63 amin, FOV: (19.72 amin, 19.76 amin) \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,254\u001B[0m] \u001B[38;2;50;50;205m    INFO\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m EA15: DDI 1: Suggested cell size 1.63 amin, FOV: (19.72 amin, 19.76 amin) \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,256\u001B[0m] \u001B[38;2;50;50;205m    INFO\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m EA02: DDI 1: Suggested cell size 1.63 amin, FOV: (19.72 amin, 19.76 amin) \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,259\u001B[0m] \u001B[38;2;50;50;205m    INFO\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m EA08: DDI 1: Suggested cell size 1.63 amin, FOV: (19.72 amin, 19.76 amin) \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,263\u001B[0m] \u001B[38;2;50;50;205m    INFO\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m EA20: DDI 1: Suggested cell size 1.63 amin, FOV: (19.72 amin, 19.76 amin) \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,267\u001B[0m] \u001B[38;2;50;50;205m    INFO\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m EA19: DDI 1: Suggested cell size 1.63 amin, FOV: (19.72 amin, 19.76 amin) \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,270\u001B[0m] \u001B[38;2;50;50;205m    INFO\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m EA25: DDI 1: Suggested cell size 1.63 amin, FOV: (19.73 amin, 19.76 amin) \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,272\u001B[0m] \u001B[38;2;50;50;205m    INFO\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m EA09: DDI 1: Suggested cell size 1.63 amin, FOV: (19.72 amin, 19.76 amin) \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,340\u001B[0m] \u001B[38;2;255;160;0m WARNING\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m Mapping antenna ea06 has no data \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,341\u001B[0m] \u001B[38;2;255;160;0m WARNING\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m Mapping antenna ea01 has no data \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,341\u001B[0m] \u001B[38;2;255;160;0m WARNING\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m Mapping antenna ea07 has no data \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,342\u001B[0m] \u001B[38;2;255;160;0m WARNING\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m Mapping antenna ea14 has no data \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,406\u001B[0m] \u001B[38;2;255;160;0m WARNING\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m Mapping antenna ea02 has no data \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,407\u001B[0m] \u001B[38;2;255;160;0m WARNING\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m Mapping antenna ea08 has no data \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,407\u001B[0m] \u001B[38;2;255;160;0m WARNING\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m Mapping antenna ea20 has no data \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,408\u001B[0m] \u001B[38;2;255;160;0m WARNING\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m Mapping antenna ea19 has no data \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,409\u001B[0m] \u001B[38;2;255;160;0m WARNING\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m Mapping antenna ea25 has no data \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,409\u001B[0m] \u001B[38;2;255;160;0m WARNING\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m Mapping antenna ea09 has no data \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,410\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 holog_map_key: map_0 \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,411\u001B[0m] \u001B[38;2;50;50;205m    INFO\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m Finished processing \n"
+      "[\u001B[38;2;128;05;128m2025-12-30 14:46:30,701\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",
+      "[\u001B[38;2;128;05;128m2025-12-30 14:46:33,472\u001B[0m] \u001B[38;2;50;50;205m    INFO\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m Finished processing \n",
+      "[\u001B[38;2;128;05;128m2025-12-30 14:46:33,526\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 ... 13] \n",
+      "[\u001B[38;2;128;05;128m2025-12-30 14:46:39,827\u001B[0m] \u001B[38;2;50;50;205m    INFO\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m EA17: DDI 0: Suggested cell size 1.64 amin, FOV: (19.73 amin, 19.76 amin) \n",
+      "[\u001B[38;2;128;05;128m2025-12-30 14:46:39,830\u001B[0m] \u001B[38;2;50;50;205m    INFO\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m EA15: DDI 0: Suggested cell size 1.64 amin, FOV: (19.72 amin, 19.76 amin) \n",
+      "[\u001B[38;2;128;05;128m2025-12-30 14:46:41,154\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: 0 holog_map_key: map_0 \n",
+      "[\u001B[38;2;128;05;128m2025-12-30 14:46:41,155\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 ... 13] \n",
+      "[\u001B[38;2;128;05;128m2025-12-30 14:46:41,413\u001B[0m] \u001B[38;2;50;50;205m    INFO\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m EA17: DDI 1: Suggested cell size 1.63 amin, FOV: (19.73 amin, 19.76 amin) \n",
+      "[\u001B[38;2;128;05;128m2025-12-30 14:46:41,415\u001B[0m] \u001B[38;2;50;50;205m    INFO\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m EA15: DDI 1: Suggested cell size 1.63 amin, FOV: (19.72 amin, 19.76 amin) \n",
+      "[\u001B[38;2;128;05;128m2025-12-30 14:46:41,521\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 holog_map_key: map_0 \n",
+      "[\u001B[38;2;128;05;128m2025-12-30 14:46:41,522\u001B[0m] \u001B[38;2;50;50;205m    INFO\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m Finished processing \n"
      ]
     }
    ],
@@ -204,15 +267,15 @@
   {
    "metadata": {
     "ExecuteTime": {
-     "end_time": "2025-12-30T19:29:13.888731535Z",
-     "start_time": "2025-12-30T19:29:11.589761961Z"
+     "end_time": "2025-12-30T21:46:43.403230112Z",
+     "start_time": "2025-12-30T21:46:41.619129366Z"
     }
    },
    "cell_type": "code",
    "source": [
     "from astrohack import beamcut\n",
     "\n",
-    "beamcut_name = f'{basename}.beamcut.zarr'\n",
+    "beamcut_name = f'data/{basename}.beamcut.zarr'\n",
     "\n",
     "beamcut_mds = beamcut(holog_name,\n",
     "        beamcut_name,\n",
@@ -226,13 +289,12 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,593\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",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,600\u001B[0m] \u001B[38;2;255;160;0m WARNING\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m kband_beamcut_small.beamcut.zarr will be overwritten. \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,627\u001B[0m] \u001B[38;2;50;50;205m    INFO\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m processing EA17: DDI 1 \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:11,876\u001B[0m] \u001B[38;2;50;50;205m    INFO\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m processing EA17: DDI 0 \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:12,086\u001B[0m] \u001B[38;2;50;50;205m    INFO\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m processing EA15: DDI 1 \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:13,252\u001B[0m] \u001B[38;2;50;50;205m    INFO\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m processing EA15: DDI 0 \n",
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:13,561\u001B[0m] \u001B[38;2;50;50;205m    INFO\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m Finished processing \n"
+      "[\u001B[38;2;128;05;128m2025-12-30 14:46:41,622\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",
+      "[\u001B[38;2;128;05;128m2025-12-30 14:46:41,646\u001B[0m] \u001B[38;2;50;50;205m    INFO\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m processing EA17: DDI 1 \n",
+      "[\u001B[38;2;128;05;128m2025-12-30 14:46:41,909\u001B[0m] \u001B[38;2;50;50;205m    INFO\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m processing EA17: DDI 0 \n",
+      "[\u001B[38;2;128;05;128m2025-12-30 14:46:42,094\u001B[0m] \u001B[38;2;50;50;205m    INFO\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m processing EA15: DDI 1 \n",
+      "[\u001B[38;2;128;05;128m2025-12-30 14:46:42,925\u001B[0m] \u001B[38;2;50;50;205m    INFO\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m processing EA15: DDI 0 \n",
+      "[\u001B[38;2;128;05;128m2025-12-30 14:46:43,153\u001B[0m] \u001B[38;2;50;50;205m    INFO\u001B[0m\u001B[38;2;112;128;144m   astrohack: \u001B[0m Finished processing \n"
      ]
     }
    ],
@@ -247,8 +309,8 @@
   {
    "metadata": {
     "ExecuteTime": {
-     "end_time": "2025-12-30T19:29:14.032771572Z",
-     "start_time": "2025-12-30T19:29:13.891520025Z"
+     "end_time": "2025-12-30T21:46:43.532105009Z",
+     "start_time": "2025-12-30T21:46:43.405461864Z"
     }
    },
    "cell_type": "code",
@@ -267,31 +329,31 @@
      "text": [
       "####################################################################################################\n",
       "###                                         Summary for:                                         ###\n",
-      "###                               kband_beamcut_small.beamcut.zarr                               ###\n",
+      "###                            data/kband_beamcut_small.beamcut.zarr                             ###\n",
       "####################################################################################################\n",
       "\n",
       "Full documentation for AstrohackBeamcutFile objects' API at: \n",
       "https://astrohack.readthedocs.io/en/stable/_api/autoapi/astrohack/mds/index.html#astrohack.mds.AstrohackBeamcutFile\n",
       "\n",
       "Input Parameters:\n",
-      "+--------------+----------------------------------+\n",
-      "| Parameter    | Value                            |\n",
-      "+--------------+----------------------------------+\n",
-      "| ant          | all                              |\n",
-      "| azel_unit    | deg                              |\n",
-      "| beamcut_name | kband_beamcut_small.beamcut.zarr |\n",
-      "| ddi          | all                              |\n",
-      "| destination  | None                             |\n",
-      "| display      | False                            |\n",
-      "| dpi          | 300                              |\n",
-      "| holog_name   | kband_beamcut_small.holog.zarr   |\n",
-      "| lm_unit      | amin                             |\n",
-      "| origin       | beamcut                          |\n",
-      "| overwrite    | True                             |\n",
-      "| parallel     | False                            |\n",
-      "| version      | 0.9.4                            |\n",
-      "| y_scale      | None                             |\n",
-      "+--------------+----------------------------------+\n",
+      "+--------------+---------------------------------------+\n",
+      "| Parameter    | Value                                 |\n",
+      "+--------------+---------------------------------------+\n",
+      "| ant          | all                                   |\n",
+      "| azel_unit    | deg                                   |\n",
+      "| beamcut_name | data/kband_beamcut_small.beamcut.zarr |\n",
+      "| ddi          | all                                   |\n",
+      "| destination  | None                                  |\n",
+      "| display      | False                                 |\n",
+      "| dpi          | 300                                   |\n",
+      "| holog_name   | data/kband_beamcut_small.holog.zarr   |\n",
+      "| lm_unit      | amin                                  |\n",
+      "| origin       | beamcut                               |\n",
+      "| overwrite    | True                                  |\n",
+      "| parallel     | False                                 |\n",
+      "| version      | 0.9.4                                 |\n",
+      "| y_scale      | None                                  |\n",
+      "+--------------+---------------------------------------+\n",
       "\n",
       "Contents:\n",
       "+----------+-------+--------------------+\n",
@@ -328,8 +390,8 @@
   {
    "metadata": {
     "ExecuteTime": {
-     "end_time": "2025-12-30T19:29:14.279359214Z",
-     "start_time": "2025-12-30T19:29:14.035178967Z"
+     "end_time": "2025-12-30T21:46:43.828985640Z",
+     "start_time": "2025-12-30T21:46:43.536367474Z"
     }
    },
    "cell_type": "code",
@@ -340,7 +402,7 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "[\u001B[38;2;128;05;128m2025-12-30 12:29:14,038\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",
+      "[\u001B[38;2;128;05;128m2025-12-30 14:46:43,539\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",
       "############################################################\n",
       "###                   ant_ea15, ddi_0                    ###\n",
       "############################################################\n",
@@ -496,8 +558,8 @@
   {
    "metadata": {
     "ExecuteTime": {
-     "end_time": "2025-12-30T19:29:14.392297512Z",
-     "start_time": "2025-12-30T19:29:14.281185408Z"
+     "end_time": "2025-12-30T21:46:44.009943590Z",
+     "start_time": "2025-12-30T21:46:43.836314728Z"
     }
    },
    "cell_type": "code",
@@ -562,12 +624,12 @@
        "            RR_weight     (lm_dist) float64 4kB dask.array\n",
        "            lm_offsets    (time, lm) float64 8kB dask.array\n",
        "        Attributes: (12/19)\n",
-       "            LL_amp_fit_pars:           [-0.0021472318786903435, 0.07025333233659152, ...\n",
-       "            LL_first_side_lobe_ratio:  0.938663012205753\n",
+       "            LL_amp_fit_pars:           [-0.0021472318109970845, 0.07025296275717853, ...\n",
+       "            LL_first_side_lobe_ratio:  0.9386383160351017\n",
        "            LL_fit_succeeded:          True\n",
        "            LL_n_peaks:                5\n",
-       "            LL_pb_center:              7.306736296828256e-06\n",
-       "            LL_pb_fwhm:                0.0007120542454906039\n",
+       "            LL_pb_center:              7.307101442132769e-06\n",
+       "            LL_pb_fwhm:                0.0007120555281659058\n",
        "            ...                        ...\n",
        "            available_corrs:           ['RR', 'LL']\n",
        "            direction:                 Az. cut (W -> E)\n",
@@ -952,7 +1014,7 @@
        "Data variables:\n",
        "    *empty*\n",
        "Attributes:\n",
-       "    summary:  {'aperture': None, 'beam': {'cell size': 0.0004767736386022435,...