diff --git a/.github/workflows/continuous-integration-workflow.yaml b/.github/workflows/continuous-integration-workflow.yaml index 558b6d85..769044d3 100644 --- a/.github/workflows/continuous-integration-workflow.yaml +++ b/.github/workflows/continuous-integration-workflow.yaml @@ -20,7 +20,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9"] + python-version: ["3.8", "3.9"] os: [ubuntu-latest, macos-latest] steps: diff --git a/HISTORY.rst b/HISTORY.rst deleted file mode 100644 index 54b05484..00000000 --- a/HISTORY.rst +++ /dev/null @@ -1,8 +0,0 @@ -======= -History -======= - -0.1.0 (2021-12-01) ------------------- - -* First release on the NREL Github page. diff --git a/docs/source/installation.rst b/docs/source/installation.rst index 48e18f85..80ff0560 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -19,8 +19,4 @@ code will be available directly through your local Python. Remember to re-import the FLASC module when changes are made if you are working in an interactive environment like Jupyter. -In terms of dependencies, the flasc toolbox currently relies on floris -v2.4, and is not yet compatible with floris v3.0rc1. - - .. seealso:: `Return to table of contents `_ \ No newline at end of file diff --git a/docs/source/overview.rst b/docs/source/overview.rst index 0ee03aae..d3f9ac99 100644 --- a/docs/source/overview.rst +++ b/docs/source/overview.rst @@ -24,7 +24,7 @@ Citation If FLASC played a role in your research, please cite it. This software can be cited as: - FLASC. Version 0.1 (2022). Available at https://github.com/NREL/flasc. + FLASC. Version 1.0 (2022). Available at https://github.com/NREL/flasc. For LaTeX users: @@ -32,7 +32,7 @@ For LaTeX users: @misc{flasc2022, author = {NREL}, - title = {FLASC. Version 0.1}, + title = {FLASC. Version 1.0}, year = {2022}, publisher = {GitHub}, journal = {GitHub repository}, diff --git a/examples/demo_dataset/demo_floris_input.json b/examples/demo_dataset/demo_floris_input.json deleted file mode 100644 index 2c649ab9..00000000 --- a/examples/demo_dataset/demo_floris_input.json +++ /dev/null @@ -1,254 +0,0 @@ -{ - "description": "Example FLORIS Input file for FSA", - "farm": { - "description": "An example wind farm based on the U.S. onshore WED Plainfield wind farm with 7 Vensys 120/3000 wind turbines, resized to NREL 5MW turbines. Information about the farm was derived from the U.S. Wind Turbine Database (USWTDB), https://eerscmap.usgs.gov/uswtdb/viewer/#14.08/41.79589/-71.5389.", - "name": "example_plainfield_fsa", - "properties": { - "__comment__": "specified_wind_height of -1 uses the first turbine's hub height; After initialization, specified_wind_height is a free parameter.", - "air_density": 1.225, - "layout_x": [1630.222, 1176.733, 816.389, 755.938, 0.000, 1142.240, 1553.102], - "layout_y": [0.000, 297.357, 123.431, 575.544, 647.779, 772.262, 504.711], - "specified_wind_height": -1, - "turbulence_intensity": [ - 0.08 - ], - "wind_direction": [ - 90.0 - ], - "wind_shear": 0.12, - "wind_speed": [ - 9.0 - ], - "wind_veer": 0.0, - "wind_x": [ - 0 - ], - "wind_y": [ - 0 - ] - }, - "type": "farm" - }, - "floris_version": "v2.0.0", - "logging": { - "console": { - "enable": true, - "level": "INFO" - }, - "file": { - "enable": false, - "level": "INFO" - } - }, - "name": "floris_input_file_Example", - "turbine": { - "description": "NREL 5MW", - "name": "nrel_5mw", - "properties": { - "TSR": 8.0, - "blade_count": 3, - "blade_pitch": 0.0, - "generator_efficiency": 1.0, - "hub_height": 90.0, - "ngrid": 5, - "pP": 1.88, - "pT": 1.88, - "power_thrust_table": { - "power": [ - 0.0, - 0.0, - 0.1780851, - 0.28907459, - 0.34902166, - 0.3847278, - 0.40605878, - 0.4202279, - 0.42882274, - 0.43387274, - 0.43622267, - 0.43684468, - 0.43657497, - 0.43651053, - 0.4365612, - 0.43651728, - 0.43590309, - 0.43467276, - 0.43322955, - 0.43003137, - 0.37655587, - 0.33328466, - 0.29700574, - 0.26420779, - 0.23839379, - 0.21459275, - 0.19382354, - 0.1756635, - 0.15970926, - 0.14561785, - 0.13287856, - 0.12130194, - 0.11219941, - 0.10311631, - 0.09545392, - 0.08813781, - 0.08186763, - 0.07585005, - 0.07071926, - 0.06557558, - 0.06148104, - 0.05755207, - 0.05413366, - 0.05097969, - 0.04806545, - 0.04536883, - 0.04287006, - 0.04055141 - ], - "thrust": [ - 1.19187945, - 1.17284634, - 1.09860817, - 1.02889592, - 0.97373036, - 0.92826162, - 0.89210543, - 0.86100905, - 0.835423, - 0.81237673, - 0.79225789, - 0.77584769, - 0.7629228, - 0.76156073, - 0.76261984, - 0.76169723, - 0.75232027, - 0.74026851, - 0.72987175, - 0.70701647, - 0.54054532, - 0.45509459, - 0.39343381, - 0.34250785, - 0.30487242, - 0.27164979, - 0.24361964, - 0.21973831, - 0.19918151, - 0.18131868, - 0.16537679, - 0.15103727, - 0.13998636, - 0.1289037, - 0.11970413, - 0.11087113, - 0.10339901, - 0.09617888, - 0.09009926, - 0.08395078, - 0.0791188, - 0.07448356, - 0.07050731, - 0.06684119, - 0.06345518, - 0.06032267, - 0.05741999, - 0.05472609 - ], - "wind_speed": [ - 2.0, - 2.5, - 3.0, - 3.5, - 4.0, - 4.5, - 5.0, - 5.5, - 6.0, - 6.5, - 7.0, - 7.5, - 8.0, - 8.5, - 9.0, - 9.5, - 10.0, - 10.5, - 11.0, - 11.5, - 12.0, - 12.5, - 13.0, - 13.5, - 14.0, - 14.5, - 15.0, - 15.5, - 16.0, - 16.5, - 17.0, - 17.5, - 18.0, - 18.5, - 19.0, - 19.5, - 20.0, - 20.5, - 21.0, - 21.5, - 22.0, - 22.5, - 23.0, - 23.5, - 24.0, - 24.5, - 25.0, - 25.5 - ] - }, - "rloc": 0.5, - "rotor_diameter": 126.0, - "tilt_angle": 0.0, - "use_points_on_perimeter": false, - "yaw_angle": 0.0 - }, - "type": "turbine" - }, - "type": "floris_input", - "wake": { - "description": "wake", - "name": "wake_default", - "properties": { - "combination_model": "sosfs", - "deflection_model": "gauss", - "parameters": { - "wake_deflection_parameters": { - "gauss": { - "dm": 1.0, - "eps_gain": 0.2, - "use_secondary_steering": true - } - }, - "wake_turbulence_parameters": { - "crespo_hernandez": { - "ai": 0.8, - "constant": 0.5, - "downstream": -0.32, - "initial": 0.1 - } - }, - "wake_velocity_parameters": { - "gauss_legacy": { - "calculate_VW_velocities": true, - "eps_gain": 0.2, - "ka": 0.38, - "kb": 0.004, - "use_yaw_added_recovery": true - } - } - }, - "turbulence_model": "crespo_hernandez", - "velocity_model": "gauss_legacy" - }, - "type": "wake" - } -} diff --git a/examples/demo_dataset/demo_floris_input.yaml b/examples/demo_dataset/demo_floris_input.yaml new file mode 100644 index 00000000..508ba743 --- /dev/null +++ b/examples/demo_dataset/demo_floris_input.yaml @@ -0,0 +1,65 @@ +description: Example FLORIS Input file for FLASC +farm: + layout_x: + - 1630.222 + - 1176.733 + - 816.389 + - 755.938 + - 0.0 + - 1142.24 + - 1553.102 + layout_y: + - 0.0 + - 297.357 + - 123.431 + - 575.544 + - 647.779 + - 772.262 + - 504.711 + turbine_type: + - nrel_5MW +floris_version: 3.0 +flow_field: + air_density: 1.225 + reference_wind_height: 90 + turbulence_intensity: 0.08 + wind_directions: + - - 90.0 + wind_shear: 0.12 + wind_speeds: + - - 9.0 + wind_veer: 0.0 +logging: + console: + enable: true + level: INFO + file: + enable: false + level: INFO +name: floris_input_file_example +solver: + turbine_grid_points: 5 + type: turbine_grid +wake: + enable_secondary_steering: true + enable_transverse_velocities: true + enable_yaw_added_recovery: true + model_strings: + combination_model: sosfs + deflection_model: gauss + turbulence_model: crespo_hernandez + velocity_model: gauss + wake_deflection_parameters: + gauss: + dm: 1.0 + eps_gain: 0.2 + wake_turbulence_parameters: + crespo_hernandez: + ai: 0.8 + constant: 0.5 + downstream: -0.32 + initial: 0.1 + wake_velocity_parameters: + gauss: + ka: 0.38 + kb: 0.004 diff --git a/examples/demo_dataset/generate_demo_dataset.py b/examples/demo_dataset/generate_demo_dataset.py index 964946dd..a3e3aeae 100644 --- a/examples/demo_dataset/generate_demo_dataset.py +++ b/examples/demo_dataset/generate_demo_dataset.py @@ -17,7 +17,7 @@ import numpy as np import pandas as pd -import floris.tools as wfct +from floris.tools.floris_interface import FlorisInterface from floris.utilities import wrap_360 from flasc.dataframe_operations import dataframe_manipulations as dfm @@ -75,8 +75,8 @@ def get_wind_data_from_nwtc(): # Initialize the FLORIS interface fi print("Initializing the FLORIS object for our demo wind farm") file_path = os.path.dirname(os.path.abspath(__file__)) - fi_path = os.path.join(file_path, "demo_floris_input.json") - fi = wfct.floris_interface.FlorisInterface(fi_path) + fi_path = os.path.join(file_path, "demo_floris_input.yaml") + fi = FlorisInterface(fi_path) # Format columns to generic names print("Formatting the dataframe with met mast data...") @@ -106,10 +106,6 @@ def get_wind_data_from_nwtc(): wd_array=np.arange(0.0, 360.0, 3.0), ws_array=np.arange(0.0, 27.0, 1.0), ti_array=np.arange(0.03, 0.30, 0.03), - num_workers=4, - num_threads=20, - include_unc=False, - use_mpi=False, ) df_approx.to_feather(fn_approx) diff --git a/examples/energy_ratio/compare_energy_ratios_between_dfs.py b/examples/energy_ratio/compare_energy_ratios_between_dfs.py index 98a3ac71..feb7a694 100644 --- a/examples/energy_ratio/compare_energy_ratios_between_dfs.py +++ b/examples/energy_ratio/compare_energy_ratios_between_dfs.py @@ -41,7 +41,7 @@ def load_floris(): # Initialize the FLORIS interface fi print('Initializing the FLORIS object for our demo wind farm') file_path = os.path.dirname(os.path.abspath(__file__)) - fi_path = os.path.join(file_path, "../demo_dataset/demo_floris_input.json") + fi_path = os.path.join(file_path, "../demo_dataset/demo_floris_input.yaml") fi = wfct.floris_interface.FlorisInterface(fi_path) return fi @@ -52,7 +52,14 @@ def load_floris(): fi = load_floris() # Visualize layout - fi.vis_layout() + fig, ax = plt.subplots() + ax.plot(fi.layout_x, fi.layout_y, 'ko') + for ti in range(len(fi.layout_x)): + ax.text(fi.layout_x[ti], fi.layout_y[ti], "T{:02d}".format(ti)) + ax.axis("equal") + ax.grid(True) + ax.set_xlabel("x-direction (m)") + ax.set_ylabel("y-direction (m)") # We first need to define a wd against which we plot the energy ratios # In this example, we set the wind direction to be equal to the mean diff --git a/examples/energy_ratio/energy_ratio_for_single_df.py b/examples/energy_ratio/energy_ratio_for_single_df.py index ae2e6c35..99f7ab8a 100644 --- a/examples/energy_ratio/energy_ratio_for_single_df.py +++ b/examples/energy_ratio/energy_ratio_for_single_df.py @@ -39,7 +39,7 @@ def load_floris(): # Initialize the FLORIS interface fi print('Initializing the FLORIS object for our demo wind farm') file_path = os.path.dirname(os.path.abspath(__file__)) - fi_path = os.path.join(file_path, "../demo_dataset/demo_floris_input.json") + fi_path = os.path.join(file_path, "../demo_dataset/demo_floris_input.yaml") fi = wfct.floris_interface.FlorisInterface(fi_path) return fi @@ -50,7 +50,14 @@ def load_floris(): fi = load_floris() # Visualize layout - fi.vis_layout() + fig, ax = plt.subplots() + ax.plot(fi.layout_x, fi.layout_y, 'ko') + for ti in range(len(fi.layout_x)): + ax.text(fi.layout_x[ti], fi.layout_y[ti], "T{:02d}".format(ti)) + ax.axis("equal") + ax.grid(True) + ax.set_xlabel("x-direction (m)") + ax.set_ylabel("y-direction (m)") # We first need to define a wd against which we plot the energy ratios # In this example, we set the wind direction to be equal to the mean diff --git a/examples/raw_data_processing/a_04_wspowercurve_filtering_code.py b/examples/raw_data_processing/a_04_wspowercurve_filtering_code.py index fa1d2aed..90a9ce5c 100644 --- a/examples/raw_data_processing/a_04_wspowercurve_filtering_code.py +++ b/examples/raw_data_processing/a_04_wspowercurve_filtering_code.py @@ -19,18 +19,19 @@ import numpy as np import pandas as pd -from floris.tools import floris_interface as wfct +from floris import tools as wfct from flasc.dataframe_operations import dataframe_filtering as dff from flasc.turbine_analysis import ws_pow_filtering as wspcf from flasc import time_operations as top - + def load_floris(): - root_path = os.path.dirname(os.path.abspath(__file__)) - fi = wfct.FlorisInterface( - os.path.join(root_path, "..", "demo_dataset", "demo_floris_input.json") - ) + # Initialize the FLORIS interface fi + print('Initializing the FLORIS object for our demo wind farm') + file_path = os.path.dirname(os.path.abspath(__file__)) + fi_path = os.path.join(file_path, "../demo_dataset/demo_floris_input.yaml") + fi = wfct.floris_interface.FlorisInterface(fi_path) return fi diff --git a/examples/raw_data_processing/a_05a_plot_faults_with_layout.py b/examples/raw_data_processing/a_05a_plot_faults_with_layout.py index 6416f50e..401434e5 100644 --- a/examples/raw_data_processing/a_05a_plot_faults_with_layout.py +++ b/examples/raw_data_processing/a_05a_plot_faults_with_layout.py @@ -17,6 +17,8 @@ import numpy as np import pandas as pd +from floris import tools as wfct + from flasc.dataframe_operations import ( dataframe_filtering as dff, dataframe_manipulations as dfm, @@ -24,12 +26,11 @@ def load_floris(): - from floris.tools import floris_interface as wfct - - root_path = os.path.dirname(os.path.abspath(__file__)) - fi = wfct.FlorisInterface( - os.path.join(root_path, "..", "demo_dataset", "demo_floris_input.json") - ) + # Initialize the FLORIS interface fi + print('Initializing the FLORIS object for our demo wind farm') + file_path = os.path.dirname(os.path.abspath(__file__)) + fi_path = os.path.join(file_path, "../demo_dataset/demo_floris_input.yaml") + fi = wfct.floris_interface.FlorisInterface(fi_path) return fi diff --git a/examples/raw_data_processing/a_05b_cross_compare_wd_measurement_calibrations.py b/examples/raw_data_processing/a_05b_cross_compare_wd_measurement_calibrations.py index 426c34bb..d4c162b1 100644 --- a/examples/raw_data_processing/a_05b_cross_compare_wd_measurement_calibrations.py +++ b/examples/raw_data_processing/a_05b_cross_compare_wd_measurement_calibrations.py @@ -17,6 +17,7 @@ import pandas as pd import os +from floris import tools as wfct from floris.utilities import wrap_360 from flasc import ( @@ -27,12 +28,11 @@ def load_floris(): - from floris.tools import floris_interface as wfct - - root_path = os.path.dirname(os.path.abspath(__file__)) - fi = wfct.FlorisInterface( - os.path.join(root_path, "..", "demo_dataset", "demo_floris_input.json") - ) + # Initialize the FLORIS interface fi + print('Initializing the FLORIS object for our demo wind farm') + file_path = os.path.dirname(os.path.abspath(__file__)) + fi_path = os.path.join(file_path, "../demo_dataset/demo_floris_input.yaml") + fi = wfct.floris_interface.FlorisInterface(fi_path) return fi diff --git a/examples/raw_data_processing/a_07a_estimate_wd_bias_per_turbine.py b/examples/raw_data_processing/a_07a_estimate_wd_bias_per_turbine.py index 73cef09d..48e8da72 100644 --- a/examples/raw_data_processing/a_07a_estimate_wd_bias_per_turbine.py +++ b/examples/raw_data_processing/a_07a_estimate_wd_bias_per_turbine.py @@ -21,7 +21,7 @@ import pandas as pd from floris.utilities import wrap_360 -from floris.tools import floris_interface as wfct +from floris import tools as wfct from flasc.energy_ratio import energy_ratio_wd_bias_estimation as best from flasc.dataframe_operations import dataframe_manipulations as dfm @@ -31,13 +31,15 @@ def load_floris(): - root_path = os.path.dirname(os.path.abspath(__file__)) - fi = wfct.FlorisInterface( - os.path.join(root_path, "..", "demo_dataset", "demo_floris_input.json") - ) + # Initialize the FLORIS interface fi + print('Initializing the FLORIS object for our demo wind farm') + file_path = os.path.dirname(os.path.abspath(__file__)) + fi_path = os.path.join(file_path, "../demo_dataset/demo_floris_input.yaml") + fi = wfct.floris_interface.FlorisInterface(fi_path) return fi + def load_data(): # Load the data print("Loading .ftr data. This may take a minute or two...") diff --git a/examples/raw_data_processing/a_07b_wd_bias_to_df.py b/examples/raw_data_processing/a_07b_wd_bias_to_df.py index 670b62d0..5d8c49f2 100644 --- a/examples/raw_data_processing/a_07b_wd_bias_to_df.py +++ b/examples/raw_data_processing/a_07b_wd_bias_to_df.py @@ -12,23 +12,12 @@ import os - import pandas as pd -from floris.utilities import wrap_180, wrap_360 -from floris.tools import floris_interface as wfct - +from floris.utilities import wrap_360 from flasc.dataframe_operations import dataframe_manipulations as dfm -def load_floris(): - root_path = os.path.dirname(os.path.abspath(__file__)) - fi = wfct.FlorisInterface( - os.path.join(root_path, "..", "demo_dataset", "demo_floris_input.json") - ) - return fi - - def load_data(): # Load the data print("Loading .ftr data. This may take a minute or two...") diff --git a/examples/raw_data_processing/a_08_plot_energy_ratios.py b/examples/raw_data_processing/a_08_plot_energy_ratios.py index 1d886c35..9f0de61a 100644 --- a/examples/raw_data_processing/a_08_plot_energy_ratios.py +++ b/examples/raw_data_processing/a_08_plot_energy_ratios.py @@ -4,7 +4,7 @@ import numpy as np import pandas as pd -from floris.tools import floris_interface as wfct +from floris import tools as wfct from flasc.dataframe_operations import dataframe_manipulations as dfm from flasc.energy_ratio import energy_ratio_suite @@ -13,10 +13,11 @@ def load_floris(): - root_path = os.path.dirname(os.path.abspath(__file__)) - fi = wfct.FlorisInterface( - os.path.join(root_path, "..", "demo_dataset", "demo_floris_input.json") - ) + # Initialize the FLORIS interface fi + print('Initializing the FLORIS object for our demo wind farm') + file_path = os.path.dirname(os.path.abspath(__file__)) + fi_path = os.path.join(file_path, "../demo_dataset/demo_floris_input.yaml") + fi = wfct.floris_interface.FlorisInterface(fi_path) return fi @@ -49,8 +50,15 @@ def load_data(): df = load_data() fi = load_floris() - # Plot wind farm layout - fi.vis_layout() + # Visualize layout + fig, ax = plt.subplots() + ax.plot(fi.layout_x, fi.layout_y, 'ko') + for ti in range(len(fi.layout_x)): + ax.text(fi.layout_x[ti], fi.layout_y[ti], "T{:02d}".format(ti)) + ax.axis("equal") + ax.grid(True) + ax.set_xlabel("x-direction (m)") + ax.set_ylabel("y-direction (m)") # Get dataframe defining which turbines are upstream for what wind dirs df_upstream = ftools.get_upstream_turbs_floris(fi) diff --git a/flasc/floris_tools.py b/flasc/floris_tools.py index a2204b6c..19647583 100644 --- a/flasc/floris_tools.py +++ b/flasc/floris_tools.py @@ -11,7 +11,7 @@ # the License. -from copy import deepcopy +from copy import deepcopy as dcopy import matplotlib.pyplot as plt import numpy as np import pandas as pd @@ -45,11 +45,20 @@ def _run_fi_serial(df_subset, fi, include_unc=False, pow_00N. """ nturbs = len(fi.layout_x) - df_out = df_subset + df_out = df_subset.sort_values(by=["wd", "ws"]) use_model_params = ('model_params_dict' in df_subset.columns) use_yaw = ('yaw_000' in df_subset.columns) + if (use_model_params | include_unc): + raise NotImplementedError("Functionality not yet implemented since moving to floris v3.0.") + + # Specify dataframe columns + pow_cols = ["pow_{:03d}".format(ti) for ti in range(nturbs)] + ws_cols = ["ws_{:03d}".format(ti) for ti in range(nturbs)] + wd_cols = ["wd_{:03d}".format(ti) for ti in range(nturbs)] + ti_cols = ["ti_{:03d}".format(ti) for ti in range(nturbs)] + yaw_rel = np.zeros((df_out.shape[0], nturbs)) if use_yaw: yaw_cols = ['yaw_%03d' % ti for ti in range(nturbs)] @@ -61,44 +70,120 @@ def _run_fi_serial(df_subset, fi, include_unc=False, ) ) - if np.any(np.abs(yaw_rel) > 30.): + if np.any(np.abs(yaw_rel) > 30.0): raise DataError('Yaw should be defined in domain [0, 360) deg.') if 'ti' not in df_out.columns: df_out['ti'] = np.min(fi.floris.farm.turbulence_intensity) - for iii, idx in enumerate(df_out.index): - if ( - verbose and - ((np.remainder(idx, 100) == 0) or idx == df_out.shape[0]-1) - ): - print(' Progress: finished %.1f percent (%d/%d cases).' - % (100.*idx/df_out.shape[0], idx, df_out.shape[0])) - - # Update model parameters, if present in dataframe - if use_model_params: - params = df_out.loc[idx, 'model_params_dict'] - fi.set_model_parameters(params=params, verbose=False) - - fi.reinitialize_flow_field(wind_speed=df_out.loc[idx, 'ws'], - wind_direction=df_out.loc[idx, 'wd'], - turbulence_intensity=df_out.loc[idx, 'ti']) - - fi.calculate_wake(yaw_rel[iii, :]) - turbine_powers = fi.get_turbine_power(include_unc=include_unc, - unc_pmfs=unc_pmfs, - unc_options=unc_options) - for ti in range(nturbs): - df_out.loc[idx, 'pow_%03d' % ti] = turbine_powers[ti] / 1000. - df_out.loc[idx, 'wd_%03d' % ti] = df_out.loc[idx, 'wd'] # Assume uniform for now - df_out.loc[idx, 'ws_%03d' % ti] = fi.floris.farm.turbines[ti].average_velocity - df_out.loc[idx, 'ti_%03d' % ti] = fi.floris.farm.turbines[ti]._turbulence_intensity + # Perform grid-style calculation, if possible + n_unq = ( + df_out["ws"].nunique() * + df_out["wd"].nunique() * + df_out["ti"].nunique() + ) + if n_unq == df_out.shape[0]: + # Reformat things to grid style calculation + wd_array = np.sort(df_out["wd"].unique()) + ws_array = np.sort(df_out["ws"].unique()) + ti = df_out["ti"].unique()[0] + + # Specify interpolant to map data appropriately + X, Y = np.meshgrid(wd_array, ws_array, indexing='ij') + if use_yaw: + # Map the yaw angles in the appropriate format + F = interpolate.NearestNDInterpolator( + df_out[["wd", "ws"]], + yaw_rel + ) + yaw_angles = F(X, Y) + else: + yaw_angles = np.zeros((len(wd_array), len(ws_array), nturbs)) + + # Calculate the FLORIS solutions in grid-style + fi.reinitialize( + wind_directions=wd_array, + wind_speeds=ws_array, + turbulence_intensity=ti, + ) + fi.calculate_wake(yaw_angles=yaw_angles) + turbine_powers = fi.get_turbine_powers( + # include_unc=include_unc, + # unc_pmfs=unc_pmfs, + # unc_options=unc_options + ) + + # Format the found solutions back to the dataframe format + Fp = interpolate.NearestNDInterpolator( + np.vstack([X.flatten(), Y.flatten()]).T, + np.reshape(turbine_powers, (-1, nturbs)) + ) + Fws = interpolate.NearestNDInterpolator( + np.vstack([X.flatten(), Y.flatten()]).T, + np.reshape( + np.mean(fi.floris.flow_field.u, axis=(3, 4)), + (-1, nturbs) + ) + ) + Fti = interpolate.NearestNDInterpolator( + np.vstack([X.flatten(), Y.flatten()]).T, + np.reshape( + fi.floris.flow_field.turbulence_intensity_field[:, :, :, 0, 0], + (-1, nturbs) + ) + ) + + # Finally save solutions to the dataframe + df_out.loc[df_out.index, pow_cols] = Fp(df_out[["wd", "ws"]]) / 1000.0 + df_out.loc[df_out.index, wd_cols] = np.tile(df_out["wd"], (nturbs, 1)).T + df_out.loc[df_out.index, ws_cols] = Fws(df_out[["wd", "ws"]]) + df_out.loc[df_out.index, ti_cols] = Fti(df_out[["wd", "ws"]]) + + else: + # If cannot process in grid-style format, process one by one (SLOW) + for iii, idx in enumerate(df_out.index): + if ( + verbose and + ((np.remainder(idx, 100) == 0) or idx == df_out.shape[0]-1) + ): + print(' Progress: finished %.1f percent (%d/%d cases).' + % (100.*idx/df_out.shape[0], idx, df_out.shape[0])) + + # # Update model parameters, if present in dataframe + # if use_model_params: + # params = df_out.loc[idx, 'model_params_dict'] + # fi.set_model_parameters(params=params, verbose=False) + + fi.reinitialize( + wind_speeds=[df_out.loc[idx, 'ws']], + wind_directions=[df_out.loc[idx, 'wd']], + turbulence_intensity=df_out.loc[idx, 'ti'] + ) + + fi.calculate_wake(np.expand_dims(yaw_rel[iii, :], axis=[0, 1])) + turbine_powers = np.squeeze( + fi.get_turbine_powers( + # include_unc=include_unc, + # unc_pmfs=unc_pmfs, + # unc_options=unc_options + ) + ) + df_out.loc[idx, pow_cols] = turbine_powers / 1000. + df_out.loc[idx, wd_cols] = np.repeat( + df_out.loc[idx, 'wd'], + nturbs # Assumed to be uniform + ) + df_out.loc[idx, ws_cols] = np.squeeze( + np.mean(fi.floris.flow_field.u, axis=(3, 4)) + ) + df_out.loc[idx, ti_cols] = np.squeeze( + fi.floris.flow_field.turbulence_intensity_field + ) return df_out -# Define an approximate calc_floris() function -def calc_floris(df, fi, num_workers, num_threads, include_unc=False, +def calc_floris(df, fi, num_workers, job_worker_ratio=5, include_unc=False, unc_pmfs=None, unc_options=None, use_mpi=False): """Calculate the FLORIS predictions for a particular wind direction, wind speed and turbulence intensity set. This function calculates the exact solutions. @@ -119,14 +204,6 @@ def calc_floris(df, fi, num_workers, num_threads, include_unc=False, [type]: [description] """ - if (num_threads > 1) or (num_workers > 1): - if num_threads < (5 * num_workers): - print("Found 'num_threads < 2 * num_workers'.") - print("Try num_threads = (5..10) * num_workers for performance.") - elif num_threads > (10 * num_workers): - print("Found 'num_threads > 10 * num_workers'.") - print("Try num_threads = (5...10) * num_workers for performance.") - nturbs = len(fi.layout_x) # Create placeholders @@ -138,37 +215,57 @@ def calc_floris(df, fi, num_workers, num_threads, include_unc=False, if len(yaw_cols) > 0: if np.any(df[yaw_cols] < 0.): raise DataError('Yaw should be defined in domain [0, 360) deg.') - # df_out[yaw_cols] = df[yaw_cols].copy() - - # Split dataframe into smaller dataframes - N = df.shape[0] - dN = int(np.ceil(N / num_threads)) - df_list = [] - for ii in range(num_threads): - if ii == num_threads - 1: - df_list.append(df.iloc[ii*dN::]) + + # Split dataframe into subset dataframes for parallelization, if necessary + if num_workers > 1: + df_list = [] + + # See if we can simply split the problem up into a grid of conditions + num_jobs = num_workers * job_worker_ratio + n_unq = df["ws"].nunique() * df["wd"].nunique() * df["ti"].nunique() + if n_unq == df.shape[0]: + # Data is a grid of atmospheric conditions. Can divide and exploit + # the benefit of grid processing in floris v3.0. + Nconds_per_ti = df["ws"].nunique() * df["wd"].nunique() + Njobs_per_ti = int(np.floor(num_jobs / df["ti"].nunique())) + dN = int(np.ceil(Nconds_per_ti / Njobs_per_ti)) + + for ti in df["ti"].unique(): + df_subset = df[df["ti"] == ti] + for ij in range(Njobs_per_ti): + df_list.append(df_subset.iloc[(ij*dN):((ij+1)*dN)]) + else: - df_list.append(df.iloc[ii*dN:(ii+1)*dN]) + # If cannot be formatted to grid style, split blindly + dN = int(np.ceil(df.shape[0] / num_jobs)) + for ij in range(num_jobs): + df_list.append(df.iloc[(ij*dN):((ij+1)*dN)]) + # Calculate solutions - print('Calculating with num_threads = %d and num_workers = %d.' - % (num_threads, num_workers)) - print('Each thread contains about %d FLORIS evaluations.' % dN) start_time = timerpc() - if num_workers == 1: - df_out = _run_fi_serial(df_subset=df, - fi=fi, - include_unc=include_unc, - unc_pmfs=unc_pmfs, - unc_options=unc_options, - verbose=True) + if num_workers <= 1: + print("Calculating floris solutions (non-parallelized)") + df_out = _run_fi_serial( + df_subset=df, + fi=fi, + include_unc=include_unc, + unc_pmfs=unc_pmfs, + unc_options=unc_options, + verbose=True + ) else: + print('Calculating with num_workers = %d and job_worker_ratio = %d' + % (num_workers, job_worker_ratio)) + print('Each thread contains about %d FLORIS evaluations.' % dN) + # Define a tuple of arguments multiargs = [] for df_mp in df_list: df_mp = df_mp.reset_index(drop=True) - multiargs.append((df_mp, deepcopy(fi), include_unc, - unc_pmfs, unc_options, False)) + multiargs.append( + (df_mp, dcopy(fi), include_unc, unc_pmfs, unc_options, False) + ) if use_mpi: # Use an MPI implementation, useful for HPC @@ -187,7 +284,7 @@ def calc_floris(df, fi, num_workers, num_threads, include_unc=False, t = timerpc() - start_time print('Finished calculating the FLORIS solutions for the dataframe.') print('Total wall time: %.3f s.' % t) - print('Mean wall time / function evaluation: %.3f s.' % (t/N)) + print('Mean wall time / function evaluation: %.3f s.' % (t/df.shape[0])) return df_out @@ -296,7 +393,7 @@ def calc_floris_approx_table( ws_array=np.arange(0., 20., 0.5), ti_array=None, num_workers=1, - num_threads=1, + job_worker_ratio=5, include_unc=False, unc_pmfs=None, unc_options=None, @@ -312,9 +409,12 @@ def calc_floris_approx_table( ) ) df_approx = pd.DataFrame( - {'wd': np.reshape(xyz_grid[0], [-1, 1]).flatten(), - 'ws': np.reshape(xyz_grid[1], [-1, 1]).flatten(), - 'ti': np.reshape(xyz_grid[2], [-1, 1]).flatten()}) + { + 'wd': np.reshape(xyz_grid[0], [-1, 1]).flatten(), + 'ws': np.reshape(xyz_grid[1], [-1, 1]).flatten(), + 'ti': np.reshape(xyz_grid[2], [-1, 1]).flatten() + } + ) N_approx = df_approx.shape[0] print( @@ -326,7 +426,7 @@ def calc_floris_approx_table( df=df_approx, fi=fi, num_workers=num_workers, - num_threads=num_threads, + job_worker_ratio=job_worker_ratio, include_unc=include_unc, unc_pmfs=unc_pmfs, unc_options=unc_options, @@ -412,8 +512,9 @@ def get_upstream_turbs_floris(fi, wd_step=0.1, wake_slope=0.10, # Get farm layout x = fi.layout_x y = fi.layout_y - D = np.array([t.rotor_diameter for t in fi.floris.farm.turbines]) n_turbs = len(x) + D = [t["rotor_diameter"] for t in fi.floris.farm.turbine_definitions] + D = np.array(D, dtype=float) # Setup output list upstream_turbs_ids = [] # turbine numbers that are freestream diff --git a/flasc/time_operations.py b/flasc/time_operations.py index 29c5f23e..769bec23 100644 --- a/flasc/time_operations.py +++ b/flasc/time_operations.py @@ -80,8 +80,8 @@ def df_movingaverage( df_tmp = df_tmp.rolling(window_width, center=center, axis=0, on="time")["tmp"] # Grab index of first and last time entry for each window - windows_min = df_tmp.apply(lambda x: x.index[0]).astype(int) - windows_max = df_tmp.apply(lambda x: x.index[-1]).astype(int) + windows_min = list(df_tmp.apply(lambda x: x.index[0]).astype(int)) + windows_max = list(df_tmp.apply(lambda x: x.index[-1]).astype(int)) # Now create a large array that contains the array of indices, with # the values in each row corresponding to the indices upon which that @@ -206,7 +206,7 @@ def df_downsample( # Figure out which indices/data points belong to each window if (return_index_mapping or calc_median_min_max_std): - df_tmp = df[[]].copy().reset_index(drop=False) + df_tmp = df[[]].copy().reset_index() df_tmp["tmp"] = 1 df_tmp = df_tmp.resample(window_width, on="time", label="right", axis=0)["tmp"] @@ -222,8 +222,8 @@ def get_last_index(x): else: return x.index[-1] - windows_min = df_tmp.apply(get_first_index).astype(int) - windows_max = df_tmp.apply(get_last_index).astype(int) + windows_min = list(df_tmp.apply(get_first_index).astype(int)) + windows_max = list(df_tmp.apply(get_last_index).astype(int)) # Now create a large array that contains the array of indices, with # the values in each row corresponding to the indices upon which that diff --git a/flasc/turbine_analysis/ws_pow_filtering.py b/flasc/turbine_analysis/ws_pow_filtering.py index 198fc27c..ce671bf5 100644 --- a/flasc/turbine_analysis/ws_pow_filtering.py +++ b/flasc/turbine_analysis/ws_pow_filtering.py @@ -757,11 +757,11 @@ def plot( label="Approximate power curve", ) if fi is not None: - fi_turb = fi.floris.farm.turbines[ti] - Ad = 0.25 * np.pi * fi_turb.rotor_diameter ** 2.0 - ws_array = np.array(fi_turb.power_thrust_table["wind_speed"]) - cp_array = np.array(fi_turb.fCpInterp(ws_array)) - rho = fi.floris.farm.air_density + fi_turb = fi.floris.farm.turbine_definitions[ti] + Ad = 0.25 * np.pi * fi_turb["rotor_diameter"] ** 2.0 + ws_array = np.array(fi_turb["power_thrust_table"]["wind_speed"]) + cp_array = np.array(fi_turb["power_thrust_table"]["power"]) + rho = fi.floris.flow_field.air_density pow_array = ( 0.5 * rho * ws_array ** 3.0 * Ad * cp_array * 1.0e-3 ) diff --git a/requirements.txt b/requirements.txt index 8af5d4f5..054fe5dd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,10 @@ -floris==2.4 +floris>=3.0 feather-format>=0.4.1 matplotlib>=3 openoa>=2.0.1 numpy==1.21 numba>=0.55.0 -pandas>=1.3.0 +pandas>=1.3.0,<=1.4.0 pyproj>=2.1 pytest>=4 SALib>=1.4.0.2 diff --git a/setup.py b/setup.py index fd6ac3b5..b3201e01 100644 --- a/setup.py +++ b/setup.py @@ -7,17 +7,14 @@ with open('README.rst') as readme_file: readme = readme_file.read() -with open('HISTORY.rst') as history_file: - history = history_file.read() - requirements = [ - 'floris==2.4', + 'floris>=3.0', 'feather-format>=0.4.1', 'matplotlib>=3', 'numpy==1.21', 'numba>=0.55.0', 'openoa>=2.0.1', - 'pandas>=1.3.0', + 'pandas>=1.3.0,<=1.4.0', 'pyproj>=2.1', 'pytest>=4', 'SALib>=1.4.0.2', @@ -37,9 +34,9 @@ setup( name='flasc', - version='0.1.0', + version='1.0', description="FLASC provides a rich suite of analysis tools for SCADA data filtering & analysis, wind farm model validation, field experiment design, and field experiment monitoring.", - long_description=readme + '\n\n' + history, + long_description=readme, author="Bart Doekemeijer", author_email='bart.doekemeijer@nrel.gov', url='https://github.com/NREL/flasc', @@ -55,17 +52,11 @@ zip_safe=False, keywords='flasc', classifiers=[ - 'Development Status :: 2 - Pre-Alpha', + 'Development Status :: Release', 'Intended Audience :: Developers', 'License :: OSI Approved :: Apache Software License', 'Natural Language :: English', - "Programming Language :: Python :: 2", - 'Programming Language :: Python :: 2.6', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', ], test_suite='tests', tests_require=test_requirements, diff --git a/tests/energy_ratio_test.py b/tests/energy_ratio_test.py index 03e58456..70fb29f4 100644 --- a/tests/energy_ratio_test.py +++ b/tests/energy_ratio_test.py @@ -14,7 +14,7 @@ def load_floris(): # Initialize the FLORIS interface fi print('Initializing the FLORIS object for our demo wind farm') file_path = os.path.dirname(os.path.abspath(__file__)) - fi_path = os.path.join(file_path, "..", "examples", "demo_dataset", "demo_floris_input.json") + fi_path = os.path.join(file_path, "../examples/demo_dataset/demo_floris_input.yaml") fi = wfct.floris_interface.FlorisInterface(fi_path) return fi diff --git a/tests/floris_tools_test.py b/tests/floris_tools_test.py index ffc1d509..a15a4ef1 100644 --- a/tests/floris_tools_test.py +++ b/tests/floris_tools_test.py @@ -15,15 +15,14 @@ def load_floris(): # Initialize the FLORIS interface fi print('Initializing the FLORIS object for our demo wind farm') file_path = os.path.dirname(os.path.abspath(__file__)) - fi_path = os.path.join(file_path, "..", "examples", "demo_dataset", "demo_floris_input.json") + fi_path = os.path.join(file_path, "../examples/demo_dataset/demo_floris_input.yaml") fi = wfct.floris_interface.FlorisInterface(fi_path) return fi class TestFlorisTools(unittest.TestCase): def test_floris_approx_table(self): -# if __name__ == "__main__": -# if True: + # Load FLORIS object fi = load_floris() # Single core calculation @@ -33,7 +32,6 @@ def test_floris_approx_table(self): ws_array=[8.0, 9.0], ti_array=[0.08], num_workers=1, - num_threads=1, ) # Multi core calculation @@ -43,7 +41,6 @@ def test_floris_approx_table(self): ws_array=[8.0, 9.0], ti_array=[0.08], num_workers=2, - num_threads=2, ) # Make sure singlecore and multicore solutions are equal