Skip to content

Commit

Permalink
Cleanup and codecov
Browse files Browse the repository at this point in the history
- rename df to less ambiguous frequency_increment
- add solve_single_time_window to helper functions
- add a test for solve_single_time_window
- factor z-time-series calculation out of coherence into spectral features
  • Loading branch information
kkappler committed Mar 16, 2024
1 parent af348f6 commit 886cac6
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 138 deletions.
7 changes: 7 additions & 0 deletions aurora/pipelines/transfer_function_kernel.py
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,13 @@ def validate_save_fc_settings(self):
for dec_level_config in self.config.decimations:
# if dec_level_config.save_fcs:
dec_level_config.save_fcs = False

# TODO: Add logic here that checks if remote reference station is in processing and save_FCs is True
# -- logger.critical() This is not a supported configuration (but it might just work!)
# -- basically, you would need to have runs at the local and remote station be simultaneous to work
# -- if they overlap in a janky way, then the saved FCs will be janky
# -- recommend that you apply single station processing to local and remote station with save FCs =True
# -- and then run remote reference processing (with save FCs =True)
return

def get_mth5_file_open_mode(self):
Expand Down
14 changes: 7 additions & 7 deletions aurora/time_series/frequency_band_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,8 @@ def adjust_band_for_coherence_sorting(frequency_band, spectrogram, rule="min3"):
logger.warning("Cant evaluate coherence with only 1 harmonic")
logger.info(f"Widening band according to {rule} rule")
if rule == "min3":
band.frequency_min -= spectrogram.df
band.frequency_max += spectrogram.df
band.frequency_min -= spectrogram.frequency_increment
band.frequency_max += spectrogram.frequency_increment

Check warning on line 189 in aurora/time_series/frequency_band_helpers.py

View check run for this annotation

Codecov / codecov/patch

aurora/time_series/frequency_band_helpers.py#L183-L189

Added lines #L183 - L189 were not covered by tests
else:
msg = f"Band adjustment rule {rule} not recognized"
logger.error(msg)
Expand Down Expand Up @@ -264,18 +264,18 @@ class Spectrogram(object):

def __init__(self, dataset=None):
self._dataset = dataset
self._df = None
self._frequency_increment = None

Check warning on line 267 in aurora/time_series/frequency_band_helpers.py

View check run for this annotation

Codecov / codecov/patch

aurora/time_series/frequency_band_helpers.py#L266-L267

Added lines #L266 - L267 were not covered by tests

@property
def dataset(self):
return self._dataset

Check warning on line 271 in aurora/time_series/frequency_band_helpers.py

View check run for this annotation

Codecov / codecov/patch

aurora/time_series/frequency_band_helpers.py#L271

Added line #L271 was not covered by tests

@property
def delta_freq(self):
if self._df is None:
def frequency_increment(self):
if self._frequency_increment is None:
frequency_axis = self.dataset.frequency
self._df = frequency_axis.data[1] - frequency_axis.data[0]
return self._df
self._frequency_increment = frequency_axis.data[1] - frequency_axis.data[0]
return self._frequency_increment

Check warning on line 278 in aurora/time_series/frequency_band_helpers.py

View check run for this annotation

Codecov / codecov/patch

aurora/time_series/frequency_band_helpers.py#L275-L278

Added lines #L275 - L278 were not covered by tests

def num_harmonics_in_band(self, frequency_band, epsilon=1e-7):
"""
Expand Down
31 changes: 31 additions & 0 deletions aurora/transfer_function/regression/helper_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,34 @@ def rme_beta(r0):
"""
beta = 1.0 - np.exp(-r0)
return beta


