Skip to content

Commit

Permalink
New parallel option that is fully consistent with FlorisModel (#982)
Browse files Browse the repository at this point in the history
* Initial work on a consistent parallel FLORIS model (#977)

* Copy for easier comparison and development.

* temp commit to save progress.

* Starting to build out new ParallelFlorisModel (name not yet fixed).

* Begin building tests.

* Cleanup and formatting.

* Now able to use run() in parallel.

* Switch to hidden _get_turbine_powers.

* Add backcompatibility with existing parallel model; print timings; add powers_only option.

* Example comparing timing.

* Formatting fixes.

* Test for return_turbine_powers_only flag.

* Backcompatibility in configuration.

* Appears there is no saving in calling run() twice---all overhead occurs again.

* Fix type hints; default for n_wind_condition_splits; remove mistakenly committed yaw optimization function.

* Tests for WindData objects; remove related comment.

* Tests control setpoints (and add bugfix). Also limit number of workers for testing.

* Update UncertainFlorisModel to work with new Parallel framework (#983)

* Update uncertain floris model to accept par floris

* Raise an error in old parallel floris model blocking passing in uncertain floris model

* Add an example of parallelized uncertain floris

* Expand parallel processing interface options (#985)

* Update example to profile more fully.

* Handling for pathos parallel processing package; comparison example to multiprocessing.

* Add test for pathos interface.

* Add concurrent handling.

* minor doc updates.

* add pathos requirement

* Clean up of new ParFlorisModel class (#986)

* Update example to profile more fully.

* Handling for pathos parallel processing package; comparison example to multiprocessing.

* Add test for pathos interface.

* Add concurrent handling.

* minor doc updates.

* add pathos requirement

* Change name to ParFlorisModel throughout.

* Add deprecation warning for ParallelFlorisModel.

* Add import; isort.

* Update example to use ParFlorisModel.

* Update tests; formatting.

* Fix uncertain-parallel example; add uncertain-parallel test.

* Update example docstring.

* Remove temporary examples from repository.

* Add examples and documentation for ParFlorisModel (#990)

* Add short docstring

* Minor comment change.

* Add approx model to docs

* A couple of rewordings and update to initial paragraph.

---------

Co-authored-by: paulf81 <[email protected]>
  • Loading branch information
misi9170 and paulf81 authored Oct 7, 2024
1 parent a1c2134 commit 25aa0ff
Show file tree
Hide file tree
Showing 17 changed files with 1,334 additions and 155 deletions.
3 changes: 2 additions & 1 deletion docs/_toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ parts:
- caption: User Reference
chapters:
- file: intro_concepts
- file: advanced_concepts
- file: wind_data_user
- file: floris_models
- file: advanced_concepts
- file: heterogeneous_map
- file: floating_wind_turbine
- file: turbine_interaction
Expand Down
2 changes: 1 addition & 1 deletion docs/advanced_concepts.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"# Advanced Concepts\n",
"\n",
"More information regarding the numerical and computational formulation in FLORIS\n",
"are detailed here. See [](concepts_intro) for a guide on the basics."
"are detailed here. See [Introductory Concepts](intro_concepts) for a guide on the basics."
]
},
{
Expand Down
390 changes: 390 additions & 0 deletions docs/floris_models.ipynb

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/intro_concepts.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"id": "86e53920",
"metadata": {},
"source": [
"(concepts_intro)=\n",
"(intro_concepts)=\n",
"# Introductory Concepts\n",
"\n",
"FLORIS is a Python-based software library for calculating wind farm performance considering\n",
Expand Down
95 changes: 95 additions & 0 deletions examples/009_parallel_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
"""Example 9: Parallel Models
This example demonstrates how to use the ParFlorisModel class to parallelize the
calculation of the FLORIS model. ParFlorisModel inherits from the FlorisModel
and so can be used in the same way with a consistent interface. ParFlorisModel
replaces the ParallelFlorisModel, which will be deprecated in a future release.
"""

import numpy as np

from floris import (
FlorisModel,
ParFlorisModel,
TimeSeries,
UncertainFlorisModel,
)


# When using parallel optimization it is important the "root" script include this
# if __name__ == "__main__": block to avoid problems
if __name__ == "__main__":
# Instantiate the FlorisModel
fmodel = FlorisModel("inputs/gch.yaml")

# The ParFlorisModel can be instantiated either from a FlorisModel or from
# the input file.
pfmodel_1 = ParFlorisModel("inputs/gch.yaml") # Via input file
pfmodel_2 = ParFlorisModel(fmodel) # Via FlorisModel

# The ParFlorisModel has additional inputs which define the parallelization
# but don't affect the output.
pfmodel_3 = ParFlorisModel(
fmodel,
interface="multiprocessing", # Default
max_workers=2, # Defaults to num_cpu
n_wind_condition_splits=2, # Defaults to max_workers
)

# Define a simple inflow
time_series = TimeSeries(
wind_speeds=np.arange(1, 25, 0.5), wind_directions=270.0, turbulence_intensities=0.06
)

# Demonstrate that interface and results are the same
fmodel.set(wind_data=time_series)
pfmodel_1.set(wind_data=time_series)
pfmodel_2.set(wind_data=time_series)
pfmodel_3.set(wind_data=time_series)

fmodel.run()
pfmodel_1.run()
pfmodel_2.run()
pfmodel_3.run()

# Compare the results
powers_fmodel = fmodel.get_turbine_powers()
powers_pfmodel_1 = pfmodel_1.get_turbine_powers()
powers_pfmodel_2 = pfmodel_2.get_turbine_powers()
powers_pfmodel_3 = pfmodel_3.get_turbine_powers()

print(
f"Testing if outputs of fmodel and pfmodel_1 are "
f"close: {np.allclose(powers_fmodel, powers_pfmodel_1)}"
)
print(
f"Testing if outputs of fmodel and pfmodel_2 are "
f"close: {np.allclose(powers_fmodel, powers_pfmodel_2)}"
)
print(
f"Testing if outputs of fmodel and pfmodel_3 are "
f"close: {np.allclose(powers_fmodel, powers_pfmodel_3)}"
)

# Because ParFlorisModel is a subclass of FlorisModel, it can also be used as
# an input to the UncertainFlorisModel class. This allows for parallelization of
# the uncertainty calculations.
ufmodel = UncertainFlorisModel(fmodel)
pufmodel = UncertainFlorisModel(pfmodel_1)

# Demonstrate matched results
ufmodel.set(wind_data=time_series)
pufmodel.set(wind_data=time_series)

ufmodel.run()
pufmodel.run()

powers_ufmodel = ufmodel.get_turbine_powers()
powers_pufmodel = pufmodel.get_turbine_powers()

print("--------------------")
print(
f"Testing if outputs of ufmodel and pufmodel are "
f"close: {np.allclose(powers_ufmodel, powers_pufmodel)}"
)
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Example 9: Compare farm power with neighboring farm
"""Example 10: Compare farm power with neighboring farm
This example demonstrates how to use turbine_weights to define a set of turbines belonging
to a neighboring farm which impacts the power production of the farm under consideration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@
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
# When using parallel optimization it is important the "root" script include this
# if __name__ == "__main__": block to avoid problems
if __name__ == "__main__":

Expand All @@ -33,66 +33,52 @@
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
# using a rule of thumb
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
Expand All @@ -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
Expand Down Expand Up @@ -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))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""Example: Uncertain Model With Parallelization
This example demonstrates how to combined the parallelized model with the uncertain model
"""

import matplotlib.pyplot as plt
import numpy as np

from floris import (
FlorisModel,
TimeSeries,
UncertainFlorisModel,
)
from floris.par_floris_model import ParFlorisModel


# 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 and shows that the result obtained from the UncertainFlorisModel
# 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 = ParFlorisModel("../inputs/gch.yaml")

# Use the above model to declare a serial and parallel UncertainFlorisModel
ufmodel = UncertainFlorisModel(fmodel)
pufmodel = UncertainFlorisModel(pfmodel)


# Define an inflow where wind direction is swept while
# wind speed and turbulence intensity are held constant
wind_directions = np.arange(240.0, 300.0, 1.0)
time_series = TimeSeries(
wind_directions=wind_directions,
wind_speeds=8.0,
turbulence_intensities=0.06,
)

# Define a two turbine farm and apply the inflow
D = 126.0
layout_x = np.array([0, D * 6])
layout_y = [0, 0]

# Apply to fmodel, ufmodel, and pufmodel
fmodel.set(
layout_x=layout_x,
layout_y=layout_y,
wind_data=time_series,
)

ufmodel.set(
layout_x=layout_x,
layout_y=layout_y,
wind_data=time_series,
)

pufmodel.set(
layout_x=layout_x,
layout_y=layout_y,
wind_data=time_series,
)

# Run the models
fmodel.run()
ufmodel.run()
pufmodel.run()

# Collect the farm power results from each model
farm_powers_nom = fmodel.get_farm_power() / 1e3
farm_powers_unc = ufmodel.get_farm_power() / 1e3
farm_powers_punc = pufmodel.get_farm_power() / 1e3

# Compare the results
fig, ax = plt.subplots()
ax.plot(wind_directions, farm_powers_nom.flatten(), 'k-', label="Nominal power")
ax.plot(wind_directions, farm_powers_unc.flatten(), 'bs-', label="Uncertain power")
ax.plot(wind_directions, farm_powers_punc.flatten(), 'r.--', label="Parallel uncertain power")
ax.grid(True)
ax.legend()
ax.set_xlabel("Wind Direction (deg)")
ax.set_ylabel("Power (kW)")

plt.show()
1 change: 1 addition & 0 deletions floris/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
4 changes: 2 additions & 2 deletions floris/floris_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -619,8 +619,8 @@ def _get_weighted_turbine_powers(
# Confirm run() has been run
if self.core.state is not State.USED:
raise RuntimeError(
"Can't run function `FlorisModel.get_farm_power` without "
"first running `FlorisModel.run`."
f"Can't run function `{self.__class__.__name__}.get_farm_power` without "
f"first running `{self.__class__.__name__}.run`."
)

if turbine_weights is None:
Expand Down
Loading

0 comments on commit 25aa0ff

Please sign in to comment.