From 8fd16af50870e5c4761a642d3ada9904a8c2df1c Mon Sep 17 00:00:00 2001 From: Laura Sandoval Date: Mon, 13 May 2024 19:16:33 -0600 Subject: [PATCH] ssd addition --- .../ultra/unit/test_ultra_l1b_theta_0.py | 187 ++++++++++-------- imap_processing/ultra/l1b/ultra_l1b.py | 35 ++-- ...rtup1_ULTRA_IMGPARAMS_20240207T134735_.csv | 2 +- .../Ultra90_image-params071823.xlsx | Bin 0 -> 16856 bytes 4 files changed, 131 insertions(+), 93 deletions(-) create mode 100644 imap_processing/ultra/lookup_tables/Ultra90_image-params071823.xlsx diff --git a/imap_processing/tests/ultra/unit/test_ultra_l1b_theta_0.py b/imap_processing/tests/ultra/unit/test_ultra_l1b_theta_0.py index 1de5a50cd..8b6348699 100644 --- a/imap_processing/tests/ultra/unit/test_ultra_l1b_theta_0.py +++ b/imap_processing/tests/ultra/unit/test_ultra_l1b_theta_0.py @@ -17,8 +17,10 @@ get_front_y_position, get_particle_velocity, get_path_length, + get_ssd_positions, ) + @pytest.fixture() def decom_ultra_events(ccsds_path_theta_0, xtce_path): """Data for decom_ultra_events""" @@ -46,13 +48,13 @@ def indices_start_type_1_or_2(decom_ultra_events, decom_ultra_aux): # Create the dataset events_dataset = create_dataset( - {ULTRA_EVENTS.apid[0]: decom_ultra_events, - ULTRA_AUX.apid[0]: decom_ultra_aux} + {ULTRA_EVENTS.apid[0]: decom_ultra_events, ULTRA_AUX.apid[0]: decom_ultra_aux} ) # Remove start_type with fill values - events_dataset = events_dataset.where(events_dataset["START_TYPE"] != - GlobalConstants.INT_FILLVAL, drop=True) + events_dataset = events_dataset.where( + events_dataset["START_TYPE"] != GlobalConstants.INT_FILLVAL, drop=True + ) # Check top and bottom index_1 = np.where(events_dataset["START_TYPE"] == 1)[0] @@ -70,12 +72,12 @@ def indices_stop_type_1_or_2(decom_ultra_events, decom_ultra_aux): # Create the dataset events_dataset = create_dataset( - {ULTRA_EVENTS.apid[0]: decom_ultra_events, - ULTRA_AUX.apid[0]: decom_ultra_aux} + {ULTRA_EVENTS.apid[0]: decom_ultra_events, ULTRA_AUX.apid[0]: decom_ultra_aux} ) # Remove start_type with fill values - events_dataset = events_dataset.where(events_dataset["START_TYPE"] != - GlobalConstants.INT_FILLVAL, drop=True) + events_dataset = events_dataset.where( + events_dataset["START_TYPE"] != GlobalConstants.INT_FILLVAL, drop=True + ) # Check top and bottom index_1 = np.where(events_dataset["STOP_TYPE"] == 1)[0] @@ -84,11 +86,32 @@ def indices_stop_type_1_or_2(decom_ultra_events, decom_ultra_aux): return index_1, index_2, events_dataset +@pytest.fixture() +def indices_stop_type_8_to_15(decom_ultra_events, decom_ultra_aux): + """ + A pytest fixture to extract indices from events_dataset where STOP_TYPE is 1 or 2 + and COUNT is not 0. Assumes the dataset is structured with an 'epoch' dimension. + """ + + # Create the dataset + events_dataset = create_dataset( + {ULTRA_EVENTS.apid[0]: decom_ultra_events, ULTRA_AUX.apid[0]: decom_ultra_aux} + ) + # Remove start_type with fill values + events_dataset = events_dataset.where( + events_dataset["START_TYPE"] != GlobalConstants.INT_FILLVAL, drop=True + ) + + # Check top and bottom + index = np.where(events_dataset["STOP_TYPE"] >= 8)[0] + + return index, events_dataset + + def test_xf( indices_start_type_1_or_2, events_fsw_comparison_theta_0, ): - indices_1, indices_2, events_dataset = indices_start_type_1_or_2 df = pd.read_csv(events_fsw_comparison_theta_0) @@ -96,22 +119,25 @@ def test_xf( selected_rows_1 = df_filt.iloc[indices_1] selected_rows_2 = df_filt.iloc[indices_2] - xf_1 = get_front_x_position(events_dataset["START_TYPE"].data[indices_1], - events_dataset["START_POS_TDC"].data[indices_1]) - xf_2 = get_front_x_position(events_dataset["START_TYPE"].data[indices_2], - events_dataset["START_POS_TDC"].data[indices_2]) + xf_1 = get_front_x_position( + events_dataset["START_TYPE"].data[indices_1], + events_dataset["START_POS_TDC"].data[indices_1], + ) + xf_2 = get_front_x_position( + events_dataset["START_TYPE"].data[indices_2], + events_dataset["START_POS_TDC"].data[indices_2], + ) # The value 180 was added to xf_1 since that is the offset from the FSW xft_off - assert np.allclose(xf_1+180, selected_rows_1.Xf.values.astype('float'), rtol=1e-3) + assert np.allclose(xf_1 + 180, selected_rows_1.Xf.values.astype("float"), rtol=1e-3) # The value 25 was subtracted from xf_2 since that is the offset from the FSW xft_off - assert np.allclose(xf_2-25, selected_rows_2.Xf.values.astype('float'), rtol=1e-3) + assert np.allclose(xf_2 - 25, selected_rows_2.Xf.values.astype("float"), rtol=1e-3) @pytest.fixture() -def tof(indices_stop_type_1_or_2, - events_fsw_comparison_theta_0): +def tof(indices_stop_type_1_or_2, events_fsw_comparison_theta_0): indices_1, indices_2, events_dataset = indices_stop_type_1_or_2 - indices = np.concatenate((indices_1,indices_2)) + indices = np.concatenate((indices_1, indices_2)) indices.sort() df = pd.read_csv(events_fsw_comparison_theta_0) @@ -119,9 +145,7 @@ def tof(indices_stop_type_1_or_2, selected_rows_1 = df_filt.iloc[indices] tof, t2, xb, yb = get_back_positions( - indices, - events_dataset, - selected_rows_1.Xf.values.astype('float') + indices, events_dataset, selected_rows_1.Xf.values.astype("float") ) return tof, t2, xb, yb @@ -133,42 +157,70 @@ def test_xb_yb( ): _, _, xb, yb = tof indices_1, indices_2, events_dataset = indices_stop_type_1_or_2 - indices = np.concatenate((indices_1,indices_2)) + indices = np.concatenate((indices_1, indices_2)) df = pd.read_csv(events_fsw_comparison_theta_0) df_filt = df[df["StartType"] != -1] selected_rows_1 = df_filt.iloc[indices] - np.testing.assert_array_equal(xb[indices], - selected_rows_1["Xb"].astype('float')) - np.testing.assert_array_equal(yb[indices], - selected_rows_1["Yb"].astype('float')) + np.testing.assert_array_equal(xb[indices], selected_rows_1["Xb"].astype("float")) + np.testing.assert_array_equal(yb[indices], selected_rows_1["Yb"].astype("float")) + + +@pytest.fixture() +def tof_ssd(indices_stop_type_8_to_15, events_fsw_comparison_theta_0): + indices, events_dataset = indices_stop_type_8_to_15 + + df = pd.read_csv(events_fsw_comparison_theta_0) + df_filt = df[df["StartType"] != -1] + selected_rows_1 = df_filt.iloc[indices] + + tof, xb, yb = get_ssd_positions( + indices, events_dataset, selected_rows_1.Xf.values.astype("float") + ) + return xb, yb, tof + + +def test_xb_yb_ssd( + indices_stop_type_8_to_15, + tof_ssd, + events_fsw_comparison_theta_0, +): + xb, yb, tof = tof_ssd + indices_1, indices_2, events_dataset = indices_stop_type_1_or_2 + indices = np.concatenate((indices_1, indices_2)) + + df = pd.read_csv(events_fsw_comparison_theta_0) + df_filt = df[df["StartType"] != -1] + selected_rows_1 = df_filt.iloc[indices] + + np.testing.assert_array_equal(xb[indices], selected_rows_1["Xb"].astype("float")) + np.testing.assert_array_equal(yb[indices], selected_rows_1["Yb"].astype("float")) def test_yf( indices_start_type_1_or_2, indices_stop_type_1_or_2, events_fsw_comparison_theta_0, - tof + tof, ): - _, _, events_dataset = indices_start_type_1_or_2 + index_1, index_2, events_dataset = indices_start_type_1_or_2 df = pd.read_csv(events_fsw_comparison_theta_0) df_filt = df[df["StartType"] != -1] - d, yf = get_front_y_position(events_dataset, - df_filt.Yb.values.astype('float')) + d, yf = get_front_y_position(events_dataset, df_filt.Yb.values.astype("float")) - assert yf == pytest.approx(df_filt["Yf"].astype('float'), 1e-3) + assert yf == pytest.approx(df_filt["Yf"].astype("float"), 1e-3) - xf_test = df_filt["Xf"].astype('float').values - yf_test = df_filt["Yf"].astype('float').values + xf_test = df_filt["Xf"].astype("float").values + yf_test = df_filt["Yf"].astype("float").values - xb_test = df_filt["Xb"].astype('float').values - yb_test = df_filt["Yb"].astype('float').values + xb_test = df_filt["Xb"].astype("float").values + yb_test = df_filt["Yb"].astype("float").values r = get_path_length((xf_test, yf_test), (xb_test, yb_test), d) - assert r == pytest.approx(df_filt["r"].astype('float'), rel=1e-3) + assert r == pytest.approx(df_filt["r"].astype("float"), rel=1e-3) # TODO: test get_energy_pulse_height # pulse_height = events_dataset["ENERGY_PH"].data[index] @@ -177,49 +229,30 @@ def test_yf( # TODO: needs lookup table to test bin tof, t2, xb, yb = tof indices_1, indices_2, events_dataset = indices_stop_type_1_or_2 - indices = np.concatenate((indices_1,indices_2)) + indices = np.concatenate((indices_1, indices_2)) indices.sort() - energy = df_filt["Xf"].iloc[indices].astype('float') - r = df_filt["Xf"].iloc[indices].astype('float') - - ctof, bin = determine_species_pulse_height(energy, tof[indices], r) - assert ctof * 100 == pytest.approx(df_filt["cTOF"].iloc[indices].astype('float'), rel=1e-3) - # - # energy = float(df["Xf"].iloc[index]) - # - # # TODO: needs lookup table to test bin - # ctof, bin = determine_species_pulse_height(energy, tof, r) - # assert ctof * 100 == pytest.approx(float(df["cTOF"].iloc[index]), rel=1e-3) - - -def test_positions_3( - tests_indices, - events_fsw_comparison_theta_0, -): - """TODO.""" - - indices, events_dataset = tests_indices - - df = pd.read_csv(events_fsw_comparison_theta_0) - df.replace("FILL", GlobalConstants.INT_FILLVAL, inplace=True) - selected_rows = df.iloc[indices] - - - if events_dataset["STOP_TYPE"].data[index] in [1, 2]: - - energy = float(df["Xf"].iloc[index]) - + energy = df_filt["Xf"].iloc[indices].astype("float") + r = df_filt["r"].iloc[indices].astype("float") + ctof, bin = determine_species_pulse_height(energy, tof[indices] * 100, r) + assert ctof.values == pytest.approx( + df_filt["cTOF"].iloc[indices].astype("float").values, rel=1e-3 + ) - velocity = get_particle_velocity((xf, yf), (xb, yb), d, tof) + vhat_x, vhat_y, vhat_z = get_particle_velocity( + (xf_test[indices], yf_test[indices]), + (xb_test[indices], yb_test[indices]), + d[indices], + tof[indices], + ) - assert velocity[0] == pytest.approx( - float(df["vhatX"].iloc[index]), rel=1e-2 - ) - assert velocity[1] == pytest.approx( - float(df["vhatY"].iloc[index]), rel=1e-2 - ) - assert velocity[2] == pytest.approx( - float(df["vhatZ"].iloc[index]), rel=1e-2 - ) + assert vhat_x == pytest.approx( + df_filt["vhatX"].iloc[indices].astype("float").values, rel=1e-2 + ) + assert vhat_y == pytest.approx( + df_filt["vhatY"].iloc[indices].astype("float").values, rel=1e-2 + ) + assert vhat_z == pytest.approx( + df_filt["vhatZ"].iloc[indices].astype("float").values, rel=1e-2 + ) diff --git a/imap_processing/ultra/l1b/ultra_l1b.py b/imap_processing/ultra/l1b/ultra_l1b.py index 4f29b166c..926cebb80 100644 --- a/imap_processing/ultra/l1b/ultra_l1b.py +++ b/imap_processing/ultra/l1b/ultra_l1b.py @@ -2,7 +2,6 @@ # TODO: Decide on consistent fill values. import logging -import math from collections import defaultdict import numpy as np @@ -95,9 +94,10 @@ def get_back_positions(indices, events_dataset, xf: float): yb[index_top] = get_back_position(yb_index[stop_type_top], "YBkTp", "ultra45") # Correction for the propagation delay of the start anode and other effects. - t2[index_top] = get_image_params("TOFSC") * t1[stop_type_top] / 1024 + \ - get_image_params("TOFTPOFF") - tof[index_top] = t2[index_top] + xf[stop_type_top] * get_image_params("XFTTOF") / 32768 + t2[index_top] = get_image_params("TOFSC") * t1[stop_type_top] + get_image_params( + "TOFTPOFF" + ) + tof[index_top] = t2[index_top] + xf[stop_type_top] * get_image_params("XFTTOF") index_bottom = indices[events_dataset["STOP_TYPE"].data[indices] == 2] stop_type_bottom = events_dataset["STOP_TYPE"].data[indices] == 2 @@ -105,9 +105,12 @@ def get_back_positions(indices, events_dataset, xf: float): yb[index_bottom] = get_back_position(yb_index[stop_type_bottom], "YBkBt", "ultra45") # Correction for the propagation delay of the start anode and other effects. - t2[index_bottom] = get_image_params("TOFSC") * t1[stop_type_bottom] / 1024 + \ - get_image_params("TOFBTOFF") - tof[index_bottom] = t2[stop_type_bottom] + xf[stop_type_bottom] * get_image_params("XFTTOF") / 32768 + t2[index_bottom] = get_image_params("TOFSC") * t1[ + stop_type_bottom + ] + get_image_params("TOFBTOFF") + tof[index_bottom] = t2[index_bottom] + xf[stop_type_bottom] * get_image_params( + "XFTTOF" + ) return tof, t2, xb, yb @@ -134,7 +137,6 @@ def get_front_x_position(start_type: np.array, start_position_tdc: np.array): xf : np.array x front position (hundredths of a millimeter). """ - if np.any((start_type != 1) & (start_type != 2)): raise ValueError("Error: Invalid Start Type") @@ -146,7 +148,7 @@ def get_front_x_position(start_type: np.array, start_position_tdc: np.array): # Calculate xf and convert to hundredths of a millimeter # Note FSW uses xft_off+1.8, but the lookup table uses xft_off # Note FSW uses xft_off-.25, but the lookup table uses xft_off - xf = (xftsc * -start_position_tdc + xft_off)*100 + xf = (xftsc * -start_position_tdc + xft_off) * 100 return xf @@ -200,7 +202,9 @@ def get_front_y_position(events_dataset, yb: float): yf_estimate_2 = -40 # front position of particle (mm) # TODO: make certain yb units correct - dy_lut_2 = np.round((yb[start_type_right] / 100 - yf_estimate_2) * 256 / 81.92) # mm + dy_lut_2 = np.round( + (yb[start_type_right] / 100 - yf_estimate_2) * 256 / 81.92 + ) # mm yadj_2 = get_y_adjust(dy_lut_2) / 100 # mm yf[index_right] = (yf_estimate_2 + yadj_2) * 100 dadj_2 = np.sqrt(2) * df - yadj_2 # mm# hundredths of a millimeter @@ -322,7 +326,7 @@ def get_ssd_index(index: int, events_dataset: xarray.Dataset): return ssd_index -def get_ssd_positions(index: int, events_dataset: xarray.Dataset, xf: float): +def get_ssd_positions(indices, events_dataset: xarray.Dataset, xf: float): """ Calculate back xb, yb position for the SSDs. @@ -360,6 +364,9 @@ def get_ssd_positions(index: int, events_dataset: xarray.Dataset, xf: float): """ xb = 0 + index_left = indices[events_dataset["START_TYPE"].data[indices] == 1] + index_right = indices[events_dataset["START_TYPE"].data[indices] == 2] + # Start Type: 1=Left, 2=Right if events_dataset["START_TYPE"].data[index] == 1: # Left side = "Lt" @@ -620,15 +627,13 @@ def get_particle_velocity( v_z = d / tof # Magnitude of the velocity vector - magnitude_v = math.sqrt(v_x**2 + v_y**2 + v_z**2) + magnitude_v = np.sqrt(v_x**2 + v_y**2 + v_z**2) vhat_x = v_x / magnitude_v vhat_y = v_y / magnitude_v vhat_z = v_z / magnitude_v - velocity = (vhat_x, vhat_y, vhat_z) - - return velocity + return vhat_x, vhat_y, vhat_z def process_count_zero(data_dict: dict): diff --git a/imap_processing/ultra/lookup_tables/FM45_Startup1_ULTRA_IMGPARAMS_20240207T134735_.csv b/imap_processing/ultra/lookup_tables/FM45_Startup1_ULTRA_IMGPARAMS_20240207T134735_.csv index 1c3fe7ff6..f972c1789 100644 --- a/imap_processing/ultra/lookup_tables/FM45_Startup1_ULTRA_IMGPARAMS_20240207T134735_.csv +++ b/imap_processing/ultra/lookup_tables/FM45_Startup1_ULTRA_IMGPARAMS_20240207T134735_.csv @@ -1,2 +1,2 @@ SHCOARSE,XFTSC,XFTLTOFF,XFTRTOFF,TOFSC,TOFTPOFF,TOFBTOFF,XFTTOF,XCOINTPSC,XCOINTPOFF,XCOINBTSC,XCOINBTOFF,ETOFSC,ETOFTPOFF,ETOFBTOFF,TOFDIFFTPMIN,TOFDIFFTPMAX,TOFDIFFBTMIN,TOFDIFFBTMAX,ETOFMIN,ETOFMAX,ETOFSLOPE1,ETOFOFF1,ETOFSLOPE2,ETOFOFF2,SPTPPHOFF,SPBTPHOFF,YBKSSD0,YBKSSD1,YBKSSD2,YBKSSD3,YBKSSD4,YBKSSD5,YBKSSD6,YBKSSD7,TOFSSDSC,TOFSSDLTOFF0,TOFSSDLTOFF1,TOFSSDLTOFF2,TOFSSDLTOFF3,TOFSSDLTOFF4,TOFSSDLTOFF5,TOFSSDLTOFF6,TOFSSDLTOFF7,TOFSSDRTOFF0,TOFSSDRTOFF1,TOFSSDRTOFF2,TOFSSDRTOFF3,TOFSSDRTOFF4,TOFSSDRTOFF5,TOFSSDRTOFF6,TOFSSDRTOFF7,TOFSSDTOTOFF,PATHSTEEPTHRESH,PATHMEDIUMTHRESH -445027647,0.172998046875,47.5,48.5,0.05,-53.0,-53.300000000000004,0.018310546875,0.068486328125,42.25,0.068486328125,-39.75,0.1,-44.5,-44.5,22.6,26.6,22.6,26.6,-40.0,9.0,0.66669921875,10.0,0.75,-5.0,540,540,29.3,37.300000000000004,7.1000000000000005,15.1,-15.1,-7.1000000000000005,-37.300000000000004,-29.3,0.19648437500000002,-6.0,-7.0,-3.5,-4.0,-3.8000000000000003,-3.5,-6.800000000000001,-5.5,-5.5,-6.800000000000001,-3.5,-3.8000000000000003,-4.0,-3.5,-7.0,-6.0,5.300000000000001,50.0,65.0 +445027647,0.172998047,47.5,48.5,0.5,-528,-525,0.001831055,0.068486328,42.25,0.068486328,-39.75,0.1,-44.5,-44.5,22.6,26.6,22.6,26.6,-40,9,0.666699219,10,0.75,-5,540,540,29.3,37.300000000000004,7.1000000000000005,15.1,-15.1,-7.1,-37.3,-29.3,0.19648437500000002,-6,-7,-3.5,-4,-3.8,-3.5,-6.8,-5.5,-5.5,-6.8,-3.5,-3.8,-4,-3.5,-7,-6,5.300000000000001,50,65 \ No newline at end of file diff --git a/imap_processing/ultra/lookup_tables/Ultra90_image-params071823.xlsx b/imap_processing/ultra/lookup_tables/Ultra90_image-params071823.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..58eab7559de6955a017a7b798421d43c1cd08291 GIT binary patch literal 16856 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 MpZ0{z^~bOO2VU)CR{#J2 literal 0 HcmV?d00001