def solve_single_time_window(Y, X, R=None):
"""
Cast problem Y = Xb into scipy.linalg.solve form which solves: a @ x = b
- This function is used for testing vectorized, direct solver.
Parameters
----------
Y: numpy.ndarray
The "output" of regression problem. For testing this often has shape (2,).
X: numpy.ndarray
The "input" of regression problem. For testing this often has shape (2,2).
R: numpy.ndarray or None
Remote reference channels (optional)
Returns
-------
z: numpy.ndarray
The TF estimate -- Usually two complex numbers, (Zxx, Zxy), or (Zyx, Zyy)
"""
if R is None:
xH = X.conjugate().transpose()

Check warning on line 65 in aurora/transfer_function/regression/helper_functions.py

View check run for this annotation

Codecov / codecov/patch

aurora/transfer_function/regression/helper_functions.py#L64-L65

Added lines #L64 - L65 were not covered by tests
else:
xH = R.conjugate().transpose()
a = xH @ X
b = xH @ Y
z = np.linalg.solve(a, b)
return z

Check warning on line 71 in aurora/transfer_function/regression/helper_functions.py

View check run for this annotation

Codecov / codecov/patch

aurora/transfer_function/regression/helper_functions.py#L67-L71

Added lines #L67 - L71 were not covered by tests
134 changes: 3 additions & 131 deletions aurora/transfer_function/weights/coherence_weights.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@

from aurora.time_series.frequency_band_helpers import adjust_band_for_coherence_sorting
from aurora.time_series.frequency_band_helpers import Spectrogram
from aurora.transfer_function.weights.spectral_features import (

Check warning on line 24 in aurora/transfer_function/weights/coherence_weights.py

View check run for this annotation

Codecov / codecov/patch

aurora/transfer_function/weights/coherence_weights.py#L22-L24

Added lines #L22 - L24 were not covered by tests
estimate_time_series_of_impedances,
)
from collections import namedtuple
from loguru import logger

Check warning on line 28 in aurora/transfer_function/weights/coherence_weights.py

View check run for this annotation

Codecov / codecov/patch

aurora/transfer_function/weights/coherence_weights.py#L27-L28

Added lines #L27 - L28 were not covered by tests

Expand Down Expand Up @@ -313,137 +316,6 @@ def multiple_coherence_channel_sets(local_or_remote):
elif local_or_remote == "remote":
return (CoherenceChannel("remote", "hx"), CoherenceChannel("remote", "hy"))

Check warning on line 317 in aurora/transfer_function/weights/coherence_weights.py

View check run for this annotation

Codecov / codecov/patch

aurora/transfer_function/weights/coherence_weights.py#L314-L317

Added lines #L314 - L317 were not covered by tests

def solve_single_time_window(Y, X, R=None):
"""remember scipy.linalg.solve solves: a @ x == b"""
if R is None:
xH = X.conjugate().transpose()
else:
xH = R.conjugate().transpose()
a = xH @ X
b = xH @ Y
z = np.linalg.solve(a, b)
return z

def estimate_time_series_of_impedances(band, output_ch="ex", use_remote=True):
"""
solve:
[<rx,ex>] = [<rx,hx>, <rx,hy>] [Zxx]
[<ry,ex>] [<ry,hx>, <ry,hy>] [Zyx]
[a, b]
[c, d]
determinant where det(A) = 1/(ad-bc)
:param band: band dataset: xarray, will be spectrogram in future
:return:
Requires some nomenclature setup... for now just hard code:/
TODO: Note that cross powers can be computed once only by using Spectrogram class
and spectrogram.cross_power("CH1", "CH2")
which returns self._ch1_ch2
which initialized to None and is written when requested
:param band_dataset:
:return:
"""

def cross_power_series(ch1, ch2):
"""<ch1.H ch2> summed along frequnecy"""
return (ch1.conjugate().transpose() * ch2).sum(dim="frequency")

# def multiply_2x2xN_by_2x1xN(m2x2, m2x1):
# """
# This is sort of a sad function. There are a few places in TF estimation, where we would
# like to do matrix multiplication, of a 2x1 array on the left by a 2x2. If we want to do
# this tens of thousands of times, the options are for-looping (too slow!), reshape the problem
# to sparse matrix and do all at once (matrix too big when built naively!) or this function.
#
# An alternative, becuase the multiplication is very straigghtforward
# [a11 a12] [b1] = [a11*b1 + a12*b2]
# [a21 a22] [b2] = [a11*b1 + a12*b2]
# We can just assign the values vectorially.
#
# :param a2x2:
# :param b2x1:
# :return: ab2x1
# """
# pass

