diff --git a/floris/wind_data.py b/floris/wind_data.py index ab202e670..2ecac6fac 100644 --- a/floris/wind_data.py +++ b/floris/wind_data.py @@ -538,6 +538,90 @@ def plot_ti_over_ws( ax.set_ylabel("Turbulence Intensity (%)") ax.grid(True) + @staticmethod + def read_csv_long(file_path: str, + ws_col: str = 'wind_speeds', + wd_col: str = 'wind_directions', + ti_col_or_value: str | float = 'turbulence_intensities', + freq_col: str | None = None, + sep: str = ",", + ) -> WindRose: + """ + Read a long-formatted CSV file into the wind rose object. By long, what is meant + is that the wind speed, wind direction combination is given for each row in the + CSV file. The wind speed, wind direction, are + given in separate columns, and the frequency of occurrence of each combination + is given in a separate column. The frequency column is optional, and if not + provided, uniform frequency of all bins is assumed. + + The value of ti_col_or_value can be either a string or a float. If it is a string, + it is assumed to be the name of the column in the CSV file that contains the + turbulence intensity values. If it is a float, it is assumed to be a constant + turbulence intensity value for all wind speed and direction combinations. + + Args: + file_path (str): Path to the CSV file. + ws_col (str): Name of the column in the CSV file that contains the wind speed + values. Defaults to 'wind_speeds'. + wd_col (str): Name of the column in the CSV file that contains the wind direction + values. Defaults to 'wind_directions'. + ti_col_or_value (str or float): Name of the column in the CSV file that contains + the turbulence intensity values, or a constant turbulence intensity value. + freq_col (str): Name of the column in the CSV file that contains the frequency + values. Defaults to None in which case constant frequency assumed. + sep (str): Delimiter to use. Defaults to ','. + + Returns: + WindRose: Wind rose object created from the CSV file. + """ + + # Read in the CSV file + df = pd.read_csv(file_path, sep=sep) + + # Check that ti_col_or_value is a string or a float + if not isinstance(ti_col_or_value, (str, float)): + raise TypeError("ti_col_or_value must be a string or a float") + + # Check that the required columns are present + if ws_col not in df.columns: + raise ValueError(f"Column {ws_col} not found in CSV file") + if wd_col not in df.columns: + raise ValueError(f"Column {wd_col} not found in CSV file") + if ti_col_or_value not in df.columns and isinstance(ti_col_or_value, str): + raise ValueError(f"Column {ti_col_or_value} not found in CSV file") + if freq_col not in df.columns and freq_col is not None: + raise ValueError(f"Column {freq_col} not found in CSV file") + + # Get the wind speed, wind direction, and turbulence intensity values + wind_directions = df[wd_col].values + wind_speeds = df[ws_col].values + if isinstance(ti_col_or_value, str): + turbulence_intensities = df[ti_col_or_value].values + else: + turbulence_intensities = ti_col_or_value * np.ones(len(wind_speeds)) + if freq_col is not None: + freq_values = df[freq_col].values + else: + freq_values = np.ones(len(wind_speeds)) + + # Normalize freq_values + freq_values = freq_values / np.sum(freq_values) + + # Get the unique values of wind directions and wind speeds + unique_wd = np.unique(wind_directions) + unique_ws = np.unique(wind_speeds) + + # Get the step side for wind direction and wind speed + wd_step = unique_wd[1] - unique_wd[0] + ws_step = unique_ws[1] - unique_ws[0] + + # Now use TimeSeries to create a wind rose + time_series = TimeSeries(wind_directions, wind_speeds, turbulence_intensities) + + # Now build a new wind rose using the new steps + return time_series.to_wind_rose( + wd_step=wd_step, ws_step=ws_step, bin_weights=freq_values + ) class WindTIRose(WindDataBase): """ @@ -901,6 +985,83 @@ def plot_ti_over_ws( ax.set_ylabel("Mean Turbulence Intensity (%)") ax.grid(True) + @staticmethod + def read_csv_long(file_path: str, + ws_col: str = 'wind_speeds', + wd_col: str = 'wind_directions', + ti_col: str = 'turbulence_intensities', + freq_col: str | None = None, + sep: str = ",", + ) -> WindTIRose: + """ + Read a long-formatted CSV file into the WindTIRose object. By long, what is meant + is that the wind speed, wind direction and turbulence intensities + combination is given for each row in the + CSV file. The wind speed, wind direction, and turbulence intensity are + given in separate columns, and the frequency of occurrence of each combination + is given in a separate column. The frequency column is optional, and if not + provided, uniform frequency of all bins is assumed. + + Args: + file_path (str): Path to the CSV file. + ws_col (str): Name of the column in the CSV file that contains the wind speed + values. Defaults to 'wind_speeds'. + wd_col (str): Name of the column in the CSV file that contains the wind direction + values. Defaults to 'wind_directions'. + ti_col (str): Name of the column in the CSV file that contains + the turbulence intensity values. + freq_col (str): Name of the column in the CSV file that contains the frequency + values. Defaults to None in which case constant frequency assumed. + sep (str): Delimiter to use. Defaults to ','. + + Returns: + WindRose: Wind rose object created from the CSV file. + """ + + # Read in the CSV file + df = pd.read_csv(file_path, sep=sep) + + + # Check that the required columns are present + if ws_col not in df.columns: + raise ValueError(f"Column {ws_col} not found in CSV file") + if wd_col not in df.columns: + raise ValueError(f"Column {wd_col} not found in CSV file") + if ti_col not in df.columns: + raise ValueError(f"Column {ti_col} not found in CSV file") + if freq_col not in df.columns and freq_col is not None: + raise ValueError(f"Column {freq_col} not found in CSV file") + + # Get the wind speed, wind direction, and turbulence intensity values + wind_directions = df[wd_col].values + wind_speeds = df[ws_col].values + turbulence_intensities = df[ti_col].values + if freq_col is not None: + freq_values = df[freq_col].values + else: + freq_values = np.ones(len(wind_speeds)) + + # Normalize freq_values + freq_values = freq_values / np.sum(freq_values) + + # Get the unique values of wind directions and wind speeds + unique_wd = np.unique(wind_directions) + unique_ws = np.unique(wind_speeds) + unique_ti = np.unique(turbulence_intensities) + + # Get the step side for wind direction and wind speed + wd_step = unique_wd[1] - unique_wd[0] + ws_step = unique_ws[1] - unique_ws[0] + ti_step = unique_ti[1] - unique_ti[0] + + # Now use TimeSeries to create a wind rose + time_series = TimeSeries(wind_directions, wind_speeds, turbulence_intensities) + + # Now build a new wind rose using the new steps + return time_series.to_wind_ti_rose( + wd_step=wd_step, ws_step=ws_step, ti_step=ti_step,bin_weights=freq_values + ) + class TimeSeries(WindDataBase): """ diff --git a/tests/data/wind_rose.csv b/tests/data/wind_rose.csv new file mode 100644 index 000000000..fd7279d49 --- /dev/null +++ b/tests/data/wind_rose.csv @@ -0,0 +1,4 @@ +ws,wd,freq_val +8,270,0.25 +9,270,0.25 +8,280,0.5 diff --git a/tests/data/wind_ti_rose.csv b/tests/data/wind_ti_rose.csv new file mode 100644 index 000000000..e293c3e63 --- /dev/null +++ b/tests/data/wind_ti_rose.csv @@ -0,0 +1,4 @@ +ws,wd,ti,freq_val +8,270,0.06,0.25 +9,270,0.06,0.25 +8,280,0.07,0.5 diff --git a/tests/wind_data_integration_test.py b/tests/wind_data_integration_test.py index ecc8281b3..778c35403 100644 --- a/tests/wind_data_integration_test.py +++ b/tests/wind_data_integration_test.py @@ -1,3 +1,5 @@ +from pathlib import Path + import numpy as np import pytest @@ -9,6 +11,9 @@ from floris.wind_data import WindDataBase +TEST_DATA = Path(__file__).resolve().parent / "data" + + class ChildClassTest(WindDataBase): def __init__(self): pass @@ -37,13 +42,13 @@ def test_time_series_instantiation(): # Test that passing floats to wind directions and wind speeds returns a list of # length turbulence intensities - time_series = TimeSeries(270., 8.0, turbulence_intensities=np.array([0.06, 0.07, 0.08])) + time_series = TimeSeries(270.0, 8.0, turbulence_intensities=np.array([0.06, 0.07, 0.08])) np.testing.assert_allclose(time_series.wind_directions, [270, 270, 270]) np.testing.assert_allclose(time_series.wind_speeds, [8, 8, 8]) # Test that passing in all floats raises a type error with pytest.raises(TypeError): - TimeSeries(270., 8.0, 0.06) + TimeSeries(270.0, 8.0, 0.06) # Test casting of both wind speeds and TI time_series = TimeSeries(wind_directions, 8.0, 0.06) @@ -54,9 +59,7 @@ def test_time_series_instantiation(): # wind directions and wind speeds raises an error with pytest.raises(ValueError): TimeSeries( - wind_directions, - wind_speeds, - turbulence_intensities=np.array([0.06, 0.07, 0.08, 0.09]) + wind_directions, wind_speeds, turbulence_intensities=np.array([0.06, 0.07, 0.08, 0.09]) ) @@ -71,8 +74,7 @@ def test_wind_rose_init(): # Pass ti_table in as a single float and confirm it is broadcast to the correct shape wind_rose = WindRose(wind_directions, wind_speeds, ti_table=0.06) np.testing.assert_allclose( - wind_rose.ti_table, - np.array([[0.06, 0.06], [0.06, 0.06], [0.06, 0.06]]) + wind_rose.ti_table, np.array([[0.06, 0.06], [0.06, 0.06], [0.06, 0.06]]) ) # Pass ti_table in as a 2D array and confirm it is used as is @@ -83,9 +85,7 @@ def test_wind_rose_init(): # Confirm passing in a ti_table that is 1D raises an error with pytest.raises(ValueError): WindRose( - wind_directions, - wind_speeds, - ti_table=np.array([0.06, 0.06, 0.06, 0.06, 0.06, 0.06]) + wind_directions, wind_speeds, ti_table=np.array([0.06, 0.06, 0.06, 0.06, 0.06, 0.06]) ) # Confirm passing in a ti_table that is wrong dimensions raises an error @@ -94,12 +94,12 @@ def test_wind_rose_init(): # This should be ok since the frequency array shape matches the wind directions # and wind speeds - _ = WindRose(wind_directions, wind_speeds, ti_table= .06 ,freq_table=np.ones((3, 2))) + _ = WindRose(wind_directions, wind_speeds, ti_table=0.06, freq_table=np.ones((3, 2))) # This should raise an error since the frequency array shape does not # match the wind directions and wind speeds with pytest.raises(ValueError): - WindRose(wind_directions, wind_speeds, 0.06, np.ones((3, 3))) + WindRose(wind_directions, wind_speeds, 0.06, np.ones((3, 3))) def test_wind_rose_grid(): @@ -171,7 +171,7 @@ def test_unpack_for_reinitialize(): freq_table = np.array([[1.0, 0.0], [0, 1.0], [0, 0]]) # First test using default assumption only non-zero frequency cases computed - wind_rose = WindRose(wind_directions, wind_speeds, 0.06, freq_table) + wind_rose = WindRose(wind_directions, wind_speeds, 0.06, freq_table) ( wind_directions_unpack, @@ -479,88 +479,146 @@ def test_time_series_to_wind_ti_rose(): freq_table = wind_rose.freq_table np.testing.assert_almost_equal(freq_table[0, 1, :], [0, 0]) -def test_get_speed_multipliers_by_wd(): +def test_get_speed_multipliers_by_wd(): heterogenous_inflow_config_by_wd = { - 'speed_multipliers': np.array( + "speed_multipliers": np.array( [ [1.0, 1.1, 1.2], [1.1, 1.1, 1.1], [1.3, 1.4, 1.5], ] ), - 'wind_directions': np.array([0, 90, 270]) + "wind_directions": np.array([0, 90, 270]), } # Check for correctness - wind_directions = np.array([240, 80,15]) - expected_output = np.array( - [ - [1.3, 1.4, 1.5], - [1.1, 1.1, 1.1], - [1.0, 1.1, 1.2] - ] - ) + wind_directions = np.array([240, 80, 15]) + expected_output = np.array([[1.3, 1.4, 1.5], [1.1, 1.1, 1.1], [1.0, 1.1, 1.2]]) wind_data = WindDataBase() result = wind_data.get_speed_multipliers_by_wd( - heterogenous_inflow_config_by_wd, - wind_directions + heterogenous_inflow_config_by_wd, wind_directions ) assert np.allclose(result, expected_output) # Confirm wrapping behavior wind_directions = np.array([350, 10]) - expected_output = np.array([[1.0, 1.1, 1.2], - [1.0, 1.1, 1.2]]) + expected_output = np.array([[1.0, 1.1, 1.2], [1.0, 1.1, 1.2]]) result = wind_data.get_speed_multipliers_by_wd( - heterogenous_inflow_config_by_wd, - wind_directions + heterogenous_inflow_config_by_wd, wind_directions ) assert np.allclose(result, expected_output) # Confirm can expand the result to match wind directions - wind_directions = np.arange(0.0,360.0,10.0) + wind_directions = np.arange(0.0, 360.0, 10.0) num_wd = len(wind_directions) - result = wind_data.get_speed_multipliers_by_wd(heterogenous_inflow_config_by_wd, - wind_directions) + result = wind_data.get_speed_multipliers_by_wd( + heterogenous_inflow_config_by_wd, wind_directions + ) assert result.shape[0] == num_wd -def test_gen_heterogenous_inflow_config(): +def test_gen_heterogenous_inflow_config(): wind_directions = np.array([259.8, 260.2, 260.3, 260.1, 270.0]) wind_speeds = 8 turbulence_intensities = 0.06 heterogenous_inflow_config_by_wd = { - 'speed_multipliers': np.array( + "speed_multipliers": np.array( [ [0.9, 0.9], [1.0, 1.0], [1.1, 1.2], ] ), - 'wind_directions' : np.array([250, 260, 270]), - 'x' : np.array([0, 1000]), - 'y' : np.array([0, 0]), + "wind_directions": np.array([250, 260, 270]), + "x": np.array([0, 1000]), + "y": np.array([0, 0]), } time_series = TimeSeries( wind_directions, wind_speeds, turbulence_intensities=turbulence_intensities, - heterogenous_inflow_config_by_wd=heterogenous_inflow_config_by_wd + heterogenous_inflow_config_by_wd=heterogenous_inflow_config_by_wd, ) (_, _, _, _, _, heterogenous_inflow_config) = time_series.unpack() - expected_result = np.array( - [ - [1.0, 1.0], - [1.0, 1.0], - [1.0, 1.0], - [1.0, 1.0], - [1.1, 1.2] - ] + expected_result = np.array([[1.0, 1.0], [1.0, 1.0], [1.0, 1.0], [1.0, 1.0], [1.1, 1.2]]) + np.testing.assert_allclose(heterogenous_inflow_config["speed_multipliers"], expected_result) + np.testing.assert_allclose( + heterogenous_inflow_config["x"], heterogenous_inflow_config_by_wd["x"] ) - np.testing.assert_allclose(heterogenous_inflow_config['speed_multipliers'], expected_result) - np.testing.assert_allclose(heterogenous_inflow_config['x'],heterogenous_inflow_config_by_wd['x']) + + +def test_read_csv_long(): + # Read in the wind rose data from the csv file + + # First confirm that the data raises value error when wrong columns passed + with pytest.raises(ValueError): + wind_rose = WindRose.read_csv_long(TEST_DATA / "wind_rose.csv") + + # Since TI not specified in table, not giving a fixed TI should raise an error + with pytest.raises(ValueError): + wind_rose = WindRose.read_csv_long( + TEST_DATA / "wind_rose.csv", wd_col="wd", ws_col="ws", freq_col="freq_val" + ) + + # Now read in with correct columns + wind_rose = WindRose.read_csv_long( + TEST_DATA / "wind_rose.csv", + wd_col="wd", + ws_col="ws", + freq_col="freq_val", + ti_col_or_value=0.06, + ) + + # Confirm that data read in correctly, and the missing wd/ws bins are filled with zeros + expected_result = np.array([[0.25, 0.25], [0.5, 0]]) + np.testing.assert_allclose(wind_rose.freq_table, expected_result) + + # Confirm expected wind direction and wind speed values + expected_result = np.array([270, 280]) + np.testing.assert_allclose(wind_rose.wind_directions, expected_result) + + expected_result = np.array([8, 9]) + np.testing.assert_allclose(wind_rose.wind_speeds, expected_result) + + # Confirm expected TI values + expected_result = np.array([[0.06, 0.06], [0.06, np.nan]]) + + # Confirm all elements which aren't nan are close + np.testing.assert_allclose( + wind_rose.ti_table[~np.isnan(wind_rose.ti_table)], + expected_result[~np.isnan(expected_result)], + ) + + +def test_read_csv_long_ti(): + # Read in the wind rose data from the csv file + + + + # Now read in with correct columns + wind_ti_rose = WindTIRose.read_csv_long( + TEST_DATA / "wind_ti_rose.csv", + wd_col="wd", + ws_col="ws", + ti_col="ti", + freq_col="freq_val", + + ) + + # Confirm the shape of the frequency table + assert wind_ti_rose.freq_table.shape == (2, 2, 2) + + # Confirm expected wind direction and wind speed values + expected_result = np.array([270, 280]) + np.testing.assert_allclose(wind_ti_rose.wind_directions, expected_result) + + expected_result = np.array([8, 9]) + np.testing.assert_allclose(wind_ti_rose.wind_speeds, expected_result) + + expected_result = np.array([0.06, 0.07]) + np.testing.assert_allclose(wind_ti_rose.turbulence_intensities, expected_result)