Skip to content

Add functions for broadband TS spectrum - #1664

Open
LOCEANlloydizard wants to merge 32 commits into
echostack-org:mainfrom
LOCEANlloydizard:broadband_processing
Open

LOCEANlloydizard wants to merge 32 commits into
echostack-org:mainfrom
LOCEANlloydizard:broadband_processing

Conversation

@LOCEANlloydizard

@LOCEANlloydizard LOCEANlloydizard commented May 17, 2026 •

Copy link
Copy Markdown
Collaborator

Refs: #582, #1033, #1034, #1651

- Add public `compute_Sv_f()` and `compute_TS_f()` for the API
- Add `_cal_complex_samples_f()` prototype
- Expose `frequency_resolution` and `range_step` parameters for future spectral calibration
- Return prototype `Sv_f` xarray output structure:
  `(channel, ping_time, svf_range, frequency)`
Use lazy method getattr in calibration dispatcher to avoid access to unsupported compute_Sv_f / compute_TS_f methods on non-FM calibrators
@codecov-commenter

codecov-commenter commented May 17, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 55.00000% with 18 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.56%. Comparing base (6cf6cee) to head (c6886e3).
⚠️ Report is 45 commits behind head on main.

Files with missing lines Patch % Lines
echopype/calibrate/calibrate_ek.py 35.29% 11 Missing ⚠️
echopype/calibrate/api.py 66.66% 7 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1664      +/-   ##
==========================================
- Coverage   85.58%   85.56%   -0.02%     
==========================================
  Files          79       78       -1     
  Lines        6998     7074      +76     
==========================================
+ Hits         5989     6053      +64     
- Misses       1009     1021      +12     
Flag Coverage Δ
integration 80.81% <55.00%> (+0.17%) ⬆️
unit 60.22% <55.00%> (-0.20%) ⬇️
unittests 85.46% <55.00%> (-0.02%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@leewujung

Copy link
Copy Markdown
Member

Hey @LOCEANlloydizard, I thought of a few other params after our discussion the other day so thought I'd lay them out and see what you think. Feel free to ping for more discussions!

def compute_Sv_spectrum(
    self,
    NFFT: int,
    range_step: float = 0.5,
    window: str = "hann",  # TODO: should this be default to None?
    frequency_resolution: Optional[float] = None,
) -> xr.Dataset:
    """Compute broadband EK80 Sv spectrum, or Sv(f).

    Parameters
    ----------
    NFFT : int
        Number of points in the FFT window for computing spectrum.
    range_step : float, default 0.5
        Range spacing in meters for the output Sv(f) window centres.
    window : str, default "hann"
        Type of window function to apply to each FFT segment.
        Options include "hann", "hamming", etc.
    frequency_resolution : optional[float], default None
        Frequency spacing in Hz for the output spectral grid.
        When this is provided, the output Sv(f) will be interpolated to this frequency grid.
        If None, the output Sv(f) will be on the original FFT frequency grid.

    Returns
    -------
    xr.Dataset
        A Dataset containing frequency-dependent volume backscattering strength.
    """


def compute_TS_spectrum(
    self,
    point_locations: xr.DataSet,
    NFFT: int,
    split_front: float = 0.25,
    window: str = "hann",
    frequency_resolution: Optional[float] = None,
) -> xr.Dataset:
    """Compute broadband EK80 Sv spectrum, or Sv(f).

    Parameters
    ----------
    point_locations : xr.DataSet
        Locations of the points for which to compute the spectrum.
        This is required to be an xarray.DataSet with dimensions ("target_id") and
        data variables "ping_time" and "depth" for each ``target_id``.
    NFFT : int
        Number of points in the FFT window for computing spectrum.
    split_front : float, default 0.25
        Each echo spectrum is computed from a segment of the complex echo signal.
        This parameter specifies how to identify the segment around the specified point location.
        For example, if split_front=0.25, then 25% the NFFT length will be before the point location,
        and the remaining 75% will be after the point location.
    window : str, default "hann"
        Type of window function to apply to each FFT segment.
        Options include "hann", "hamming", etc.
    frequency_resolution : optional[float], default None
        Frequency spacing in Hz for the output spectral grid.
        When this is provided, the output Sv(f) will be interpolated to this frequency grid.
        If None, the output Sv(f) will be on the original FFT frequency grid.

    Returns
    -------
    xr.Dataset
        A Dataset containing frequency-dependent point backscattering strength.
    """

@LOCEANlloydizard LOCEANlloydizard changed the title Add prototype API for FM EK80 Add functions for broadband Sv and TS spectrum Jun 6, 2026
- Add compute_TS_spectrum() for EK80 FM complex data
- Add broadband TS spectrum processing pipeline and calibration helpers
- Add CRIMAC reference dataset and validation tests for TS(f)
- Add frequency-dependent absorption validation against CRIMAC
- Add broadband Sp and Sv(f) integration tests
- Add pooch support for ts_spectrum_example_data bundle
- Refactor TS/Sp calibration workflow and API exports
- Add cached test-data handling to avoid unnecessary downloads
adding tqdm, most likely temporarily
try to fix error occuring only in ubuntu job
@LOCEANlloydizard LOCEANlloydizard changed the title Add functions for broadband Sv and TS spectrum Add functions for broadband TS spectrum Jun 8, 2026
Comment thread echopype/calibrate/api.py
stacklevel=2,
)

