Initial commit: GasFlux project with core processing pipelines
This commit is contained in:
205
tests/test_processing.py
Normal file
205
tests/test_processing.py
Normal file
@ -0,0 +1,205 @@
|
||||
import pandas as pd
|
||||
from src.gasflux import processing
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
import numpy as np
|
||||
from src.gasflux.processing import min_angular_displacement
|
||||
import pytest
|
||||
|
||||
|
||||
testdf = pd.read_csv(Path(__file__).parents[1] / "src" / "gasflux" / "testdata" / "testdata.csv")
|
||||
testconfig = yaml.safe_load(open(Path(__file__).parents[1] / "src" / "gasflux" / "testdata" / "testconfig.yaml"))
|
||||
|
||||
|
||||
def load_cols(cols):
|
||||
return testdf[cols]
|
||||
|
||||
|
||||
def test_min_angular_diff_def():
|
||||
def test_min_angular_displacement():
|
||||
assert min_angular_displacement(10, 350) == 20
|
||||
assert min_angular_displacement(0, 180) == 180
|
||||
x = np.array([10, 0])
|
||||
y = np.array([350, 180])
|
||||
expected = np.array([20, 180])
|
||||
result = min_angular_displacement(x, y)
|
||||
assert np.all(result == expected), "Vectorized function failed"
|
||||
|
||||
|
||||
def test_circ_median():
|
||||
x = np.array([0, 1, 2, 359, 4, 3])
|
||||
median = processing.circ_median(x)
|
||||
assert median == 1.5, "Circular median not calculated correctly"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"plane_angle,expected_winddir_rel,expected_windspeed_normal",
|
||||
[
|
||||
(
|
||||
90,
|
||||
[0, 90, 0, 90, 0],
|
||||
[5, 0, 5, 0, 5],
|
||||
),
|
||||
(
|
||||
30,
|
||||
[60, 30, 60, 30, 60],
|
||||
np.array([1 / 2, np.sqrt(3) / 2, 1 / 2, np.sqrt(3) / 2, 1 / 2]) * 5,
|
||||
),
|
||||
(
|
||||
60,
|
||||
[30, 60, 30, 60, 30],
|
||||
np.array([np.sqrt(3) / 2, 1 / 2, np.sqrt(3) / 2, 1 / 2, np.sqrt(3) / 2]) * 5,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_wind_offset_correction_parametrized(plane_angle, expected_winddir_rel, expected_windspeed_normal):
|
||||
data = {"winddir": [0, 90, 180, 270, 360], "windspeed": [5, 5, 5, 5, 5]}
|
||||
df = pd.DataFrame(data)
|
||||
corrected_df = processing.wind_offset_correction(df, plane_angle)
|
||||
assert "winddir_rel" in corrected_df.columns, f"Relative wind direction column not added for angle {plane_angle}"
|
||||
assert "windspeed" in corrected_df.columns, f"Normalised wind speed column not added for angle {plane_angle}"
|
||||
assert np.allclose(corrected_df["winddir_rel"], expected_winddir_rel, rtol=1e-5, atol=1e-10), (
|
||||
f"Relative wind directions not calculated correctly for angle {plane_angle}"
|
||||
)
|
||||
assert np.allclose(corrected_df["windspeed"], expected_windspeed_normal, rtol=1e-5, atol=1e-10), (
|
||||
f"Normalised wind speeds not calculated correctly for angle {plane_angle}"
|
||||
)
|
||||
|
||||
|
||||
def test_bimodal_azimuth():
|
||||
input_mode = testconfig["transect_azimuth"]
|
||||
input_reciprocal_mode = (input_mode + 180) % 360
|
||||
df = load_cols(["course_azimuth", "height_ato"])
|
||||
mode1, mode2 = processing.bimodal_azimuth(df)
|
||||
assert (
|
||||
min_angular_displacement(mode1, input_mode) < 3 or min_angular_displacement(mode1, input_reciprocal_mode) < 3
|
||||
), "Mode1 does not match expected azimuth or its reciprocal within 3 degrees"
|
||||
|
||||
if min_angular_displacement(mode1, input_mode) < 3:
|
||||
assert min_angular_displacement(mode2, input_reciprocal_mode) < 3, (
|
||||
"Mode2 does not match expected reciprocal azimuth within 3 degrees"
|
||||
)
|
||||
else:
|
||||
assert min_angular_displacement(mode2, input_mode) < 3, "Mode2 does not match expected azimuth within 3 degrees"
|
||||
|
||||
|
||||
def test_bimodal_elevation():
|
||||
df = load_cols(["course_elevation", "height_ato"])
|
||||
input_mode = 0
|
||||
input_reciprocal_mode = 0 - input_mode
|
||||
mode1, mode2 = processing.bimodal_elevation(df)
|
||||
assert (
|
||||
min_angular_displacement(mode1, input_mode) < 3 or min_angular_displacement(mode1, input_reciprocal_mode) < 3
|
||||
), "Mode1 does not match expected elevation or its reciprocal within 3 degrees"
|
||||
if min_angular_displacement(mode1, input_mode) < 3:
|
||||
assert min_angular_displacement(mode2, input_reciprocal_mode) < 3, (
|
||||
"Mode2 does not match expected reciprocal elevation within 3 degrees"
|
||||
)
|
||||
else:
|
||||
assert min_angular_displacement(mode2, input_mode) < 3, (
|
||||
"Mode2 does not match expected elevation within 3 degrees"
|
||||
)
|
||||
|
||||
|
||||
def test_height_transect_splitter():
|
||||
df = load_cols(["height_ato"])
|
||||
df, fig = processing.height_transect_splitter(df)
|
||||
assert "transect_num" in df.columns, "Transect number column not added to dataframe"
|
||||
assert df["transect_num"].nunique() == testconfig["number_of_transects"], (
|
||||
"Dataframe was not split into the right number of transects"
|
||||
)
|
||||
|
||||
|
||||
def test_add_transect_azimuth_switches():
|
||||
df = load_cols(["course_azimuth"])
|
||||
df = processing.add_transect_azimuth_switches(df)
|
||||
assert df["transect_num"].nunique() == testconfig["number_of_transects"], (
|
||||
"Transect azimuth switches not added to dataframe"
|
||||
)
|
||||
|
||||
|
||||
def test_course_filter():
|
||||
df = load_cols(["course_azimuth", "course_elevation", "height_ato"])
|
||||
azimuth_filter = testconfig["filters"]["course_filter"]["azimuth_filter"]
|
||||
azimuth_window = testconfig["filters"]["course_filter"]["azimuth_window"]
|
||||
elevation_filter = testconfig["filters"]["course_filter"]["elevation_filter"]
|
||||
df_filtered, df_unfiltered = processing.course_filter(
|
||||
df, azimuth_filter=azimuth_filter, azimuth_window=azimuth_window, elevation_filter=elevation_filter
|
||||
)
|
||||
input_mode = testconfig["transect_azimuth"]
|
||||
input_reciprocal_mode = (input_mode + 180) % 360
|
||||
# assert that the filtered dataframe contains the expected azimuth or its reciprocal within the window
|
||||
df_filtered["near_mode1"] = df_filtered["rolling_course_azimuth"].apply(
|
||||
lambda x: min_angular_displacement(x, input_mode) < azimuth_window
|
||||
)
|
||||
df_filtered["near_mode2"] = df_filtered["rolling_course_azimuth"].apply(
|
||||
lambda x: min_angular_displacement(x, input_reciprocal_mode) < azimuth_window
|
||||
)
|
||||
assert df_filtered["near_mode1"].any() or df_filtered["near_mode2"].any(), (
|
||||
"Filtered dataframe does not contain expected azimuth or its reciprocal within the window"
|
||||
)
|
||||
|
||||
|
||||
def test_mCount_max():
|
||||
data_dict = {1: -5.4, 2: 0.6, 3: 5.6, 4: 3.2, 5: 10.4, 6: 18.4, 7: 20.8, 8: 19.4}
|
||||
start, end = processing.mCount_max(data_dict)
|
||||
assert start == 4, "Start index of max count not calculated correctly"
|
||||
assert end == 7, "End index of max count not calculated correctly"
|
||||
|
||||
|
||||
def test_largest_monotonic_transect_series():
|
||||
df = load_cols(
|
||||
["timestamp", "height_ato", "course_azimuth", "longitude", "latitude", "utm_easting", "utm_northing"]
|
||||
)
|
||||
df, starttransect, endtransect = processing.largest_monotonic_transect_series(df)
|
||||
starttransect = 1
|
||||
endtransect = testconfig["number_of_transects"]
|
||||
assert starttransect == starttransect, "Start index of largest monotonic transect not calculated correctly"
|
||||
assert endtransect == endtransect, "End index of largest monotonic transect not calculated correctly"
|
||||
|
||||
|
||||
def test_remove_non_transects():
|
||||
df = load_cols(
|
||||
["height_ato", "course_azimuth", "course_elevation", "longitude", "latitude", "utm_easting", "utm_northing"]
|
||||
)
|
||||
retained_df, removed_df = processing.remove_non_transects(df)
|
||||
assert retained_df is not None, "Retained dataframe is None"
|
||||
assert removed_df is not None, "Removed dataframe is None"
|
||||
|
||||
|
||||
def test_flatten_linear_plane():
|
||||
df = load_cols(["height_ato", "utm_easting", "utm_northing"])
|
||||
df, plane_angle = processing.flatten_linear_plane(df)
|
||||
input_plane_angle = testconfig["transect_azimuth"]
|
||||
reciprocal_plane_angle = (input_plane_angle + 180) % 360
|
||||
assert (
|
||||
min_angular_displacement(plane_angle, input_plane_angle) < 3
|
||||
or min_angular_displacement(plane_angle, reciprocal_plane_angle) < 3
|
||||
), "Plane angle not calculated correctly"
|
||||
|
||||
|
||||
def test_drone_anemo_to_point_wind():
|
||||
data = {
|
||||
"yaw": [0, 90, 0, -90, 180],
|
||||
"anemo_u": [0, 0, 10, 10, 10],
|
||||
"anemo_v": [0, 0, 0, 0, 0],
|
||||
"easting": [0, 10, 0, 10, 0],
|
||||
"northing": [0, 0, 0, 0, 10],
|
||||
}
|
||||
df_test = pd.DataFrame(data)
|
||||
yaw_col = "yaw"
|
||||
anemo_u_col = "anemo_u"
|
||||
anemo_v_col = "anemo_v"
|
||||
easting_col = "easting"
|
||||
northing_col = "northing"
|
||||
result_df = processing.drone_anemo_to_point_wind(
|
||||
df_test, yaw_col, anemo_u_col, anemo_v_col, easting_col, northing_col
|
||||
)
|
||||
expected_windspeed = np.array([0, 10, 10, np.sqrt(200), np.sqrt(200)])
|
||||
expected_winddir = np.array(
|
||||
[180, 270, 270, 225, 135]
|
||||
) # 180 not zero because of the way IEEE 754 handles floating point numbers
|
||||
windspeed_diff = np.abs(result_df["windspeed"].values - expected_windspeed)
|
||||
winddir_diff = processing.min_angular_displacement(result_df["winddir"].to_numpy(), expected_winddir)
|
||||
assert np.all(windspeed_diff < 1e-10), "Wind speed not calculated correctly"
|
||||
assert np.all(np.array(winddir_diff) < 3), "Wind direction not calculated correctly"
|
||||
Reference in New Issue
Block a user