# Start by computing relevant cross powers
if use_remote:
rx = band["rx"]
ry = band["ry"]
else:
rx = band["hx"]
ry = band["hy"]
rxex = cross_power_series(rx, band["ex"])
ryex = cross_power_series(ry, band["ex"])
rxhx = cross_power_series(rx, band["hx"])
ryhx = cross_power_series(ry, band["hx"])
rxhy = cross_power_series(rx, band["hy"])
ryhy = cross_power_series(ry, band["hy"])

N = len(rxex)
# Compute determinants (one per time window)
# Note that when no remote reference (rx=hx, ry=hy) then det is
# the product of the auto powers in h minus the product of the cross powers
det = rxhx * ryhy - rxhy * ryhx
det = np.real(det)

# Construct the Inverse matrices (2 x 2 x N)
inverse_matrices = np.zeros((2, 2, N), dtype=np.complex128)
inverse_matrices[0, 0, :] = ryhy / det
inverse_matrices[1, 1, :] = rxhx / det
inverse_matrices[0, 1, :] = -rxhy / det
inverse_matrices[1, 0, :] = -ryhx / det

Zxx = (
inverse_matrices[0, 0, :] * rxex.data
+ inverse_matrices[0, 1, :] * ryex.data
)
Zxy = (
inverse_matrices[1, 0, :] * rxex.data
+ inverse_matrices[1, 1, :] * ryex.data
)

# Below code is a spot check that the calculation of Zxx and Zxy from the "fast vectorized"
# method above is consistent with for-looping on np.solve

# Set up the problem in terms of linalg.solve()
idx = 0 # a time-window index to check
E = band["ex"][idx, :]
H = band[["hx", "hy"]].to_array()[:, idx].T
HH = H.conj().transpose()
a = HH.data @ H.data
b = HH.data @ E.data

# Solve using direct inverse
inv_a = np.linalg.inv(a)
zz0 = inv_a @ b
# solve using linalg.solve
zz = solve_single_time_window(b, a, None)
# compare the two solutions
assert np.isclose(np.abs(zz0 - zz), 0, atol=1e-10)

# Estimate multiple coherence with linalg.solve soln
solved_residual = E - H[:, 0] * zz[0] - H[:, 1] * zz[1]
solved_residual_energy = (np.abs(solved_residual) ** 2).sum()
output_energy = (np.abs(E) ** 2).sum()
multiple_coherence_1 = solved_residual_energy / output_energy
print("solve", multiple_coherence_1)
# Estimate multiple coherence with Zxx Zxy
homebrew_residual = E - H[:, 0] * Zxx[0] - H[:, 1] * Zxy[0]
homebrew_residual_energy = (np.abs(homebrew_residual) ** 2).sum()
multiple_coherence_2 = homebrew_residual_energy / output_energy
print(multiple_coherence_2)
assert np.isclose(
multiple_coherence_1.data, multiple_coherence_2.data, 0, atol=1e-14
)
return Zxx, Zxy

# cutoff_type = "threshold"
cutoffs = {}
cutoffs["local"] = {}
Expand Down
102 changes: 102 additions & 0 deletions aurora/transfer_function/weights/spectral_features.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import numpy as np

Check warning on line 1 in aurora/transfer_function/weights/spectral_features.py

View check run for this annotation

Codecov / codecov/patch

aurora/transfer_function/weights/spectral_features.py#L1

Added line #L1 was not covered by tests

from aurora.transfer_function.regression.helper_functions import (

Check warning on line 3 in aurora/transfer_function/weights/spectral_features.py

View check run for this annotation

Codecov / codecov/patch

aurora/transfer_function/weights/spectral_features.py#L3

Added line #L3 was not covered by tests
solve_single_time_window,
)


