From ae21c540f9f132399c80d1d048aaf34aa3d9c45e Mon Sep 17 00:00:00 2001 From: rbuckley-png Date: Wed, 10 Dec 2025 12:35:13 -0800 Subject: [PATCH 1/4] Functionality to process calibration files for 10 turn BDots. --- bapsflib/lapd/tools/bdot.py | 152 ++++++++++++++++++++++++++++++++++++ requirements/install.txt | 1 + setup.cfg | 2 +- 3 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 bapsflib/lapd/tools/bdot.py diff --git a/bapsflib/lapd/tools/bdot.py b/bapsflib/lapd/tools/bdot.py new file mode 100644 index 00000000..0ed91e78 --- /dev/null +++ b/bapsflib/lapd/tools/bdot.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +import numpy as np +import matplotlib.pyplot as plt +import argparse +import os + +# ========================= +# Configuration +# ========================= + +# Suffixes for all 9 combinations +SUFFIXES = ["BXPX", "BXPY", "BXPZ", "BYPX", "BYPY", "BYPZ", "BZPX", "BZPY", "BZPZ"] + +# ========================= +# Data loading +# ========================= + +def load_csv(filename): + """ + Loads an E5100A-style CSV file, stripping quotes and skipping non-numeric header lines. + Returns: freq, mag, phase (all numpy arrays) + """ + data_lines = [] + if not os.path.exists(filename): + print(f"WARNING: File {filename} not found. Skipping.") + return None, None, None + + with open(filename, 'r') as f: + for line in f: + line = line.strip().strip('"') + if line == "": + continue + if not line[0].isdigit() and not line[0] == "-": + continue + parts = line.split("\t") if "\t" in line else line.split(",") + try: + parts = [float(p) for p in parts] + data_lines.append(parts) + except ValueError: + continue + + data = np.array(data_lines) + freq = data[:, 0] + mag = data[:, 1] + phase = data[:, 2] + + return freq, mag, phase + +# ========================= +# Effective Area Calculation +# ========================= + +def calculate_effective_area_per_point(freq, mag): + """ + Computes effective area point-by-point using physics formula: + Ae(f) = (mag * 0.55) / (0.00180765 * freq) + """ + Ae = (mag * 0.55) / (0.00180765 * freq) + return Ae + +def calculate_effective_area_linear_fit(freq, mag): + """ + Linear fit of magnitude vs frequency for single-value Ae. + """ + slope, intercept = np.polyfit(freq, mag, 1) + Ae_single = slope * 307.1389 # calibration factor + return Ae_single, slope, intercept + +# ========================= +# Plotting and Saving +# ========================= + +def save_plots_and_effective_area(freq, mag, phase, Ae, file_basename, output_dir): + """ + Saves magnitude+effective area and phase plots as PNG, + and saves effective area array to CSV. + """ + # Ensure output directory exists + os.makedirs(output_dir, exist_ok=True) + + # Dual-axis plot: Magnitude & Effective Area + fig, ax1 = plt.subplots(figsize=(10,5)) + ax1.plot(freq, mag, 'b-', label='Magnitude') + ax1.set_xlabel('Frequency (Hz)') + ax1.set_ylabel('Magnitude (linear)', color='b') + ax1.tick_params(axis='y', labelcolor='b') + ax1.grid(True) + + ax2 = ax1.twinx() + ax2.plot(freq, Ae, 'g-', label='Effective Area') + ax2.set_ylabel('Effective Area', color='g') + ax2.tick_params(axis='y', labelcolor='g') + + plt.title(f'{file_basename}: Magnitude & Effective Area vs Frequency') + fig.tight_layout() + mag_ae_file = os.path.join(output_dir, f"{file_basename}_mag_ae.png") + plt.savefig(mag_ae_file) + plt.close(fig) + + # Phase plot + plt.figure(figsize=(10,4)) + plt.plot(freq, phase, color='orange') + plt.xlabel('Frequency (Hz)') + plt.ylabel('Phase (degrees)') + plt.title(f'{file_basename}: Phase vs Frequency') + plt.grid(True) + phase_file = os.path.join(output_dir, f"{file_basename}_phase.png") + plt.savefig(phase_file) + plt.close() + + # Save effective area array to CSV + ae_file = os.path.join(output_dir, f"{file_basename}_Ae.csv") + np.savetxt(ae_file, np.column_stack((freq, Ae)), delimiter=',', header="Frequency,EffectiveArea", comments='') + +# ========================= +# Main function +# ========================= + +def main(): + parser = argparse.ArgumentParser(description="Process E5100A CSV files by target prefix.") + parser.add_argument("target", type=str, help="Target prefix (e.g., C19)") + args = parser.parse_args() + + target = args.target.upper() # Ensure uppercase + output_dir = f"{target}_results" + + # Generate all 9 possible filenames + filenames = [f"{target}{suffix}.csv" for suffix in SUFFIXES] + + for file in filenames: + file_basename = os.path.splitext(os.path.basename(file))[0] + print(f"Processing {file}...") + freq, mag, phase = load_csv(file) + if freq is None: + continue + + # Compute effective area + Ae = calculate_effective_area_per_point(freq, mag) + Ae_single, slope, intercept = calculate_effective_area_linear_fit(freq, mag) + + # Print fitted single-value effective area + print(f"{file_basename} Fitted Effective Area: {Ae_single:.6f}") + + # Save plots and Ae array + save_plots_and_effective_area(freq, mag, phase, Ae, file_basename, output_dir) + + print(f"\nAll processed files saved in folder: {output_dir}") + +if __name__ == "__main__": + main() + + diff --git a/requirements/install.txt b/requirements/install.txt index a54aa973..29f1b2e7 100644 --- a/requirements/install.txt +++ b/requirements/install.txt @@ -6,3 +6,4 @@ bapsf_motion >= 0.2 h5py >= 3.0 numpy >= 1.20 scipy >= 0.19 +matplotlib >= 3.10.0 \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 5946c9bf..9f1c4013 100644 --- a/setup.cfg +++ b/setup.cfg @@ -46,7 +46,7 @@ install_requires = h5py >= 3.0 numpy >= 1.20 scipy >= 0.19 - + matplotlib >= 3.10.0 [options.extras_require] extras = # ought to mirror requirements/extras.txt From 02ea1e180719e980be640f57792fad9053223e31 Mon Sep 17 00:00:00 2001 From: rbuckley-png Date: Wed, 10 Dec 2025 13:02:32 -0800 Subject: [PATCH 2/4] Functionality to process calibration files for 10 turn BDots. --- bapsflib/lapd/tools/bdot.py | 53 ++++++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/bapsflib/lapd/tools/bdot.py b/bapsflib/lapd/tools/bdot.py index 0ed91e78..0e3ca3be 100644 --- a/bapsflib/lapd/tools/bdot.py +++ b/bapsflib/lapd/tools/bdot.py @@ -15,6 +15,7 @@ # Data loading # ========================= + def load_csv(filename): """ Loads an E5100A-style CSV file, stripping quotes and skipping non-numeric header lines. @@ -25,7 +26,7 @@ def load_csv(filename): print(f"WARNING: File {filename} not found. Skipping.") return None, None, None - with open(filename, 'r') as f: + with open(filename, "r") as f: for line in f: line = line.strip().strip('"') if line == "": @@ -46,10 +47,12 @@ def load_csv(filename): return freq, mag, phase + # ========================= # Effective Area Calculation # ========================= + def calculate_effective_area_per_point(freq, mag): """ Computes effective area point-by-point using physics formula: @@ -58,6 +61,7 @@ def calculate_effective_area_per_point(freq, mag): Ae = (mag * 0.55) / (0.00180765 * freq) return Ae + def calculate_effective_area_linear_fit(freq, mag): """ Linear fit of magnitude vs frequency for single-value Ae. @@ -66,10 +70,12 @@ def calculate_effective_area_linear_fit(freq, mag): Ae_single = slope * 307.1389 # calibration factor return Ae_single, slope, intercept + # ========================= # Plotting and Saving # ========================= + def save_plots_and_effective_area(freq, mag, phase, Ae, file_basename, output_dir): """ Saves magnitude+effective area and phase plots as PNG, @@ -79,30 +85,30 @@ def save_plots_and_effective_area(freq, mag, phase, Ae, file_basename, output_di os.makedirs(output_dir, exist_ok=True) # Dual-axis plot: Magnitude & Effective Area - fig, ax1 = plt.subplots(figsize=(10,5)) - ax1.plot(freq, mag, 'b-', label='Magnitude') - ax1.set_xlabel('Frequency (Hz)') - ax1.set_ylabel('Magnitude (linear)', color='b') - ax1.tick_params(axis='y', labelcolor='b') + fig, ax1 = plt.subplots(figsize=(10, 5)) + ax1.plot(freq, mag, "b-", label="Magnitude") + ax1.set_xlabel("Frequency (Hz)") + ax1.set_ylabel("Magnitude (linear)", color="b") + ax1.tick_params(axis="y", labelcolor="b") ax1.grid(True) ax2 = ax1.twinx() - ax2.plot(freq, Ae, 'g-', label='Effective Area') - ax2.set_ylabel('Effective Area', color='g') - ax2.tick_params(axis='y', labelcolor='g') + ax2.plot(freq, Ae, "g-", label="Effective Area") + ax2.set_ylabel("Effective Area", color="g") + ax2.tick_params(axis="y", labelcolor="g") - plt.title(f'{file_basename}: Magnitude & Effective Area vs Frequency') + plt.title(f"{file_basename}: Magnitude & Effective Area vs Frequency") fig.tight_layout() mag_ae_file = os.path.join(output_dir, f"{file_basename}_mag_ae.png") plt.savefig(mag_ae_file) plt.close(fig) # Phase plot - plt.figure(figsize=(10,4)) - plt.plot(freq, phase, color='orange') - plt.xlabel('Frequency (Hz)') - plt.ylabel('Phase (degrees)') - plt.title(f'{file_basename}: Phase vs Frequency') + plt.figure(figsize=(10, 4)) + plt.plot(freq, phase, color="orange") + plt.xlabel("Frequency (Hz)") + plt.ylabel("Phase (degrees)") + plt.title(f"{file_basename}: Phase vs Frequency") plt.grid(True) phase_file = os.path.join(output_dir, f"{file_basename}_phase.png") plt.savefig(phase_file) @@ -110,14 +116,24 @@ def save_plots_and_effective_area(freq, mag, phase, Ae, file_basename, output_di # Save effective area array to CSV ae_file = os.path.join(output_dir, f"{file_basename}_Ae.csv") - np.savetxt(ae_file, np.column_stack((freq, Ae)), delimiter=',', header="Frequency,EffectiveArea", comments='') + np.savetxt( + ae_file, + np.column_stack((freq, Ae)), + delimiter=",", + header="Frequency,EffectiveArea", + comments="", + ) + # ========================= # Main function # ========================= + def main(): - parser = argparse.ArgumentParser(description="Process E5100A CSV files by target prefix.") + parser = argparse.ArgumentParser( + description="Process E5100A CSV files by target prefix." + ) parser.add_argument("target", type=str, help="Target prefix (e.g., C19)") args = parser.parse_args() @@ -146,7 +162,6 @@ def main(): print(f"\nAll processed files saved in folder: {output_dir}") + if __name__ == "__main__": main() - - From 9399a353dfd598ef6b0e5b822c33fb850d60f4b9 Mon Sep 17 00:00:00 2001 From: rbuckley-png Date: Wed, 10 Dec 2025 13:04:55 -0800 Subject: [PATCH 3/4] Functionality to process calibration files for 10 turn BDots. --- requirements/install.txt | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements/install.txt b/requirements/install.txt index 29f1b2e7..2d3f042d 100644 --- a/requirements/install.txt +++ b/requirements/install.txt @@ -6,4 +6,4 @@ bapsf_motion >= 0.2 h5py >= 3.0 numpy >= 1.20 scipy >= 0.19 -matplotlib >= 3.10.0 \ No newline at end of file +matplotlib diff --git a/setup.cfg b/setup.cfg index 9f1c4013..7fcc2396 100644 --- a/setup.cfg +++ b/setup.cfg @@ -46,7 +46,7 @@ install_requires = h5py >= 3.0 numpy >= 1.20 scipy >= 0.19 - matplotlib >= 3.10.0 + matplotlib [options.extras_require] extras = # ought to mirror requirements/extras.txt From c2833fb86c741810222dd1201d473453dfb41c28 Mon Sep 17 00:00:00 2001 From: rbuckley-png Date: Wed, 10 Dec 2025 13:05:47 -0800 Subject: [PATCH 4/4] Functionality to process calibration files for 10 turn BDots. --- bapsflib/lapd/tools/bdot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bapsflib/lapd/tools/bdot.py b/bapsflib/lapd/tools/bdot.py index 0e3ca3be..ec6532a4 100644 --- a/bapsflib/lapd/tools/bdot.py +++ b/bapsflib/lapd/tools/bdot.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 -import numpy as np -import matplotlib.pyplot as plt import argparse +import matplotlib.pyplot as plt +import numpy as np import os # =========================