From fc9d9fc38509ceb5882f11b3f706f96f28d2ad65 Mon Sep 17 00:00:00 2001 From: carleyjmartin Date: Tue, 14 Mar 2023 14:11:17 -0600 Subject: [PATCH 01/30] add option for true velocity --- pydarn/plotting/maps.py | 67 +++++++++++++++++++++++++++++++++++++--- pydarn/utils/plotting.py | 1 + 2 files changed, 63 insertions(+), 5 deletions(-) diff --git a/pydarn/plotting/maps.py b/pydarn/plotting/maps.py index 265b5366..076c20e5 100644 --- a/pydarn/plotting/maps.py +++ b/pydarn/plotting/maps.py @@ -110,6 +110,7 @@ def plot_mapdata(cls, dmap_data: List[dict], ax=None, MapParams.FITTED_VELOCITY: 'plasma', MapParams.MODEL_VELOCITY: 'plasma', MapParams.RAW_VELOCITY: 'plasma', + MapParams.TRUE_VELOCITY: 'plasma', MapParams.POWER: 'plasma_r', MapParams.SPECTRAL_WIDTH: PyDARNColormaps.PYDARN_VIRIDIS zmin: int @@ -117,6 +118,7 @@ def plot_mapdata(cls, dmap_data: List[dict], ax=None, Default: MapParams.FITTED_VELOCITY: [0], MapParams.MODEL_VELOCITY: [0], MapParams.RAW_VELOCITY: [0], + MapParams.TRUE_VELOCITY: [0], MapParams.POWER: [0], MapParams.SPECTRAL_WIDTH: [0] zmax: int @@ -124,6 +126,7 @@ def plot_mapdata(cls, dmap_data: List[dict], ax=None, Default: MapParams.FITTED_VELOCITY: [1000], MapParams.MODEL_VELOCITY: [1000], MapParams.RAW_VELOCITY: [1000], + MapParams.TRUE_VELOCITY: [1000], MapParams.POWER: [250], MapParams.SPECTRAL_WIDTH: [250] colorbar: bool @@ -167,6 +170,7 @@ def plot_mapdata(cls, dmap_data: List[dict], ax=None, cmap = {MapParams.FITTED_VELOCITY: 'plasma_r', MapParams.MODEL_VELOCITY: 'plasma_r', MapParams.RAW_VELOCITY: 'plasma_r', + MapParams.TRUE_VELOCITY: 'plasma_r', MapParams.POWER: 'plasma', MapParams.SPECTRAL_WIDTH: PyDARNColormaps.PYDARN_VIRIDIS} cmap = plt.cm.get_cmap(cmap[parameter]) @@ -174,6 +178,7 @@ def plot_mapdata(cls, dmap_data: List[dict], ax=None, defaultzminmax = {MapParams.FITTED_VELOCITY: [0, 1000], MapParams.MODEL_VELOCITY: [0, 1000], MapParams.RAW_VELOCITY: [0, 1000], + MapParams.TRUE_VELOCITY: [0, 1000], MapParams.POWER: [0, 250], MapParams.SPECTRAL_WIDTH: [0, 250]} if zmin is None: @@ -249,16 +254,34 @@ def plot_mapdata(cls, dmap_data: List[dict], ax=None, elif parameter == MapParams.RAW_VELOCITY: v_mag = dmap_data[record]['vector.vel.median'] azm_v = np.radians(dmap_data[record]['vector.kvect']) + elif parameter == MapParams.TRUE_VELOCITY: + # Get LOS velocities + v_los = dmap_data[record]['vector.vel.median'] + a_los = np.radians(dmap_data[record]['vector.kvect']) + # Get fitted velocities + v_fit, a_fit = \ + cls.calculated_fitted_velocities(mlats=mlats, + mlons=np.radians( + data_lons), + hemisphere=hemisphere, + fit_coefficient=dmap_data[ + record]['N+2'], + fit_order=dmap_data[ + record]['fit.order'], + lat_min=dmap_data[ + record]['latmin'], + len_factor=len_factor) + v_mag, azm_v = cls.calculated_true_velocities(v_los, a_los, + v_fit, a_fit) elif parameter == MapParams.POWER: v_mag = dmap_data[record]['vector.pwr.median'] azm_v = np.radians(dmap_data[record]['vector.kvect']) - elif parameter == MapParams.SPECTRAL_WIDTH: v_mag = dmap_data[record]['vector.wdt.median'] azm_v = np.radians(dmap_data[record]['vector.kvect']) if parameter in [MapParams.FITTED_VELOCITY, MapParams.MODEL_VELOCITY, - MapParams.RAW_VELOCITY]: + MapParams.RAW_VELOCITY, MapParams.TRUE_VELOCITY]: # Make reference vector and add it to the array to # be calculated too reflat = (np.abs(plt.gca().get_ylim()[1]) - 5) * hemisphere.value @@ -318,7 +341,8 @@ def plot_mapdata(cls, dmap_data: List[dict], ax=None, if color_vectors is True: if parameter in [MapParams.FITTED_VELOCITY, MapParams.MODEL_VELOCITY, - MapParams.RAW_VELOCITY]: + MapParams.RAW_VELOCITY, + MapParams.TRUE_VELOCITY]: if reference_vector > 0: plt.scatter(mlons[:], mlats[:], c=v_mag[:], s=2.0, vmin=zmin, vmax=zmax, cmap=cmap, zorder=5.0, @@ -340,7 +364,8 @@ def plot_mapdata(cls, dmap_data: List[dict], ax=None, colorbar = False if parameter in [MapParams.FITTED_VELOCITY, MapParams.MODEL_VELOCITY, - MapParams.RAW_VELOCITY]: + MapParams.RAW_VELOCITY, + MapParams.TRUE_VELOCITY]: if reference_vector > 0: plt.scatter(mlons[:], mlats[:], c='#292929', s=2.0, zorder=5.0, clip_on=False) @@ -374,7 +399,8 @@ def plot_mapdata(cls, dmap_data: List[dict], ax=None, else: if parameter in [MapParams.FITTED_VELOCITY, MapParams.MODEL_VELOCITY, - MapParams.RAW_VELOCITY]: + MapParams.RAW_VELOCITY, + MapParams.TRUE_VELOCITY]: cb.set_label('Velocity (m s$^{-1}$)') elif parameter is MapParams.SPECTRAL_WIDTH: cb.set_label('Spectral Width (m s$^{-1}$)') @@ -451,6 +477,37 @@ def index_legendre(cls, l: int, m: int): and (m != 0) and l**2 + 2 * m - 1) or 0 + @classmethod + def calculated_true_velocities(cls, v_los: list, a_los: list, + v_fit: list, a_fit: list): + """ + Calculates the true velocities + The True velocity is calculated as the combined LOS vector and the + perpendicular-to-LOS component of the fitted velocity + + Parameters + ---------- + v_los: array + raw magnitude of LOS velocity + a_los: array + angle of direction of raw LOS velocity + v_fit: array + raw magnitude of LOS velocity + a_fit: array + angle of direction of raw LOS velocity + """ + # Get vector component of fitted velocities + # perpendicular to LOS velocity + v_perp = np.sqrt( abs( v_fit**2 - v_los**2 )) + a_perp = (a_los - np.pi/2) + np.arctan2(v_los, v_perp) + + # Calculate the true velocities as the resultatnt of the + # perpendicular and LOS velocities + v_true = np.sqrt( v_perp**2 + v_los**2 ) + a_true = a_perp + np.arctan2(v_los, v_perp) + return v_true, a_true + + @classmethod def calculated_fitted_velocities(cls, mlats: list, mlons: list, fit_coefficient: list, diff --git a/pydarn/utils/plotting.py b/pydarn/utils/plotting.py index 25aff0ce..7e0fe731 100644 --- a/pydarn/utils/plotting.py +++ b/pydarn/utils/plotting.py @@ -29,6 +29,7 @@ class MapParams(enum.Enum): FITTED_VELOCITY = "fitted" MODEL_VELOCITY = "model.vel.median" RAW_VELOCITY = "vector.vel.median" + TRUE_VELOCITY = "true" POWER = "vector.pwr.median" SPECTRAL_WIDTH = "vector.wdt.median" From 3c3b8f7f1c38506ecc3ef6a6675b2d9c435787d8 Mon Sep 17 00:00:00 2001 From: carleyjmartin Date: Tue, 14 Mar 2023 14:17:11 -0600 Subject: [PATCH 02/30] mod line --- pydarn/plotting/maps.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pydarn/plotting/maps.py b/pydarn/plotting/maps.py index 076c20e5..73ef7653 100644 --- a/pydarn/plotting/maps.py +++ b/pydarn/plotting/maps.py @@ -11,6 +11,7 @@ # vector # 2022-08-15: CJM - Removed plot_FOV call for default uses # 2022-12-13: CJM - Limited reference vectors to only velocity use +# 2023-03-14: CJM - Added true vector option # # Disclaimer: # pyDARN is under the LGPL v3 license found in the root directory LICENSE.md From ecd30f9d460b5b60811eea4e711dffc828c3c8b9 Mon Sep 17 00:00:00 2001 From: carleyjmartin Date: Wed, 26 Apr 2023 15:47:49 -0600 Subject: [PATCH 03/30] correction for true velocity calculations --- pydarn/plotting/maps.py | 38 ++++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/pydarn/plotting/maps.py b/pydarn/plotting/maps.py index 73ef7653..972f7e54 100644 --- a/pydarn/plotting/maps.py +++ b/pydarn/plotting/maps.py @@ -482,9 +482,11 @@ def index_legendre(cls, l: int, m: int): def calculated_true_velocities(cls, v_los: list, a_los: list, v_fit: list, a_fit: list): """ - Calculates the true velocities + Calculates the true velocities (Lasse Clausen/Ade Grocott Version) The True velocity is calculated as the combined LOS vector and the perpendicular-to-LOS component of the fitted velocity + Be aware many of the vectors in this function are in multi-dimensional + arrays Parameters ---------- @@ -497,15 +499,31 @@ def calculated_true_velocities(cls, v_los: list, a_los: list, a_fit: array angle of direction of raw LOS velocity """ - # Get vector component of fitted velocities - # perpendicular to LOS velocity - v_perp = np.sqrt( abs( v_fit**2 - v_los**2 )) - a_perp = (a_los - np.pi/2) + np.arctan2(v_los, v_perp) - - # Calculate the true velocities as the resultatnt of the - # perpendicular and LOS velocities - v_true = np.sqrt( v_perp**2 + v_los**2 ) - a_true = a_perp + np.arctan2(v_los, v_perp) + # Reduce LOS vector to components normalized + tkvect = np.empty([2, len(a_los)]) + for j in range(0, len(a_los)): + tkvect[:,j] = [-np.cos(a_los[j]), np.sin(a_los[j])] + + # Get vector components + rvect = np.empty([2, len(v_fit)]) + rvect[0,:] = v_fit * np.cos(a_fit) + rvect[1,:] = v_fit * np.sin(a_fit) + + # Get perpendicular vector components + vv = np.empty([2, len(v_fit)]) + vn = np.squeeze(np.sum(rvect * tkvect, axis=0)) + for i in range(0,len(vn)): + vv[:,i] = vn[i] * tkvect[:,i] + tvect = rvect - vv + + # Combine vectors + for k in range(0,len(v_los)): + tvect[:,k] = tvect[:,k] + v_los[k] * tkvect[:,k] + + # Calculate the magnitude and azimuth + v_true = np.sqrt(tvect[0,:]**2 + tvect[1,:]**2) + a_true = np.arctan2(tvect[1,:], -tvect[0,:]) + return v_true, a_true From df900871ce3d40c557d43a33aa60eef2032737ad Mon Sep 17 00:00:00 2001 From: Carley <60905856+carleyjmartin@users.noreply.github.com> Date: Mon, 27 Oct 2025 15:26:33 -0600 Subject: [PATCH 04/30] Apply suggestions from code review --- pydarn/plotting/maps.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pydarn/plotting/maps.py b/pydarn/plotting/maps.py index f11ea8ca..f8240595 100644 --- a/pydarn/plotting/maps.py +++ b/pydarn/plotting/maps.py @@ -637,13 +637,13 @@ def calculated_true_velocities(cls, v_los: list, a_los: list, Parameters ---------- v_los: array - raw magnitude of LOS velocity + magnitude of LOS velocity vector a_los: array - angle of direction of raw LOS velocity + angle of LOS velocity vector from north v_fit: array - raw magnitude of LOS velocity + magnitude of fitted velocity vector a_fit: array - angle of direction of raw LOS velocity + angle of fitted velocity vector from magnetic north """ # Reduce LOS vector to components normalized tkvect = np.empty([2, len(a_los)]) From 44f940e85eadbac0558499e16a7bba14a0ebae89 Mon Sep 17 00:00:00 2001 From: billetd Date: Sat, 7 Feb 2026 13:44:51 +0100 Subject: [PATCH 05/30] Initial function --- pydarn/utils/detrend.py | 178 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 pydarn/utils/detrend.py diff --git a/pydarn/utils/detrend.py b/pydarn/utils/detrend.py new file mode 100644 index 00000000..8271a9f9 --- /dev/null +++ b/pydarn/utils/detrend.py @@ -0,0 +1,178 @@ +import copy +import pydarn +import datetime as dt +import numpy as np +from typing import List + +def detrend_running_mean(timeseries: List[float], + half_k: int, + n_times: int=None + ): + """ + Detrend a given timeseries with a running mean low-pass filter. + + Parameters + ----------- + timeseries: List[float] + List timeseries data to detrend + half_k: int + Half the window size for the running mean window + n_times: int + Size of the timeseries list. + Default: None, and the code will work it out + Giving the size is faster for loops if you already have it. + + Returns + ------- + detrended: List[float] + Detrended timeseries data + """ + + if n_times is None: + n_times = len(timeseries) + + detrended = [] + for i in range(n_times): + # If the original value is None, keep it None and move on + if timeseries[i] is None: + detrended.append(None) + continue + + # Define the window boundaries (clamped to edges - beware of edge effects) + start = max(0, i - half_k) + end = min(n_times, i + half_k + 1) + + # Extract window and filter out None values + window_values = [v for v in timeseries[start:end] if v is not None] + + if window_values: + # Calculate running mean and subtract + running_mean = sum(window_values) / len(window_values) + detrended.append(timeseries[i] - running_mean) + else: + # Fallback if the entire window is Nones + detrended.append(0) + + return detrended + + +def detrend(fitacf_data: List[dict], + parameter: str = 'all', + window_length: int = 600, + ): + """ + Detrend a series of input fitacf records using a low-pass moving average filter. + Useful for the study of period ULF waves or generally removing background flows. + + Parameters + ----------- + fitacf_data: List[dict] + List of dictionaries where each dictionary contains a fitacf record (from pydarn.read_fitacf()) + parameter: str + The parameter to be detrended + Default: 'both' (Velocity and SNR) + Options: 'both', 'v', 'p_l' + window_length: int + Length of the detrending low-pass filter in seconds + + Returns + ------- + fitacf_data_detrended: List[dict] + Copy of input dmap data with detrended data substituted + """ + + # Make a copy of the fitacf for the detrended data to be substituted into + fitacf_data_detrended = copy.deepcopy(fitacf_data) + + # Max beams and range gates for this data + no_beams = pydarn.SuperDARNRadars.radars[pydarn.RadarID(fitacf_data[0]['stid'])].hardware_info.beams + no_rang = fitacf_data[0]['nrang'] + + # Grab slist and time lists for all records. "None" indicates no data for that record. + slists = [rec.get('slist') for rec in fitacf_data] + rec_times = [dt.datetime(rec.get('time.yr'), + rec.get('time.mo'), + rec.get('time.dy'), + rec.get('time.hr'), + rec.get('time.mt'), + rec.get('time.sc'), + rec.get('time.us')) + for rec in fitacf_data] + + # Handle parameter choice(s) + params = [] + if parameter == 'both' or parameter == 'v': + params.append('v') + if parameter == 'both' or parameter == 'p_l': + params.append('p_l') + + # Grab the data to be detrended + values = [] + for param in params: + values.append([rec.get(param) for rec in fitacf_data]) + + # Iterate over beams + for bmnum in range(0, no_beams): + + # Get all the slists and times for each record on this beam + this_beam_indexes = [i for i, d in enumerate(fitacf_data) if d.get("bmnum") == bmnum] + this_beam_slists = [slists[i]for i in this_beam_indexes] + this_beam_times = [rec_times[i]for i in this_beam_indexes] + + # Calculate the interval between samples in seconds + time_delta = (this_beam_times[1] - this_beam_times[0]).total_seconds() + + # Convert window length from seconds to number of records (k) + # We use an odd number for k to make centering cleaner + k = int(window_length / time_delta) + if k % 2 == 0: k += 1 + half_k = k // 2 + n_times = len(this_beam_times) + + # Get the velocities and/or the powers for this beam + this_beam_values = [] + for value in values: + this_beam_values.append([s for rec, s in zip(fitacf_data, value) if rec.get('bmnum') == bmnum]) + + # Iterate over range gates + all_range_detrends = [] + for rangnum in range(0, no_rang): + indexes = [ + (np.where(sublist == rangnum)[0][0] if rangnum in sublist else None) + if sublist is not None else None + for sublist in this_beam_slists + ] + + detrended_timeseries = [] + for this_beam_value in this_beam_values: + timeseries = [ + sublist[idx] if (sublist is not None and idx is not None) else None + for sublist, idx in zip(this_beam_value, indexes) + ] + + # This bit is just a sinwave substitution to test that the detrending code is working + # timeseries = [np.sin(x) for x in np.linspace(0, 6*np.pi, num=len(this_beam_times))] + + # Running mean detrend + detrended = detrend_running_mean(timeseries, half_k, n_times) + + # Collect detrended timeseries for all parameters + detrended_timeseries.append(detrended) + + # # Testing the detrend + # fig, ax, = plt.subplots() + # ax.plot(this_beam_times, detrended, 'b-') + # ax.plot(this_beam_times, timeseries, 'r') + # fig.autofmt_xdate() + # plt.show() + + # Collect detrended timeseries for all beams List[List] + all_range_detrends.append(detrended_timeseries) + + # Insert values into copied dmap List[dict] + for beam_order, fitacf_index in enumerate(this_beam_indexes): + for param_no, param in enumerate(params): + this_beam_detrended = [sublist[param_no][beam_order] for sublist in all_range_detrends] + fitacf_data_detrended[fitacf_index][param] = np.array([x for x in this_beam_detrended if x is not None]) + + return fitacf_data_detrended From bdd304d8566caeb756f1ac13fa49b63bda19b824 Mon Sep 17 00:00:00 2001 From: billetd Date: Sat, 7 Feb 2026 15:15:25 +0100 Subject: [PATCH 06/30] Class-ified things. Removed test code. --- pydarn/utils/detrend.py | 329 +++++++++++++++++++++------------------- 1 file changed, 169 insertions(+), 160 deletions(-) diff --git a/pydarn/utils/detrend.py b/pydarn/utils/detrend.py index 8271a9f9..4e21eb8e 100644 --- a/pydarn/utils/detrend.py +++ b/pydarn/utils/detrend.py @@ -4,175 +4,184 @@ import numpy as np from typing import List -def detrend_running_mean(timeseries: List[float], - half_k: int, - n_times: int=None - ): - """ - Detrend a given timeseries with a running mean low-pass filter. - - Parameters - ----------- - timeseries: List[float] - List timeseries data to detrend - half_k: int - Half the window size for the running mean window - n_times: int - Size of the timeseries list. - Default: None, and the code will work it out - Giving the size is faster for loops if you already have it. - - Returns - ------- - detrended: List[float] - Detrended timeseries data - """ - - if n_times is None: - n_times = len(timeseries) - - detrended = [] - for i in range(n_times): - # If the original value is None, keep it None and move on - if timeseries[i] is None: - detrended.append(None) - continue - - # Define the window boundaries (clamped to edges - beware of edge effects) - start = max(0, i - half_k) - end = min(n_times, i + half_k + 1) - # Extract window and filter out None values - window_values = [v for v in timeseries[start:end] if v is not None] - - if window_values: - # Calculate running mean and subtract - running_mean = sum(window_values) / len(window_values) - detrended.append(timeseries[i] - running_mean) - else: - # Fallback if the entire window is Nones - detrended.append(0) - - return detrended - - -def detrend(fitacf_data: List[dict], - parameter: str = 'all', - window_length: int = 600, - ): +class Detrend: """ - Detrend a series of input fitacf records using a low-pass moving average filter. - Useful for the study of period ULF waves or generally removing background flows. - - Parameters - ----------- - fitacf_data: List[dict] - List of dictionaries where each dictionary contains a fitacf record (from pydarn.read_fitacf()) - parameter: str - The parameter to be detrended - Default: 'both' (Velocity and SNR) - Options: 'both', 'v', 'p_l' - window_length: int - Length of the detrending low-pass filter in seconds - - Returns + Methods ------- - fitacf_data_detrended: List[dict] - Copy of input dmap data with detrended data substituted + detrend_running_mean + detrend_fitacf """ - # Make a copy of the fitacf for the detrended data to be substituted into - fitacf_data_detrended = copy.deepcopy(fitacf_data) - - # Max beams and range gates for this data - no_beams = pydarn.SuperDARNRadars.radars[pydarn.RadarID(fitacf_data[0]['stid'])].hardware_info.beams - no_rang = fitacf_data[0]['nrang'] - - # Grab slist and time lists for all records. "None" indicates no data for that record. - slists = [rec.get('slist') for rec in fitacf_data] - rec_times = [dt.datetime(rec.get('time.yr'), - rec.get('time.mo'), - rec.get('time.dy'), - rec.get('time.hr'), - rec.get('time.mt'), - rec.get('time.sc'), - rec.get('time.us')) - for rec in fitacf_data] - - # Handle parameter choice(s) - params = [] - if parameter == 'both' or parameter == 'v': - params.append('v') - if parameter == 'both' or parameter == 'p_l': - params.append('p_l') - - # Grab the data to be detrended - values = [] - for param in params: - values.append([rec.get(param) for rec in fitacf_data]) - - # Iterate over beams - for bmnum in range(0, no_beams): - - # Get all the slists and times for each record on this beam - this_beam_indexes = [i for i, d in enumerate(fitacf_data) if d.get("bmnum") == bmnum] - this_beam_slists = [slists[i]for i in this_beam_indexes] - this_beam_times = [rec_times[i]for i in this_beam_indexes] - - # Calculate the interval between samples in seconds - time_delta = (this_beam_times[1] - this_beam_times[0]).total_seconds() - - # Convert window length from seconds to number of records (k) - # We use an odd number for k to make centering cleaner - k = int(window_length / time_delta) - if k % 2 == 0: k += 1 - half_k = k // 2 - n_times = len(this_beam_times) - - # Get the velocities and/or the powers for this beam - this_beam_values = [] - for value in values: - this_beam_values.append([s for rec, s in zip(fitacf_data, value) if rec.get('bmnum') == bmnum]) - - # Iterate over range gates - all_range_detrends = [] - for rangnum in range(0, no_rang): - indexes = [ - (np.where(sublist == rangnum)[0][0] if rangnum in sublist else None) - if sublist is not None else None - for sublist in this_beam_slists - ] - - detrended_timeseries = [] - for this_beam_value in this_beam_values: - timeseries = [ - sublist[idx] if (sublist is not None and idx is not None) else None - for sublist, idx in zip(this_beam_value, indexes) + def __str__(self): + return "This class is static class that provides"\ + " the following methods: \n"\ + " - detrend_running_mean()\n"\ + " - dentrend()\n"\ + + @classmethod + def detrend_running_mean(cls, + timeseries: List[float], + half_k: int, + n_times: int=None + ): + """ + Detrend a given timeseries with a running mean low-pass filter. + + Parameters + ----------- + timeseries: List[float] + List timeseries data to detrend + half_k: int + Half the window size for the running mean window + n_times: int + Size of the timeseries list. + Default: None, and the code will work it out + Giving the size is faster for loops if you already have it. + + Returns + ------- + detrended: List[float] + Detrended timeseries data + """ + + if n_times is None: + n_times = len(timeseries) + + detrended = [] + for i in range(n_times): + # If the original value is None, keep it None and move on + if timeseries[i] is None: + detrended.append(None) + continue + + # Define the window boundaries (clamped to edges - beware of edge effects) + start = max(0, i - half_k) + end = min(n_times, i + half_k + 1) + + # Extract window and filter out None values + window_values = [v for v in timeseries[start:end] if v is not None] + + if window_values: + # Calculate running mean and subtract + running_mean = sum(window_values) / len(window_values) + detrended.append(timeseries[i] - running_mean) + else: + # Fallback if the entire window is Nones + detrended.append(0) + + return detrended + + + @classmethod + def detrend_fitacf(cls, + fitacf_data: List[dict], + parameter: str = 'all', + window_length: int = 600, + ): + """ + Detrend a series of input fitacf records using a low-pass moving average filter. + Useful for the study of period ULF waves or generally removing background flows. + + Parameters + ----------- + fitacf_data: List[dict] + List of dictionaries where each dictionary contains a fitacf record (from pydarn.read_fitacf()) + parameter: str + The parameter to be detrended + Default: 'both' (Velocity and SNR) + Options: 'both', 'v', 'p_l' + window_length: int + Length of the detrending low-pass filter in seconds + + Returns + ------- + fitacf_data_detrended: List[dict] + Copy of input dmap data with detrended data substituted + """ + + # Make a copy of the fitacf for the detrended data to be substituted into + fitacf_data_detrended = copy.deepcopy(fitacf_data) + + # Max beams and range gates for this data + no_beams = pydarn.SuperDARNRadars.radars[pydarn.RadarID(fitacf_data[0]['stid'])].hardware_info.beams + no_rang = fitacf_data[0]['nrang'] + + # Grab slist and time lists for all records. "None" indicates no data for that record. + slists = [rec.get('slist') for rec in fitacf_data] + rec_times = [dt.datetime(rec.get('time.yr'), + rec.get('time.mo'), + rec.get('time.dy'), + rec.get('time.hr'), + rec.get('time.mt'), + rec.get('time.sc'), + rec.get('time.us')) + for rec in fitacf_data] + + # Handle parameter choice(s) + params = [] + if parameter == 'both' or parameter == 'v': + params.append('v') + if parameter == 'both' or parameter == 'p_l': + params.append('p_l') + + # Grab the data to be detrended + values = [] + for param in params: + values.append([rec.get(param) for rec in fitacf_data]) + + # Iterate over beams + for bmnum in range(0, no_beams): + + # Get all the slists and times for each record on this beam + this_beam_indexes = [i for i, d in enumerate(fitacf_data) if d.get("bmnum") == bmnum] + this_beam_slists = [slists[i]for i in this_beam_indexes] + this_beam_times = [rec_times[i]for i in this_beam_indexes] + + # Calculate the interval between samples in seconds + time_delta = (this_beam_times[1] - this_beam_times[0]).total_seconds() + + # Convert window length from seconds to number of records (k) + # We use an odd number for k to make centering cleaner + k = int(window_length / time_delta) + if k % 2 == 0: k += 1 + half_k = k // 2 + n_times = len(this_beam_times) + + # Get the velocities and/or the powers for this beam + this_beam_values = [] + for value in values: + this_beam_values.append([s for rec, s in zip(fitacf_data, value) if rec.get('bmnum') == bmnum]) + + # Iterate over range gates + all_range_detrends = [] + for rangnum in range(0, no_rang): + indexes = [ + (np.where(sublist == rangnum)[0][0] if rangnum in sublist else None) + if sublist is not None else None + for sublist in this_beam_slists ] - # This bit is just a sinwave substitution to test that the detrending code is working - # timeseries = [np.sin(x) for x in np.linspace(0, 6*np.pi, num=len(this_beam_times))] - - # Running mean detrend - detrended = detrend_running_mean(timeseries, half_k, n_times) + detrended_timeseries = [] + for this_beam_value in this_beam_values: + timeseries = [ + sublist[idx] if (sublist is not None and idx is not None) else None + for sublist, idx in zip(this_beam_value, indexes) + ] - # Collect detrended timeseries for all parameters - detrended_timeseries.append(detrended) + # Running mean detrend + detrended = Detrend.detrend_running_mean(timeseries, half_k, n_times) - # # Testing the detrend - # fig, ax, = plt.subplots() - # ax.plot(this_beam_times, detrended, 'b-') - # ax.plot(this_beam_times, timeseries, 'r') - # fig.autofmt_xdate() - # plt.show() + # Collect detrended timeseries for all parameters + detrended_timeseries.append(detrended) - # Collect detrended timeseries for all beams List[List] - all_range_detrends.append(detrended_timeseries) + # Collect detrended timeseries for all beams List[List] + all_range_detrends.append(detrended_timeseries) - # Insert values into copied dmap List[dict] - for beam_order, fitacf_index in enumerate(this_beam_indexes): - for param_no, param in enumerate(params): - this_beam_detrended = [sublist[param_no][beam_order] for sublist in all_range_detrends] - fitacf_data_detrended[fitacf_index][param] = np.array([x for x in this_beam_detrended if x is not None]) + # Insert values into copied dmap List[dict] + for beam_order, fitacf_index in enumerate(this_beam_indexes): + for param_no, param in enumerate(params): + this_beam_detrended = [sublist[param_no][beam_order] for sublist in all_range_detrends] + fitacf_data_detrended[fitacf_index][param] = np.array([x for x in this_beam_detrended if x is not None]) - return fitacf_data_detrended + return fitacf_data_detrended From ee03ac6066e870806e9429ebd07a4f69c430c2a7 Mon Sep 17 00:00:00 2001 From: billetd Date: Sat, 7 Feb 2026 15:34:22 +0100 Subject: [PATCH 07/30] Formatting --- pydarn/utils/detrend.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pydarn/utils/detrend.py b/pydarn/utils/detrend.py index 4e21eb8e..11015699 100644 --- a/pydarn/utils/detrend.py +++ b/pydarn/utils/detrend.py @@ -144,7 +144,8 @@ def detrend_fitacf(cls, # Convert window length from seconds to number of records (k) # We use an odd number for k to make centering cleaner k = int(window_length / time_delta) - if k % 2 == 0: k += 1 + if k % 2 == 0: + k += 1 half_k = k // 2 n_times = len(this_beam_times) From 8ddc6dc46f1f899761f45fc6997e8f10f8f85ac2 Mon Sep 17 00:00:00 2001 From: billetd Date: Sat, 7 Feb 2026 21:28:49 +0100 Subject: [PATCH 08/30] Added Savitsky-Golay filter option. --- pydarn/utils/detrend.py | 80 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 78 insertions(+), 2 deletions(-) diff --git a/pydarn/utils/detrend.py b/pydarn/utils/detrend.py index 11015699..cab4fa85 100644 --- a/pydarn/utils/detrend.py +++ b/pydarn/utils/detrend.py @@ -2,6 +2,7 @@ import pydarn import datetime as dt import numpy as np +from scipy.signal import savgol_filter from typing import List @@ -19,6 +20,66 @@ def __str__(self): " - detrend_running_mean()\n"\ " - dentrend()\n"\ + + @classmethod + def detrend_savgol(cls, + timeseries: List[float], + half_k: int, + **kwargs + ): + """ + Detrend a given timeseries with a Savitsky-Golay filter. + + Parameters + ----------- + timeseries: List[float] + List timeseries data to detrend + half_k: int + Half the window size for the running mean window + **kwargs: + Optional inputs for detrending using scipy's `savgol_filter()` + E.g., polyorder, mode + Defaults are polyorder = 2, mode = 'interp' (edge truncation) + Returns + ------- + detrended: List[float] + Detrended timeseries data + """ + + # Convert to numpy array for processing + y = np.array(timeseries, dtype=float) + + # Map out where the Nones are + mask = np.isnan(y) + # If everything is None, return as is + if np.all(mask): + return [None] * len(timeseries) + indices = np.arange(len(y)) + + # Linearly interpolate over "None"s so the filter can work + # Will result in anomalous velocities/SNR's if data is sparse + y_interp = np.copy(y) + y_interp[mask] = np.interp(indices[mask], indices[~mask], y[~mask]) + + # Generate filter series + window_len = (half_k * 2) + 1 + if 'polyorder' not in kwargs: + polyorder = 2 + background = savgol_filter(y_interp, window_length=window_len, polyorder=polyorder, **kwargs) + + # Detrend + detrended_tmp = y_interp - background + + # Restore the original None positions + # Convert back to list and replace masked values + detrended = detrended_tmp.tolist() + for i in range(len(detrended)): + if mask[i]: + detrended[i] = None + + return detrended + + @classmethod def detrend_running_mean(cls, timeseries: List[float], @@ -78,9 +139,11 @@ def detrend_fitacf(cls, fitacf_data: List[dict], parameter: str = 'all', window_length: int = 600, + detrend_type: str='mean', + **kwargs ): """ - Detrend a series of input fitacf records using a low-pass moving average filter. + Detrend a series of input fitacf records using a low-pass filter. Useful for the study of period ULF waves or generally removing background flows. Parameters @@ -93,6 +156,14 @@ def detrend_fitacf(cls, Options: 'both', 'v', 'p_l' window_length: int Length of the detrending low-pass filter in seconds + detrend_type: str + Type of detrending to be used + Default: 'mean' - subtract a running mean of window_length + Option: 'savgol' - subtract a Savitsky-Golay filter instead + **kwargs: + Optional inputs for detrending using scipy's `savgol_filter()` + E.g., polyorder, mode + Defaults are polyorder = 2, mode = 'interp' (edge truncation) Returns ------- @@ -171,7 +242,12 @@ def detrend_fitacf(cls, ] # Running mean detrend - detrended = Detrend.detrend_running_mean(timeseries, half_k, n_times) + if detrend_type == 'mean': + detrended = Detrend.detrend_running_mean(timeseries, half_k, n_times) + elif detrend_type == 'savgol': + detrended = Detrend.detrend_savgol(timeseries, half_k, **kwargs) + else: + raise NameError('No valid detrending type specified') # Collect detrended timeseries for all parameters detrended_timeseries.append(detrended) From 475b939c2f3424f4487396aea4bcb99c784f485c Mon Sep 17 00:00:00 2001 From: billetd Date: Sat, 7 Feb 2026 21:41:48 +0100 Subject: [PATCH 09/30] Fixed docstrings. Updated polyorder handling. --- pydarn/utils/detrend.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/pydarn/utils/detrend.py b/pydarn/utils/detrend.py index cab4fa85..2f80a8b2 100644 --- a/pydarn/utils/detrend.py +++ b/pydarn/utils/detrend.py @@ -10,15 +10,17 @@ class Detrend: """ Methods ------- + detrend_savgol detrend_running_mean detrend_fitacf """ def __str__(self): return "This class is static class that provides"\ - " the following methods: \n"\ - " - detrend_running_mean()\n"\ - " - dentrend()\n"\ + " the following methods: \n" \ + " - detrend_savgol()\n" \ + " - detrend_running_mean()\n"\ + " - dentrend()\n"\ @classmethod @@ -63,7 +65,9 @@ def detrend_savgol(cls, # Generate filter series window_len = (half_k * 2) + 1 - if 'polyorder' not in kwargs: + try: + polyorder = kwargs['polyorder'] + except KeyError: polyorder = 2 background = savgol_filter(y_interp, window_length=window_len, polyorder=polyorder, **kwargs) From cfb8ca157a59515af0aee14cdca6fbae659ef143 Mon Sep 17 00:00:00 2001 From: billetd Date: Sat, 7 Feb 2026 21:48:26 +0100 Subject: [PATCH 10/30] Fix broken kwargs --- pydarn/utils/detrend.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pydarn/utils/detrend.py b/pydarn/utils/detrend.py index 2f80a8b2..0f105979 100644 --- a/pydarn/utils/detrend.py +++ b/pydarn/utils/detrend.py @@ -66,10 +66,10 @@ def detrend_savgol(cls, # Generate filter series window_len = (half_k * 2) + 1 try: - polyorder = kwargs['polyorder'] + kwargs['polyorder'] except KeyError: - polyorder = 2 - background = savgol_filter(y_interp, window_length=window_len, polyorder=polyorder, **kwargs) + kwargs['polyorder'] = 2 + background = savgol_filter(y_interp, window_length=window_len, **kwargs) # Detrend detrended_tmp = y_interp - background From 075206c6685fa294cbe37b130d073ef8087bfc3d Mon Sep 17 00:00:00 2001 From: Daniel Billett Date: Tue, 17 Feb 2026 12:33:23 -0600 Subject: [PATCH 11/30] Incorrect default fixed --- pydarn/utils/detrend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydarn/utils/detrend.py b/pydarn/utils/detrend.py index 0f105979..8e7eafd3 100644 --- a/pydarn/utils/detrend.py +++ b/pydarn/utils/detrend.py @@ -141,7 +141,7 @@ def detrend_running_mean(cls, @classmethod def detrend_fitacf(cls, fitacf_data: List[dict], - parameter: str = 'all', + parameter: str = 'both', window_length: int = 600, detrend_type: str='mean', **kwargs From 49d413fd471e1e1cee1ee05ce9c4b20b9946094a Mon Sep 17 00:00:00 2001 From: Carley Date: Wed, 18 Feb 2026 13:54:24 -0600 Subject: [PATCH 12/30] update to true velocity code --- pydarn/plotting/maps.py | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/pydarn/plotting/maps.py b/pydarn/plotting/maps.py index f8240595..f914c7d2 100644 --- a/pydarn/plotting/maps.py +++ b/pydarn/plotting/maps.py @@ -265,7 +265,6 @@ def plot_mapdata(cls, dmap_data: List[dict], ax=None, record]['fit.order'], lat_min=dmap_data[ record]['latmin']) - elif parameter == MapParams.MODEL_VELOCITY: v_mag = dmap_data[record]['model.vel.median'] azm_v = np.radians(dmap_data[record]['model.kvect']) @@ -287,8 +286,7 @@ def plot_mapdata(cls, dmap_data: List[dict], ax=None, fit_order=dmap_data[ record]['fit.order'], lat_min=dmap_data[ - record]['latmin'], - len_factor=len_factor) + record]['latmin']) v_mag, azm_v = cls.calculated_true_velocities(v_los, a_los, v_fit, a_fit) elif parameter == MapParams.POWER: @@ -645,26 +643,26 @@ def calculated_true_velocities(cls, v_los: list, a_los: list, a_fit: array angle of fitted velocity vector from magnetic north """ - # Reduce LOS vector to components normalized + # Reduce LOS vector to components tkvect = np.empty([2, len(a_los)]) - for j in range(0, len(a_los)): - tkvect[:,j] = [-np.cos(a_los[j]), np.sin(a_los[j])] + tkvect[0,:] = - v_los * np.cos(a_los) + tkvect[1,:] = v_los * np.sin(a_los) - # Get vector components - rvect = np.empty([2, len(v_fit)]) - rvect[0,:] = v_fit * np.cos(a_fit) + # Get fitted vector components + rvect = np.empty([2, len(a_fit)]) + rvect[0,:] = - v_fit * np.cos(a_fit) rvect[1,:] = v_fit * np.sin(a_fit) - # Get perpendicular vector components - vv = np.empty([2, len(v_fit)]) - vn = np.squeeze(np.sum(rvect * tkvect, axis=0)) - for i in range(0,len(vn)): - vv[:,i] = vn[i] * tkvect[:,i] - tvect = rvect - vv + # Get fitted component along the los direction + # (a.b / b.b) * b + vn = (np.sum(rvect * tkvect, axis=0) / np.sum(tkvect * tkvect, axis=0)) * tkvect + + # Perp is original vect - parallel + tvect = rvect - vn # Combine vectors for k in range(0,len(v_los)): - tvect[:,k] = tvect[:,k] + v_los[k] * tkvect[:,k] + tvect[:,k] = tvect[:,k] + tkvect[:,k] # Calculate the magnitude and azimuth v_true = np.sqrt(tvect[0,:]**2 + tvect[1,:]**2) From 6bc344ef4ff47c0fde86796cec9c72ce273e076c Mon Sep 17 00:00:00 2001 From: Carley Date: Mon, 23 Feb 2026 12:48:05 -0600 Subject: [PATCH 13/30] restrict scipy to >=1.17 --- pydarn/plotting/maps.py | 9 ++++++--- setup.cfg | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/pydarn/plotting/maps.py b/pydarn/plotting/maps.py index 00f6796c..7d671709 100644 --- a/pydarn/plotting/maps.py +++ b/pydarn/plotting/maps.py @@ -638,7 +638,8 @@ def calculated_fitted_velocities(cls, mlats: np.array, mlons: np.array, # i is the index of the list # x_i is the element of x at ith index for i, x_i in enumerate(x): - temp_poly = special.lpmn(fit_order, fit_order, x_i) + temp_poly = special.assoc_legendre_p_all(fit_order, fit_order, x_i) + temp_poly = ((temp_poly[0].T).tolist()[0:fit_order+1],) if i == 0: legendre_poly = np.append([temp_poly[0]], [temp_poly[0]], axis=0) @@ -973,7 +974,8 @@ def calculate_potentials(cls, fit_coefficient: list, lat_min: list, x = np.cos(alpha*theta) # Legendre Polys for j, xj in enumerate(x): - plm_tmp = special.lpmn(fit_order, fit_order, xj) + plm_tmp = special.assoc_legendre_p_all(fit_order, fit_order, xj) + plm_tmp = ((plm_tmp[0].T).tolist()[0:fit_order+1],) if j == 0: plm_fit = np.append([plm_tmp[0]], [plm_tmp[0]], axis=0) else: @@ -1075,7 +1077,8 @@ def calculate_potentials_pos(cls, mlat, mlon, fit_coefficient: list, # Legendre Polys for j, xj in enumerate(x): - plm_tmp = special.lpmn(fit_order, fit_order, xj) + plm_tmp = special.assoc_legendre_p_all(fit_order, fit_order, xj) + plm_tmp = ((plm_tmp[0].T).tolist()[0:fit_order+1],) if j == 0: plm_fit = np.append([plm_tmp[0]], [plm_tmp[0]], axis=0) else: diff --git a/setup.cfg b/setup.cfg index d887ac58..2c4da23f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -29,7 +29,7 @@ install_requires = matplotlib>=3.7.0 aacgmv2 pydarnio>=2.0.0 - scipy<1.15.0 + scipy>=1.17.0 cartopy>=0.22.0 [options.packages.find] From f11828e90c028b4f08333f25f93dcd8a3a31ec18 Mon Sep 17 00:00:00 2001 From: Carley Date: Mon, 23 Feb 2026 15:48:54 -0600 Subject: [PATCH 14/30] changing mag longitude to MLT in the plot_centre option as the MAG proj plots with MLT not mag longitude --- docs/user/axis.md | 2 +- pydarn/plotting/projections.py | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/user/axis.md b/docs/user/axis.md index 5488d26a..c1ac3f3c 100644 --- a/docs/user/axis.md +++ b/docs/user/axis.md @@ -83,7 +83,7 @@ plt.show() | coastline_linewidth=(float) | Uses Cartopy to set line width of the coastlines | | grid_lines=(bool) | Uses Cartopy to plot grid lines | | nightshade=(int) | Uses the value given to calculate and show where on the plot the Earth is in shadow | -| plot_center=[float,float] | Longitude and latitude of the desired center of the plot (e.g. [-90, 60]) | +| plot_center=[float,float] | MLT and latitude of the desired center of the plot (e.g. [6, 60]) | | plot_extent=[float,float] | Plotting extent in terms of percentage of the earth (e.g. [80,50]) | This choice will return an `ax` object and a Cartopy `ccrs` object (coordinate reference system). diff --git a/pydarn/plotting/projections.py b/pydarn/plotting/projections.py index 914b1f85..649244ca 100644 --- a/pydarn/plotting/projections.py +++ b/pydarn/plotting/projections.py @@ -123,7 +123,8 @@ def axis_geomagnetic(date, ax: axes.Axes = None, lowlat: int = 30, Setting this option will change the rotation and 12 MLT may no longer be at the bottom of the plot Default: None - Example: [-90, 60] will show the Earth centered on Canada + Example: [0, 75] will show the Earth centered on wherever 0 MLT + is and 75 degrees north plot_extent: list [float, float] Plotting extent in terms of a percentage of Earth shown in the x and y plotting field @@ -158,8 +159,11 @@ def axis_geomagnetic(date, ax: axes.Axes = None, lowlat: int = 30, else: # If the center of the plot is given- shift it around - lon = plot_center[0] + lon = plot_center[0] * 15 + # Given in MLT as this plot is plotted in mlt lat = plot_center[1] + #lon = (lon1 + (aacgmv2.convert_mlt(0, date, m2a=True)) * 15)[0] + #print(lon) if ax is None: proj = ccrs.Orthographic(lon, lat) ax = plt.axes(projection=proj) From f7569530d255f92c831b71f31b2b9d6012f7b0c2 Mon Sep 17 00:00:00 2001 From: Carley Date: Mon, 23 Feb 2026 15:54:25 -0600 Subject: [PATCH 15/30] tidy --- pydarn/plotting/projections.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pydarn/plotting/projections.py b/pydarn/plotting/projections.py index 649244ca..5f38d558 100644 --- a/pydarn/plotting/projections.py +++ b/pydarn/plotting/projections.py @@ -160,10 +160,8 @@ def axis_geomagnetic(date, ax: axes.Axes = None, lowlat: int = 30, else: # If the center of the plot is given- shift it around lon = plot_center[0] * 15 - # Given in MLT as this plot is plotted in mlt + # Given in MLT as this proj is plotted in mlt lat = plot_center[1] - #lon = (lon1 + (aacgmv2.convert_mlt(0, date, m2a=True)) * 15)[0] - #print(lon) if ax is None: proj = ccrs.Orthographic(lon, lat) ax = plt.axes(projection=proj) From eb9724bb7a3f1ec62eb8918ebfca8784bddb000a Mon Sep 17 00:00:00 2001 From: Carley Date: Tue, 24 Feb 2026 12:41:45 -0600 Subject: [PATCH 16/30] plot_tight option, centered FOV --- docs/user/fan.md | 4 ++ docs/user/fov.md | 5 ++ pydarn/plotting/fan.py | 106 ++++++++++++++++++++++++++++++++- pydarn/plotting/projections.py | 4 +- 4 files changed, 115 insertions(+), 4 deletions(-) diff --git a/docs/user/fan.md b/docs/user/fan.md index 818440b4..ca2d57af 100644 --- a/docs/user/fan.md +++ b/docs/user/fan.md @@ -134,6 +134,7 @@ Here is a list of all the current options than can be used with `plot_fan` | colorbar_label=(string) | Label that appears next to the color bar, requires colorbar to be True | | coastline=(bool) | Plots outlines of coastlines below data (Uses Cartopy) | | beam=(int) | Only plots data/outline of specified beam (default: None) | +| plot_tight=(bool)* | Centers the radars FOV in the plot and calculates extents based on FOV (default: False) | | kwargs ** | Axis Polar settings. See [polar axis](axis.md) | @@ -142,6 +143,9 @@ Here is a list of all the current options than can be used with `plot_fan` In other cases, the user may want to specify the channel and use an integer (N) for the `scan_index`. Be aware that this will show the data for the Nth scan of only the chosen channel, not that of the entire file. +!!! Note + * plot_tight option only works with MAG and GEO projections, plot_tight will overwrite plot_center and plot_extent options from axis setup + !!! Warning Not all data is designed to be plotted on a fan plot. Some CPID's, such as camping beam/themisscan, do not plot well due to overlapping beams in a single scan. It is up to the user to interpret the suitability of the plotting method used. diff --git a/docs/user/fov.md b/docs/user/fov.md index 4e1b9f3b..e118acb1 100644 --- a/docs/user/fov.md +++ b/docs/user/fov.md @@ -60,8 +60,13 @@ Here is a list of all the current options than can be used with `plot_fov` | radar_label=(bool) | Places the radar 3-letter abbreviation next to the radar location | | coastline=(bool) | Plots outlines of coastlines below FOV (Uses Cartopy) | | beam=(int) | Only plots outline/fill of specified beam (default: None) | +| plot_tight=(bool)* | Centers the radars FOV in the plot and calculates extents based on FOV (default: False) | | kwargs ** | Axis Polar settings. See [polar axis](axis.md) | +!!! Note + * plot_tight option only works with MAG and GEO projections, plot_tight will overwrite plot_center and plot_extent options from axis setup + + ### Examples diff --git a/pydarn/plotting/fan.py b/pydarn/plotting/fan.py index 4c5cfe08..82c85d83 100644 --- a/pydarn/plotting/fan.py +++ b/pydarn/plotting/fan.py @@ -27,6 +27,8 @@ # 2023-06-28: CJM - Refactored return values # 2023-10-14: CJM - Add embargoed data method # 2024-10-09: DDB - Control marker and its size in plot_radar_position() +# 2026-02-24: CJM - Added the optiont of plot_tight and corresponding +# private method # # Disclaimer: # pyDARN is under the LGPL v3 license found in the root directory LICENSE.md @@ -91,7 +93,8 @@ def plot_fan(dmap_data: List[dict], ax=None, ranges=None, projs: Projs = Projs.POLAR, coords: Coords = Coords.AACGM_MLT, channel: int = 'all', ball_and_stick: bool = False, - len_factor: float = 300, beam: int = None, **kwargs): + len_factor: float = 300, beam: int = None, + plot_tight: bool = False, **kwargs): """ Plots a radar's Field Of View (FOV) fan plot for the given data and scan number @@ -172,6 +175,9 @@ def plot_fan(dmap_data: List[dict], ax=None, ranges=None, len_factor : float control the length of the ball and stick plot stick length Default : 300 + plot_tight: boolean + if True, FOV is centered in the plot and zoomed in to fill + Default: False kwargs: key = value Additional keyword arguments to be used in projection plotting and plot_fov for possible keywords, see: projections.axis_polar @@ -330,6 +336,12 @@ def plot_fan(dmap_data: List[dict], ax=None, ranges=None, stid = RadarID(dmap_data[0]['stid']) kwargs['hemisphere'] = SuperDARNRadars.radars[stid].hemisphere + # Set plot center and plot extent if plot tight is given: + if plot_tight and projs != Projs.POLAR: + kwargs = Fan.__calculate_tight_layout(beam_corners_lats, + beam_corners_lons, + projs, **kwargs) + ax, ccrs = projs(date=date, ax=ax, **kwargs) if ccrs is None: @@ -499,7 +511,8 @@ def plot_fan_input(data_array: list = [], data_datetime: dt.datetime = [], zmax: int = None, colorbar: bool = True, colorbar_label: str = '', cax=None, boundary: bool = True, projs: Projs = Projs.POLAR, - coords: Coords = Coords.AACGM_MLT, **kwargs): + coords: Coords = Coords.AACGM_MLT, + plot_tight: bool =False, **kwargs): """ Plots a radar's Field Of View (FOV) fan plot for the given data and scan number @@ -558,6 +571,9 @@ def plot_fan_input(data_array: list = [], data_datetime: dt.datetime = [], coords: Enum choice of plotting coordinates default: Coords.AACGM_MLT (Magnetic Lat and MLT) + plot_tight: boolean + if True, FOV is centered in the plot and zoomed in to fill + Default: False kwargs: key = value Additional keyword arguments to be used in projection plotting and plot_fov for possible keywords, see: projections.axis_polar @@ -624,6 +640,11 @@ def plot_fan_input(data_array: list = [], data_datetime: dt.datetime = [], norm = norm(zmin, zmax) kwargs['hemisphere'] = SuperDARNRadars.radars[stid].hemisphere + # Set plot center and plot extent if plot tight is given: + if plot_tight and projs != Projs.POLAR: + kwargs = Fan.__calculate_tight_layout(beam_corners_lats, + beam_corners_lons, + projs, **kwargs) ax, ccrs = projs(date=data_datetime, ax=ax, **kwargs) if ccrs is None: @@ -697,7 +718,8 @@ def plot_fov(stid: RadarID, date: dt.datetime, radar_location: bool = True, radar_label: bool = False, line_color: str = 'black', grid: bool = False, beam: int = None, - line_alpha: float = 0.5, **kwargs): + line_alpha: float = 0.5, plot_tight: bool = False, + **kwargs): """ plots only the field of view (FOV) for a given radar station ID (stid) @@ -754,6 +776,9 @@ def plot_fov(stid: RadarID, date: dt.datetime, radar_label: bool Add a label with the radar abbreviation if True Default: False + plot_tight: boolean + if True, FOV is centered in the plot and zoomed in to fill + Default: False kwargs: key = value Additional keyword arguments to be used in projection plotting For possible keywords, see: projections.axis_polar @@ -797,6 +822,11 @@ def plot_fov(stid: RadarID, date: dt.datetime, if ax is None: # Get the hemisphere to pass to plotting projection kwargs['hemisphere'] = hemisphere + # Set plot center and plot extent if plot tight is given: + if plot_tight and projs != Projs.POLAR: + kwargs = Fan.__calculate_tight_layout(beam_corners_lats, + beam_corners_lons, + projs, **kwargs) ax, ccrs = projs(date=date, **kwargs) if ccrs is None: transform = ax.transData @@ -1077,3 +1107,73 @@ def __add_title__(first_timestamp: dt.datetime, zfill(2), end_second=str(end_timestamp.second).zfill(2)) return title + + @staticmethod + def __calculate_tight_layout(beam_corners_lats: list, + beam_corners_lons: list, + projs, **kwargs): + """ + Calculates the plot_center and plot_extent values needed + to center the FOV in the middle of the plot + + Overwrites any given for these values and only works for + projs MAG and GEO. + + Parameters + ----------- + beam_corners_lats: list + indices of the beams and gates positions: lat + beam_corners_lons: list + indices of the beams and gates positions: lon + projs: Projs object + + Returns + ------- + kwargs: amended kwargs dictionary, including plot_center + and plot_extent + """ + # Three corners of the fov + c1 = [np.radians(beam_corners_lats[-1, -1]), + np.radians(beam_corners_lons[-1, -1])] + c2 = [np.radians(beam_corners_lats[-1, 0]), + np.radians(beam_corners_lons[-1, 0])] + c3 = [np.radians(beam_corners_lats[0, 0]), + np.radians(beam_corners_lons[0, 0])] + + # Calculate center of FOV + x1 = np.cos(c1[0]) * np.cos(c1[1]) + x2 = np.cos(c2[0]) * np.cos(c2[1]) + x3 = np.cos(c3[0]) * np.cos(c3[1]) + y1 = np.cos(c1[0]) * np.sin(c1[1]) + y2 = np.cos(c2[0]) * np.sin(c2[1]) + y3 = np.cos(c3[0]) * np.sin(c3[1]) + z1 = np.sin(c1[0]) + z2 = np.sin(c2[0]) + z3 = np.sin(c3[0]) + + x_av = np.mean([x1,x2,x3]) + y_av = np.mean([y1,y2,y3]) + z_av = np.mean([z1,z2,z3]) + + lat = np.atan2(z_av, np.sqrt(x_av**2 + y_av**2)) + lon = np.atan2(y_av, x_av) + if projs == Projs.GEO: + plot_center = [np.degrees(lon), np.degrees(lat)] + elif projs == Projs.MAG: + plot_center = [np.degrees(lon)/15, np.degrees(lat)] + + # Farthest point from the center + c4 = [lat,lon] + d1 = np.acos((np.sin(c1[0]) * np.sin(c4[0])) + + (np.cos(c1[0]) * np.cos(c4[0]) * np.cos(c1[1]- c4[1]))) + d2 = np.acos((np.sin(c2[0]) * np.sin(c4[0])) + + (np.cos(c2[0]) * np.cos(c4[0]) * np.cos(c2[1]- c4[1]))) + d3 = np.acos((np.sin(c3[0]) * np.sin(c4[0])) + + (np.cos(c3[0]) * np.cos(c4[0]) * np.cos(c3[1]- c4[1]))) + d4 = max([d1,d2,d3]) * 100 + + plot_extent = [d4+5,d4+5] + + kwargs['plot_center'] = plot_center + kwargs['plot_extent'] = plot_extent + return kwargs diff --git a/pydarn/plotting/projections.py b/pydarn/plotting/projections.py index 5f38d558..72d2ba40 100644 --- a/pydarn/plotting/projections.py +++ b/pydarn/plotting/projections.py @@ -34,7 +34,9 @@ import matplotlib.ticker as mticker import numpy as np -from pydarn import (Hemisphere, Re, nightshade_warning) +from typing import Union +from pydarn import (Hemisphere, Re, nightshade_warning, SuperDARNRadars, + RadarID) def convert_geo_coastline_to_mag(geom, date, alt: float = 0.0, mag_lon: bool = False): From 2ab4bdc668f073e447f58f74c2ab4cad0a714e83 Mon Sep 17 00:00:00 2001 From: Carley Date: Tue, 24 Feb 2026 12:56:36 -0600 Subject: [PATCH 17/30] amend docs --- docs/user/axis.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user/axis.md b/docs/user/axis.md index c1ac3f3c..9c986478 100644 --- a/docs/user/axis.md +++ b/docs/user/axis.md @@ -83,7 +83,7 @@ plt.show() | coastline_linewidth=(float) | Uses Cartopy to set line width of the coastlines | | grid_lines=(bool) | Uses Cartopy to plot grid lines | | nightshade=(int) | Uses the value given to calculate and show where on the plot the Earth is in shadow | -| plot_center=[float,float] | MLT and latitude of the desired center of the plot (e.g. [6, 60]) | +| plot_center=[float,float] | MLT(MAG)/Longitude(GEO) and latitude of the desired center of the plot (e.g. [6, 60]) | | plot_extent=[float,float] | Plotting extent in terms of percentage of the earth (e.g. [80,50]) | This choice will return an `ax` object and a Cartopy `ccrs` object (coordinate reference system). From 29057729117b467038a833c2307a026f091889d1 Mon Sep 17 00:00:00 2001 From: Carley Date: Tue, 24 Feb 2026 13:15:46 -0600 Subject: [PATCH 18/30] flake 8 and ruff fix --- pydarn/plotting/fan.py | 67 ++++++++++++++++-------------- pydarn/plotting/projections.py | 74 ++++++++++++++++++---------------- 2 files changed, 77 insertions(+), 64 deletions(-) diff --git a/pydarn/plotting/fan.py b/pydarn/plotting/fan.py index 82c85d83..b49171da 100644 --- a/pydarn/plotting/fan.py +++ b/pydarn/plotting/fan.py @@ -22,7 +22,6 @@ # as this has yet to be figured out # 2023-02-06: CJM - Added option to plot single beams in a scan or FOV diagram # 2023-03-01: CJM - Added ball and stick plotting options -# 2023-03-01: CJM - Added ball and stick plotting options (merged later in year) # 2023-08-16: CJM - Corrected for winding order in geo plots # 2023-06-28: CJM - Refactored return values # 2023-10-14: CJM - Add embargoed data method @@ -147,7 +146,8 @@ def plot_fan(dmap_data: List[dict], ax=None, ranges=None, Requires colorbar to be true Default: '' cax: axes.Axes - Pre-defined axis for the colorbar. If not specified and colorbar + Pre-defined axis for the colorbar. + If not specified and colorbar is True, a new axis will be created. Default: None title: bool @@ -341,7 +341,7 @@ def plot_fan(dmap_data: List[dict], ax=None, ranges=None, kwargs = Fan.__calculate_tight_layout(beam_corners_lats, beam_corners_lons, projs, **kwargs) - + ax, ccrs = projs(date=date, ax=ax, **kwargs) if ccrs is None: @@ -429,9 +429,9 @@ def plot_fan(dmap_data: List[dict], ax=None, ranges=None, end_thetas = np.degrees(end_thetas) # Plot sticks! ax.plot([t_center, end_thetas], - [r_center, end_rs], - color=col, zorder=3.0, linewidth=0.5, - transform=transform) + [r_center, end_rs], + color=col, zorder=3.0, linewidth=0.5, + transform=transform) # Plot ground scatter balls (no sticks) if groundscatter and grndsct[i, j] != 0.0: @@ -502,17 +502,19 @@ def plot_fan(dmap_data: List[dict], ax=None, ranges=None, 'ground_scatter': grndsct} } - @staticmethod def plot_fan_input(data_array: list = [], data_datetime: dt.datetime = [], - ax: object = None, stid: RadarID = None, data_groundscatter: list = [], + ax: object = None, stid: RadarID = None, + data_groundscatter: list = [], rsep: int = 45, frang: int = 180, - data_parameter: str = 'v', cmap: str = None, zmin: int = None, + data_parameter: str = 'v', cmap: str = None, + zmin: int = None, zmax: int = None, colorbar: bool = True, - colorbar_label: str = '', cax=None, boundary: bool = True, + colorbar_label: str = '', cax=None, + boundary: bool = True, projs: Projs = Projs.POLAR, coords: Coords = Coords.AACGM_MLT, - plot_tight: bool =False, **kwargs): + plot_tight: bool = False, **kwargs): """ Plots a radar's Field Of View (FOV) fan plot for the given data and scan number @@ -560,7 +562,8 @@ def plot_fan_input(data_array: list = [], data_datetime: dt.datetime = [], Requires colorbar to be true Default: '' cax: axes.Axes - Pre-defined axis for the colorbar. If not specified and colorbar + Pre-defined axis for the colorbar. + If not specified and colorbar is True, a new axis will be created. boundary: bool if true then plots the FOV boundaries @@ -710,7 +713,8 @@ def plot_fan_input(data_array: list = [], data_datetime: dt.datetime = [], @staticmethod def plot_fov(stid: RadarID, date: dt.datetime, - ax=None, ccrs=None, ranges: List = None, boundary: bool = True, + ax=None, ccrs=None, ranges: List = None, + boundary: bool = True, rsep: int = 45, frang: int = 180, projs: Projs = Projs.POLAR, coords: Coords = Coords.AACGM_MLT, @@ -1009,9 +1013,11 @@ def plot_radar_position(stid: RadarID, ax: axes.Axes, marker: str Controls which symbol is plotted. Default: "." - See https://matplotlib.org/stable/api/markers_api.html#module-matplotlib.markers for options + See https://matplotlib.org/stable/api/markers_api.html#module-matplotlib.markers + for options markersize: int - Controls the size of the symbol plotted, "s" passed to ax.scatter(). + Controls the size of the symbol plotted, + "s" passed to ax.scatter(). Default: 5 Returns @@ -1033,7 +1039,8 @@ def plot_radar_position(stid: RadarID, ax: axes.Axes, if projs == Projs.POLAR: lon = np.radians(lon) # Plot a dot at the radar site - ax.scatter(lon, lat, c=line_color, s=markersize, transform=transform, marker=marker) + ax.scatter(lon, lat, c=line_color, s=markersize, + transform=transform, marker=marker) return @staticmethod @@ -1116,7 +1123,7 @@ def __calculate_tight_layout(beam_corners_lats: list, Calculates the plot_center and plot_extent values needed to center the FOV in the middle of the plot - Overwrites any given for these values and only works for + Overwrites any given for these values and only works for projs MAG and GEO. Parameters @@ -1134,11 +1141,11 @@ def __calculate_tight_layout(beam_corners_lats: list, """ # Three corners of the fov c1 = [np.radians(beam_corners_lats[-1, -1]), - np.radians(beam_corners_lons[-1, -1])] + np.radians(beam_corners_lons[-1, -1])] c2 = [np.radians(beam_corners_lats[-1, 0]), - np.radians(beam_corners_lons[-1, 0])] + np.radians(beam_corners_lons[-1, 0])] c3 = [np.radians(beam_corners_lats[0, 0]), - np.radians(beam_corners_lons[0, 0])] + np.radians(beam_corners_lons[0, 0])] # Calculate center of FOV x1 = np.cos(c1[0]) * np.cos(c1[1]) @@ -1151,9 +1158,9 @@ def __calculate_tight_layout(beam_corners_lats: list, z2 = np.sin(c2[0]) z3 = np.sin(c3[0]) - x_av = np.mean([x1,x2,x3]) - y_av = np.mean([y1,y2,y3]) - z_av = np.mean([z1,z2,z3]) + x_av = np.mean([x1, x2, x3]) + y_av = np.mean([y1, y2, y3]) + z_av = np.mean([z1, z2, z3]) lat = np.atan2(z_av, np.sqrt(x_av**2 + y_av**2)) lon = np.atan2(y_av, x_av) @@ -1163,17 +1170,17 @@ def __calculate_tight_layout(beam_corners_lats: list, plot_center = [np.degrees(lon)/15, np.degrees(lat)] # Farthest point from the center - c4 = [lat,lon] + c4 = [lat, lon] d1 = np.acos((np.sin(c1[0]) * np.sin(c4[0])) + - (np.cos(c1[0]) * np.cos(c4[0]) * np.cos(c1[1]- c4[1]))) + (np.cos(c1[0]) * np.cos(c4[0]) * np.cos(c1[1] - c4[1]))) d2 = np.acos((np.sin(c2[0]) * np.sin(c4[0])) + - (np.cos(c2[0]) * np.cos(c4[0]) * np.cos(c2[1]- c4[1]))) + (np.cos(c2[0]) * np.cos(c4[0]) * np.cos(c2[1] - c4[1]))) d3 = np.acos((np.sin(c3[0]) * np.sin(c4[0])) + - (np.cos(c3[0]) * np.cos(c4[0]) * np.cos(c3[1]- c4[1]))) - d4 = max([d1,d2,d3]) * 100 + (np.cos(c3[0]) * np.cos(c4[0]) * np.cos(c3[1] - c4[1]))) + d4 = max([d1, d2, d3]) * 100 - plot_extent = [d4+5,d4+5] + plot_extent = [d4 + 5, d4 + 5] - kwargs['plot_center'] = plot_center + kwargs['plot_center'] = plot_center kwargs['plot_extent'] = plot_extent return kwargs diff --git a/pydarn/plotting/projections.py b/pydarn/plotting/projections.py index 72d2ba40..237934d5 100644 --- a/pydarn/plotting/projections.py +++ b/pydarn/plotting/projections.py @@ -9,7 +9,7 @@ # 2022-06-13 Elliott Day don't create new ax if ax passed in to Projs # 2023-02-22 CJM added options for nightshade # 2023-08-11 RR added crude check for unmodified axes, handle both hemispheres -# 2024-05-15 CJM refactored geographic axes to add plot zoom and center, +# 2024-05-15 CJM refactored geographic axes to add plot zoom and center, # and added the geomagnetic version to do the same # 2024-07-10 CJM removed cartopy logic to allow full dependency # @@ -34,12 +34,11 @@ import matplotlib.ticker as mticker import numpy as np -from typing import Union -from pydarn import (Hemisphere, Re, nightshade_warning, SuperDARNRadars, - RadarID) +from pydarn import (Hemisphere, Re, nightshade_warning) -def convert_geo_coastline_to_mag(geom, date, alt: float = 0.0, mag_lon: bool = False): +def convert_geo_coastline_to_mag(geom, date, alt: float = 0.0, + mag_lon: bool = False): """ Takes the geometry object of coastlines and converts the coordinates into AACGM_MLT @@ -59,7 +58,7 @@ def convert_geo_coastline_to_mag(geom, date, alt: float = 0.0, mag_lon: bool = F """ [mlats, lon_mag, _] = \ aacgmv2.convert_latlon_arr(geom.coords.xy[1], geom.coords.xy[0], alt, - date, method_code='G2A') + date, method_code='G2A') # Finds the first not nan value to calculate the mlt shift # Substitutes NaN if not found which results in no data to plot @@ -68,8 +67,8 @@ def convert_geo_coastline_to_mag(geom, date, alt: float = 0.0, mag_lon: bool = F notnan_lon = next((x for x in lon_mag if x == x), float('NaN')) # Shift to MLT - shifted_mlts = notnan_lon - \ - (aacgmv2.convert_mlt(notnan_lon, date) * 15) + shifted_mlts = notnan_lon - (aacgmv2.convert_mlt(notnan_lon, + date) * 15) shifted_lons = lon_mag - shifted_mlts if mag_lon: @@ -81,19 +80,19 @@ def convert_geo_coastline_to_mag(geom, date, alt: float = 0.0, mag_lon: bool = F def axis_geomagnetic(date, ax: axes.Axes = None, lowlat: int = 30, - hemisphere: Hemisphere = Hemisphere.North, - coastline: bool = False, cartopy_scale: str = '110m', - coastline_color: str = 'k', - coastline_linewidth: float = 0.5, - nightshade: int = 0, grid_lines: bool = True, - plot_center: list = None, - plot_extent: list = None, **kwargs): + hemisphere: Hemisphere = Hemisphere.North, + coastline: bool = False, cartopy_scale: str = '110m', + coastline_color: str = 'k', + coastline_linewidth: float = 0.5, + nightshade: int = 0, grid_lines: bool = True, + plot_center: list = None, + plot_extent: list = None, **kwargs): """ Sets up the cartopy orthographic plot axis object, for use in various other functions. This plot assumes you are giving geoMAGNETIC values for plotting, so extra additional cartopy functions such as coastlines do not work in this context, you must convert to geomagnetic. - + Parameters ---------- date: datetime object @@ -129,10 +128,10 @@ def axis_geomagnetic(date, ax: axes.Axes = None, lowlat: int = 30, is and 75 degrees north plot_extent: list [float, float] Plotting extent in terms of a percentage of Earth shown in - the x and y plotting field + the x and y plotting field Default: None Example: [30, 50] shows a plot centered on the pole or specified - plot_center coord that shows 30% of the Earth in x and + plot_center coord that shows 30% of the Earth in x and 50% of the Earth in y. See tutorials for plotted example. """ if plot_center is None: @@ -156,12 +155,14 @@ def axis_geomagnetic(date, ax: axes.Axes = None, lowlat: int = 30, pady = (plot_extent[1] / 100) * Re*1000 ax.set_extent(extents=(-padx, padx, -pady, pady), crs=proj) else: - extent = abs(proj.transform_point(lon, lat, ccrs.PlateCarree())[1]) - ax.set_extent(extents=(-extent, extent, -extent, extent), crs=proj) + extent = abs(proj.transform_point(lon, lat, + ccrs.PlateCarree())[1]) + ax.set_extent(extents=(-extent, extent, -extent, extent), + crs=proj) else: # If the center of the plot is given- shift it around - lon = plot_center[0] * 15 + lon = plot_center[0] * 15 # Given in MLT as this proj is plotted in mlt lat = plot_center[1] if ax is None: @@ -217,9 +218,11 @@ def mlt_ticklabels(x, pos): for geom in cc.geometries(): if geom.__class__.__name__ == 'MultiLineString': for g in geom.geoms: - geom_mag.append(convert_geo_coastline_to_mag(g, date, mag_lon=True)) + geom_mag.append( + convert_geo_coastline_to_mag(g, date, mag_lon=True)) else: - geom_mag.append(convert_geo_coastline_to_mag(geom, date, mag_lon=True)) + geom_mag.append( + convert_geo_coastline_to_mag(geom, date, mag_lon=True)) cc_mag = cfeature.ShapelyFeature(geom_mag, ccrs.Geodetic(), color='k', zorder=2.0) @@ -227,7 +230,7 @@ def mlt_ticklabels(x, pos): for geom in cc_mag.geometries(): plt.plot(*geom.coords.xy, color=coastline_color, linewidth=coastline_linewidth, zorder=2.0, - transform = ccrs.Geodetic()) + transform=ccrs.Geodetic()) if nightshade: nightshade_warning() @@ -236,18 +239,19 @@ def mlt_ticklabels(x, pos): def axis_geomagnetic_polar(date, ax: axes.Axes = None, lowlat: int = 30, - hemisphere: Hemisphere = Hemisphere.North, - coastline: bool = False, cartopy_scale: str = '110m', - coastline_color: str = 'k', - coastline_linewidth: float = 0.5, - nightshade: int = 0, **kwargs): + hemisphere: Hemisphere = Hemisphere.North, + coastline: bool = False, + cartopy_scale: str = '110m', + coastline_color: str = 'k', + coastline_linewidth: float = 0.5, + nightshade: int = 0, **kwargs): """ Sets up the polar plot matplotlib axis object, for use in various other functions. Magnetic latitude - magnetic local time projection. This projection is defunct now MAG exists, however is kept available for - compatibility and non-cartopy options. + compatibility and non-cartopy options. Parameters ----------- @@ -395,10 +399,10 @@ def axis_geographic(date, ax: axes.Axes = None, Example: [-90, 60] will show the Earth centered on Canada plot_extent: list [float, float] Plotting extent in terms of a percentage of Earth shown in - the x and y plotting field + the x and y plotting field Default: None Example: [30, 50] shows a plot centered on the pole or specified - plot_center coord that shows 30% of the Earth in x and + plot_center coord that shows 30% of the Earth in x and 50% of the Earth in y. See tutorials for plotted example. """ if plot_center is None: @@ -421,8 +425,10 @@ def axis_geographic(date, ax: axes.Axes = None, pady = (plot_extent[1] / 100) * Re*1000 ax.set_extent(extents=(-padx, padx, -pady, pady), crs=proj) else: - extent = abs(proj.transform_point(lon, lat, ccrs.PlateCarree())[1]) - ax.set_extent(extents=(-extent, extent, -extent, extent), crs=proj) + extent = abs(proj.transform_point(lon, lat, + ccrs.PlateCarree())[1]) + ax.set_extent(extents=(-extent, extent, -extent, extent), + crs=proj) else: # If the center of the plot is given- shift it around lon = plot_center[0] From b096f278bf7904fd68ea408c01ebec8540fd0daf Mon Sep 17 00:00:00 2001 From: Wtristen Date: Wed, 4 Mar 2026 19:46:50 -0500 Subject: [PATCH 19/30] Modified get_hdw_files and read_hdw_files for file handling and portability --- pydarn/utils/superdarn_radars.py | 346 ++++++++++++++++--------------- 1 file changed, 184 insertions(+), 162 deletions(-) diff --git a/pydarn/utils/superdarn_radars.py b/pydarn/utils/superdarn_radars.py index dfbbff8a..664bcc44 100644 --- a/pydarn/utils/superdarn_radars.py +++ b/pydarn/utils/superdarn_radars.py @@ -26,8 +26,11 @@ from typing import NamedTuple from enum import Enum from datetime import datetime -from subprocess import check_call +#from subprocess import check_call +#new imports +from urllib import request +from zipfile import ZipFile def get_hdw_files(force: bool = True, version: str = None): """ @@ -46,35 +49,42 @@ def get_hdw_files(force: bool = True, version: str = None): have yet to be versioned. """ - # Path should the path where pydarn is installed - hdw_path = "{}/hdw/".format(os.path.dirname(pydarn.utils.__file__)) - + # Path should the path where pydarn utils are located, joined with 'hdw'. + hdw_path = os.path.join(os.path.dirname(pydarn.utils.__file__), "hdw") if not os.path.exists(hdw_path): os.makedirs(hdw_path) # TODO: implement when DSWG starts versioning hardware files if version is not None: - raise Exception("This feature is not implemented yet") + raise NotImplementedError("This feature is not implemented, Versioned hardware does not exist yet.") #clarified error # if there is no files in hdw folder or force is true # download the hdw files if len(os.listdir(hdw_path)) == 0 or force: - # pycurl doesn't download a zip folder easily so - # use the command line command - check_call(['curl', '-L', '-o', hdw_path+'/main.zip', - 'https://github.com/SuperDARN/hdw/archive/main.zip']) - # use unzip command because zipfile on works with files and not folders - # though this is possible with zipfile but this was easier for me to - # get it working - check_call(['unzip', '-d', hdw_path, hdw_path+'/main.zip']) - dat_files = glob.glob(hdw_path+'/hdw-main/*') - # shutil only moves specific files so we need to move - # everything one at a time - for hdw_file in dat_files: - shutil.move(hdw_file, hdw_path+os.path.basename(hdw_file)) - # delete the empty folder - os.removedirs(hdw_path+'/hdw-main/') - + # Define URL and temporary file paths + url = 'https://github.com/SuperDARN/hdw/archive/main.zip' + zip_path = os.path.join(hdw_path, 'main.zip') + unzip_dir_name = 'hdw-main' # The unzipped folder will have this name + unzip_dir_path = os.path.join(hdw_path, unzip_dir_name) + try: + # Download the file using Python's urllib + request.urlretrieve(url, zip_path) + + # Extract the zip file using Python's zipfile module + with ZipFile(zip_path, 'r') as zip_ref: + zip_ref.extractall(hdw_path) + + # Move files from the subdirectory to the parent hdw_path + source_dir = os.path.join(hdw_path, unzip_dir_name) + files_to_move = glob.glob(os.path.join(source_dir, '*')) + for file_path in files_to_move: + shutil.move(file_path, hdw_path) + finally: + # Clean up the downloaded zip file and the empty subdirectory + if os.path.exists(zip_path): + os.remove(zip_path) + if os.path.exists(unzip_dir_path): + shutil.rmtree(unzip_dir_path, ignore_errors=True) def read_hdw_file(abbrv, date: datetime = None, update: bool = False): """ @@ -104,151 +114,163 @@ def read_hdw_file(abbrv, date: datetime = None, update: bool = False): if date is None: date = datetime.now() - hdw_path = os.path.dirname(__file__)+'/hdw/' - hdw_file = "{path}/hdw.dat.{radar}".format(path=hdw_path, radar=abbrv) - hdw_data = [] - hdw_lines_date = [] - # if the file does not exist then try - # and download it - if os.path.exists(hdw_file) is False: - get_hdw_files(force=update) + hdw_path = os.path.join(os.path.dirname(__file__), 'hdw') + hdw_file = os.path.join(hdw_path, f"hdw.dat.{abbrv}") + + if update: + get_hdw_files(force=True) + try: with open(hdw_file, 'r') as reader: lines = reader.readlines() - for i in range(len(lines)): - if '#' not in lines[i] and len(lines[i].split()) > 1: - hdw_data.append(lines[i].split()) - """ - Hardware files give the year and seconds from the beginning - of that year. Thus to check the date if it corresponds we - need to convert to a datetime object and then compare. - """ - j = len(hdw_data)-1 - hdw_lines_date.append( - datetime(year=int(hdw_data[j][2][0:4]), - month=int(hdw_data[j][2][4:6]), - day=int(hdw_data[j][2][6:8]), - hour=int(hdw_data[j][3][0:2]), - minute=int(hdw_data[j][3][3:5]), - second=int(hdw_data[j][3][6:8]))) - if hdw_lines_date[j] > date: - j = j-1 - break - """ - Hardware data array positions definitions: - 0: Station ID (unique numerical value). - 1: Status code (1 operational, -1 offline). - 2: First date that parameter string is valid - (YYYYMMDD). - 3: First time that parameter string is valid - (HH:MM:SS). - 4: Geographic latitude of radar site - (Given in decimal degrees to 3 - decimal places. Southern hemisphere - values are negative) - 5: Geographic longitude of radar site - (Given in decimal degrees to - 3 decimal places. - West longitude values are negative) - 6: Altitude of the radar site (meters) - 7: Physical scanning boresight - (Direction of the center beam, measured in - degrees relative to geographic north. - CCW rotations are negative.) - 8: Electronic shift to radar scanning - boresight (Degrees relative to - physical antenna boresight. - Normally 0.0 degrees) - 9: Beam separation (Angular - separation in degrees between adjacent - beams. Normally 3.24 degrees) - 10: Velocity sign (At the radar level, - backscattered signals with - frequencies above the transmitted - frequency are assigned positive - Doppler velocities while backscattered - signals with frequencies below - the transmitted frequency are assigned - negative Doppler velocity. This - convention can be reversed by changes - in receiver design or in the - data sampling rate. This parameter - is set to +1 or -1 to maintain the - convention.) - 11: Phase sign (Cabling errors can - lead to a 180 degree shift of the - interferometry phase measurement. - +1 indicates that the sign is - correct, -1 indicates that it must be flipped.) - 12: Tdiff [Channel A] - (Propagation time from interferometer - array antenna to phasing matrix input - minus propagation time from main array antenna - through transmitter to phasing matrix input. - Units are decimal - microseconds) - 13: Tdiff [Channel B] - (Propagation time from interferometer - array antenna to phasing matrix input minus - propagation time from main array antenna - through transmitter to phasing matrix input. - Units are decimal microseconds) - 14: Interferometer X offset - (Displacement of midpoint of interferometer - array from midpoint of main array, - along the line of antennas - with +X toward higher antenna numbers. - Units are meters) - 15: Interferometer Y offset - (Displacement of midpoint of - interferometer array from midpoint of - main array, along the array - normal direction with +Y in the direction of - the array normal. Units are meters) - 16: Interferometer Z offset - (Displacement of midpoint of - interferometer array from midpoint of - main array, in terms of altitude - difference with +Z up. Units are meters) - 17: Analog Rx rise time - (Time given in microseconds. Time delays of - less than ~10 microseconds can be ignored. - If narrow-band filters are - used in analog receivers or front-ends, - the time delays should be - specified.) - 18: Analog Rx attenuator step (dB) - 19: Analog attenuation stages (Number of stages. - This is used for gain control of an analog - receiver or front-end.) - 20: Maximum of range gates used - 21: Maximum number of beams - """ - return _HdwInfo(stid=int(hdw_data[j][0]), - status=Status(int(hdw_data[j][1])), - abbrev=abbrv, - date=hdw_lines_date[j], - geographic=_Coord(float(hdw_data[j][4]), - float(hdw_data[j][5]), - float(hdw_data[j][6])), - boresight=_Boresight(float(hdw_data[j][7]), - float(hdw_data[j][8])), - beam_separation=float(hdw_data[j][9]), - velocity_sign=float(hdw_data[j][10]), - phase_sign=float(hdw_data[j][11]), - tdiff=_Tdiff(float(hdw_data[j][12]), - float(hdw_data[j][13])), - interferometer_offset=_InterferometerOffset( - float(hdw_data[j][14]), - float(hdw_data[j][15]), - float(hdw_data[j][16])), - rx_rise_time=float(hdw_data[j][17]), - rx_attenuator=float(hdw_data[j][18]), - attenuation_stages=int(hdw_data[j][19]), - gates=int(hdw_data[j][20]), - beams=int(hdw_data[j][21])) except FileNotFoundError: - raise pydarn.radar_exceptions.HardwareFileNotFoundError(abbrv) + print(f"Hardware file for '{abbrv}' not found. Attempting download...") + get_hdw_files(force=True) + try: + with open(hdw_file, 'r') as reader: + lines = reader.readlines() + except FileNotFoundError: + raise pydarn.radar_exceptions.HardwareFileNotFoundError(abbrv) + + hdw_data = [] + hdw_lines_date = [] + j = -1 + + for i in range(len(lines)): + if '#' not in lines[i] and len(lines[i].split()) > 1: + hdw_data.append(lines[i].split()) + current_j = len(hdw_data) - 1 + current_date = datetime( + year=int(hdw_data[current_j][2][0:4]), + month=int(hdw_data[current_j][2][4:6]), + day=int(hdw_data[current_j][2][6:8]), + hour=int(hdw_data[current_j][3][0:2]), + minute=int(hdw_data[current_j][3][3:5]), + second=int(hdw_data[current_j][3][6:8]) + ) + hdw_lines_date.append(current_date) + if current_date > date: + break + j = current_j + + if j == -1: + if hdw_data: + j = 0 + else: + raise pydarn.radar_exceptions.HardwareFileNotFoundError(abbrv) + """ + Hardware data array positions definitions: + 0: Station ID (unique numerical value). + 1: Status code (1 operational, -1 offline). + 2: First date that parameter string is valid + (YYYYMMDD). + 3: First time that parameter string is valid + (HH:MM:SS). + 4: Geographic latitude of radar site + (Given in decimal degrees to 3 + decimal places. Southern hemisphere + values are negative) + 5: Geographic longitude of radar site + (Given in decimal degrees to + 3 decimal places. + West longitude values are negative) + 6: Altitude of the radar site (meters) + 7: Physical scanning boresight + (Direction of the center beam, measured in + degrees relative to geographic north. + CCW rotations are negative.) + 8: Electronic shift to radar scanning + boresight (Degrees relative to + physical antenna boresight. + Normally 0.0 degrees) + 9: Beam separation (Angular + separation in degrees between adjacent + beams. Normally 3.24 degrees) + 10: Velocity sign (At the radar level, + backscattered signals with + frequencies above the transmitted + frequency are assigned positive + Doppler velocities while backscattered + signals with frequencies below + the transmitted frequency are assigned + negative Doppler velocity. This + convention can be reversed by changes + in receiver design or in the + data sampling rate. This parameter + is set to +1 or -1 to maintain the + convention.) + 11: Phase sign (Cabling errors can + lead to a 180 degree shift of the + interferometry phase measurement. + +1 indicates that the sign is + correct, -1 indicates that it must be flipped.) + 12: Tdiff [Channel A] + (Propagation time from interferometer + array antenna to phasing matrix input + minus propagation time from main array antenna + through transmitter to phasing matrix input. + Units are decimal + microseconds) + 13: Tdiff [Channel B] + (Propagation time from interferometer + array antenna to phasing matrix input minus + propagation time from main array antenna + through transmitter to phasing matrix input. + Units are decimal microseconds) + 14: Interferometer X offset + (Displacement of midpoint of interferometer + array from midpoint of main array, + along the line of antennas + with +X toward higher antenna numbers. + Units are meters) + 15: Interferometer Y offset + (Displacement of midpoint of + interferometer array from midpoint of + main array, along the array + normal direction with +Y in the direction of + the array normal. Units are meters) + 16: Interferometer Z offset + (Displacement of midpoint of + interferometer array from midpoint of + main array, in terms of altitude + difference with +Z up. Units are meters) + 17: Analog Rx rise time + (Time given in microseconds. Time delays of + less than ~10 microseconds can be ignored. + If narrow-band filters are + used in analog receivers or front-ends, + the time delays should be + specified.) + 18: Analog Rx attenuator step (dB) + 19: Analog attenuation stages (Number of stages. + This is used for gain control of an analog + receiver or front-end.) + 20: Maximum of range gates used + 21: Maximum number of beams + """ + return _HdwInfo(stid=int(hdw_data[j][0]), + status=Status(int(hdw_data[j][1])), + abbrev=abbrv, + date=hdw_lines_date[j], + geographic=_Coord(float(hdw_data[j][4]), + float(hdw_data[j][5]), + float(hdw_data[j][6])), + boresight=_Boresight(float(hdw_data[j][7]), + float(hdw_data[j][8])), + beam_separation=float(hdw_data[j][9]), + velocity_sign=float(hdw_data[j][10]), + phase_sign=float(hdw_data[j][11]), + tdiff=_Tdiff(float(hdw_data[j][12]), + float(hdw_data[j][13])), + interferometer_offset=_InterferometerOffset( + float(hdw_data[j][14]), + float(hdw_data[j][15]), + float(hdw_data[j][16])), + rx_rise_time=float(hdw_data[j][17]), + rx_attenuator=float(hdw_data[j][18]), + attenuation_stages=int(hdw_data[j][19]), + gates=int(hdw_data[j][20]), + beams=int(hdw_data[j][21])) class Hemisphere(Enum): From 8f6c6d729626481e5565983be4ac482266c4bb0d Mon Sep 17 00:00:00 2001 From: Wtristen Date: Wed, 4 Mar 2026 20:29:15 -0500 Subject: [PATCH 20/30] reverted read_hdw_file because it seems like it was correct originially --- pydarn/utils/superdarn_radars.py | 295 +++++++++++++++---------------- test/test_hdw.py | 13 ++ 2 files changed, 154 insertions(+), 154 deletions(-) create mode 100644 test/test_hdw.py diff --git a/pydarn/utils/superdarn_radars.py b/pydarn/utils/superdarn_radars.py index 664bcc44..fcfa9c0e 100644 --- a/pydarn/utils/superdarn_radars.py +++ b/pydarn/utils/superdarn_radars.py @@ -114,164 +114,151 @@ def read_hdw_file(abbrv, date: datetime = None, update: bool = False): if date is None: date = datetime.now() - hdw_path = os.path.join(os.path.dirname(__file__), 'hdw') - hdw_file = os.path.join(hdw_path, f"hdw.dat.{abbrv}") - - if update: - get_hdw_files(force=True) - + hdw_path = os.path.dirname(__file__)+'/hdw/' + hdw_file = "{path}/hdw.dat.{radar}".format(path=hdw_path, radar=abbrv) + hdw_data = [] + hdw_lines_date = [] + # if the file does not exist then try + # and download it + if os.path.exists(hdw_file) is False: + get_hdw_files(force=update) try: with open(hdw_file, 'r') as reader: lines = reader.readlines() + for i in range(len(lines)): + if '#' not in lines[i] and len(lines[i].split()) > 1: + hdw_data.append(lines[i].split()) + """ + Hardware files give the year and seconds from the beginning + of that year. Thus to check the date if it corresponds we + need to convert to a datetime object and then compare. + """ + j = len(hdw_data)-1 + hdw_lines_date.append( + datetime(year=int(hdw_data[j][2][0:4]), + month=int(hdw_data[j][2][4:6]), + day=int(hdw_data[j][2][6:8]), + hour=int(hdw_data[j][3][0:2]), + minute=int(hdw_data[j][3][3:5]), + second=int(hdw_data[j][3][6:8]))) + if hdw_lines_date[j] > date: + j = j-1 + break + """ + Hardware data array positions definitions: + 0: Station ID (unique numerical value). + 1: Status code (1 operational, -1 offline). + 2: First date that parameter string is valid + (YYYYMMDD). + 3: First time that parameter string is valid + (HH:MM:SS). + 4: Geographic latitude of radar site + (Given in decimal degrees to 3 + decimal places. Southern hemisphere + values are negative) + 5: Geographic longitude of radar site + (Given in decimal degrees to + 3 decimal places. + West longitude values are negative) + 6: Altitude of the radar site (meters) + 7: Physical scanning boresight + (Direction of the center beam, measured in + degrees relative to geographic north. + CCW rotations are negative.) + 8: Electronic shift to radar scanning + boresight (Degrees relative to + physical antenna boresight. + Normally 0.0 degrees) + 9: Beam separation (Angular + separation in degrees between adjacent + beams. Normally 3.24 degrees) + 10: Velocity sign (At the radar level, + backscattered signals with + frequencies above the transmitted + frequency are assigned positive + Doppler velocities while backscattered + signals with frequencies below + the transmitted frequency are assigned + negative Doppler velocity. This + convention can be reversed by changes + in receiver design or in the + data sampling rate. This parameter + is set to +1 or -1 to maintain the + convention.) + 11: Phase sign (Cabling errors can + lead to a 180 degree shift of the + interferometry phase measurement. + +1 indicates that the sign is + correct, -1 indicates that it must be flipped.) + 12: Tdiff [Channel A] + (Propagation time from interferometer + array antenna to phasing matrix input + minus propagation time from main array antenna + through transmitter to phasing matrix input. + Units are decimal + microseconds) + 13: Tdiff [Channel B] + (Propagation time from interferometer + array antenna to phasing matrix input minus + propagation time from main array antenna + through transmitter to phasing matrix input. + Units are decimal microseconds) + 14: Interferometer X offset + (Displacement of midpoint of interferometer + array from midpoint of main array, + along the line of antennas + with +X toward higher antenna numbers. + Units are meters) + 15: Interferometer Y offset + (Displacement of midpoint of + interferometer array from midpoint of + main array, along the array + normal direction with +Y in the direction of + the array normal. Units are meters) + 16: Interferometer Z offset + (Displacement of midpoint of + interferometer array from midpoint of + main array, in terms of altitude + difference with +Z up. Units are meters) + 17: Analog Rx rise time + (Time given in microseconds. Time delays of + less than ~10 microseconds can be ignored. + If narrow-band filters are + used in analog receivers or front-ends, + the time delays should be + specified.) + 18: Analog Rx attenuator step (dB) + 19: Analog attenuation stages (Number of stages. + This is used for gain control of an analog + receiver or front-end.) + 20: Maximum of range gates used + 21: Maximum number of beams + """ + return _HdwInfo(stid=int(hdw_data[j][0]), + status=Status(int(hdw_data[j][1])), + abbrev=abbrv, + date=hdw_lines_date[j], + geographic=_Coord(float(hdw_data[j][4]), + float(hdw_data[j][5]), + float(hdw_data[j][6])), + boresight=_Boresight(float(hdw_data[j][7]), + float(hdw_data[j][8])), + beam_separation=float(hdw_data[j][9]), + velocity_sign=float(hdw_data[j][10]), + phase_sign=float(hdw_data[j][11]), + tdiff=_Tdiff(float(hdw_data[j][12]), + float(hdw_data[j][13])), + interferometer_offset=_InterferometerOffset( + float(hdw_data[j][14]), + float(hdw_data[j][15]), + float(hdw_data[j][16])), + rx_rise_time=float(hdw_data[j][17]), + rx_attenuator=float(hdw_data[j][18]), + attenuation_stages=int(hdw_data[j][19]), + gates=int(hdw_data[j][20]), + beams=int(hdw_data[j][21])) except FileNotFoundError: - print(f"Hardware file for '{abbrv}' not found. Attempting download...") - get_hdw_files(force=True) - try: - with open(hdw_file, 'r') as reader: - lines = reader.readlines() - except FileNotFoundError: - raise pydarn.radar_exceptions.HardwareFileNotFoundError(abbrv) - - hdw_data = [] - hdw_lines_date = [] - j = -1 - - for i in range(len(lines)): - if '#' not in lines[i] and len(lines[i].split()) > 1: - hdw_data.append(lines[i].split()) - current_j = len(hdw_data) - 1 - current_date = datetime( - year=int(hdw_data[current_j][2][0:4]), - month=int(hdw_data[current_j][2][4:6]), - day=int(hdw_data[current_j][2][6:8]), - hour=int(hdw_data[current_j][3][0:2]), - minute=int(hdw_data[current_j][3][3:5]), - second=int(hdw_data[current_j][3][6:8]) - ) - hdw_lines_date.append(current_date) - if current_date > date: - break - j = current_j - - if j == -1: - if hdw_data: - j = 0 - else: - raise pydarn.radar_exceptions.HardwareFileNotFoundError(abbrv) - """ - Hardware data array positions definitions: - 0: Station ID (unique numerical value). - 1: Status code (1 operational, -1 offline). - 2: First date that parameter string is valid - (YYYYMMDD). - 3: First time that parameter string is valid - (HH:MM:SS). - 4: Geographic latitude of radar site - (Given in decimal degrees to 3 - decimal places. Southern hemisphere - values are negative) - 5: Geographic longitude of radar site - (Given in decimal degrees to - 3 decimal places. - West longitude values are negative) - 6: Altitude of the radar site (meters) - 7: Physical scanning boresight - (Direction of the center beam, measured in - degrees relative to geographic north. - CCW rotations are negative.) - 8: Electronic shift to radar scanning - boresight (Degrees relative to - physical antenna boresight. - Normally 0.0 degrees) - 9: Beam separation (Angular - separation in degrees between adjacent - beams. Normally 3.24 degrees) - 10: Velocity sign (At the radar level, - backscattered signals with - frequencies above the transmitted - frequency are assigned positive - Doppler velocities while backscattered - signals with frequencies below - the transmitted frequency are assigned - negative Doppler velocity. This - convention can be reversed by changes - in receiver design or in the - data sampling rate. This parameter - is set to +1 or -1 to maintain the - convention.) - 11: Phase sign (Cabling errors can - lead to a 180 degree shift of the - interferometry phase measurement. - +1 indicates that the sign is - correct, -1 indicates that it must be flipped.) - 12: Tdiff [Channel A] - (Propagation time from interferometer - array antenna to phasing matrix input - minus propagation time from main array antenna - through transmitter to phasing matrix input. - Units are decimal - microseconds) - 13: Tdiff [Channel B] - (Propagation time from interferometer - array antenna to phasing matrix input minus - propagation time from main array antenna - through transmitter to phasing matrix input. - Units are decimal microseconds) - 14: Interferometer X offset - (Displacement of midpoint of interferometer - array from midpoint of main array, - along the line of antennas - with +X toward higher antenna numbers. - Units are meters) - 15: Interferometer Y offset - (Displacement of midpoint of - interferometer array from midpoint of - main array, along the array - normal direction with +Y in the direction of - the array normal. Units are meters) - 16: Interferometer Z offset - (Displacement of midpoint of - interferometer array from midpoint of - main array, in terms of altitude - difference with +Z up. Units are meters) - 17: Analog Rx rise time - (Time given in microseconds. Time delays of - less than ~10 microseconds can be ignored. - If narrow-band filters are - used in analog receivers or front-ends, - the time delays should be - specified.) - 18: Analog Rx attenuator step (dB) - 19: Analog attenuation stages (Number of stages. - This is used for gain control of an analog - receiver or front-end.) - 20: Maximum of range gates used - 21: Maximum number of beams - """ - return _HdwInfo(stid=int(hdw_data[j][0]), - status=Status(int(hdw_data[j][1])), - abbrev=abbrv, - date=hdw_lines_date[j], - geographic=_Coord(float(hdw_data[j][4]), - float(hdw_data[j][5]), - float(hdw_data[j][6])), - boresight=_Boresight(float(hdw_data[j][7]), - float(hdw_data[j][8])), - beam_separation=float(hdw_data[j][9]), - velocity_sign=float(hdw_data[j][10]), - phase_sign=float(hdw_data[j][11]), - tdiff=_Tdiff(float(hdw_data[j][12]), - float(hdw_data[j][13])), - interferometer_offset=_InterferometerOffset( - float(hdw_data[j][14]), - float(hdw_data[j][15]), - float(hdw_data[j][16])), - rx_rise_time=float(hdw_data[j][17]), - rx_attenuator=float(hdw_data[j][18]), - attenuation_stages=int(hdw_data[j][19]), - gates=int(hdw_data[j][20]), - beams=int(hdw_data[j][21])) - + raise pydarn.radar_exceptions.HardwareFileNotFoundError(abbrv) class Hemisphere(Enum): """ diff --git a/test/test_hdw.py b/test/test_hdw.py new file mode 100644 index 00000000..5381f815 --- /dev/null +++ b/test/test_hdw.py @@ -0,0 +1,13 @@ +from datetime import datetime + +# This will fail on the old code if 'curl' or 'unzip' are not installed like as default on Windows. +from pydarn.utils import superdarn_radars + +# It should succeed with the new code, and the following should download and extract the files automatically. +print("--- Testing automatic download and read for 'wal' radar ---") +try: + # Use a recent date + wal_hdw = superdarn_radars.read_hdw_file('wal', date=datetime(2023, 1, 1)) + print(f" Station ID: {wal_hdw.stid}") #some test output +except Exception as e: + print(f"An error occurred: {e}") From 69e9e111e050c89dc5382763e395c105ef563f41 Mon Sep 17 00:00:00 2001 From: Carley Date: Tue, 24 Mar 2026 14:44:02 -0600 Subject: [PATCH 21/30] include option to return full array of data including NaNs when required --- pydarn/utils/coordinates.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/pydarn/utils/coordinates.py b/pydarn/utils/coordinates.py index f843775d..ecf7c0c1 100644 --- a/pydarn/utils/coordinates.py +++ b/pydarn/utils/coordinates.py @@ -32,7 +32,9 @@ def geo_coordinates(stid: RadarID, beams: int = None, - gates: tuple = None, **kwargs): + gates: tuple = None, + include_invalid: bool = False, + **kwargs): """ geographic_coordinates calculates the geographic coordinate for a given set of gates and beams @@ -58,12 +60,17 @@ def geo_coordinates(stid: RadarID, beams: int = None, **kwargs) beam_corners_lats[gate-gates[0], beam] = lat beam_corners_lons[gate-gates[0], beam] = lon - y0inx = np.min(np.where(np.isfinite(beam_corners_lats[:,0]))[0]) - return beam_corners_lats[y0inx:], beam_corners_lons[y0inx:] + if include_invalid: + return beam_corners_lats, beam_corners_lons + else: + y0inx = np.min(np.where(np.isfinite(beam_corners_lats[:,0]))[0]) + return beam_corners_lats[y0inx:], beam_corners_lons[y0inx:] def aacgm_coordinates(stid: pydarn.RadarID, beams: int = None, gates: tuple = None, - date: dt.datetime = dt.datetime.now, **kwargs): + date: dt.datetime = dt.datetime.now, + include_invalid: bool = False, + **kwargs): if gates is None: gates = [0, SuperDARNRadars.radars[stid].range_gate_45] if beams is None: @@ -86,8 +93,11 @@ def aacgm_coordinates(stid: pydarn.RadarID, beams: int = None, gates: tuple = No dtime=date)) beam_corners_lats[gate-gates[0], beam] = geomag[0] beam_corners_lons[gate-gates[0], beam] = geomag[1] - y0inx = np.min(np.where(np.isfinite(beam_corners_lats[:,0]))[0]) - return beam_corners_lats[y0inx:], beam_corners_lons[y0inx:] + if include_invalid: + return beam_corners_lats, beam_corners_lons + else: + y0inx = np.min(np.where(np.isfinite(beam_corners_lats[:,0]))[0]) + return beam_corners_lats[y0inx:], beam_corners_lons[y0inx:] def aacgm_MLT_coordinates(**kwargs): From bc96dd743bb9b2a710bf937564fbe7301ed01446 Mon Sep 17 00:00:00 2001 From: Wtristen <151891162+Wtristen@users.noreply.github.com> Date: Wed, 8 Apr 2026 10:19:17 -0400 Subject: [PATCH 22/30] Update pydarn/utils/superdarn_radars.py Co-authored-by: Carley <60905856+carleyjmartin@users.noreply.github.com> --- pydarn/utils/superdarn_radars.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydarn/utils/superdarn_radars.py b/pydarn/utils/superdarn_radars.py index fcfa9c0e..9a875c58 100644 --- a/pydarn/utils/superdarn_radars.py +++ b/pydarn/utils/superdarn_radars.py @@ -56,7 +56,7 @@ def get_hdw_files(force: bool = True, version: str = None): # TODO: implement when DSWG starts versioning hardware files if version is not None: - raise NotImplementedError("This feature is not implemented, Versioned hardware does not exist yet.") #clarified error + raise NotImplementedError("This feature is not implemented, Versioned hardware does not exist yet.") # if there is no files in hdw folder or force is true # download the hdw files From 6fcc38fda81f0468f3f29e017367426cd84ee100 Mon Sep 17 00:00:00 2001 From: Wtristen <151891162+Wtristen@users.noreply.github.com> Date: Wed, 8 Apr 2026 10:19:27 -0400 Subject: [PATCH 23/30] Update pydarn/utils/superdarn_radars.py Co-authored-by: Carley <60905856+carleyjmartin@users.noreply.github.com> --- pydarn/utils/superdarn_radars.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pydarn/utils/superdarn_radars.py b/pydarn/utils/superdarn_radars.py index 9a875c58..911970c4 100644 --- a/pydarn/utils/superdarn_radars.py +++ b/pydarn/utils/superdarn_radars.py @@ -26,9 +26,6 @@ from typing import NamedTuple from enum import Enum from datetime import datetime -#from subprocess import check_call - -#new imports from urllib import request from zipfile import ZipFile From 914f4e57e77f11e1c51850950699be261ff0085f Mon Sep 17 00:00:00 2001 From: Wtristen <151891162+Wtristen@users.noreply.github.com> Date: Wed, 8 Apr 2026 10:24:29 -0400 Subject: [PATCH 24/30] Delete test/test_hdw.py removed test_hdw.py added by this pull request --- test/test_hdw.py | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 test/test_hdw.py diff --git a/test/test_hdw.py b/test/test_hdw.py deleted file mode 100644 index 5381f815..00000000 --- a/test/test_hdw.py +++ /dev/null @@ -1,13 +0,0 @@ -from datetime import datetime - -# This will fail on the old code if 'curl' or 'unzip' are not installed like as default on Windows. -from pydarn.utils import superdarn_radars - -# It should succeed with the new code, and the following should download and extract the files automatically. -print("--- Testing automatic download and read for 'wal' radar ---") -try: - # Use a recent date - wal_hdw = superdarn_radars.read_hdw_file('wal', date=datetime(2023, 1, 1)) - print(f" Station ID: {wal_hdw.stid}") #some test output -except Exception as e: - print(f"An error occurred: {e}") From faa8a743fe6bcf1f51cb1c15ae06343711c7e54f Mon Sep 17 00:00:00 2001 From: Carley Date: Mon, 20 Apr 2026 14:02:42 -0600 Subject: [PATCH 25/30] added option ot remove ionospheric scatter or ground scatter --- pydarn/exceptions/plot_exceptions.py | 8 ++++ pydarn/plotting/fan.py | 42 +++++++++++++++++++- pydarn/plotting/rtp.py | 58 +++++++++++++++++++++++++++- 3 files changed, 105 insertions(+), 3 deletions(-) diff --git a/pydarn/exceptions/plot_exceptions.py b/pydarn/exceptions/plot_exceptions.py index 7b255ed4..e1742cf8 100644 --- a/pydarn/exceptions/plot_exceptions.py +++ b/pydarn/exceptions/plot_exceptions.py @@ -16,6 +16,14 @@ # supplemented by the additional permissions listed below. import datetime +class GeneralError(Exception): + ''' + Error for general messages + ''' + def __init__(self, message): + self.message = message + super().__init__(self.message) + class PartialRecordsError(Exception): """ diff --git a/pydarn/plotting/fan.py b/pydarn/plotting/fan.py index 4c5cfe08..3de234bc 100644 --- a/pydarn/plotting/fan.py +++ b/pydarn/plotting/fan.py @@ -27,6 +27,7 @@ # 2023-06-28: CJM - Refactored return values # 2023-10-14: CJM - Add embargoed data method # 2024-10-09: DDB - Control marker and its size in plot_radar_position() +# 2026-04-20: CJM - Add options for remove_iono_scatter and remove_ground_scatter # # Disclaimer: # pyDARN is under the LGPL v3 license found in the root directory LICENSE.md @@ -85,6 +86,8 @@ def plot_fan(dmap_data: List[dict], ax=None, ranges=None, scan_time_tolerance: dt.timedelta = dt.timedelta(seconds=30), parameter: str = 'v', cmap: str = None, groundscatter: bool = False, zmin: int = None, + remove_iono_scatter: bool = False, + remove_ground_scatter: bool = False, zmax: int = None, colorbar: bool = True, colorbar_label: str = '', cax=None, title: bool = True, boundary: bool = True, @@ -130,6 +133,12 @@ def plot_fan(dmap_data: List[dict], ax=None, ranges=None, groundscatter : bool Set true to indicate if groundscatter should be plotted in grey Default: False + remove_iono_scatter: boolean + if True, ionospheric scatter will not be plotted + default: False + remove_ground_scatter: boolean + if True, ground scatter will not be plotted + default: False zmin: int The minimum parameter value for coloring Default: {'p_l': [0], 'v': [-200], 'w_l': [0], 'elv': [0]} @@ -326,6 +335,9 @@ def plot_fan(dmap_data: List[dict], ax=None, ranges=None, scan = scan[0:ranges[1]-ranges[0]] grndsct = grndsct[0:ranges[1]-ranges[0]] + if remove_iono_scatter: + iono_scatter = scan*grndsct + # Set up axes in correct hemisphere stid = RadarID(dmap_data[0]['stid']) kwargs['hemisphere'] = SuperDARNRadars.radars[stid].hemisphere @@ -345,6 +357,13 @@ def plot_fan(dmap_data: List[dict], ax=None, ranges=None, np.ma.masked_array(scan, ~scan.astype(bool)), norm=norm, cmap=cmap, transform=transform, zorder=2) + if remove_iono_scatter: + is_color = colors.ListedColormap(['white']) + ax.pcolormesh(thetas, rs, + np.ma.masked_array(iono_scatter, + iono_scatter.astype(bool)), + norm=norm, cmap=is_color, transform=transform, + zorder=2) else: # Get center of each gate instead of edges fan_shape = thetas.shape @@ -423,8 +442,12 @@ def plot_fan(dmap_data: List[dict], ax=None, ranges=None, # Plot ground scatter balls (no sticks) if groundscatter and grndsct[i, j] != 0.0: - ax.scatter(t_center, r_center, c='grey', s=1.0, - transform=transform, zorder=3.0) + if remove_ground_scatter: + ax.scatter(t_center, r_center, c='w', s=1.0, + transform=transform, zorder=3.0) + else: + ax.scatter(t_center, r_center, c='grey', s=1.0, + transform=transform, zorder=3.0) # plot the groundscatter as grey fill if groundscatter and not ball_and_stick: @@ -434,6 +457,21 @@ def plot_fan(dmap_data: List[dict], ax=None, ranges=None, ~grndsct.astype(bool)), cmap=gs_color, transform=transform, zorder=3) + elif not groundscatter and not ball_and_stick: + gs_color = colors.ListedColormap(['white']) + ax.pcolormesh(thetas, rs, + np.ma.masked_array(grndsct, + ~grndsct.astype(bool)), + cmap=cmap, alpha=0.0, + transform=transform, zorder=3) + if remove_ground_scatter: + gs_color = colors.ListedColormap(['white']) + ax.pcolormesh(thetas, rs, + np.ma.masked_array(grndsct, + ~grndsct.astype(bool)), + cmap=gs_color, + transform=transform, zorder=3) + if ccrs is None: azm = np.linspace(0, 2 * np.pi, 100) r, th = np.meshgrid(rs, azm) diff --git a/pydarn/plotting/rtp.py b/pydarn/plotting/rtp.py index d372addd..e0ac6d99 100644 --- a/pydarn/plotting/rtp.py +++ b/pydarn/plotting/rtp.py @@ -12,6 +12,8 @@ # 2023-06-12 Carley Martin added coordinate plotting method # 2023-06-28 Carley Martin refactored return values # 2023-10-14 Carley Martin added embargoed data method +# 2026-04-20 Carley Martin added options for remove_iono_scatter +# and remove_ground_scatter # # Disclaimer: # pyDARN is under the LGPL v3 license found in the root directory LICENSE.md @@ -81,6 +83,8 @@ def plot_range_time(cls, dmap_data: List[dict], parameter: str = 'v', beam_num: int = 0, channel: int = 'all', ax=None, background: str = 'w', background_alpha: float = 0.0, groundscatter: bool = False, + remove_iono_scatter: bool = False, + remove_ground_scatter: bool = False, zmin: int = None, zmax: int = None, start_time: datetime = None, end_time: datetime = None, colorbar: plt.colorbar = None, ymin: int = None, @@ -124,6 +128,12 @@ def plot_range_time(cls, dmap_data: List[dict], parameter: str = 'v', Flag to indicate if groundscatter should be plotted. If string groundscatter will be represented by that color else grey. Default : False + remove_iono_scatter: boolean + if True, ionospheric scatter will not be plotted + default: False + remove_ground_scatter: boolean + if True, ground scatter will not be plotted + default: False background : str color of the background in the plot default: white @@ -284,9 +294,12 @@ def plot_range_time(cls, dmap_data: List[dict], parameter: str = 'v', # x: time date data x = [] + # If remove_ground scatter is chosen, set gs to white + if remove_ground_scatter: + groundscatter = 'w' + # We cannot simply use numpy's built in min and max function # because of the groundscatter value :( - # These flags indicate if zmin and zmax should change set_zmin = True set_zmax = True @@ -368,6 +381,9 @@ def plot_range_time(cls, dmap_data: List[dict], parameter: str = 'v', z[i][good_gates[j]] = -1000000 # otherwise store parameter value # TODO: refactor and clean up this code + elif dmap_record['gflg'][j] == 0 and\ + remove_iono_scatter: + z[i][good_gates[j]] = -1000000 elif cls.__filter_data_check(dmap_record, plot_filter, j): z[i][good_gates[j]] = \ @@ -445,6 +461,18 @@ def plot_range_time(cls, dmap_data: List[dict], parameter: str = 'v', im = ax.pcolormesh(time_axis, y_axis, z_data, lw=0.01, cmap=cmap, norm=norm, **kwargs) + if remove_iono_scatter and not groundscatter: + iono_scatter = np.ma.masked_where(z_data != -1000000, z_data) + is_color = colors.ListedColormap(['white']) + ax.pcolormesh(time_axis, y_axis, iono_scatter, lw=0.01, + cmap=is_color, norm=norm, **kwargs) + + elif remove_iono_scatter and groundscatter: + raise plot_exceptions.GeneralError(message = + '"groundscatter" must be' + ' False to use' + ' remove_iono_scatter') + if isinstance(groundscatter, str): ground_scatter = np.ma.masked_where(z_data != -1000000, z_data) gs_color = colors.ListedColormap([groundscatter]) @@ -905,6 +933,7 @@ def plot_summary(cls, dmap_data: List[dict], plot_elv: bool = True, title=None, background: str = 'w', groundscatter: bool = True, + remove_ground_scatter: bool = False, channel: int = 'all', line_color: dict = {}, range_estimation: object = RangeEstimation.SLANT_RANGE, @@ -1284,6 +1313,9 @@ def plot_summary(cls, dmap_data: List[dict], grndflg = True else: grndflg = False + + if remove_ground_scatter: + grndflg = 'w' # with warning catch, catches all the warnings # that would be produced by time-series this would be # the citing warning. @@ -1490,6 +1522,8 @@ def plot_coord_time(cls, dmap_data: List[dict], parameter: str = 'v', beam_num: int = 0, channel: int = 'all', ax=None, background: str = 'w', background_alpha: float = 0.0, groundscatter: bool = False, + remove_iono_scatter: bool = False, + remove_ground_scatter: bool = False, zmin: int = None, zmax: int = None, coords: object = Coords.AACGM, latlon: str = 'lat', start_time: datetime = None, end_time: datetime = None, @@ -1536,6 +1570,9 @@ def plot_coord_time(cls, dmap_data: List[dict], parameter: str = 'v', Flag to indicate if groundscatter should be plotted. If string groundscatter will be represented by that color else grey. Default : False + remove_iono_scatter: boolean + if True, ionospheric scatter will not be plotted + default: False background : str color of the background in the plot default: white @@ -1707,6 +1744,10 @@ def plot_coord_time(cls, dmap_data: List[dict], parameter: str = 'v', # x: time date data x = [] + # If remove_ground scatter is chosen, set gs to white + if remove_ground_scatter: + groundscatter = 'w' + # We cannot simply use numpy's built in min and max function # because of the groundscatter value :( @@ -1790,6 +1831,9 @@ def plot_coord_time(cls, dmap_data: List[dict], parameter: str = 'v', # groundscatter a different color # from the color map z[i][good_gates[j]] = -1000000 + elif dmap_record['gflg'][j] == 0 and\ + remove_iono_scatter: + z[i][good_gates[j]] = -1000000 # otherwise store parameter value # TODO: refactor and clean up this code elif cls.__filter_data_check(dmap_record, @@ -1915,6 +1959,18 @@ def plot_coord_time(cls, dmap_data: List[dict], parameter: str = 'v', im = ax.pcolormesh(time_axis, y_axis, z_data, lw=0.01, cmap=cmap, norm=norm, **kwargs) + if remove_iono_scatter and not groundscatter: + iono_scatter = np.ma.masked_where(z_data != -1000000, z_data) + is_color = colors.ListedColormap(['white']) + ax.pcolormesh(time_axis, y_axis, iono_scatter, lw=0.01, + cmap=is_color, norm=norm, **kwargs) + + elif remove_iono_scatter and groundscatter: + raise plot_exceptions.GeneralError(message = + '"groundscatter" must be' + ' False to use' + ' remove_iono_scatter') + if isinstance(groundscatter, str): ground_scatter = np.ma.masked_where(z_data != -1000000, z_data) gs_color = colors.ListedColormap([groundscatter]) From 0ab8aed20a543b1850220814bb7e4040754cf292 Mon Sep 17 00:00:00 2001 From: Carley Date: Mon, 20 Apr 2026 14:15:40 -0600 Subject: [PATCH 26/30] fix bug in rmove iono scatter for fan --- pydarn/plotting/fan.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pydarn/plotting/fan.py b/pydarn/plotting/fan.py index 3de234bc..38a61544 100644 --- a/pydarn/plotting/fan.py +++ b/pydarn/plotting/fan.py @@ -336,7 +336,7 @@ def plot_fan(dmap_data: List[dict], ax=None, ranges=None, grndsct = grndsct[0:ranges[1]-ranges[0]] if remove_iono_scatter: - iono_scatter = scan*grndsct + iono_scatter = scan*~grndsct.astype(bool) # Set up axes in correct hemisphere stid = RadarID(dmap_data[0]['stid']) @@ -361,7 +361,7 @@ def plot_fan(dmap_data: List[dict], ax=None, ranges=None, is_color = colors.ListedColormap(['white']) ax.pcolormesh(thetas, rs, np.ma.masked_array(iono_scatter, - iono_scatter.astype(bool)), + ~iono_scatter.astype(bool)), norm=norm, cmap=is_color, transform=transform, zorder=2) else: From 706b911f956b2857c6a64777946e659ebed97d30 Mon Sep 17 00:00:00 2001 From: Carley Date: Fri, 1 May 2026 12:59:11 -0600 Subject: [PATCH 27/30] suggestions from code review --- pydarn/__init__.py | 1 + pydarn/utils/detrend.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pydarn/__init__.py b/pydarn/__init__.py index 0241e8fd..30ef44d5 100644 --- a/pydarn/__init__.py +++ b/pydarn/__init__.py @@ -55,6 +55,7 @@ from .utils.terminator import terminator from .utils.recalculate_elevation import recalculate_elevation from .utils.filters import Boxcar +from .utils.detrend import Detrend # import plotting from .plotting.color_maps import PyDARNColormaps diff --git a/pydarn/utils/detrend.py b/pydarn/utils/detrend.py index 8e7eafd3..25706736 100644 --- a/pydarn/utils/detrend.py +++ b/pydarn/utils/detrend.py @@ -1,7 +1,7 @@ import copy -import pydarn import datetime as dt import numpy as np +from pydarn import SuperDARNRadars, RadarID from scipy.signal import savgol_filter from typing import List @@ -179,7 +179,7 @@ def detrend_fitacf(cls, fitacf_data_detrended = copy.deepcopy(fitacf_data) # Max beams and range gates for this data - no_beams = pydarn.SuperDARNRadars.radars[pydarn.RadarID(fitacf_data[0]['stid'])].hardware_info.beams + no_beams = SuperDARNRadars.radars[RadarID(fitacf_data[0]['stid'])].hardware_info.beams no_rang = fitacf_data[0]['nrang'] # Grab slist and time lists for all records. "None" indicates no data for that record. From 72745afe91d7325225c0c32285b20f6e7ce7b6d7 Mon Sep 17 00:00:00 2001 From: Carley Date: Tue, 19 May 2026 15:27:45 -0600 Subject: [PATCH 28/30] update documentation for new features in develop --- docs/imgs/detrend.png | Bin 0 -> 111440 bytes docs/user/coordinates.md | 2 +- docs/user/filters.md | 23 +++++++++++++++++------ docs/user/install.md | 5 ++++- docs/user/io.md | 2 +- docs/user/map.md | 6 ++++++ docs/user/superdarn_data.md | 2 +- setup.cfg | 1 + 8 files changed, 31 insertions(+), 10 deletions(-) create mode 100644 docs/imgs/detrend.png diff --git a/docs/imgs/detrend.png b/docs/imgs/detrend.png new file mode 100644 index 0000000000000000000000000000000000000000..266e54470a7ece5ec5a4933388548f7223b59b1c GIT binary patch literal 111440 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 -# Filtering Data +# Filtering and Detrending Data --- - - -Currently pyDARN has one filtering option. - ## Boxcar Filtering !!! Note @@ -81,4 +77,19 @@ plt.show() ``` ![](../imgs/unfiltered.png) -![](../imgs/filtered.png) \ No newline at end of file +![](../imgs/filtered.png) + +## Data Detrending + +FITACF level data can be detrended to view background periodic fluctuations in data. +You can choose between using a mean value of the window length ('mean'), or using the Sovitsky-Golay filter ('sov-gal'). + +```python +import pydarn + +data, _ = pydarn.read_fitacf('superdarn.data.file.fitacf') + +dmap_detrended = pydarn.Detrend.detrend_fitacf(data, parameter='both', window_length=600, detrend_type='mean') +``` + +![](../imgs/detrend.png) diff --git a/docs/user/install.md b/docs/user/install.md index 99b2c6a4..e0cfbfb0 100644 --- a/docs/user/install.md +++ b/docs/user/install.md @@ -35,6 +35,9 @@ pip3 install --upgrade pydarn Installing in virtual environments is recommended, see below for details. +!!! Note + Many funding agencies count acknowledgements and citations when assessing projects. Please remember to [acknowledge](/citing.md) the SuperDARN data, and tools you are using so we can keep producing high quality data and tools to use it! + ## Prerequisites pyDARN requires **python 3.8** or later, see list below for library dependencies. @@ -59,7 +62,7 @@ You can check your python version using On installation, pyDARN will download the following dependencies: - [NumPy](https://numpy.org/) -- [scipy <1.15.0](https://scipy.org/) +- [scipy 1.17.0+](https://scipy.org/) - [matplotlib 3.7.0+](https://matplotlib.org/) - [PyYAML](https://pyyaml.org/wiki/PyYAMLDocumentation) - [pyDARNio 2.0.0+](https://pydarnio.readthedocs.io/en/latest/user/install/) diff --git a/docs/user/io.md b/docs/user/io.md index c004bd99..e5ab5d34 100644 --- a/docs/user/io.md +++ b/docs/user/io.md @@ -18,7 +18,7 @@ the additional permissions listed below. # Reading in DMap structured SuperDARN data files --- -Data Map (DMap) is a binary self-describing format that was developed by Rob Barnes. +Data Map (DMap) is a binary self-describing format that was developed by the SuperDARN community. This format is currently the primary format used by SuperDARN. For more information on DMap please see [RST Documentation](https://radar-software-toolkit-rst.readthedocs.io/en/latest/). Types of files used by SuperDARN which are usually accessed in DMap format are: diff --git a/docs/user/map.md b/docs/user/map.md index d58d5e32..5f3823aa 100644 --- a/docs/user/map.md +++ b/docs/user/map.md @@ -24,6 +24,7 @@ Map field descriptions can be found [here](https://radar-software-toolkit-rst.re | ------------------ | ----------------------------- | ----------------------------- | | Fitted Velocity | `MapParams.FITTED_VELOCITIES` | see Fitted Velocities section | | Modeled Velocities | `MapParams.MODEL_VELOCITIES` | `model.vel.median` | +| True Velocities | `MapParams.TRUE_VELOCITIES` | see True Velocities section | | Raw Velocities | `MapParams.RAW_VELOCITIES` | `vector.vel.median` | | Power | `MapParams.POWER` | `vector.pwr.median` | | Spectral Width | `MapParams.SPECTRAL_WIDTH` | `vector.wdt.median` | @@ -38,6 +39,11 @@ Fitted velocities are velocity vectors which represent the fitted convection pat Fitted velocity vectors are by default only calculated at the same positions of the line-of-sight vectors, but fit vectors at an arbitrary position can be obtained by using the `calculated_fitted_velocities` function in `map.py`. +### True Velocities + +True velocity is a name given to a value of velocity derived from the fitted and raw velocities. Note that is not the 'absolute true' velocity that is occuring in the ionosphere, it is just a different representation of the velocities. +The True velocity is given by combining the average line-of-sight velocity measured at each grid cell with the component of the fitted velocity which is perpendicular to the line-of-sight direction. More information can be read in [Chisham et al.2002](https://agupubs.onlinelibrary.wiley.com/doi/10.1029/2001JA009124). + ## Basic usage pyDARN and pyplot need to be imported and the desired MAP file needs to be [read in](https://pydarn.readthedocs.io/en/main/user/io/): diff --git a/docs/user/superdarn_data.md b/docs/user/superdarn_data.md index cd79c841..f333f9a3 100644 --- a/docs/user/superdarn_data.md +++ b/docs/user/superdarn_data.md @@ -24,7 +24,7 @@ The [Data Distribution Working Group (DDWG)](https://github.com/SuperDARN/DDWG) ## Data Mirrors To get access to rawacf, fitacf and sometimes higher lever data, there are three possible data servers: one utilizes *Globus*, others use `rsync`, `sftp` and `scp`: - - [SuperDARN Canada](https://superdarn.ca/): uses [Globus](https://github.com/SuperDARNCanada/globus) to allow access to the SuperDARN data. Contact [superdarn@usask.ca](mailto:superdarn@usask.ca) for access. + - [SuperDARN Canada](https://superdarn.ca/data-access): uses [Globus](https://github.com/SuperDARNCanada/globus) to allow access to the SuperDARN data. Contact [superdarn@usask.ca](mailto:superdarn@usask.ca) for access or fill in the online form at the link above. - [BAS](https://www.bas.ac.uk/project/superdarn/#about): information on data access can be found [here](https://www.bas.ac.uk/project/superdarn/#data) - [NSSC](https://www.nssdc.ac.cn/nssdc_en/html/task/sdarn.html): please contact NSSC for access. diff --git a/setup.cfg b/setup.cfg index 2c4da23f..528f2ea8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,6 +16,7 @@ classifiers = Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 + Programming Language :: Python :: 3.12 Topic :: Scientific/Engineering :: Physics Topic :: Software Development :: Libraries From 43dcf5e4ae2372f335805cbf5248e488256a84ed Mon Sep 17 00:00:00 2001 From: Carley Date: Wed, 20 May 2026 13:03:19 -0600 Subject: [PATCH 29/30] pre-release checklist --- .zenodo.json | 14 +++++++++----- README.md | 13 +++++++------ pydarn/version.py | 2 +- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/.zenodo.json b/.zenodo.json index 412ce534..2d4f8bac 100644 --- a/.zenodo.json +++ b/.zenodo.json @@ -3,11 +3,6 @@ { "name": "SuperDARN Data Visualization Working Group" }, - { - "affiliation": "University of Saskatchewan", - "name": "Rohel, R.A.", - "orcid": "0000-0003-2208-1553" - }, { "affiliation": "University of Saskatchewan", "name": "Martin, C.J.", @@ -18,6 +13,15 @@ "name": "Billett, D.D.", "orcid": "0000-0002-8905-8609" }, + { + "affiliation": "Virginia Tech", + "name": "Wanner, T." + }, + { + "affiliation": "University of Saskatchewan", + "name": "Rohel, R.A.", + "orcid": "0000-0003-2208-1553" + }, { "affiliation": "Virginia Tech", "name": "Sterne, K.T.", diff --git a/README.md b/README.md index a55426ff..a1ac50c0 100644 --- a/README.md +++ b/README.md @@ -9,12 +9,13 @@ Python data visualization library for the Super Dual Auroral Radar Network (Supe ## Changelog -## Version 4.2 - Major Release! +## Version 4.3 - Minor Release! -This major release includes: -- Updates to pyDARNio interface: faster reading of files -- `nightshade` added to range-time plot options -- ENUM use for retrieving radar information +This minor release includes: +- Updated SciPy restriction and changes associated +- NEW: True velocity in map plots +- NEW: FITACF data detrending algorithm +- Bug Fix: Updating HDW files on Windows fixed ## Documentation @@ -51,7 +52,7 @@ plt.show() For more information and tutorials on pyDARN please see the [tutorial section](https://pydarn.readthedocs.io/en/main/). -We also have a [Jupyter notebook](https://zenodo.org/record/7005203) with many examples to support our recent [publication](https://doi.org/10.3389/fspas.2022.1022690). +We also have a [Jupyter notebook](https://zenodo.org/record/7005203) with many examples to support our [publication](https://doi.org/10.3389/fspas.2022.1022690). This notebook may be out of date. ## Getting involved diff --git a/pydarn/version.py b/pydarn/version.py index 306bb72d..2fdad7a9 100644 --- a/pydarn/version.py +++ b/pydarn/version.py @@ -17,4 +17,4 @@ This file contains the version number of pydarn """ -__version__='4.2' +__version__='4.3' From 81170fa9b77b90842c1e2c36a4f9c1d22f521c83 Mon Sep 17 00:00:00 2001 From: Carley Date: Mon, 25 May 2026 11:19:52 -0600 Subject: [PATCH 30/30] update zenodo --- .zenodo.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.zenodo.json b/.zenodo.json index 2d4f8bac..1c9b7caa 100644 --- a/.zenodo.json +++ b/.zenodo.json @@ -15,7 +15,16 @@ }, { "affiliation": "Virginia Tech", - "name": "Wanner, T." + "name": "Wanner, T.D.", + "orcid":"0009-0007-0616-5796" + }, + { + "affiliation": "University of Saskatchewan", + "name": "Kucharyshen, K." + }, + { + "affiliation": "University of Saskatchewan", + "name": "Sylvestre, R." }, { "affiliation": "University of Saskatchewan",