def estimate_time_series_of_impedances(band, output_ch="ex", use_remote=True):

Check warning on line 8 in aurora/transfer_function/weights/spectral_features.py

View check run for this annotation

Codecov / codecov/patch

aurora/transfer_function/weights/spectral_features.py#L8

Added line #L8 was not covered by tests
"""
solve:
[<rx,ex>] = [<rx,hx>, <rx,hy>] [Zxx]
[<ry,ex>] [<ry,hx>, <ry,hy>] [Zyx]
[a, b]
[c, d]
determinant where det(A) = 1/(ad-bc)
:param band: band dataset: xarray, will be spectrogram in future
:return:
Requires some nomenclature setup... for now just hard code:/
TODO: Note that cross powers can be computed once only by using Spectrogram class
and spectrogram.cross_power("CH1", "CH2")
which returns self._ch1_ch2
which initialized to None and is written when requested
:param band_dataset:
:return:
"""

def cross_power_series(ch1, ch2):

Check warning on line 34 in aurora/transfer_function/weights/spectral_features.py

View check run for this annotation

Codecov / codecov/patch

aurora/transfer_function/weights/spectral_features.py#L34

Added line #L34 was not covered by tests
"""<ch1.H ch2> summed along frequnecy"""
return (ch1.conjugate().transpose() * ch2).sum(dim="frequency")

Check warning on line 36 in aurora/transfer_function/weights/spectral_features.py

View check run for this annotation

Codecov / codecov/patch

aurora/transfer_function/weights/spectral_features.py#L36

Added line #L36 was not covered by tests

# Start by computing relevant cross powers
if use_remote:
rx = band["rx"]
ry = band["ry"]

Check warning on line 41 in aurora/transfer_function/weights/spectral_features.py

View check run for this annotation

Codecov / codecov/patch

aurora/transfer_function/weights/spectral_features.py#L39-L41

Added lines #L39 - L41 were not covered by tests
else:
rx = band["hx"]
ry = band["hy"]
rxex = cross_power_series(rx, band["ex"])
ryex = cross_power_series(ry, band["ex"])
rxhx = cross_power_series(rx, band["hx"])
ryhx = cross_power_series(ry, band["hx"])
rxhy = cross_power_series(rx, band["hy"])
ryhy = cross_power_series(ry, band["hy"])

Check warning on line 50 in aurora/transfer_function/weights/spectral_features.py

View check run for this annotation

Codecov / codecov/patch

aurora/transfer_function/weights/spectral_features.py#L43-L50

Added lines #L43 - L50 were not covered by tests

N = len(rxex)

Check warning on line 52 in aurora/transfer_function/weights/spectral_features.py

View check run for this annotation

Codecov / codecov/patch

aurora/transfer_function/weights/spectral_features.py#L52

Added line #L52 was not covered by tests
# Compute determinants (one per time window)
# Note that when no remote reference (rx=hx, ry=hy) then det is
# the product of the auto powers in h minus the product of the cross powers
det = rxhx * ryhy - rxhy * ryhx
det = np.real(det)

Check warning on line 57 in aurora/transfer_function/weights/spectral_features.py

View check run for this annotation

Codecov / codecov/patch

aurora/transfer_function/weights/spectral_features.py#L56-L57

Added lines #L56 - L57 were not covered by tests

# Construct the Inverse matrices (2 x 2 x N)
inverse_matrices = np.zeros((2, 2, N), dtype=np.complex128)
inverse_matrices[0, 0, :] = ryhy / det
inverse_matrices[1, 1, :] = rxhx / det
inverse_matrices[0, 1, :] = -rxhy / det
inverse_matrices[1, 0, :] = -ryhx / det

Check warning on line 64 in aurora/transfer_function/weights/spectral_features.py

View check run for this annotation

Codecov / codecov/patch