waveform_mode = "BB" if waveform_mode in ("FM", "BB") else waveform_mode

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This basically makes BB the default instead of FM. I think it'll be better to just wire everything to be based on FM right now, so that when we think it's time to remove support for the BB syntax, we just need to remove this line and the warning.

Comment thread echopype/calibrate/api.py

if compute_method is None:
raise ValueError(
f"{cal_type} calibration is not supported for " f"{echodata.sonar_model} data."

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
f"{cal_type} calibration is not supported for " f"{echodata.sonar_model} data."
f"{cal_type} computation is not supported for " f"{echodata.sonar_model} data."

Comment thread echopype/calibrate/api.py
"Sp": "compute_Sp",
"TS": "compute_TS",
"Sv": "compute_Sv",
# add Sp_spectrum??

@leewujung leewujung Jul 31, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I think there should be compute_Sp_spectrum and compute_Sv_spectrum.

updates: from our chat sounds like they will get added after the single target detection PR gets updated (since it's tied to this one in some specific aspects)?

Comment thread echopype/calibrate/api.py
Comment on lines +191 to +192
if "filter_time" in cal_ds_iteration:
cal_ds_iteration = cal_ds_iteration.drop_vars("filter_time")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are the cases that cal_ds_iteration would not have filter_time? I have forgotten since I review this section last time, but is it from dataset where there is only 1 filter_time?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this is related to #1653?

Comment thread echopype/calibrate/api.py
Comment on lines +234 to 257
if "range_sample" in ds:
ds["range_sample"].attrs = {"long_name": "Along-range sample number, base 0"}

if "echo_range" in ds:
ds["echo_range"].attrs = {
"long_name": "Range distance",
"units": "m",
}

if "frequency" in ds:
ds["frequency"].attrs = {
"long_name": "Frequency",
"units": "Hz",
}

ds[cal_type].attrs = {
"long_name": {
"Sv": "Volume backscattering strength (Sv re 1 m-1)",
"Sp": "Point scattering strength (Sp re 1 m^2)",
"TS": "Target strength (TS re 1 m^2)",
"Sv": "Volume backscattering strength (Sv re 1 m-1)",
"TS_spectrum": "Frequency-dependent target strength spectrum (TS(f) re 1 m^2)",
}[cal_type],
"units": "dB",
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it'll be nice for us to have a consolidated place to specify all the attributes for variables. Right now we define these in an ad-hoc way and missed many of them. It seems would be a good idea to just have a YAML or similar to list the corresponding long_name, units, and other attributes for specific variables, and in functions that generates organized datasets, always pass the dataset through a function that calls the YAML to add attributes -- so like a decorator? I'll add an issue for this.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tracked in #1747.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one more take on this:
since _add_attrs is only used once and these vars are "under our control," wouldn't it be better to expect that ALL of these vars would exist so should error out when they don't exist (and therefore there shouldn't be if-else statement)? Or, if some of these only exist for some cal_type, I think it would be better to have these assignments to go as expected behaviors instead of if-else.

Comment thread echopype/calibrate/api.py
Comment on lines +409 to +411
For CW data, Sp is computed from received power samples on the range grid.
For EK80 broadband/FM complex data, Sp is computed after pulse compression
and represents a band-averaged point-scattering-strength echogram.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not exactly sure what you mean between "on the range grid" vs "point-scattering-strength echogram". Aren't both still on the range grid, either it's the original one or the one after pulse compression?

Comment thread echopype/calibrate/api.py
Comment on lines +106 to +115
try:
method_name = compute_methods[cal_type]
except KeyError:
raise ValueError(f"Unsupported calibration type: {cal_type}") from None

compute_method = getattr(cal_obj, method_name, None)

if compute_method is None:
raise ValueError(
f"{cal_type} calibration is not supported for " f"{echodata.sonar_model} data."

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not following the logic here - wouldn't it be better to just do a check if cal_type exists in compute_methods instead of have it error out? And also from None suppresses the trace back (I just learned this!) - is this intentional?

And then there's a second getattr check that seems to have a very similar intention.

Maybe these can be consolidated to something simpler, that checks if cal_type is not None nor valid, and if otherwise spit out clear error messages?

Comment on lines +12 to +63
def _get_transducer_halves(
pc: xr.DataArray,
) -> tuple[xr.DataArray, xr.DataArray, xr.DataArray, xr.DataArray]:
"""Calculate half-transducer pulse-compressed signals.

Equivalent to CRIMAC ``calcTransducerHalves`` for 4-sector transducers.
"""
if pc.sizes["beam"] != 4:
raise NotImplementedError(
"Transducer halves are only defined for 4-sector split-beam data."
)

pc_fore = 0.5 * (pc.isel(beam=2) + pc.isel(beam=3))
pc_aft = 0.5 * (pc.isel(beam=0) + pc.isel(beam=1))
pc_star = 0.5 * (pc.isel(beam=0) + pc.isel(beam=3))
pc_port = 0.5 * (pc.isel(beam=1) + pc.isel(beam=2))

return pc_fore, pc_aft, pc_star, pc_port


def _get_splitbeam_angles(
pc: xr.DataArray,
gamma_alongship,
gamma_athwartship,
) -> tuple[xr.DataArray, xr.DataArray]:
"""Calculate raw split-beam physical angles before angle-offset correction.

For 4-sector data this follows CRIMAC ``calcAngles``. For 3-sector data,
the sector geometry follows the same convention used by ``add_splitbeam_angle``.
Angle offsets are not applied here because TS(f) beam compensation applies
frequency-dependent offsets later.
"""
if pc.sizes["beam"] == 4:
pc_fore, pc_aft, pc_star, pc_port = _get_transducer_halves(pc)

y_theta = pc_fore * np.conj(pc_aft)
y_phi = pc_star * np.conj(pc_port)

theta = np.rad2deg(
np.arcsin(np.arctan2(np.imag(y_theta), np.real(y_theta)) / gamma_alongship)
)

phi = np.rad2deg(np.arcsin(np.arctan2(np.imag(y_phi), np.real(y_phi)) / gamma_athwartship))
else:
raise NotImplementedError(
f"Split-beam angle calculation is not implemented for {pc.sizes['beam']} sectors."
)

theta.name = "angle_alongship"
phi.name = "angle_athwartship"

return theta, phi

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe these could be combined and refactored with what was implemented in add_splitbeam_angle so that we have a single place to maintain for these calculations? Also because the code there handles 3-sector transducers as well from a recent PR.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to clarify: I saw your notes under _get_beam_compensated_gain and the differences. What I meant is that the underlying functions can be merged, so that there is only 1 copy of the operation for split beam angle calculation from complex signals. The additional compensation related to angle offset can be a flag and hence optional for the add_splitbeam_angle function, for example.

Comment on lines +254 to +264
def _get_average_signal(
signal: xr.DataArray,
) -> xr.DataArray:
"""Average complex signal over transducer sectors.

Equivalent to CRIMAC ``calcAverageSignal``.
"""
out = signal.mean(dim="beam")
out.name = "average_signal"

return out

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need to keep this as a function? Seems too small and easier to keep track of if the .mean operation is used directly instead.

Comment on lines +236 to +251
def _get_pulse_compressed_signal(
beam: xr.Dataset,
matched_filter: Dict,
) -> xr.DataArray:
"""Calculate pulse-compressed complex samples for each transducer sector.

Equivalent to CRIMAC ``calcPulseCompressedSignals``.
"""
pc = compress_pulse(
backscatter=beam["backscatter_r"] + 1j * beam["backscatter_i"],
chirp=matched_filter,
)
pc = pc / get_norm_fac(chirp=matched_filter)
pc.name = "pulse_compressed_signal"

return pc

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same operation is used in split_beam_angle.get_angle_complex_samples() - maybe use this function there too. This would be part of the merging/refactoring to have a single place to host split-beam angle calculation code as commented here.

Comment on lines +805 to +842
gain_db = np.interp(
frequency,
self.vend["cal_frequency"].values,
self.vend["gain"].sel(cal_channel_id=channel).values,
)

angle_offset_alongship = np.interp(
frequency,
self.vend["cal_frequency"].values,
self.vend["angle_offset_alongship"].sel(cal_channel_id=channel).values,
)

angle_offset_athwartship = np.interp(
frequency,
self.vend["cal_frequency"].values,
self.vend["angle_offset_athwartship"].sel(cal_channel_id=channel).values,
)

beamwidth_alongship = np.interp(
frequency,
self.vend["cal_frequency"].values,
self.vend["beamwidth_alongship"].sel(cal_channel_id=channel).values,
)

beamwidth_athwartship = np.interp(
frequency,
self.vend["cal_frequency"].values,
self.vend["beamwidth_athwartship"].sel(cal_channel_id=channel).values,
)

beam_correction_db = self._get_beam_correction(
theta=theta,
phi=phi,
angle_offset_alongship=angle_offset_alongship,
angle_offset_athwartship=angle_offset_athwartship,
beamwidth_alongship=beamwidth_alongship,
beamwidth_athwartship=beamwidth_athwartship,
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part of the code that does the scaling of parameters based on frequency overlaps with what's in cal_params.get_cal_params_EK(). Similar to the split-beam angle calculation, maybe it makes sense for us to come up with a module that does the scaling, and just use it in both here and cal parameter preparation?

return mf_auto[idx_start:idx_stop]


def _extract_target_from_range_gate(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel this function should probably go to a separate module and not in ek80_complex.py since it's not related to the complex data operations. Not exactly sure how to call it.

@leewujung leewujung Sep 21, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the ops in this function, I am not sure I agree with this approach to change everything to pure numpy array and then use pure indexing to find the values corresponding with a specific range gate. I think the same operations can be completed in xarray and the intention would be clearer because the dimensions/coordinates are explicit.

Comment on lines +1009 to +1010
except ValueError:
continue

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is very weird - silent failure without any warning???

Comment on lines +1029 to +1034
if mf_auto_red.size < n_fft:
mf_pad = np.zeros(n_fft, dtype=complex)
mf_pad[: mf_auto_red.size] = mf_auto_red
mf_auto_red = mf_pad
elif mf_auto_red.size > n_fft:
mf_auto_red = mf_auto_red[:n_fft]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This padding or truncation operation is very important. I think we should have it in the docstring and also the example notebook, to demonstrate what would happen with the different length specified when computing the spectrum.

Comment on lines +196 to +211
def _compute_ts_spectrum_power(
normalized_spectrum: np.ndarray,
n_beams: int,
z_et: float,
z_er: float,
):
"""Convert normalised TS(f) spectrum to received power spectrum.

Equivalent to CRIMAC ``calcPowerFreqTS``.
"""
return _compute_complex_power(
normalized_spectrum=normalized_spectrum,
n_beams=n_beams,
z_et=z_et,
z_er=z_er,
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is over factoring, since _compute_complex_power is only used once here. _compute_complex_power also overlaps with what's in _compute_power_from_complex_signal. I think these should all be combined and refactored to use the same function.

Comment on lines +1058 to +1066
ts = _compute_ts_spectrum_calibrated(
power_spectrum=power_spectrum,
target_range=target_range,
frequency=frequency,
sound_speed=sound_speed,
absorption_f=absorption_f,
transmit_power=transmit_power,
gain_f=gain_f,
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest keeping the "gut" exposed here for readability. The docstring for the function can just become inline comments.

Comment thread echopype/calibrate/api.py
Comment on lines +523 to +558
def compute_TS_spectrum(echodata: EchoData, **kwargs) -> xr.Dataset:
"""
Compute broadband frequency-dependent target strength spectrum, TS(f),
from EK80 broadband/FM complex data.

Parameters
----------
point_locations : xr.Dataset
Locations of targets for which TS(f) should be computed.
Must contain ``channel``, ``ping_time``, and ``target_range`` for each
``target_id``. If ``target_range_min`` and ``target_range_max`` are
provided, they define the target echo segment. Otherwise, the segment
is built around ``target_range`` using ``NFFT`` and ``split_front``.

NFFT : int, optional
Number of FFT points used to compute the target spectrum. If not
provided, a value is inferred from the output frequency grid.

n_f_points : int, optional
Number of frequency points in the output TS(f) spectrum. Used when
``frequency_resolution`` is not provided.

split_front : float, default 0.25
Each echo spectrum is computed from a segment of the complex echo signal.
This parameter specifies how to position that segment around the target location
when only ``target_range`` is provided. For example, if ``split_front=0.25``,
then 25% of the NFFT window is placed before ``target_range`` and the remaining
75% after it.

window : str, tuple, float or None, default None
Window passed directly to ``scipy.signal.get_window``. If ``None``,
a rectangular/boxcar window is used.

frequency_resolution : float, optional
Desired spacing of the output frequency grid in Hz. Used to define the
frequency grid on which TS(f) is evaluated.

@leewujung leewujung Sep 21, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think allowing n_f_points and frequency_resolution is confusing when they are together with NFFT and split_front -- these two are messy enough. I would suggest that we show people how to do interpolation outside by operating on the output of this function, instead of doing it silently inside the function. Only the selected signal, NFFT, and the related padding really matters, the interpolation is cosmetic. I worry that by giving all these options people could get lost in what is really important for processing the data.

I also suggest split_front to be remained as fraction_before_point_location since "split" could be confused with split-beam.

if not out_by_channel:
raise ValueError("No valid TS_spectrum targets produced.")

return xr.concat(out_by_channel, dim="channel")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens to the frequency coordinate (specific to the channel) when you do xr.concat? Does it concat the multiple frequency vectors sequentially?

Comment on lines +1080 to +1081
if not ts_list:
continue

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better to not have silent skip like this since the user may end up having an empty dataset and it can be hard to keep track of why.


raise ValueError(f"Unsupported calibration type: {cal_type}")

def _compute_cal(self, cal_type, **kwargs) -> xr.Dataset:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this was in place before this PR already, but looking back at this I think we can actually do without the _compute_cal structure and just dispatch the following 3 functions directly in compute_Sv/Sp/TS/TS_spectrum:

  • _cal_power_samples
  • _cal_complex_samples
  • _cal_complex_samples_f

Is there an advantage in keeping this _compute_cal function?


return xr.concat(out_by_channel, dim="channel")

def _cal_complex_samples_f(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like you can just absorb what's in _cal_complex_samples_TS_spectrum here? I don't see much advantage of separating these 2. But I will make some comments about what I think could be factor out in _cal_complex_samples_TS_spectrum.

return theta, phi


def _compute_power_from_complex_signal(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This overlaps with _compute_complex_power

@leewujung leewujung left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @LOCEANlloydizard : Thanks for the PR! Sorry it took a very long time for me to review this!

Below are a few higher-level things that may make the inline comments easier to read:

  • There are some redundancy within the new code and with the old code that I think would benefit from combining and refactoring, such as:
    • frequency-based scaling of calibration parameters
    • split-beam angle estimation between beampattern compensation and add_splitbeam_angle
    • computing power from complex signals with the right scaling related to impedance
  • I'd suggest removing n_f_points and frequency_resolution as input arguments when computing spectrum, so that the function output would just be what people would get based on NFFT or target_range_min/max; any downstream interpolation the users will do outside of the function, so that there's no potential hidden/unintended errors.
  • refactoring and commenting code in _cal_complex_samples_TS_spectrum
    • it was hard to follow the logic because the code jumps between doing different things -- I think those can be better bunched up to be functions called by this method, to make things easier to follow
    • there is very minimal inline comments in this function -- it would be better to add the intention for the operations and any important caveats as comments
  • would it make sense to just always first compute the split-beam angles before estimating the TS? That is the logical step and we can enforce it in the code, so that the _cal_complex_samples_TS_spectrum function for example does not need to jump between computing the spectrum and the angles. It'll then just be computing the spectrum and grab the corresponding angles to get the right beampattern compensation.

Maybe we can talk through the function when we meet to make a plan for the above changes? I think it would be great to make this PR a one-time thing to get clean code, since EK80 is stable now and the code likely won't change much later, so it'll be a worthwhile investment.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: In Review

Development

Successfully merging this pull request may close these issues.

3 participants