diff --git a/docs/.DS_Store b/docs/.DS_Store index 375e40c..9a874b5 100644 Binary files a/docs/.DS_Store and b/docs/.DS_Store differ diff --git a/docs/source/dev_reference/index.rst b/docs/source/dev_reference/index.rst index 4cc188f..8f60b93 100644 --- a/docs/source/dev_reference/index.rst +++ b/docs/source/dev_reference/index.rst @@ -43,6 +43,28 @@ Main module for visualization. :undoc-members: :show-inheritance: +======================== +:mod:`triggering` Module +======================== + +Main module for triggering lidar scans. + +.. automodule:: adam.triggering + :members: + :undoc-members: + :show-inheritance: + +======================== +:mod:`testing` Module +======================== + +Main module for testing. + +.. automodule:: adam.triggering + :members: + :undoc-members: + :show-inheritance: + ================== :mod:`util` Module ================== diff --git a/docs/source/user_guide/show_me_the_lakebreeze.rst b/docs/source/user_guide/show_me_the_lakebreeze.rst index e4eec16..d8b2cef 100644 --- a/docs/source/user_guide/show_me_the_lakebreeze.rst +++ b/docs/source/user_guide/show_me_the_lakebreeze.rst @@ -5,8 +5,25 @@ multiple radar scans with one inference step. Batch inference will save computat by loading the model only once and utilizing vectorization to perform the inference on multiple radar scans at a time. +.. code-block :: python + import adam + # List of radar scans to process + radar_scans = [ + ('KLOT', '2025-07-15T18:00:00'), + ('KLOT', '2025-07-15T19:00:00'), + ('KLOT', '2025-07-15T20:00:00'), + ] + + # Preprocess all radar scans + preprocessed_scans = [adam.io.preprocess_radar_image(station, time) for station, time in radar_scans] + + # Perform batch inference + lake_breeze_results = adam.model.infer_lake_breeze_batch( + preprocessed_scans, model_name='lakebreeze_model_fcn_resnet50_no_augmentation') + + Analyzing the mask data in custom workflows =========================================== The :py:meth:`RadarImage` class contains all of the information you need to perform diff --git a/examples/README.txt b/examples/README.txt index 8e5d9b4..4641749 100644 --- a/examples/README.txt +++ b/examples/README.txt @@ -2,5 +2,5 @@ ADAM's Example Gallery ====================== In this example gallery, we show how to use ADAM to: - * Get a laze breeze mask from a NEXRAD scan - * Determine the instrument pointing direction using ADAM \ No newline at end of file + * Get a laze breeze mask from a NEXRAD scan. + * Determine the instrument pointing direction using ADAM. \ No newline at end of file diff --git a/notebooks/lake_breeze_detection_example.ipynb b/notebooks/lake_breeze_detection_example.ipynb index 7901d1f..a519a90 100644 --- a/notebooks/lake_breeze_detection_example.ipynb +++ b/notebooks/lake_breeze_detection_example.ipynb @@ -24,7 +24,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "04aecfd3", "metadata": {}, "outputs": [ @@ -48,13 +48,10 @@ ], "source": [ "import adam\n", - "import pyart\n", "import numpy as np\n", "import matplotlib.pyplot as plt\n", "import cartopy.crs as ccrs\n", - "import cartopy.feature as cfeature\n", - "from scipy.optimize import minimize\n", - "from scipy.ndimage import center_of_mass, label" + "import cartopy.feature as cfeature\n" ] }, { diff --git a/pyproject.toml b/pyproject.toml index 0cfdf65..1c5de55 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,7 @@ dependencies = [ "safetensors", "cartopy", "boto3", + "paramiko" ] dynamic = ["version"] diff --git a/src/adam/__init__.py b/src/adam/__init__.py index 49b70f9..d3afaaa 100644 --- a/src/adam/__init__.py +++ b/src/adam/__init__.py @@ -1,12 +1,14 @@ """Top-level package for ATMOS Analogue Digital Twin.""" -__author__ = """Robert Jackson, Seongha Park""" -__version__ = '0.3.1' +__author__ = """Robert Jackson, Bhupendra Raut, Seongha Park""" +__version__ = '0.4.0' from . import io # noqa from . import model # noqa from . import vis # noqa from . import util # noqa +from . import testing # noqa +from . import triggering # noqa diff --git a/src/adam/testing/__init__.py b/src/adam/testing/__init__.py new file mode 100644 index 0000000..845a545 --- /dev/null +++ b/src/adam/testing/__init__.py @@ -0,0 +1,29 @@ +""" +============================ +adam.testing (adam.testing) +============================ + +.. currentmodule:: adam.testing + +This module handles testing utilities for ADAM. + + + +.. autosummary:: + :toctree: generated/ + + FakeSSHClient + FakeSFTP + TEST_RHI_FILE + TEST_PPI_FILE + TEST_PPI_TRIGGERED_SCAN + TEST_RHI_TRIGGERED_SCAN +""" +import os + +from .fake_lidar import FakeSFTP, FakeSSHClient # noqa + +TEST_RHI_FILE = os.path.join(os.path.dirname(__file__), "data/test_scan_rhi.txt") +TEST_PPI_FILE = os.path.join(os.path.dirname(__file__), "data/test_scan_ppi.txt") +TEST_PPI_TRIGGERED_SCAN = os.path.join(os.path.dirname(__file__), "data/test_scan_ppi_lakebreeze_close.txt") +TEST_RHI_TRIGGERED_SCAN = os.path.join(os.path.dirname(__file__), "data/test_scan_rhi_lakebreeze.txt") diff --git a/src/adam/testing/data/test_scan_ppi.txt b/src/adam/testing/data/test_scan_ppi.txt new file mode 100644 index 0000000..e5c81ec --- /dev/null +++ b/src/adam/testing/data/test_scan_ppi.txt @@ -0,0 +1,15 @@ +7 +6 +20 +A.1=30,S.1=1388,P.1=-125000*A.2=30,S.2=1388,P.2=0 +W0 +A.1=30,S.1=1388,P.1=-250000*A.2=30,S.2=1388,P.2=0 +W0 +A.1=30,S.1=1388,P.1=-125000*A.2=30,S.2=1388,P.2=-3472 +W0 +A.1=30,S.1=1388,P.1=-250000*A.2=30,S.2=1388,P.2=-3472 +W0 +A.1=30,S.1=1388,P.1=-125000*A.2=30,S.2=1388,P.2=-6944 +W0 +A.1=30,S.1=1388,P.1=-250000*A.2=30,S.2=1388,P.2=-6944 +W0 diff --git a/src/adam/testing/data/test_scan_ppi_lakebreeze_close.txt b/src/adam/testing/data/test_scan_ppi_lakebreeze_close.txt new file mode 100644 index 0000000..8e5e30e --- /dev/null +++ b/src/adam/testing/data/test_scan_ppi_lakebreeze_close.txt @@ -0,0 +1,15 @@ +7 +6 +20 +A.1=30,S.1=1388,P.1=-279857*A.2=30,S.2=69,P.2=0 +W0 +A.1=30,S.1=1388,P.1=-321524*A.2=30,S.2=69,P.2=0 +W0 +A.1=30,S.1=1388,P.1=-279857*A.2=30,S.2=69,P.2=-3472 +W0 +A.1=30,S.1=1388,P.1=-321524*A.2=30,S.2=69,P.2=-3472 +W0 +A.1=30,S.1=1388,P.1=-279857*A.2=30,S.2=69,P.2=-6944 +W0 +A.1=30,S.1=1388,P.1=-321524*A.2=30,S.2=69,P.2=-6944 +W0 diff --git a/src/adam/testing/data/test_scan_rhi.txt b/src/adam/testing/data/test_scan_rhi.txt new file mode 100644 index 0000000..ef48413 --- /dev/null +++ b/src/adam/testing/data/test_scan_rhi.txt @@ -0,0 +1,7 @@ +7 +2 +20 +A.1=30,S.1=1388,P.1=-125000*A.2=30,S.2=1388,P.2=0 +W0 +A.1=30,S.1=1388,P.1=-125000*A.2=30,S.2=1388,P.2=-62500 +W0 diff --git a/src/adam/testing/data/test_scan_rhi_lakebreeze.txt b/src/adam/testing/data/test_scan_rhi_lakebreeze.txt new file mode 100644 index 0000000..71036e1 --- /dev/null +++ b/src/adam/testing/data/test_scan_rhi_lakebreeze.txt @@ -0,0 +1,7 @@ +7 +2 +20 +A.1=30,S.1=1388,P.1=-300691*A.2=30,S.2=69,P.2=0 +W0 +A.1=30,S.1=1388,P.1=-300691*A.2=30,S.2=69,P.2=-31250 +W0 diff --git a/src/adam/testing/fake_lidar.py b/src/adam/testing/fake_lidar.py new file mode 100644 index 0000000..ba69723 --- /dev/null +++ b/src/adam/testing/fake_lidar.py @@ -0,0 +1,56 @@ +import os +import logging + +class FakeSFTP: + """ + A fake SFTP client for testing purposes. It simulates the behavior of an SFTP client by storing files in the current directory. + """ + + def __init__(self): + self.wd = os.path.dirname(__file__) + logging.info(f"Working directory for fake SFTP: {self.wd}") + scan_params_path = os.path.join(self.wd, "C:/Lidar/System/Scan parameters/") + dynscan_path = os.path.join(self.wd, "C:/Users/End User/DynScan/") + os.makedirs(scan_params_path, exist_ok=True) + os.makedirs(dynscan_path, exist_ok=True) + self.files = [] + + def listdir(self, path): return os.listdir(os.path.join(self.wd, path)) + def get(self, remote, local): + remote_path = os.path.join(self.wd, remote) + with open(remote_path, "rb") as f: + data = f.read() + with open(local, "wb") as f: + f.write(data) + def put(self, local, remote): + logging.info(f"Putting file {local} to {remote} in fake SFTP at {self.wd}.") + if remote.startswith("/"): + remote = remote[1:] + remote_path = os.path.join(self.wd, remote) + with open(remote_path, "wb") as f: + f.write(open(local, "rb").read()) + self.files.append(remote_path) + def close(self): + for file in self.files: + os.remove(file) + logging.info("Removed files from fake SFTP.") + os.removedirs(os.path.join(self.wd, "C:/Lidar/System/Scan parameters/")) + logging.info("Removed scan parameters directory from fake SFTP.") + os.removedirs(os.path.join(self.wd, "C:/Users/End User/DynScan/")) + def __enter__(self): return self + def __exit__(self, exc_type, exc_val, exc_tb): self.close() + +class FakeSSHClient: + """ + A fake SSH client for testing purposes. It simulates the behavior of an SSH client by providing a context manager that returns a FakeSFTP instance. + """ + + def __init__(self): + self.sftp = FakeSFTP() + def set_missing_host_key_policy(self, policy): pass + def connect(self, ip_addr, username, password): print(f"Connected to {ip_addr} with username {username}") + def open_sftp(self): return self.sftp + def close(self): self.sftp.close() + def __enter__(self): return self + def __exit__(self, exc_type, exc_val, exc_tb): self.close() + diff --git a/src/adam/triggering/__init__.py b/src/adam/triggering/__init__.py new file mode 100644 index 0000000..0685d3b --- /dev/null +++ b/src/adam/triggering/__init__.py @@ -0,0 +1,18 @@ +""" +============================== +ADAM Triggering Module +============================== +.. currentmodule:: adam.triggering + +This module handles the generation of scan strategies and triggering of the lidar based on radar data. + +.. autosummary:: + + :toctree: generated/ + + make_scan_file + send_scan + trigger_lidar_ppis_from_mask + trigger_lidar_rhi_from_mask +""" +from .halo_lidar import make_scan_file, send_scan, trigger_lidar_ppis_from_mask, trigger_lidar_rhi_from_mask # noqa \ No newline at end of file diff --git a/src/adam/triggering/halo_lidar.py b/src/adam/triggering/halo_lidar.py new file mode 100644 index 0000000..8b8bffd --- /dev/null +++ b/src/adam/triggering/halo_lidar.py @@ -0,0 +1,242 @@ +import numpy as np +import paramiko +import datetime +import os +import xarray as xr +import logging + +from ..util import azimuth_point + +def make_scan_file(elevations, azimuths, + out_file_name, azi_speed=1., + el_speed=0.1, + wait=0, acceleration=30, repeat=7, + rays_per_point=20, dyn_csm=False, + AZ_COUNTS_PER_ROT=500000, EL_COUNTS_PER_ROT=250000): + """ + Makes a CSM scanning strategy file for a Halo Photonics Doppler Lidar. + + Parameters + ---------- + no_points: int + The number of points to collect in the ray + elevations: float 1d array or tuple + The elevation of each sweep in the scan. If this is a 2-tuple, then + the script will generate an RHI spanning the smallest to largest elevation + azimuths: float or 2-tuple + If this is a 2-tuple, then this script will generate a PPI from min_azi to max_azi. + out_file_name: str + The output name of the file. + azi_speed: float + The speed of the azimuth motor in degrees per second. + el_speed: float + The speed of the elevation motor in degrees per second. + wait: int + The wait time in milliseconds at each point in the scan. + acceleration: int + The acceleration of the motor in ticks per second squared. This is a constant that depends on the lidar hardware. + repeat: int + The number of times to repeat the scan strategy. This is used to ensure that the lidar collects enough data points for each scan. + rays_per_point: int + The number of rays to collect at each point in the scan. This is used to ensure + that the lidar collects enough data points for each scan. + dyn_csm: bool + Set to True to send CSM assuming Dynamic CSM mode + AZ_COUNTS_PER_ROT: int + The number of counts per rotation for the azimuth motor. This is a constant that depends + on the lidar hardware and is used to convert from degrees to the encoded values that the lidar uses for its scan strategy. + EL_COUNTS_PER_ROT: int + The number of counts per rotation for the elevation motor. This is a constant that depends + on the lidar hardware and is used to convert from degrees to the encoded values that the lidar uses + for its scan strategy. + + Returns + ------- + None + This function does not return anything. It generates a CSM scan strategy file for the Halo Lidar + and saves it to the specified output file name. + """ + speed_azi_encoded = int(azi_speed * (AZ_COUNTS_PER_ROT / 360.)) + speed_el_encoded = int(el_speed * (EL_COUNTS_PER_ROT / 360.)) + clockwise = True + no_points = len(azimuths) * len(elevations) + with open(out_file_name, 'w') as output: + if dyn_csm is False: + output.write('%d\r\n' % repeat) + output.write('%d\r\n' % no_points) + output.write('%d\r\n' % rays_per_point) + + for el in elevations: + if clockwise: + az_array = azimuths + else: + az_array = azimuths.reverse() + for az in az_array: + azi_encoded = -int(az * (AZ_COUNTS_PER_ROT / 360.)) + el_encoded = -int(el * (EL_COUNTS_PER_ROT / 360.)) + output.write("A.1=%d,S.1=%d,P.1=%d*A.2=%d,S.2=%d,P.2=%d\r\n" % + (acceleration, speed_azi_encoded, azi_encoded, + acceleration, speed_el_encoded, el_encoded)) + output.write('W%d\r\n' % (wait)) + clockwise = ~clockwise + return + + +def send_scan(file_name, lidar_ip_addr, lidar_uname, lidar_pwd, out_file_name='user.txt', dyn_csm=False, + client=None): + """ + Sends a scan to the lidar + + Parameters + --------- + file_name: str + Path to the CSM-format scan strategy + lidar_ip_addr: + IP address of the lidar + lidar_uname: + The username of the lidar + lidar_password: + The lidar's password + out_file_name: + The output file name on the lidar + dyn_csm: bool + Set to True to assume Dynamic CSM mode + client: paramiko.SSHClient, optional + An optional SSH client to use for the connection. If not provided, a new client will be created + and closed within this function. + + """ + if client is not None: + ssh = client + close_client = False + else: + ssh = paramiko.SSHClient() + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + ssh.connect(lidar_ip_addr, username=lidar_uname, password=lidar_pwd) + print("Connected to the Lidar!") + close_client = True + + if close_client: + with ssh.open_sftp() as sftp: + if dyn_csm is False: + logging.info(f"Writing {out_file_name} on lidar.") + sftp.put(file_name, "/C:/Lidar/System/Scan parameters/%s" % out_file_name) + else: + sftp.put(file_name, f"/C:/Users/End User/DynScan/{out_file_name}") + else: + sftp = ssh.open_sftp() + if dyn_csm is False: + print(f"Writing {out_file_name} on lidar.") + sftp.put(file_name, "/C:/Lidar/System/Scan parameters/%s" % out_file_name) + else: + sftp.put(file_name, f"/C:/Users/End User/DynScan/{out_file_name}") + + if close_client: + ssh.close() + + +def trigger_lidar_ppis_from_mask(rad_scan, lidar_lat, lidar_lon, lidar_ip_addr, lidar_uname, lidar_pwd, elevations, + az_width=30., out_file_name='user.txt', dyn_csm=False, + max_distance=5000, client=None): + """ + Triggers a PPI scan on the lidar using a scan strategy generated from a lake breeze mask. + + Parameters + ---------- + rad_scan: RadarImage + The radar scan containing the lake breeze mask. The azimuth of the largest lake breeze region will be + used to determine the center of the RHI scan. + lidar_lat: float + The latitude of the lidar + lidar_lon: float + The longitude of the lidar + lidar_ip_addr: + IP address of the lidar + lidar_uname: + The username of the lidar + lidar_password: + The lidar's password + out_file_name: + The output file name on the lidar + dyn_csm: bool + Set to True to assume Dynamic CSM mode + az_width: float + The width of the azimuth scan in degrees. The scan will be centered around the azimuth of the + largest lake breeze region as determined by the model's radar image and the location of the lidar. + max_distance: float + The maximum distance from the lidar to the lake breeze region for the scan to be triggered. + client: paramiko.SSHClient, optional + An optional SSH client to use for the connection. If not provided, a new client will be created + and closed within the send_scan function. + + Returns + ------- + bool + Returns True if the scan was triggered, and False if the scan was not triggered due to the distance from the lidar + to the lake breeze region being greater than max_distance. + """ + middle_azimuth, lat, lon, dist = azimuth_point(lidar_lon, lidar_lat, rad_scan) + if dist > max_distance: # If the distance is greater than max_distance, don't trigger the scan + logging.info(f"Distance from lidar to lake breeze region is {dist} meters. Not triggering scan.") + return False + azimuths = np.array([middle_azimuth - az_width/2, middle_azimuth + az_width/2]) + + make_scan_file(elevations, azimuths, out_file_name, dyn_csm=dyn_csm) + send_scan(out_file_name, lidar_ip_addr, lidar_uname, lidar_pwd, out_file_name=out_file_name, dyn_csm=dyn_csm, + client=client) + return True + + +def trigger_lidar_rhi_from_mask(rad_scan, lidar_lat, lidar_lon, lidar_ip_addr, lidar_uname, lidar_pwd, elevations, + out_file_name='user.txt', dyn_csm=False, max_distance=5000, client=None): + """ + Triggers a PPI scan on the lidar using a scan strategy generated from a lake breeze mask. + + Parameters + ---------- + rad_scan: RadarImage + The radar scan containing the lake breeze mask. The azimuth of the largest lake breeze region will be + used to determine the center of the RHI scan. + lidar_lat: float + The latitude of the lidar + lidar_lon: float + The longitude of the lidar + lidar_ip_addr: + IP address of the lidar + lidar_uname: + The username of the lidar + lidar_password: + The lidar's password + out_file_name: + The output file name on the lidar + dyn_csm: bool + Set to True to assume Dynamic CSM mode + az_width: float + The width of the azimuth scan in degrees. The scan will be centered around the azimuth of the + largest lake breeze region as determined by the model's radar image and the location of the lidar. + az_res: float + The resolution of the azimuth scan in degrees. This determines how many points will be in the scan. + For example, if az_width is 30 and az_res is 2, then + there will be 15 points in the azimuth scan (from -15 to +15 degrees around the center azimuth). + max_distance: float + The maximum distance from the lidar to the lake breeze region for the scan to be triggered. + client: paramiko.SSHClient, optional + An optional SSH client to use for the connection. If not provided, a new client will be created + and closed within the send_scan function. + + Returns + ------- + bool + Returns True if the scan was triggered, and False if the scan was not triggered due to + the distance from the lidar to the lake breeze region being greater than max_distance. + """ + middle_azimuth, lat, lon, dist = azimuth_point(lidar_lon, lidar_lat, rad_scan) + if dist > max_distance: # If the distance is greater than max_distance, don't trigger the scan + logging.info(f"Distance from lidar to lake breeze region is {dist} meters. Not triggering scan.") + return False + azimuths = [middle_azimuth] + + make_scan_file(elevations, azimuths, out_file_name, dyn_csm=dyn_csm) + send_scan(out_file_name, lidar_ip_addr, lidar_uname, lidar_pwd, out_file_name=out_file_name, dyn_csm=dyn_csm, + client=client) + return True \ No newline at end of file diff --git a/src/adam/util/instrument_steering.py b/src/adam/util/instrument_steering.py index b5f81e5..bccb65b 100644 --- a/src/adam/util/instrument_steering.py +++ b/src/adam/util/instrument_steering.py @@ -1,9 +1,9 @@ import numpy as np - +import logging from adam.io import RadarImage from scipy.ndimage import center_of_mass, label -def azimuth_point(instrument_lat, instrument_lon, +def azimuth_point(instrument_lon, instrument_lat, radar_image: RadarImage, index=None, area_threshold=20): """ @@ -58,10 +58,20 @@ def azimuth_point(instrument_lat, instrument_lon, num_x = len(radar_image.grid_x) center_x = radar_image.grid_x[int(center[1])] center_y = radar_image.grid_y[int(center[0])] - instrument_x = radar_image.grid_x[num_x - lon_index] + logging.info(f"Center of mass: {center}, Center lat/lon: {center_y}, {center_x}") + instrument_x = radar_image.grid_x[lon_index] instrument_y = radar_image.grid_y[lat_index] - - angle = np.atan2((center_x - instrument_x), (center_y - instrument_y)) + logging.info(f"Instrument lat/lon: {instrument_y}, {instrument_x}") + angle = np.arctan2((center_x - instrument_x), (center_y - instrument_y)) deg_angle = np.rad2deg(angle) deg_angle = (deg_angle + 360) % 360 - return deg_angle, lats[int(center[0])], lons[int(center[1])] \ No newline at end of file + + # Get the distance from the instrument to the nearest point in the lake breeze region + x, y = np.meshgrid(radar_image.grid_x, radar_image.grid_y) + dist = np.sqrt((x - instrument_x)**2 + (y - instrument_y)**2) + dist = dist[mask == 1] + dist = np.min(dist) + return deg_angle, lats[int(center[0])], lons[int(center[1])], dist + + + \ No newline at end of file diff --git a/tests/test_adaptive_scanning.py b/tests/test_adaptive_scanning.py new file mode 100644 index 0000000..a460146 --- /dev/null +++ b/tests/test_adaptive_scanning.py @@ -0,0 +1,119 @@ +import numpy as np +import os + +def test_make_scan_file(): + from adam.triggering.halo_lidar import make_scan_file + from adam.testing import TEST_RHI_FILE, TEST_PPI_FILE + elevations = [0, 90] + azimuths = [90] + out_file_name = 'test_scan_rhi.txt' + make_scan_file(elevations, azimuths, el_speed=2, out_file_name=out_file_name) + + with open(out_file_name, 'r') as f: + lines = f.readlines() + with open(TEST_RHI_FILE, 'r') as f: + expected_lines = f.readlines() + + assert len(lines) == len(expected_lines) + for i, (line, expected_line) in enumerate(zip(lines, expected_lines)): + assert line == expected_line, f"Line {i} does not match expected output.\nGot: {line}\nExpected: {expected_line}" + + elevations = [0, 5, 10] + azimuths = [90, 180] + out_file_name = 'test_scan_ppi.txt' + make_scan_file(elevations, azimuths, el_speed=2, out_file_name=out_file_name) + + with open(out_file_name, 'r') as f: + lines = f.readlines() + with open(TEST_PPI_FILE, 'r') as f: + expected_lines = f.readlines() + + assert len(lines) == len(expected_lines) + for i, (line, expected_line) in enumerate(zip(lines, expected_lines)): + assert line == expected_line, f"Line {i} does not match expected output.\nGot: {line}\nExpected: {expected_line}" + +def test_send_scan(): + from adam.triggering.halo_lidar import send_scanpy + from adam.testing import TEST_RHI_FILE + from adam.testing.fake_lidar import FakeSSHClient + lidar_ip_addr = None + lidar_uname = None + lidar_pwd = None + in_file_name = TEST_RHI_FILE + out_file_name = 'test_rhi_scan.txt' + out_file_name2 = 'test_rhi_scan_copy.txt' + with FakeSSHClient() as client: + send_scan(in_file_name, lidar_ip_addr, lidar_uname, lidar_pwd, out_file_name=out_file_name, + client=client) + with open(client.sftp.files[0], 'r') as f: + lines = f.readlines() + with open(TEST_RHI_FILE, 'r') as f: + expected_lines = f.readlines() + assert len(lines) == len(expected_lines) + for i, (line, expected_line) in enumerate(zip(lines, expected_lines)): + assert line == expected_line, f"Line {i} does not match expected output.\nGot: {line}\nExpected: {expected_line}" + +def test_trigger_lidar_ppis_from_mask(): + import torch + import adam + torch.manual_seed(42) + rad_scan = adam.io.preprocess_radar_image('KLOT', '2025-07-15T18:00:00') + rad_scan = adam.model.infer_lake_breeze( + rad_scan, model_name='lakebreeze_best_model_fcn_resnet50') + result = adam.triggering.trigger_lidar_ppis_from_mask(rad_scan, 41.70101404798476, -87.99577278662817, + None, None, None, elevations=[0, 5, 10], az_width=30., + out_file_name='test_scan_ppi_lakebreeze.txt', dyn_csm=False) + assert result is False, "Expected the scan to not be triggered due to distance from lidar to lake breeze region being greater than max_distance." + + rad_scan = adam.io.preprocess_radar_image('KLOT', '2025-04-24T20:03:23') + rad_scan = adam.model.infer_lake_breeze( + rad_scan, model_name='lakebreeze_model_fcn_resnet50_no_augmentation') + with adam.testing.FakeSSHClient() as client: + result = adam.triggering.trigger_lidar_ppis_from_mask( + rad_scan, 41.70101404798476, -87.99577278662817, + None, None, None, elevations=[0, 5, 10], az_width=30., + out_file_name='test_scan_ppi_lakebreeze_close.txt', dyn_csm=False, client=client) + assert result is True, "Expected the scan to be triggered since the distance from lidar to lake breeze region is less than max_distance." + client.sftp.get(client.sftp.files[0], 'test_scan_ppi_lakebreeze_close_copy.txt') + with open('test_scan_ppi_lakebreeze_close_copy.txt', 'r') as f: + lines = f.readlines() + with open(adam.testing.TEST_PPI_TRIGGERED_SCAN, 'r') as f: + expected_lines = f.readlines() + assert len(lines) == len(expected_lines) + for i, (line, expected_line) in enumerate(zip(lines, expected_lines)): + assert line == expected_line, f"Line {i} does not match expected output.\nGot: {line}\nExpected: {expected_line}" + os.remove('test_scan_ppi_lakebreeze_close_copy.txt') + os.remove('test_scan_ppi_lakebreeze_close.txt') + os.remove('test_scan_ppi.txt') + +def test_trigger_lidar_rhi_from_mask(): + import torch + import adam + torch.manual_seed(42) + rad_scan = adam.io.preprocess_radar_image('KLOT', '2025-07-15T18:00:00') + rad_scan = adam.model.infer_lake_breeze( + rad_scan, model_name='lakebreeze_best_model_fcn_resnet50') + result = adam.triggering.trigger_lidar_rhi_from_mask(rad_scan, 41.70101404798476, -87.99577278662817, + None, None, None, elevations=[0, 45], + out_file_name='test_scan_rhi_lakebreeze.txt', dyn_csm=False) + assert result is False, "Expected the scan to not be triggered due to distance from lidar to lake breeze region being greater than max_distance." + rad_scan = adam.io.preprocess_radar_image('KLOT', '2025-04-24T20:03:23') + rad_scan = adam.model.infer_lake_breeze( + rad_scan, model_name='lakebreeze_model_fcn_resnet50_no_augmentation') + with adam.testing.FakeSSHClient() as client: + result = adam.triggering.trigger_lidar_rhi_from_mask(rad_scan, 41.70101404798476, -87.99577278662817, + None, None, None, elevations=[0, 45], + out_file_name='test_scan_rhi_lakebreeze.txt', dyn_csm=False, client=client) + assert result is True, "Expected the scan to be triggered since the distance from lidar to lake breeze region" \ + " is less than max_distance." + client.sftp.get(client.sftp.files[0], 'test_scan_rhi_lakebreeze_copy.txt') + with open('test_scan_rhi_lakebreeze_copy.txt', 'r') as f: + lines = f.readlines() + with open(adam.testing.TEST_RHI_TRIGGERED_SCAN, 'r') as f: + expected_lines = f.readlines() + assert len(lines) == len(expected_lines) + for i, (line, expected_line) in enumerate(zip(lines, expected_lines)): + assert line == expected_line, f"Line {i} does not match expected output.\nGot: {line}\nExpected: {expected_line}" + os.remove('test_scan_rhi_lakebreeze_copy.txt') + os.remove('test_scan_rhi_lakebreeze.txt') + os.remove('test_scan_rhi.txt') \ No newline at end of file diff --git a/tests/test_util.py b/tests/test_util.py index ded4cba..e8ca5fc 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -5,10 +5,11 @@ def test_instrument_steering(): torch.manual_seed(42) - rad_scan = adam.io.preprocess_radar_image('KLOT', '2025-07-15T18:00:00') + rad_scan = adam.io.preprocess_radar_image('KLOT', '2025-04-24T20:03:23') rad_scan = adam.model.infer_lake_breeze( - rad_scan, model_name='lakebreeze_best_model_fcn_resnet50') - angle, lat, lon = adam.util.azimuth_point(-87.99577278662817, 41.70101404798476, rad_scan) - np.testing.assert_almost_equal(angle, 40.34, decimal=0) - np.testing.assert_almost_equal(lat, 41.9694, decimal=2) - np.testing.assert_almost_equal(lon, -87.7528, decimal=2) \ No newline at end of file + rad_scan, model_name='lakebreeze_model_fcn_resnet50_no_augmentation') + angle, lat, lon, dist = adam.util.azimuth_point(-87.99577278662817, 41.70101404798476, rad_scan) + np.testing.assert_almost_equal(angle, 224.61, decimal=0) + np.testing.assert_almost_equal(lat, 41.68, decimal=2) + np.testing.assert_almost_equal(lon, -88.01, decimal=2) + np.testing.assert_almost_equal(dist, 0, decimal=2) \ No newline at end of file