aurora/transfer_function/weights/spectral_features.py#L60-L64

Added lines #L60 - L64 were not covered by tests

Zxx = inverse_matrices[0, 0, :] * rxex.data + inverse_matrices[0, 1, :] * ryex.data
Zxy = inverse_matrices[1, 0, :] * rxex.data + inverse_matrices[1, 1, :] * ryex.data

Check warning on line 67 in aurora/transfer_function/weights/spectral_features.py

View check run for this annotation

Codecov / codecov/patch

aurora/transfer_function/weights/spectral_features.py#L66-L67

Added lines #L66 - L67 were not covered by tests

# Below code is a spot check that the calculation of Zxx and Zxy from the "fast vectorized"
# method above is consistent with for-looping on np.solve

# Set up the problem in terms of linalg.solve()
idx = 0 # a time-window index to check
E = band["ex"][idx, :]
H = band[["hx", "hy"]].to_array()[:, idx].T
HH = H.conj().transpose()
a = HH.data @ H.data
b = HH.data @ E.data

Check warning on line 78 in aurora/transfer_function/weights/spectral_features.py

View check run for this annotation

Codecov / codecov/patch

aurora/transfer_function/weights/spectral_features.py#L73-L78

Added lines #L73 - L78 were not covered by tests

# Solve using direct inverse
inv_a = np.linalg.inv(a)
zz0 = inv_a @ b

Check warning on line 82 in aurora/transfer_function/weights/spectral_features.py

View check run for this annotation

Codecov / codecov/patch

aurora/transfer_function/weights/spectral_features.py#L81-L82

Added lines #L81 - L82 were not covered by tests
# solve using linalg.solve
zz = solve_single_time_window(b, a, None)

Check warning on line 84 in aurora/transfer_function/weights/spectral_features.py

View check run for this annotation

Codecov / codecov/patch

aurora/transfer_function/weights/spectral_features.py#L84

Added line #L84 was not covered by tests
# compare the two solutions
assert np.isclose(np.abs(zz0 - zz), 0, atol=1e-10).all()

Check warning on line 86 in aurora/transfer_function/weights/spectral_features.py

View check run for this annotation

Codecov / codecov/patch

aurora/transfer_function/weights/spectral_features.py#L86

Added line #L86 was not covered by tests

# Estimate multiple coherence with linalg.solve soln
solved_residual = E - H[:, 0] * zz[0] - H[:, 1] * zz[1]
solved_residual_energy = (np.abs(solved_residual) ** 2).sum()
output_energy = (np.abs(E) ** 2).sum()
multiple_coherence_1 = solved_residual_energy / output_energy
print("solve", multiple_coherence_1)

Check warning on line 93 in aurora/transfer_function/weights/spectral_features.py

View check run for this annotation

Codecov / codecov/patch

aurora/transfer_function/weights/spectral_features.py#L89-L93

Added lines #L89 - L93 were not covered by tests
# Estimate multiple coherence with Zxx Zxy
homebrew_residual = E - H[:, 0] * Zxx[0] - H[:, 1] * Zxy[0]
homebrew_residual_energy = (np.abs(homebrew_residual) ** 2).sum()
multiple_coherence_2 = homebrew_residual_energy / output_energy
print(multiple_coherence_2)
assert np.isclose(

Check warning on line 99 in aurora/transfer_function/weights/spectral_features.py

View check run for this annotation

Codecov / codecov/patch

aurora/transfer_function/weights/spectral_features.py#L95-L99

Added lines #L95 - L99 were not covered by tests
multiple_coherence_1.data, multiple_coherence_2.data, 0, atol=1e-14
).all()
return Zxx, Zxy

Check warning on line 102 in aurora/transfer_function/weights/spectral_features.py

View check run for this annotation

Codecov / codecov/patch

aurora/transfer_function/weights/spectral_features.py#L102

Added line #L102 was not covered by tests

0 comments on commit 886cac6

Please sign in to comment.