diff --git a/examples/examples_control_optimization/005_optimize_yaw_aep_parallel.py b/examples/examples_control_optimization/005_optimize_yaw_aep_parallel.py index 17e02412b..ab4c3f02b 100644 --- a/examples/examples_control_optimization/005_optimize_yaw_aep_parallel.py +++ b/examples/examples_control_optimization/005_optimize_yaw_aep_parallel.py @@ -16,11 +16,11 @@ import numpy as np from floris import ( - FlorisModel, - ParallelFlorisModel, + ParFlorisModel, TimeSeries, WindRose, ) +from floris.optimization.yaw_optimization.yaw_optimizer_sr import YawOptimizationSR # When using parallel optimization it is importat the "root" script include this @@ -33,19 +33,26 @@ ti_col_or_value=0.06 ) - # Load FLORIS - fmodel = FlorisModel("../inputs/gch.yaml") + # Load FLORIS as a parallel model + max_workers = 16 + pfmodel = ParFlorisModel( + "../inputs/gch.yaml", + max_workers=max_workers, + n_wind_condition_splits=max_workers, + interface="pathos", + print_timings=True, + ) # Specify wind farm layout and update in the floris object N = 2 # number of turbines per row and per column X, Y = np.meshgrid( - 5.0 * fmodel.core.farm.rotor_diameters_sorted[0][0] * np.arange(0, N, 1), - 5.0 * fmodel.core.farm.rotor_diameters_sorted[0][0] * np.arange(0, N, 1), + 5.0 * pfmodel.core.farm.rotor_diameters_sorted[0][0] * np.arange(0, N, 1), + 5.0 * pfmodel.core.farm.rotor_diameters_sorted[0][0] * np.arange(0, N, 1), ) - fmodel.set(layout_x=X.flatten(), layout_y=Y.flatten()) + pfmodel.set(layout_x=X.flatten(), layout_y=Y.flatten()) # Get the number of turbines - n_turbines = len(fmodel.layout_x) + n_turbines = len(pfmodel.layout_x) # Optimize the yaw angles. This could be done for every wind direction and wind speed # but in practice it is much faster to optimize only for one speed and infer the rest @@ -53,46 +60,25 @@ time_series = TimeSeries( wind_directions=wind_rose.wind_directions, wind_speeds=8.0, turbulence_intensities=0.06 ) - fmodel.set(wind_data=time_series) - - # Set up the parallel model - parallel_interface = "concurrent" - max_workers = 16 - pfmodel = ParallelFlorisModel( - fmodel=fmodel, - max_workers=max_workers, - n_wind_condition_splits=max_workers, - interface=parallel_interface, - print_timings=True, - ) + pfmodel.set(wind_data=time_series) - # Get the optimal angles using the parallel interface start_time = timerpc() - # Now optimize the yaw angles using the Serial Refine method - df_opt = pfmodel.optimize_yaw_angles( - minimum_yaw_angle=0.0, - maximum_yaw_angle=20.0, + yaw_opt = YawOptimizationSR( + fmodel=pfmodel, + minimum_yaw_angle=0.0, # Allowable yaw angles lower bound + maximum_yaw_angle=20.0, # Allowable yaw angles upper bound Ny_passes=[5, 4], - exclude_downstream_turbines=False, + exclude_downstream_turbines=True, ) + df_opt = yaw_opt.optimize() end_time = timerpc() t_tot = end_time - start_time print("Optimization finished in {:.2f} seconds.".format(t_tot)) - # Calculate the AEP in the baseline case, using the parallel interface - fmodel.set(wind_data=wind_rose) - pfmodel = ParallelFlorisModel( - fmodel=fmodel, - max_workers=max_workers, - n_wind_condition_splits=max_workers, - interface=parallel_interface, - print_timings=True, - ) - - # Note the pfmodel does not use run() but instead uses the get_farm_power() and get_farm_AEP() - # directly, this is necessary for the parallel interface - aep_baseline = pfmodel.get_farm_AEP(freq=wind_rose.unpack_freq()) + pfmodel.set(wind_data=wind_rose) + pfmodel.run() + aep_baseline = pfmodel.get_farm_AEP() # Now need to apply the optimal yaw angles to the wind rose to get the optimized AEP # do this by applying a rule of thumb where the optimal yaw is applied between 6 and 12 m/s @@ -102,9 +88,10 @@ # yaw angles will need to be n_findex long, and accounting for the fact that some wind # directions and wind speeds may not be present in the wind rose (0 frequency) and aren't # included in the fmodel - wind_directions = fmodel.wind_directions - wind_speeds = fmodel.wind_speeds - n_findex = fmodel.n_findex + # TODO: add operation wind rose to example, once built + wind_directions = pfmodel.wind_directions + wind_speeds = pfmodel.wind_speeds + n_findex = pfmodel.n_findex # Now define how the optimal yaw angles for 8 m/s are applied over the other wind speeds @@ -133,15 +120,9 @@ # Now apply the optimal yaw angles and get the AEP - fmodel.set(yaw_angles=yaw_angles_wind_rose) - pfmodel = ParallelFlorisModel( - fmodel=fmodel, - max_workers=max_workers, - n_wind_condition_splits=max_workers, - interface=parallel_interface, - print_timings=True, - ) - aep_opt = pfmodel.get_farm_AEP(freq=wind_rose.unpack_freq(), yaw_angles=yaw_angles_wind_rose) + pfmodel.set(yaw_angles=yaw_angles_wind_rose) + pfmodel.run() + aep_opt = pfmodel.get_farm_AEP() aep_uplift = 100.0 * (aep_opt / aep_baseline - 1) print("Baseline AEP: {:.2f} GWh.".format(aep_baseline/1E9)) diff --git a/examples/examples_parallel/000_parallel_timing.py b/examples/examples_parallel/000_parallel_timing.py deleted file mode 100644 index 44e114991..000000000 --- a/examples/examples_parallel/000_parallel_timing.py +++ /dev/null @@ -1,161 +0,0 @@ -"""Example: Timing tests for parallel computation interfaces. - -Tests: -- max_workers specified, small. -- max_workers specified, large. -- max_workers unspecified. - -- various n_findex -- various n_turbines - -- return_turbine_powers_only=True -- return_turbine_powers_only=False -""" - -from time import perf_counter as timerpc - -import numpy as np -import pandas as pd - -from floris import ( - FlorisModel, - TimeSeries, -) -from floris.parallel_floris_model import ParallelFlorisModel as ParallelFlorisModel_orig -from floris.parallel_floris_model_2 import ParallelFlorisModel as ParallelFlorisModel_new - - -DEBUG = True - -if __name__ == "__main__": - max_workers_options = [2, 16, -1] - n_findex_options = [100, 1000, 10000] - n_turbines_options = [5, 10, 15] # Will be squared! - # Parallelization parameters - - def set_up_and_run_models(n_turbs, n_findex, max_workers): - # Create random wind data - np.random.seed(0) - wind_speeds = np.random.normal(loc=8.0, scale=2.0, size=n_findex) - wind_directions = np.random.normal(loc=270.0, scale=15.0, size=n_findex) - turbulence_intensities = 0.06*np.ones_like(wind_speeds) - - time_series = TimeSeries( - wind_directions=wind_directions, - wind_speeds=wind_speeds, - turbulence_intensities=turbulence_intensities, - ) - - # Clip wind_rose to specified n_findex - - fmodel = FlorisModel("../inputs/gch.yaml") - - # Specify wind farm layout and update in the floris object - N = n_turbs - - X, Y = np.meshgrid( - 5.0 * fmodel.core.farm.rotor_diameters_sorted[0][0] * np.arange(0, N, 1), - 5.0 * fmodel.core.farm.rotor_diameters_sorted[0][0] * np.arange(0, N, 1), - ) - fmodel.set(layout_x=X.flatten(), layout_y=Y.flatten()) - - # Set up original parallel Floris model - parallel_interface = "multiprocessing" - pfmodel_orig = ParallelFlorisModel_orig( - fmodel=fmodel, - max_workers=100 if max_workers < 0 else max_workers, - n_wind_condition_splits=100 if max_workers < 0 else max_workers, - interface=parallel_interface, - print_timings=True - ) - - # Set up new parallel Floris model - pfmodel_new = ParallelFlorisModel_new( - "../inputs/gch.yaml", - max_workers=max_workers, - n_wind_condition_splits=max_workers, - interface="pathos", - print_timings=True, - ) - - # Set up new parallel Floris model using only powers - pfmodel_new_p = ParallelFlorisModel_new( - "../inputs/gch.yaml", - max_workers=max_workers, - n_wind_condition_splits=max_workers, - interface=parallel_interface, - return_turbine_powers_only=True, - print_timings=True, - ) - - # Set layout, wind data on all models - fmodel.set(layout_x=X.flatten(), layout_y=Y.flatten(), wind_data=time_series) - pfmodel_orig.set(layout_x=X.flatten(), layout_y=Y.flatten(), wind_data=time_series) - pfmodel_new.set(layout_x=X.flatten(), layout_y=Y.flatten(), wind_data=time_series) - pfmodel_new_p.set(layout_x=X.flatten(), layout_y=Y.flatten(), wind_data=time_series) - - # Limit to a subset of the wind rose, maybe. - - - # Run and evaluate farm over the wind rose - t0 = timerpc() - fmodel.run() - aep_fmodel = fmodel.get_farm_AEP() - t_fmodel = timerpc() - t0 - - t0 = timerpc() - #pfmodel_orig.run() - aep_pfmodel_orig = pfmodel_orig.get_farm_AEP(freq=time_series.unpack_freq()) - t_pfmodel_orig = timerpc() - t0 - - t0 = timerpc() - pfmodel_new.run() - aep_pfmodel_new = pfmodel_new.get_farm_AEP() - t_pfmodel_new = timerpc() - t0 - - t0 = timerpc() - pfmodel_new_p.run() - aep_pfmodel_new_p = pfmodel_new_p.get_farm_AEP() - t_pfmodel_new_p = timerpc() - t0 - - # Save the data - df = pd.DataFrame({ - "model": ["FlorisModel", "ParallelFlorisModel_orig", "ParallelFlorisModel_new", - "ParallelFlorisModel_new_poweronly"], - "AEP": [aep_fmodel, aep_pfmodel_orig, aep_pfmodel_new, aep_pfmodel_new_p], - "time": [t_fmodel, t_pfmodel_orig, t_pfmodel_new, t_pfmodel_new_p], - }) - - df.to_csv(f"comptime_maxworkers{mw}_nturbs{n_turbs}_nfindex{n_findex}.csv") - - return None - - # First run max_workers tests - for mw in max_workers_options: - # Set up models - n_turbs = 2 if DEBUG else 10 # Will be squared - n_findex = 1000 - set_up_and_run_models( - n_turbs=n_turbs, n_findex=n_findex, max_workers=mw - ) - - # Then run n_turbines tests - for nt in n_turbines_options: - # Set up models - n_findex = 10 if DEBUG else 1000 - max_workers = 16 - - set_up_and_run_models( - n_turbs=nt, n_findex=n_findex, max_workers=max_workers - ) - - # Then run n_findex tests - for nf in n_findex_options: - # Set up models - n_turbs = 2 if DEBUG else 10 # Will be squared - max_workers = 16 - - set_up_and_run_models( - n_turbs=n_turbs, n_findex=nf, max_workers=max_workers - ) - diff --git a/examples/examples_parallel/001_parallel_timing_output.py b/examples/examples_parallel/001_parallel_timing_output.py deleted file mode 100644 index e1264f199..000000000 --- a/examples/examples_parallel/001_parallel_timing_output.py +++ /dev/null @@ -1,151 +0,0 @@ -"""Example: Timing tests for parallel computation interfaces. - -Tests: -- max_workers specified, small. -- max_workers specified, large. -- max_workers unspecified. - -- various n_findex -- various n_turbines - -- return_turbine_powers_only=True -- return_turbine_powers_only=False -""" - -from time import perf_counter as timerpc - -import matplotlib.pyplot as plt -import numpy as np -import pandas as pd - - -max_workers_options = [2, 16, -1] -n_findex_options = [100, 1000, 10000] -n_turbines_options = [5, 10, 15] # Will be squared! -# Parallelization parameters - -DEBUG = True - -# First run max_workers tests -timing_data = [] -for mw in max_workers_options: - # Set up models - n_turbs = 2 if DEBUG else 10 # Will be squared - n_findex = 1000 - - df = pd.read_csv(f"outputs/comptime_maxworkers{mw}_nturbs{n_turbs}_nfindex{n_findex}.csv") - - timing_data.append(df.time.values) - -timing_data = np.array(timing_data).T - -x = np.arange(len(max_workers_options)) -width = 0.2 -multiplier = 0 - -fig, ax = plt.subplots(1,1) - -for dat, lab in zip(timing_data.tolist(), df.model.values): - offset = width * multiplier - rects = ax.bar(x + offset, dat, width, label=lab) - ax.bar_label(rects, padding=3, fmt='%.1f') - multiplier += 1 - -# Add some text for labels, title and custom x-axis tick labels, etc. -ax.set_xlabel('Max. workers [-]') -ax.set_xticks(x + width, max_workers_options) -ax.set_ylabel('Time [s]') -ax.legend(loc='upper left', ncols=2) -ax.set_yscale('log') -fig.savefig("outputs/max_workers_timing.png", format='png', dpi=300) - - -# Similar now for n_turbs -timing_data = [] -for nt in n_turbines_options: - # Set up models - n_findex = 10 if DEBUG else 1000 - max_workers = -1 - df = pd.read_csv(f"outputs/comptime_maxworkers{max_workers}_nturbs{nt}_nfindex{n_findex}.csv") - timing_data.append(df.time.values) - -timing_data = np.array(timing_data).T - -x = np.arange(len(n_turbines_options)) -width = 0.2 -multiplier = 0 - -fig, ax = plt.subplots(1,1) - -for dat, lab in zip(timing_data.tolist(), df.model.values): - offset = width * multiplier - rects = ax.bar(x + offset, dat, width, label=lab) - ax.bar_label(rects, padding=3, fmt='%.1f') - multiplier += 1 - -# Add some text for labels, title and custom x-axis tick labels, etc. -ax.set_xlabel('n_turbines [-]') -ax.set_xticks(x + width, np.array(n_turbines_options)**2) -ax.set_ylabel('Time [s]') -ax.legend(loc='upper left', ncols=2) -ax.set_yscale('log') -fig.savefig("outputs/n_turbines_timing.png", format='png', dpi=300) - - -# Similar now for n_findex -timing_data = [] -for nf in n_findex_options: - # Set up models - n_turbs = 2 if DEBUG else 10 # Will be squared - max_workers = -1 - df = pd.read_csv(f"outputs/comptime_maxworkers{max_workers}_nturbs{n_turbs}_nfindex{nf}.csv") - timing_data.append(df.time.values) - -timing_data = np.array(timing_data).T - -x = np.arange(len(n_findex_options)) -width = 0.2 -multiplier = 0 - -fig, ax = plt.subplots(1,1) - -for dat, lab in zip(timing_data.tolist(), df.model.values): - offset = width * multiplier - rects = ax.bar(x + offset, dat, width, label=lab) - ax.bar_label(rects, padding=3, fmt='%.1f') - multiplier += 1 - -# Add some text for labels, title and custom x-axis tick labels, etc. -ax.set_xlabel('n_findex [-]') -ax.set_xticks(x + width, n_findex_options) -ax.set_ylabel('Time [s]') -ax.legend(loc='upper left', ncols=2) -ax.set_yscale('log') -fig.savefig("outputs/n_findex_timing.png", format='png', dpi=300) - - -plt.show() - - - - - # # Then run n_turbines tests - # for nt in n_turbines_options: - # # Set up models - # n_findex = 10 if DEBUG else 1000 - # max_workers = 16 - - # set_up_and_run_models( - # n_turbs=nt, n_findex=n_findex, max_workers=max_workers - # ) - - # # Then run n_findex tests - # for nf in n_findex_options: - # # Set up models - # n_turbs = 2 if DEBUG else 10 # Will be squared - # max_workers = 16 - - # set_up_and_run_models( - # n_turbs=n_turbs, n_findex=nf, max_workers=max_workers - # ) - diff --git a/examples/examples_parallel/002_worker_pool.py b/examples/examples_parallel/002_worker_pool.py deleted file mode 100644 index 090300b66..000000000 --- a/examples/examples_parallel/002_worker_pool.py +++ /dev/null @@ -1,107 +0,0 @@ -"""Example: Timing tests for parallel computation interfaces. - -Tests: -- max_workers specified, small. -- max_workers specified, large. -- max_workers unspecified. - -- various n_findex -- various n_turbines - -- return_turbine_powers_only=True -- return_turbine_powers_only=False -""" - -from time import perf_counter as timerpc - -import numpy as np -import pandas as pd - -from floris import ( - FlorisModel, - TimeSeries, -) -from floris.parallel_floris_model import ParallelFlorisModel as ParallelFlorisModel_orig -from floris.parallel_floris_model_2 import ParallelFlorisModel as ParallelFlorisModel_new - - -DEBUG = True - -if __name__ == "__main__": - # Create random wind data - np.random.seed(0) - n_findex = 10 if DEBUG else 1000 - wind_speeds = np.random.normal(loc=8.0, scale=2.0, size=n_findex) - wind_directions = np.random.normal(loc=270.0, scale=15.0, size=n_findex) - turbulence_intensities = 0.06*np.ones_like(wind_speeds) - - time_series = TimeSeries( - wind_directions=wind_directions, - wind_speeds=wind_speeds, - turbulence_intensities=turbulence_intensities, - ) - - # Clip wind_rose to specified n_findex - - fmodel = FlorisModel("../inputs/gch.yaml") - - # Specify wind farm layout and update in the floris object - N = 20 if DEBUG else 100 - - X, Y = np.meshgrid( - 5.0 * fmodel.core.farm.rotor_diameters_sorted[0][0] * np.arange(0, N, 1), - 5.0 * fmodel.core.farm.rotor_diameters_sorted[0][0] * np.arange(0, N, 1), - ) - - # Set up new parallel Floris model - print("Beginning multiprocessing test") - t0 = timerpc() - pfmodel_mp = ParallelFlorisModel_new( - "../inputs/gch.yaml", - max_workers=-1, - n_wind_condition_splits=-1, - interface="multiprocessing", - print_timings=True, - ) - pfmodel_mp.set(layout_x=X.flatten(), layout_y=Y.flatten(), wind_data=time_series) - t1 = timerpc() - pfmodel_mp.run() - aep1 = pfmodel_mp.get_farm_AEP() - t2 = timerpc() - pfmodel_mp.set(layout_x=X.flatten()+10, layout_y=Y.flatten()) - pfmodel_mp.run() - aep2 = pfmodel_mp.get_farm_AEP() - t3 = timerpc() - - print(f"Multiprocessing (max_workers={pfmodel_mp.max_workers})") - print(f"Time to set up: {t1-t0}") - print(f"Time to run first: {t2-t1}") - print(f"Time to run second: {t3-t2}") - - # When is the worker pool released, though?? - print("Beginning pathos test") - t0 = timerpc() - pfmodel_pathos = ParallelFlorisModel_new( - "../inputs/gch.yaml", - max_workers=-1, - n_wind_condition_splits=-1, - interface="pathos", - print_timings=True, - ) - pfmodel_pathos.set(layout_x=X.flatten(), layout_y=Y.flatten(), wind_data=time_series) - t1 = timerpc() - pfmodel_pathos.run() - aep3 = pfmodel_pathos.get_farm_AEP() - t2 = timerpc() - pfmodel_pathos.set(layout_x=X.flatten()+10, layout_y=Y.flatten()) - pfmodel_pathos.run() - aep4 = pfmodel_pathos.get_farm_AEP() - t3 = timerpc() - - print(f"Pathos (max_workers={pfmodel_pathos.max_workers})") - print(f"Time to set up: {t1-t0}") - print(f"Time to run first: {t2-t1}") - print(f"Time to run second: {t3-t2}") - - if np.isclose(aep1 + aep2 + aep3 + aep4, 4*aep4): - print("AEPs are equal!") diff --git a/examples/examples_uncertain/003_uncertain_model_with_parallelization.py b/examples/examples_uncertain/003_uncertain_model_with_parallelization.py index 198191bb3..dc4f67051 100644 --- a/examples/examples_uncertain/003_uncertain_model_with_parallelization.py +++ b/examples/examples_uncertain/003_uncertain_model_with_parallelization.py @@ -11,18 +11,18 @@ TimeSeries, UncertainFlorisModel, ) -from floris.parallel_floris_model_2 import ParallelFlorisModel +from floris.par_floris_model import ParFlorisModel -# Following the refactoring of ParallelFlorisModel, the UncertainFlorisModel can be -# parallelized by passing the ParallelFlorisModel as the model to be run. This example +# Following the refactoring of ParFlorisModel, the UncertainFlorisModel can be +# parallelized by passing the ParFlorisModel as the model to be run. This example # demonstrates the usage. Demonstrating the result obtained from the UncertainFlorisModel -# with and without parallelization is the same. The results are compared to the nomimal +# with and without parallelization is the same. The results are compared to the nominal # results. # Instantiate a FlorisModel and ParallelFlorisModel using the GCH model fmodel = FlorisModel("../inputs/gch.yaml") -pfmodel = ParallelFlorisModel("../inputs/gch.yaml") +pfmodel = ParFlorisModel("../inputs/gch.yaml") # Use the above model to declare a serial and parallel UncertainFlorisModel ufmodel = UncertainFlorisModel(fmodel) diff --git a/floris/__init__.py b/floris/__init__.py index fd8ca5ddf..d97bb24eb 100644 --- a/floris/__init__.py +++ b/floris/__init__.py @@ -13,6 +13,7 @@ visualize_quiver, ) from .heterogeneous_map import HeterogeneousMap +from .par_floris_model import ParFlorisModel from .parallel_floris_model import ParallelFlorisModel from .uncertain_floris_model import ApproxFlorisModel, UncertainFlorisModel from .wind_data import ( diff --git a/floris/parallel_floris_model_2.py b/floris/par_floris_model.py similarity index 96% rename from floris/parallel_floris_model_2.py rename to floris/par_floris_model.py index 094af0592..827b4dd6f 100644 --- a/floris/parallel_floris_model_2.py +++ b/floris/par_floris_model.py @@ -10,7 +10,7 @@ from floris.floris_model import FlorisModel -class ParallelFlorisModel(FlorisModel): +class ParFlorisModel(FlorisModel): """ This class mimics the FlorisModel, but enables parallelization of the main computational effort. @@ -18,7 +18,7 @@ class ParallelFlorisModel(FlorisModel): def __init__( self, - configuration: dict | str | Path, + configuration: dict | str | Path | FlorisModel, interface: str | None = "multiprocessing", max_workers: int = -1, n_wind_condition_splits: int = -1, @@ -26,11 +26,11 @@ def __init__( print_timings: bool = False ): """ - Initialize the ParallelFlorisModel object. + Initialize the ParFlorisModel object. Args: - configuration: The Floris configuration dictionary or YAML file. - The configuration should have the following inputs specified. + configuration: The Floris configuration dictionary or YAML file, or an instantiated + FlorisModel object. The configuration should have the following inputs specified. - **flow_field**: See `floris.simulation.flow_field.FlowField` for more details. - **farm**: See `floris.simulation.farm.Farm` for more details. - **turbine**: See `floris.simulation.turbine.Turbine` for more details. @@ -47,11 +47,6 @@ def __init__( """ # Instantiate the underlying FlorisModel if isinstance(configuration, FlorisModel): - self.logger.warning( - "Received an instantiated FlorisModel, when expected a dictionary or path" - " to a FLORIS input file. Converting to dictionary to instantiate " - " the ParallelFlorisModel." - ) configuration = configuration.core.as_dict() super().__init__(configuration) @@ -296,7 +291,7 @@ def fmodel(self): Raise deprecation warning. """ self.logger.warning( - "ParallelFlorisModel no longer contains `fmodel` as an attribute " + "ParFlorisModel no longer contains `fmodel` as an attribute " "and now directly inherits from FlorisModel. Please use the " "attributes and methods of FlorisModel directly." ) diff --git a/floris/parallel_floris_model.py b/floris/parallel_floris_model.py index aea8c2f5e..9b0b0b355 100644 --- a/floris/parallel_floris_model.py +++ b/floris/parallel_floris_model.py @@ -70,9 +70,8 @@ def __init__( parallel computing to common FlorisModel properties. Args: - fmodel (FlorisModel or UncertainFlorisModel object): Interactive FLORIS object used to - perform the wake and turbine calculations. Can either be a regular FlorisModel - object or can be an UncertainFlorisModel object. + fmodel (FlorisModel object): Interactive FLORIS object used to + perform the wake and turbine calculations. max_workers (int): Number of parallel workers, typically equal to the number of cores you have on your system or HPC. n_wind_condition_splits (int): Number of sectors to split the wind findex array over. @@ -89,6 +88,11 @@ def __init__( print_timings (bool): Print the computation time to the console. Defaults to False. """ + self.logger.warning(( + "ParallelFlorisModel is deprecated and will be removed in a future version. " + "Please switch to ParFlorisModel instead." + )) + # Set defaults for backward compatibility if use_mpi4py is not None: warnings.warn( @@ -112,17 +116,17 @@ def __init__( from concurrent.futures import ProcessPoolExecutor self._PoolExecutor = ProcessPoolExecutor else: - raise UserWarning( + raise ValueError( f"Interface '{interface}' not recognized. " "Please use 'concurrent', 'multiprocessing' or 'mpi4py'." ) # Raise error if uncertain model is passed in and refer to new parallel_floris_model_2 if isinstance(fmodel, UncertainFlorisModel): - raise UserWarning( + raise ValueError( "UncertainFlorisModel is not supported in this version of ParallelFlorisModel. " - "Please use the new version of ParallelFlorisModel (parallel_floris_model_2) " - "for UncertainFlorisModel." + "Please use the new version ParFlorisModel (par_floris_model) " + "for UncertainFlorisModel compatibility." ) # Initialize floris object and copy common properties diff --git a/floris/uncertain_floris_model.py b/floris/uncertain_floris_model.py index 7317c585d..4d9d691f5 100644 --- a/floris/uncertain_floris_model.py +++ b/floris/uncertain_floris_model.py @@ -12,7 +12,7 @@ from floris import FlorisModel from floris.core import State from floris.logging_manager import LoggingManager -from floris.parallel_floris_model_2 import ParallelFlorisModel +from floris.par_floris_model import ParFlorisModel from floris.type_dec import ( floris_array_converter, NDArrayBool, @@ -47,10 +47,10 @@ class UncertainFlorisModel(LoggingManager): conditions from within the expanded set of conditions are run. Args: - configuration (dict | str | Path | FlorisModel | ParallelFlorisModel): The configuration + configuration (dict | str | Path | FlorisModel | ParFlorisModel): The configuration for the wind farm. This can be a dictionary, a path to a yaml file, or a FlorisModel - or ParallelFlorisModel object. If dict, str or Path, a new FlorisModel object is - created. If a FlorisModel or ParallelFlorisModel object a copy of the object is made. + or ParFlorisModel object. If dict, str or Path, a new FlorisModel object is + created. If a FlorisModel or ParFlorisModel object a copy of the object is made. wd_resolution (float, optional): The resolution of wind direction for generating gaussian blends, in degrees. Defaults to 1.0. ws_resolution (float, optional): The resolution of wind speed, in m/s. Defaults to 1.0. @@ -73,7 +73,7 @@ class UncertainFlorisModel(LoggingManager): def __init__( self, - configuration: dict | str | Path | FlorisModel | ParallelFlorisModel, + configuration: dict | str | Path | FlorisModel | ParFlorisModel, wd_resolution=1.0, # Degree ws_resolution=1.0, # m/s ti_resolution=0.01, @@ -107,13 +107,13 @@ def __init__( self.weights = self._get_weights(self.wd_std, self.wd_sample_points) # Instantiate the un-expanded FlorisModel - if isinstance(configuration, (FlorisModel, ParallelFlorisModel)): + if isinstance(configuration, (FlorisModel, ParFlorisModel)): self.fmodel_unexpanded = configuration.copy() elif isinstance(configuration, (dict, str, Path)): self.fmodel_unexpanded = FlorisModel(configuration) else: raise ValueError( - "configuration must be a FlorisModel, ParallelFlorisModel, dict, str, or Path" + "configuration must be a FlorisModel, ParFlorisModel, dict, str, or Path" ) # Call set at this point with no arguments so ready to run diff --git a/tests/parallel_floris_model_2_unit_test.py b/tests/par_floris_model_unit_test.py similarity index 85% rename from tests/parallel_floris_model_2_unit_test.py rename to tests/par_floris_model_unit_test.py index 1a2e254d2..9e56e4d8c 100644 --- a/tests/parallel_floris_model_2_unit_test.py +++ b/tests/par_floris_model_unit_test.py @@ -10,7 +10,7 @@ TimeSeries, WindRose, ) -from floris.parallel_floris_model_2 import ParallelFlorisModel +from floris.par_floris_model import ParFlorisModel DEBUG = False @@ -19,14 +19,14 @@ def test_None_interface(sample_inputs_fixture): """ - With interface=None, the ParallelFlorisModel should behave exactly like the FlorisModel. - (ParallelFlorisModel.run() simply calls the parent FlorisModel.run()). + With interface=None, the ParFlorisModel should behave exactly like the FlorisModel. + (ParFlorisModel.run() simply calls the parent FlorisModel.run()). """ sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL fmodel = FlorisModel(sample_inputs_fixture.core) - pfmodel = ParallelFlorisModel( + pfmodel = ParFlorisModel( sample_inputs_fixture.core, interface=None, n_wind_condition_splits=2 # Not used when interface=None @@ -42,14 +42,14 @@ def test_None_interface(sample_inputs_fixture): def test_multiprocessing_interface(sample_inputs_fixture): """ - With interface="multiprocessing", the ParallelFlorisModel should return the same powers + With interface="multiprocessing", the ParFlorisModel should return the same powers as the FlorisModel. """ sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL fmodel = FlorisModel(sample_inputs_fixture.core) - pfmodel = ParallelFlorisModel( + pfmodel = ParFlorisModel( sample_inputs_fixture.core, interface="multiprocessing", n_wind_condition_splits=2 @@ -65,14 +65,14 @@ def test_multiprocessing_interface(sample_inputs_fixture): def test_pathos_interface(sample_inputs_fixture): """ - With interface="pathos", the ParallelFlorisModel should return the same powers + With interface="pathos", the ParFlorisModel should return the same powers as the FlorisModel. """ sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL fmodel = FlorisModel(sample_inputs_fixture.core) - pfmodel = ParallelFlorisModel( + pfmodel = ParFlorisModel( sample_inputs_fixture.core, interface="pathos", n_wind_condition_splits=2 @@ -87,7 +87,7 @@ def test_pathos_interface(sample_inputs_fixture): assert np.allclose(f_turb_powers, pf_turb_powers) # Run in powers_only mode - pfmodel = ParallelFlorisModel( + pfmodel = ParFlorisModel( sample_inputs_fixture.core, interface="pathos", n_wind_condition_splits=2, @@ -101,14 +101,14 @@ def test_pathos_interface(sample_inputs_fixture): def test_concurrent_interface(sample_inputs_fixture): """ - With interface="concurrent", the ParallelFlorisModel should return the same powers + With interface="concurrent", the ParFlorisModel should return the same powers as the FlorisModel. """ sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL fmodel = FlorisModel(sample_inputs_fixture.core) - pfmodel = ParallelFlorisModel( + pfmodel = ParFlorisModel( sample_inputs_fixture.core, interface="concurrent", n_wind_condition_splits=2, @@ -123,7 +123,7 @@ def test_concurrent_interface(sample_inputs_fixture): assert np.allclose(f_turb_powers, pf_turb_powers) # Run in powers_only mode - pfmodel = ParallelFlorisModel( + pfmodel = ParFlorisModel( sample_inputs_fixture.core, interface="concurrent", n_wind_condition_splits=2, @@ -137,14 +137,14 @@ def test_concurrent_interface(sample_inputs_fixture): def test_return_turbine_powers_only(sample_inputs_fixture): """ - With return_turbine_powers_only=True, the ParallelFlorisModel should return only the + With return_turbine_powers_only=True, the ParFlorisModel should return only the turbine powers, not the full results. """ sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL fmodel = FlorisModel(sample_inputs_fixture.core) - pfmodel = ParallelFlorisModel( + pfmodel = ParFlorisModel( sample_inputs_fixture.core, interface="multiprocessing", n_wind_condition_splits=2, @@ -166,7 +166,7 @@ def test_run_error(sample_inputs_fixture, caplog): sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL - pfmodel = ParallelFlorisModel( + pfmodel = ParFlorisModel( sample_inputs_fixture.core, interface="multiprocessing", n_wind_condition_splits=2 @@ -186,7 +186,7 @@ def test_run_error(sample_inputs_fixture, caplog): def test_configuration_compatibility(sample_inputs_fixture, caplog): """ - Check that the ParallelFlorisModel is compatible with FlorisModel and + Check that the ParFlorisModel is compatible with FlorisModel and UncertainFlorisModel configurations. """ @@ -195,12 +195,13 @@ def test_configuration_compatibility(sample_inputs_fixture, caplog): fmodel = FlorisModel(sample_inputs_fixture.core) + # Allowed to instantiate ParFlorisModel using fmodel with caplog.at_level(logging.WARNING): - ParallelFlorisModel(fmodel) - assert caplog.text != "" # Checking not empty + ParFlorisModel(fmodel) + assert caplog.text == "" # Checking empty caplog.clear() - pfmodel = ParallelFlorisModel(sample_inputs_fixture.core) + pfmodel = ParFlorisModel(sample_inputs_fixture.core) with caplog.at_level(logging.WARNING): pfmodel.fmodel assert caplog.text != "" # Checking not empty @@ -211,14 +212,14 @@ def test_configuration_compatibility(sample_inputs_fixture, caplog): def test_wind_data_objects(sample_inputs_fixture): """ - Check that the ParallelFlorisModel is compatible with WindData objects. + Check that the ParFlorisModel is compatible with WindData objects. """ sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL fmodel = FlorisModel(sample_inputs_fixture.core) - pfmodel = ParallelFlorisModel(sample_inputs_fixture.core, max_workers=2) + pfmodel = ParFlorisModel(sample_inputs_fixture.core, max_workers=2) # Create a wind rose and set onto both models wind_speeds = np.array([8.0, 10.0, 12.0, 8.0, 10.0, 12.0]) @@ -263,14 +264,14 @@ def test_wind_data_objects(sample_inputs_fixture): def test_control_setpoints(sample_inputs_fixture): """ - Check that the ParallelFlorisModel is compatible with yaw angles. + Check that the ParFlorisModel is compatible with yaw angles. """ sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL fmodel = FlorisModel(sample_inputs_fixture.core) - pfmodel = ParallelFlorisModel(sample_inputs_fixture.core, max_workers=2) + pfmodel = ParFlorisModel(sample_inputs_fixture.core, max_workers=2) # Set yaw angles yaw_angles = np.tile(np.array([[10.0, 20.0, 30.0]]), (fmodel.n_findex,1)) diff --git a/tests/parallel_floris_model_integration_test.py b/tests/parallel_floris_model_integration_test.py index 710557734..21857b5b3 100644 --- a/tests/parallel_floris_model_integration_test.py +++ b/tests/parallel_floris_model_integration_test.py @@ -1,7 +1,9 @@ import copy +import logging import numpy as np +import pytest from floris import ( FlorisModel, @@ -17,6 +19,26 @@ VELOCITY_MODEL = "gauss" DEFLECTION_MODEL = "gauss" +def test_raise_deprecation_warning(sample_inputs_fixture, caplog): + """ + Test that a warning is raised when instantiating the ParallelFlorisModel. + """ + sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL + sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + + fmodel = FlorisModel(sample_inputs_fixture.core) + + caplog.clear() + with caplog.at_level(logging.WARNING): + ParallelFlorisModel( + fmodel=fmodel, + max_workers=2, + n_wind_condition_splits=2, + interface="concurrent", + print_timings=False, + ) + assert caplog.text != "" # Checking not empty + caplog.clear() def test_parallel_turbine_powers(sample_inputs_fixture): """ @@ -76,96 +98,24 @@ def test_parallel_get_AEP(sample_inputs_fixture): assert np.allclose(parallel_farm_AEP, serial_farm_AEP) -def test_parallel_uncertain_turbine_powers(sample_inputs_fixture): - """ - - """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL - - ufmodel = UncertainFlorisModel( - sample_inputs_fixture.core, - wd_sample_points=[-3, 0, 3], - wd_std=3 - ) - pfmodel_input = copy.deepcopy(ufmodel) - ufmodel.run() - - serial_turbine_powers = ufmodel.get_turbine_powers() - - pfmodel = ParallelFlorisModel( - fmodel=pfmodel_input, - max_workers=2, - n_wind_condition_splits=2, - interface="multiprocessing", - print_timings=False, - ) - - parallel_turbine_powers = pfmodel.get_turbine_powers() - - if DEBUG: - print(serial_turbine_powers) - print(parallel_turbine_powers) - - assert_results_arrays(parallel_turbine_powers, serial_turbine_powers) - -def test_parallel_uncertain_get_AEP(sample_inputs_fixture): - """ - - """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL - - freq=np.linspace(0, 1, 16)/8 - - ufmodel = UncertainFlorisModel( - sample_inputs_fixture.core, - wd_sample_points=[-3, 0, 3], - wd_std=3 - ) - pfmodel_input = copy.deepcopy(ufmodel) - ufmodel.run() - serial_farm_AEP = ufmodel.get_farm_AEP(freq=freq) - - pfmodel = ParallelFlorisModel( - fmodel=pfmodel_input, - max_workers=2, - n_wind_condition_splits=2, - interface="multiprocessing", - print_timings=False, - ) - - parallel_farm_AEP = pfmodel.get_farm_AEP(freq=freq) - - assert np.allclose(parallel_farm_AEP, serial_farm_AEP) - - -def test_parallel_uncertain_get_AEP_no_wake(sample_inputs_fixture): +def test_parallel_uncertain_error(sample_inputs_fixture): """ """ sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL - freq=np.linspace(0, 1, 16)/8 - ufmodel = UncertainFlorisModel( sample_inputs_fixture.core, wd_sample_points=[-3, 0, 3], wd_std=3 ) - pfmodel_input = copy.deepcopy(ufmodel) - ufmodel.run_no_wake() - serial_farm_AEP = ufmodel.get_farm_AEP(freq=freq) - pfmodel = ParallelFlorisModel( - fmodel=pfmodel_input, - max_workers=2, - n_wind_condition_splits=2, - interface="multiprocessing", - print_timings=False, - ) - - parallel_farm_AEP = pfmodel.get_farm_AEP(freq=freq, no_wake=True) - - assert np.allclose(parallel_farm_AEP, serial_farm_AEP) + with pytest.raises(ValueError): + ParallelFlorisModel( + fmodel=ufmodel, + max_workers=2, + n_wind_condition_splits=2, + interface="multiprocessing", + print_timings=False, + ) diff --git a/tests/uncertain_floris_model_integration_test.py b/tests/uncertain_floris_model_integration_test.py index 9295965af..e8e95d513 100644 --- a/tests/uncertain_floris_model_integration_test.py +++ b/tests/uncertain_floris_model_integration_test.py @@ -4,7 +4,11 @@ import pytest import yaml -from floris import FlorisModel, TimeSeries +from floris import ( + FlorisModel, + ParFlorisModel, + TimeSeries, +) from floris.core.turbine.operation_models import POWER_SETPOINT_DEFAULT from floris.uncertain_floris_model import ( ApproxFlorisModel, @@ -452,3 +456,16 @@ def test_set_operation_model(): ufmodel.set(layout_x=[0, 0], layout_y=[0, 1000]) with pytest.raises(ValueError): ufmodel.set(layout_x=[0, 0, 0], layout_y=[0, 1000, 2000]) + +def test_parallel_uncertain_model(): + + ufmodel = UncertainFlorisModel(FlorisModel(configuration=YAML_INPUT)) + pufmodel = UncertainFlorisModel(ParFlorisModel(configuration=YAML_INPUT)) + + # Run the models and compare outputs + ufmodel.run() + pufmodel.run() + powers_unc = ufmodel.get_turbine_powers() + powers_punc = pufmodel.get_turbine_powers() + + assert np.allclose(powers_unc, powers_punc)