From 4685e508c818bf04d84c0a81f389909560657611 Mon Sep 17 00:00:00 2001 From: misi9170 <39596329+misi9170@users.noreply.github.com> Date: Fri, 15 Nov 2024 10:28:00 -0500 Subject: [PATCH 1/9] [BUGFIX] Address reference wind height warnings raised unnecessarily (#1017) * Set reference_wind_height in set_operation_model to avoid unnecessarily raising warning; add reference_wind_height property for ease of access. * Corresponding updates to UncertainFlorisModel. * Improve tests to explicitly set reference_wind_height. * add reference_wind_height setting to avoid unnecessary warning. * Add reference_wind_height setting to examples where turbine_type is set. --- .../003_specify_turbine_power_curve.py | 1 + .../001_compare_turbopark_implementations.py | 10 ++++++++-- floris/floris_model.py | 20 +++++++++++++++++-- floris/flow_visualization.py | 3 ++- floris/uncertain_floris_model.py | 20 +++++++++++++++++-- tests/floris_model_integration_test.py | 12 ++++++----- ...uncertain_floris_model_integration_test.py | 15 +++++++++----- 7 files changed, 64 insertions(+), 17 deletions(-) diff --git a/examples/examples_turbine/003_specify_turbine_power_curve.py b/examples/examples_turbine/003_specify_turbine_power_curve.py index 1c1b59707..aa4ced8e3 100644 --- a/examples/examples_turbine/003_specify_turbine_power_curve.py +++ b/examples/examples_turbine/003_specify_turbine_power_curve.py @@ -51,6 +51,7 @@ wind_speeds=wind_speeds, turbulence_intensities=turbulence_intensities, turbine_type=[turbine_dict], + reference_wind_height=fmodel.reference_wind_height ) fmodel.run() diff --git a/examples/examples_turbopark/001_compare_turbopark_implementations.py b/examples/examples_turbopark/001_compare_turbopark_implementations.py index b462b3561..14b85f081 100644 --- a/examples/examples_turbopark/001_compare_turbopark_implementations.py +++ b/examples/examples_turbopark/001_compare_turbopark_implementations.py @@ -35,7 +35,10 @@ ### Start by visualizing a single turbine in and its wake with the new model # Load the new TurboPark implementation and switch to constant CT turbine fmodel_new = FlorisModel("../inputs/turboparkgauss_cubature.yaml") -fmodel_new.set(turbine_type=[const_CT_turb]) +fmodel_new.set( + turbine_type=[const_CT_turb], + reference_wind_height=fmodel_new.reference_wind_height +) fmodel_new.run() u0 = fmodel_new.wind_speeds[0] @@ -94,7 +97,10 @@ ### Look at the wake profile at a single downstream distance for a range of wind directions # Load the original TurboPark implementation and switch to constant CT turbine fmodel_orig = FlorisModel("../inputs/turbopark_cubature.yaml") -fmodel_orig.set(turbine_type=[const_CT_turb]) +fmodel_orig.set( + turbine_type=[const_CT_turb], + reference_wind_height=fmodel_orig.reference_wind_height +) # Set up and solve flows wd_array = np.arange(225,315,0.1) diff --git a/floris/floris_model.py b/floris/floris_model.py index 09a5aa5d0..1f92fa092 100644 --- a/floris/floris_model.py +++ b/floris/floris_model.py @@ -1542,7 +1542,10 @@ def set_operation_model(self, operation_model: str | List[str]): # Set a single one here, then, and return turbine_type = self.core.farm.turbine_definitions[0] turbine_type["operation_model"] = operation_model - self.set(turbine_type=[turbine_type]) + self.set( + turbine_type=[turbine_type], + reference_wind_height=self.reference_wind_height + ) return else: operation_model = [operation_model]*self.core.farm.n_turbines @@ -1561,7 +1564,10 @@ def set_operation_model(self, operation_model: str | List[str]): ) turbine_type_list[tindex]["operation_model"] = operation_model[tindex] - self.set(turbine_type=turbine_type_list) + self.set( + turbine_type=turbine_type_list, + reference_wind_height=self.reference_wind_height + ) def copy(self): """Create an independent copy of the current FlorisModel object""" @@ -1702,6 +1708,16 @@ def n_turbines(self): """ return self.core.farm.n_turbines + @property + def reference_wind_height(self): + """ + Reference wind height. + + Returns: + float: Reference wind height. + """ + return self.core.flow_field.reference_wind_height + @property def turbine_average_velocities(self) -> NDArrayFloat: return average_velocity( diff --git a/floris/flow_visualization.py b/floris/flow_visualization.py index b893b172a..735dc9ddd 100644 --- a/floris/flow_visualization.py +++ b/floris/flow_visualization.py @@ -543,7 +543,8 @@ def calculate_horizontal_plane_with_turbines( awc_modes=awc_modes, awc_amplitudes=awc_amplitudes, awc_frequencies=awc_frequencies, - turbine_type=turbine_types_test + turbine_type=turbine_types_test, + reference_wind_height=fmodel_viz.reference_wind_height ) fmodel_viz.run() diff --git a/floris/uncertain_floris_model.py b/floris/uncertain_floris_model.py index 4d9d691f5..ab5b404ad 100644 --- a/floris/uncertain_floris_model.py +++ b/floris/uncertain_floris_model.py @@ -958,7 +958,10 @@ def set_operation_model(self, operation_model: str | List[str]): # Set a single one here, then, and return turbine_type = self.fmodel_unexpanded.core.farm.turbine_definitions[0] turbine_type["operation_model"] = operation_model - self.set(turbine_type=[turbine_type]) + self.set( + turbine_type=[turbine_type], + reference_wind_height=self.reference_wind_height + ) return else: operation_model = [operation_model] * self.fmodel_unexpanded.core.farm.n_turbines @@ -976,7 +979,10 @@ def set_operation_model(self, operation_model: str | List[str]): ) turbine_type_list[tindex]["operation_model"] = operation_model[tindex] - self.set(turbine_type=turbine_type_list) + self.set( + turbine_type=turbine_type_list, + reference_wind_height=self.reference_wind_height + ) def copy(self): """Create an independent copy of the current UncertainFlorisModel object""" @@ -1095,6 +1101,16 @@ def n_turbines(self): """ return self.fmodel_unexpanded.core.farm.n_turbines + @property + def reference_wind_height(self): + """ + Reference wind height. + + Returns: + float: Reference wind height. + """ + return self.fmodel_unexpanded.core.flow_field.reference_wind_height + @property def core(self): """ diff --git a/tests/floris_model_integration_test.py b/tests/floris_model_integration_test.py index fa05d43d3..395ff03a1 100644 --- a/tests/floris_model_integration_test.py +++ b/tests/floris_model_integration_test.py @@ -720,6 +720,8 @@ def test_set_operation_model(): fmodel.set_operation_model("simple-derating") assert fmodel.get_operation_model() == "simple-derating" + reference_wind_height = fmodel.reference_wind_height + # Check multiple turbine types works fmodel.set(layout_x=[0, 0], layout_y=[0, 1000]) fmodel.set_operation_model(["simple-derating", "cosine-loss"]) @@ -727,26 +729,26 @@ def test_set_operation_model(): # Check that setting a single turbine type, and then altering the operation model works fmodel.set(layout_x=[0, 0], layout_y=[0, 1000]) - fmodel.set(turbine_type=["nrel_5MW"]) + fmodel.set(turbine_type=["nrel_5MW"], reference_wind_height=reference_wind_height) fmodel.set_operation_model("simple-derating") assert fmodel.get_operation_model() == "simple-derating" # Check that setting over mutliple turbine types works - fmodel.set(turbine_type=["nrel_5MW", "iea_15MW"]) + fmodel.set(turbine_type=["nrel_5MW", "iea_15MW"], reference_wind_height=reference_wind_height) fmodel.set_operation_model("simple-derating") assert fmodel.get_operation_model() == "simple-derating" fmodel.set_operation_model(["simple-derating", "cosine-loss"]) assert fmodel.get_operation_model() == ["simple-derating", "cosine-loss"] # Check setting over single turbine type; then updating layout works - fmodel.set(turbine_type=["nrel_5MW"]) + fmodel.set(turbine_type=["nrel_5MW"], reference_wind_height=reference_wind_height) fmodel.set_operation_model("simple-derating") fmodel.set(layout_x=[0, 0, 0], layout_y=[0, 1000, 2000]) assert fmodel.get_operation_model() == "simple-derating" # Check that setting for multiple turbine types and then updating layout breaks fmodel.set(layout_x=[0, 0], layout_y=[0, 1000]) - fmodel.set(turbine_type=["nrel_5MW"]) + fmodel.set(turbine_type=["nrel_5MW"], reference_wind_height=reference_wind_height) fmodel.set_operation_model(["simple-derating", "cosine-loss"]) assert fmodel.get_operation_model() == ["simple-derating", "cosine-loss"] with pytest.raises(ValueError): @@ -754,7 +756,7 @@ def test_set_operation_model(): # Check one more variation fmodel.set(layout_x=[0, 0], layout_y=[0, 1000]) - fmodel.set(turbine_type=["nrel_5MW", "iea_15MW"]) + fmodel.set(turbine_type=["nrel_5MW", "iea_15MW"], reference_wind_height=reference_wind_height) fmodel.set_operation_model("simple-derating") fmodel.set(layout_x=[0, 0], layout_y=[0, 1000]) with pytest.raises(ValueError): diff --git a/tests/uncertain_floris_model_integration_test.py b/tests/uncertain_floris_model_integration_test.py index e8e95d513..02859e611 100644 --- a/tests/uncertain_floris_model_integration_test.py +++ b/tests/uncertain_floris_model_integration_test.py @@ -410,10 +410,15 @@ def test_get_operation_model(): def test_set_operation_model(): + # Define a reference wind height for cases when there are changes to + # turbine_type + ufmodel = UncertainFlorisModel(configuration=YAML_INPUT) ufmodel.set_operation_model("simple-derating") assert ufmodel.get_operation_model() == "simple-derating" + reference_wind_height = ufmodel.reference_wind_height + # Check multiple turbine types works ufmodel.set(layout_x=[0, 0], layout_y=[0, 1000]) ufmodel.set_operation_model(["simple-derating", "cosine-loss"]) @@ -424,26 +429,26 @@ def test_set_operation_model(): # Check that setting a single turbine type, and then altering the operation model works ufmodel.set(layout_x=[0, 0], layout_y=[0, 1000]) - ufmodel.set(turbine_type=["nrel_5MW"]) + ufmodel.set(turbine_type=["nrel_5MW"], reference_wind_height=reference_wind_height) ufmodel.set_operation_model("simple-derating") assert ufmodel.get_operation_model() == "simple-derating" # Check that setting over mutliple turbine types works - ufmodel.set(turbine_type=["nrel_5MW", "iea_15MW"]) + ufmodel.set(turbine_type=["nrel_5MW", "iea_15MW"], reference_wind_height=reference_wind_height) ufmodel.set_operation_model("simple-derating") assert ufmodel.get_operation_model() == "simple-derating" ufmodel.set_operation_model(["simple-derating", "cosine-loss"]) assert ufmodel.get_operation_model() == ["simple-derating", "cosine-loss"] # Check setting over single turbine type; then updating layout works - ufmodel.set(turbine_type=["nrel_5MW"]) + ufmodel.set(turbine_type=["nrel_5MW"], reference_wind_height=reference_wind_height) ufmodel.set_operation_model("simple-derating") ufmodel.set(layout_x=[0, 0, 0], layout_y=[0, 1000, 2000]) assert ufmodel.get_operation_model() == "simple-derating" # Check that setting for multiple turbine types and then updating layout breaks ufmodel.set(layout_x=[0, 0], layout_y=[0, 1000]) - ufmodel.set(turbine_type=["nrel_5MW"]) + ufmodel.set(turbine_type=["nrel_5MW"], reference_wind_height=reference_wind_height) ufmodel.set_operation_model(["simple-derating", "cosine-loss"]) assert ufmodel.get_operation_model() == ["simple-derating", "cosine-loss"] with pytest.raises(ValueError): @@ -451,7 +456,7 @@ def test_set_operation_model(): # Check one more variation ufmodel.set(layout_x=[0, 0], layout_y=[0, 1000]) - ufmodel.set(turbine_type=["nrel_5MW", "iea_15MW"]) + ufmodel.set(turbine_type=["nrel_5MW", "iea_15MW"], reference_wind_height=reference_wind_height) ufmodel.set_operation_model("simple-derating") ufmodel.set(layout_x=[0, 0], layout_y=[0, 1000]) with pytest.raises(ValueError): From 92141ef9c651125899d2005464e7a32cc1f69468 Mon Sep 17 00:00:00 2001 From: Rafael M Mudafort Date: Fri, 15 Nov 2024 12:42:42 -0600 Subject: [PATCH 2/9] Test on all supported Python versions (#1019) * Test on all supported Python versions Pages build and speed check are on the latest version only * Support Python 3.8 The pip to merge dicts is supported in Python 3.9+; replace with the dict.update method --- .github/workflows/check-working-examples.yaml | 2 +- .github/workflows/continuous-integration-workflow.yaml | 2 +- .github/workflows/deploy-pages.yaml | 2 +- .github/workflows/quality-metrics-workflow.yaml | 2 +- floris/flow_visualization.py | 5 +++-- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/check-working-examples.yaml b/.github/workflows/check-working-examples.yaml index 032f77fc0..7580ae3b5 100644 --- a/.github/workflows/check-working-examples.yaml +++ b/.github/workflows/check-working-examples.yaml @@ -8,7 +8,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - python-version: ["3.9", "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] os: [ubuntu-latest] #, macos-latest, windows-latest] fail-fast: False diff --git a/.github/workflows/continuous-integration-workflow.yaml b/.github/workflows/continuous-integration-workflow.yaml index 5e27b3c38..0035cdb0e 100644 --- a/.github/workflows/continuous-integration-workflow.yaml +++ b/.github/workflows/continuous-integration-workflow.yaml @@ -8,7 +8,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] os: [ubuntu-latest] #, macos-latest, windows-latest] fail-fast: False env: diff --git a/.github/workflows/deploy-pages.yaml b/.github/workflows/deploy-pages.yaml index 3da057988..708ba1930 100644 --- a/.github/workflows/deploy-pages.yaml +++ b/.github/workflows/deploy-pages.yaml @@ -18,7 +18,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: "3.10" + python-version: "3.13" - name: Install dependencies run: | diff --git a/.github/workflows/quality-metrics-workflow.yaml b/.github/workflows/quality-metrics-workflow.yaml index 3e8365ff0..f2b9a073f 100644 --- a/.github/workflows/quality-metrics-workflow.yaml +++ b/.github/workflows/quality-metrics-workflow.yaml @@ -8,7 +8,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - python-version: ["3.10"] + python-version: ["3.13"] os: [ubuntu-latest] fail-fast: False diff --git a/floris/flow_visualization.py b/floris/flow_visualization.py index 735dc9ddd..41c340ac2 100644 --- a/floris/flow_visualization.py +++ b/floris/flow_visualization.py @@ -760,8 +760,9 @@ def add_ref_lines( 'color': 'k', 'linewidth': 1.1 } - kwargs = default_params | kwargs + params = copy.deepcopy(default_params) + params.update(kwargs) for ax in self.axs[row]: for coordinate in ref_lines_D: - ax.plot([0.0, 1.0], [coordinate, coordinate], **kwargs) + ax.plot([0.0, 1.0], [coordinate, coordinate], **params) From 6fca3fb45fda3637b1d214decff1d69fbf86f9ae Mon Sep 17 00:00:00 2001 From: misi9170 <39596329+misi9170@users.noreply.github.com> Date: Tue, 19 Nov 2024 10:29:04 -0500 Subject: [PATCH 3/9] Allow yaw optimization with disabled turbines (into develop) (#1031) * Update MixedOperationModel to better handle disabling turbines, and remove disabled turbines from default power_setpoints on FlorisModel. * Update yaw optimization to pass through power_setpoints. * Raise warning for incompatible op model in scipy optimizer * Enable geomtric yaw optimization with disabled turbines. * Add example of optimizing with disabled turbines. * Update example to include all active case. * Add tests for yaw opt with turbines disabled, as well as basic serial refine operation. * Add similar test for Geometric yaw (only farm power comparisons removed). * Formatting fixes. * Limit yaw angles to zero for disabled turbines. * Update comment on disabled turbines' yaw angles. --- ...008_optimize_yaw_with_disabled_turbines.py | 48 ++++++++ floris/core/turbine/operation_models.py | 92 +++++++++----- floris/floris_model.py | 5 +- .../yaw_optimization/yaw_optimization_base.py | 12 +- .../yaw_optimizer_geometric.py | 8 +- .../yaw_optimization/yaw_optimizer_scipy.py | 6 + .../yaw_optimization/yaw_optimizer_sr.py | 5 +- tests/floris_model_integration_test.py | 21 +++- tests/geometric_yaw_unit_test.py | 115 ++++++++++++++++++ tests/serial_refine_unit_test.py | 107 ++++++++++++++++ 10 files changed, 376 insertions(+), 43 deletions(-) create mode 100644 examples/examples_control_optimization/008_optimize_yaw_with_disabled_turbines.py create mode 100644 tests/geometric_yaw_unit_test.py create mode 100644 tests/serial_refine_unit_test.py diff --git a/examples/examples_control_optimization/008_optimize_yaw_with_disabled_turbines.py b/examples/examples_control_optimization/008_optimize_yaw_with_disabled_turbines.py new file mode 100644 index 000000000..bd4d80b7b --- /dev/null +++ b/examples/examples_control_optimization/008_optimize_yaw_with_disabled_turbines.py @@ -0,0 +1,48 @@ +"""Example: Optimizing yaw angles with disabled turbines + +This example demonstrates how to optimize yaw angles in FLORIS, when some turbines are disabled. +The example optimization is run using both YawOptimizerSR and YawOptimizerGeometric, the two +yaw optimizers that support disabling turbines. +""" + +import numpy as np + +from floris import FlorisModel +from floris.optimization.yaw_optimization.yaw_optimizer_geometric import YawOptimizationGeometric +from floris.optimization.yaw_optimization.yaw_optimizer_sr import YawOptimizationSR + + +# Load a 3-turbine model +fmodel = FlorisModel("../inputs/gch.yaml") + +# Set wind conditions to be the same for two cases +fmodel.set(wind_directions=[270.]*2, wind_speeds=[8.]*2, turbulence_intensities=[.06]*2) + +# First run the case where all turbines are active and print results +yaw_opt = YawOptimizationSR(fmodel) +df_opt = yaw_opt.optimize() +print("Serial Refine optimized yaw angles (all turbines active) [deg]:\n", df_opt.yaw_angles_opt) + +yaw_opt = YawOptimizationGeometric(fmodel) +df_opt = yaw_opt.optimize() +print("\nGeometric optimized yaw angles (all turbines active) [deg]:\n", df_opt.yaw_angles_opt) + +# Disable turbines (different pattern for each of the two cases) +# First case: disable the middle turbine +# Second case: disable the front turbine +fmodel.set_operation_model('mixed') +fmodel.set(disable_turbines=np.array([[False, True, False], [True, False, False]])) + +# Rerun optimizations and print results +yaw_opt = YawOptimizationSR(fmodel) +df_opt = yaw_opt.optimize() +print( + "\nSerial Refine optimized yaw angles (some turbines disabled) [deg]:\n", + df_opt.yaw_angles_opt +) +# Note that disabled turbines are assigned a zero yaw angle, but their yaw angle is arbitrary as it +# does not affect the total power output. + +yaw_opt = YawOptimizationGeometric(fmodel) +df_opt = yaw_opt.optimize() +print("\nGeometric optimized yaw angles (some turbines disabled) [deg]:\n", df_opt.yaw_angles_opt) diff --git a/floris/core/turbine/operation_models.py b/floris/core/turbine/operation_models.py index a6c1ff160..066e26b04 100644 --- a/floris/core/turbine/operation_models.py +++ b/floris/core/turbine/operation_models.py @@ -382,22 +382,22 @@ def axial_induction( @define class MixedOperationTurbine(BaseOperationModel): + @staticmethod def power( yaw_angles: NDArrayFloat, power_setpoints: NDArrayFloat, **kwargs ): - # Yaw angles mask all yaw_angles not equal to zero - yaw_angles_mask = yaw_angles != 0.0 - power_setpoints_mask = power_setpoints < POWER_SETPOINT_DEFAULT - neither_mask = np.logical_not(yaw_angles_mask) & np.logical_not(power_setpoints_mask) - - if (power_setpoints_mask & yaw_angles_mask).any(): - raise ValueError(( - "Power setpoints and yaw angles are incompatible." - "If yaw_angles entry is nonzero, power_setpoints must be greater than" - " or equal to {0}.".format(POWER_SETPOINT_DEFAULT) - )) + ( + yaw_angles, + power_setpoints, + yaw_angles_mask, + power_setpoints_mask, + neither_mask + ) = MixedOperationTurbine._handle_mixed_operation_setpoints( + yaw_angles=yaw_angles, + power_setpoints=power_setpoints + ) powers = np.zeros_like(power_setpoints) powers[yaw_angles_mask] += CosineLossTurbine.power( @@ -414,21 +414,22 @@ def power( return powers + @staticmethod def thrust_coefficient( yaw_angles: NDArrayFloat, power_setpoints: NDArrayFloat, **kwargs ): - yaw_angles_mask = yaw_angles != 0.0 - power_setpoints_mask = power_setpoints < POWER_SETPOINT_DEFAULT - neither_mask = np.logical_not(yaw_angles_mask) & np.logical_not(power_setpoints_mask) - - if (power_setpoints_mask & yaw_angles_mask).any(): - raise ValueError(( - "Power setpoints and yaw angles are incompatible." - "If yaw_angles entry is nonzero, power_setpoints must be greater than" - " or equal to {0}.".format(POWER_SETPOINT_DEFAULT) - )) + ( + yaw_angles, + power_setpoints, + yaw_angles_mask, + power_setpoints_mask, + neither_mask + ) = MixedOperationTurbine._handle_mixed_operation_setpoints( + yaw_angles=yaw_angles, + power_setpoints=power_setpoints + ) thrust_coefficients = np.zeros_like(power_setpoints) thrust_coefficients[yaw_angles_mask] += CosineLossTurbine.thrust_coefficient( @@ -445,21 +446,22 @@ def thrust_coefficient( return thrust_coefficients + @staticmethod def axial_induction( yaw_angles: NDArrayFloat, power_setpoints: NDArrayFloat, **kwargs ): - yaw_angles_mask = yaw_angles != 0.0 - power_setpoints_mask = power_setpoints < POWER_SETPOINT_DEFAULT - neither_mask = np.logical_not(yaw_angles_mask) & np.logical_not(power_setpoints_mask) - - if (power_setpoints_mask & yaw_angles_mask).any(): - raise ValueError(( - "Power setpoints and yaw angles are incompatible." - "If yaw_angles entry is nonzero, power_setpoints must be greater than" - " or equal to {0}.".format(POWER_SETPOINT_DEFAULT) - )) + ( + yaw_angles, + power_setpoints, + yaw_angles_mask, + power_setpoints_mask, + neither_mask + ) = MixedOperationTurbine._handle_mixed_operation_setpoints( + yaw_angles=yaw_angles, + power_setpoints=power_setpoints + ) axial_inductions = np.zeros_like(power_setpoints) axial_inductions[yaw_angles_mask] += CosineLossTurbine.axial_induction( @@ -476,6 +478,34 @@ def axial_induction( return axial_inductions + @staticmethod + def _handle_mixed_operation_setpoints( + yaw_angles: NDArrayFloat, + power_setpoints: NDArrayFloat, + ): + """ + Check for incompatible yaw angles and power setpoints and raise an error if found. + Return masks and updated setpoints. + """ + # If any turbines are disabled, set their yaw angles to zero + yaw_angles[power_setpoints <= POWER_SETPOINT_DISABLED] = 0.0 + + # Create masks for whether yaw angles and power setpoints are set + yaw_angles_mask = yaw_angles != 0.0 + power_setpoints_mask = power_setpoints < POWER_SETPOINT_DEFAULT + neither_mask = np.logical_not(yaw_angles_mask) & np.logical_not(power_setpoints_mask) + + # Check for incompatibility and raise error if found. + if (power_setpoints_mask & yaw_angles_mask).any(): + raise ValueError(( + "Power setpoints and yaw angles are incompatible." + "If yaw_angles entry is nonzero, power_setpoints must be greater than" + " or equal to {0}.".format(POWER_SETPOINT_DEFAULT) + )) + + # Return updated setpoints as well as masks + return yaw_angles, power_setpoints, yaw_angles_mask, power_setpoints_mask, neither_mask + @define class AWCTurbine(BaseOperationModel): """ diff --git a/floris/floris_model.py b/floris/floris_model.py index 1f92fa092..83610e993 100644 --- a/floris/floris_model.py +++ b/floris/floris_model.py @@ -453,10 +453,7 @@ def set( # previous setting if not (_yaw_angles == 0).all(): self.core.farm.set_yaw_angles(_yaw_angles) - if not ( - (_power_setpoints == POWER_SETPOINT_DEFAULT) - | (_power_setpoints == POWER_SETPOINT_DISABLED) - ).all(): + if not (_power_setpoints == POWER_SETPOINT_DEFAULT).all(): self.core.farm.set_power_setpoints(_power_setpoints) if _awc_modes is not None: self.core.farm.set_awc_modes(_awc_modes) diff --git a/floris/optimization/yaw_optimization/yaw_optimization_base.py b/floris/optimization/yaw_optimization/yaw_optimization_base.py index 07a2f7e11..1949d132c 100644 --- a/floris/optimization/yaw_optimization/yaw_optimization_base.py +++ b/floris/optimization/yaw_optimization/yaw_optimization_base.py @@ -5,6 +5,7 @@ import numpy as np import pandas as pd +from floris.core.turbine.operation_models import POWER_SETPOINT_DISABLED from floris.logging_manager import LoggingManager from .yaw_optimization_tools import derive_downstream_turbines @@ -99,7 +100,7 @@ def __init__( """ # Save turbine object to self - self.fmodel = fmodel.copy() + self.fmodel = copy.deepcopy(fmodel) self.nturbs = len(self.fmodel.layout_x) # # Check floris options @@ -131,6 +132,11 @@ def __init__( self.minimum_yaw_angle = self._unpack_variable(minimum_yaw_angle) self.maximum_yaw_angle = self._unpack_variable(maximum_yaw_angle) + # Limit yaw angles to zero for disabled turbines + active_turbines = fmodel.core.farm.power_setpoints > POWER_SETPOINT_DISABLED + self.minimum_yaw_angle[~active_turbines] = 0.0 + self.maximum_yaw_angle[~active_turbines] = 0.0 + # Set initial condition for optimization if x0 is not None: self.x0 = self._unpack_variable(x0) @@ -224,7 +230,7 @@ def _reduce_control_problem(self): self.turbs_to_opt = (self.maximum_yaw_angle - self.minimum_yaw_angle >= 0.001) # Initialize subset variables as full set - self.fmodel_subset = self.fmodel.copy() + self.fmodel_subset = copy.deepcopy(self.fmodel) n_findex_subset = copy.deepcopy(self.fmodel.core.flow_field.n_findex) minimum_yaw_angle_subset = copy.deepcopy(self.minimum_yaw_angle) maximum_yaw_angle_subset = copy.deepcopy(self.maximum_yaw_angle) @@ -301,6 +307,7 @@ def _calculate_farm_power( ti_array=None, turbine_weights=None, heterogeneous_speed_multipliers=None, + power_setpoints=None, ): """ Calculate the wind farm power production assuming the predefined @@ -353,6 +360,7 @@ def _calculate_farm_power( wind_speeds=ws_array, turbulence_intensities=ti_array, yaw_angles=yaw_angles, + power_setpoints=power_setpoints, ) fmodel_subset.run() turbine_power = fmodel_subset.get_turbine_powers() diff --git a/floris/optimization/yaw_optimization/yaw_optimizer_geometric.py b/floris/optimization/yaw_optimization/yaw_optimizer_geometric.py index ea68204b4..68c687512 100644 --- a/floris/optimization/yaw_optimization/yaw_optimizer_geometric.py +++ b/floris/optimization/yaw_optimization/yaw_optimizer_geometric.py @@ -1,6 +1,7 @@ import numpy as np +from floris.core.turbine.operation_models import POWER_SETPOINT_DISABLED from floris.utilities import rotate_coordinates_rel_west from .yaw_optimization_base import YawOptimization @@ -46,10 +47,11 @@ def optimize(self): # Loop through every WD individually. WS ignored! wd_array = self.fmodel_subset.core.flow_field.wind_directions + active_turbines = self.fmodel_subset.core.farm.power_setpoints > POWER_SETPOINT_DISABLED for nwdi, wd in enumerate(wd_array): - self._yaw_angles_opt_subset[nwdi, :] = geometric_yaw( - self.fmodel_subset.layout_x, - self.fmodel_subset.layout_y, + self._yaw_angles_opt_subset[nwdi, active_turbines[nwdi]] = geometric_yaw( + self.fmodel_subset.layout_x[active_turbines[nwdi]], + self.fmodel_subset.layout_y[active_turbines[nwdi]], wd, self.fmodel.core.farm.turbine_definitions[0]["rotor_diameter"], top_left_yaw_upper=self.maximum_yaw_angle[0, 0], diff --git a/floris/optimization/yaw_optimization/yaw_optimizer_scipy.py b/floris/optimization/yaw_optimization/yaw_optimizer_scipy.py index cdde87656..810144c50 100644 --- a/floris/optimization/yaw_optimization/yaw_optimizer_scipy.py +++ b/floris/optimization/yaw_optimization/yaw_optimizer_scipy.py @@ -30,6 +30,12 @@ def __init__( Instantiate YawOptimizationScipy object with a FlorisModel object and assign parameter values. """ + valid_op_models = ["cosine-loss"] + if fmodel.get_operation_model() not in valid_op_models: + raise ValueError( + "YawOptimizationScipy is currently limited to the following operation models: " + + ", ".join(valid_op_models) + ) if opt_options is None: # Default SciPy parameters opt_options = { diff --git a/floris/optimization/yaw_optimization/yaw_optimizer_sr.py b/floris/optimization/yaw_optimization/yaw_optimizer_sr.py index 2b5b7ad1b..127cca01b 100644 --- a/floris/optimization/yaw_optimization/yaw_optimizer_sr.py +++ b/floris/optimization/yaw_optimization/yaw_optimizer_sr.py @@ -93,6 +93,7 @@ def _calc_powers_with_memory(self, yaw_angles_subset, use_memory=True): wd_array_subset = self.fmodel_subset.core.flow_field.wind_directions ws_array_subset = self.fmodel_subset.core.flow_field.wind_speeds ti_array_subset = self.fmodel_subset.core.flow_field.turbulence_intensities + power_setpoints_subset = self.fmodel_subset.core.farm.power_setpoints turbine_weights_subset = self._turbine_weights_subset # Reformat yaw_angles_subset, if necessary @@ -108,6 +109,7 @@ def _calc_powers_with_memory(self, yaw_angles_subset, use_memory=True): wd_array_subset = np.tile(wd_array_subset, Ny) ws_array_subset = np.tile(ws_array_subset, Ny) ti_array_subset = np.tile(ti_array_subset, Ny) + power_setpoints_subset = np.tile(power_setpoints_subset, (Ny, 1)) turbine_weights_subset = np.tile(turbine_weights_subset, (Ny, 1)) # Initialize empty matrix for floris farm power outputs @@ -143,7 +145,8 @@ def _calc_powers_with_memory(self, yaw_angles_subset, use_memory=True): ti_array=ti_array_subset[~idx], turbine_weights=turbine_weights_subset[~idx, :], yaw_angles=yaw_angles_subset[~idx, :], - heterogeneous_speed_multipliers=het_sm + heterogeneous_speed_multipliers=het_sm, + power_setpoints=power_setpoints_subset[~idx, :], ) self.time_spent_in_floris += (timerpc() - start_time) diff --git a/tests/floris_model_integration_test.py b/tests/floris_model_integration_test.py index 395ff03a1..88a242c56 100644 --- a/tests/floris_model_integration_test.py +++ b/tests/floris_model_integration_test.py @@ -10,7 +10,7 @@ TimeSeries, WindRose, ) -from floris.core.turbine.operation_models import POWER_SETPOINT_DEFAULT +from floris.core.turbine.operation_models import POWER_SETPOINT_DEFAULT, POWER_SETPOINT_DISABLED TEST_DATA = Path(__file__).resolve().parent / "data" @@ -44,7 +44,24 @@ def test_assign_setpoints(): # power_setpoints and disable_turbines (disable_turbines overrides power_setpoints) fmodel.set(power_setpoints=[[1e6, 2e6]], disable_turbines=[[True, False]]) - assert np.allclose(fmodel.core.farm.power_setpoints, np.array([[0.001, 2e6]])) + assert np.allclose(fmodel.core.farm.power_setpoints, np.array([[POWER_SETPOINT_DISABLED, 2e6]])) + + # Setting sequentially is equivalent to setting together + fmodel.reset_operation() + fmodel.set(disable_turbines=[[True, False]]) + fmodel.set(yaw_angles=[[0, 30]]) + assert np.allclose( + fmodel.core.farm.power_setpoints, + np.array([[POWER_SETPOINT_DISABLED, POWER_SETPOINT_DEFAULT]]) + ) + assert np.allclose(fmodel.core.farm.yaw_angles, np.array([[0, 30]])) + + fmodel.set(disable_turbines=[[True, False]], yaw_angles=[[0, 30]]) + assert np.allclose( + fmodel.core.farm.power_setpoints, + np.array([[POWER_SETPOINT_DISABLED, POWER_SETPOINT_DEFAULT]]) + ) + assert np.allclose(fmodel.core.farm.yaw_angles, np.array([[0, 30]])) def test_set_run(): """ diff --git a/tests/geometric_yaw_unit_test.py b/tests/geometric_yaw_unit_test.py new file mode 100644 index 000000000..2fb0452bb --- /dev/null +++ b/tests/geometric_yaw_unit_test.py @@ -0,0 +1,115 @@ + +import numpy as np +import pandas as pd + +from floris import FlorisModel +from floris.optimization.yaw_optimization.yaw_optimizer_geometric import YawOptimizationGeometric + + +DEBUG = False +VELOCITY_MODEL = "gauss" +DEFLECTION_MODEL = "gauss" + +# Inputs for basic yaw optimizations +WIND_DIRECTIONS = [0.0, 90.0, 180.0, 270.0] +WIND_SPEEDS = [8.0] * 4 +TURBULENCE_INTENSITIES = [0.06] * 4 +LAYOUT_X = [0.0, 600.0, 1200.0] +LAYOUT_Y = [0.0, 0.0, 0.0] +MAXIMUM_YAW_ANGLE = 25.0 + +def test_basic_optimization(sample_inputs_fixture): + """ + The Serial Refine (SR) method optimizes yaw angles based on a sequential, iterative yaw + optimization scheme. This test checks basic properties of the optimization result. + """ + 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) + + fmodel.set( + layout_x=LAYOUT_X, + layout_y=LAYOUT_Y, + wind_directions=WIND_DIRECTIONS, + wind_speeds=WIND_SPEEDS, + turbulence_intensities=TURBULENCE_INTENSITIES + ) + fmodel.set_operation_model("cosine-loss") + + yaw_opt = YawOptimizationGeometric( + fmodel, + minimum_yaw_angle=0.0, + maximum_yaw_angle=MAXIMUM_YAW_ANGLE + ) + df_opt = yaw_opt.optimize() + + # Unaligned conditions + assert np.allclose(df_opt.loc[0, "yaw_angles_opt"], 0.0) + assert np.allclose(df_opt.loc[2, "yaw_angles_opt"], 0.0) + + # Check aligned conditions + # Check maximum and minimum are respected + assert (df_opt.loc[1, "yaw_angles_opt"] <= MAXIMUM_YAW_ANGLE).all() + assert (df_opt.loc[3, "yaw_angles_opt"] <= MAXIMUM_YAW_ANGLE).all() + assert (df_opt.loc[1, "yaw_angles_opt"] >= 0.0).all() + assert (df_opt.loc[3, "yaw_angles_opt"] >= 0.0).all() + + # Check 90.0 and 270.0 are symmetric + assert np.allclose(df_opt.loc[1, "yaw_angles_opt"], np.flip(df_opt.loc[3, "yaw_angles_opt"])) + + # Check last turbine's angles are zero at 270.0 + assert np.allclose(df_opt.loc[3, "yaw_angles_opt"][-1], 0.0) + + # YawOptimizationGeometric does not compute farm powers + +def test_disabled_turbines(sample_inputs_fixture): + """ + Tests SR when some turbines are disabled and checks that the results are equivalent to removing + those turbines from the wind farm. Need a tight layout to ensure that the front-to-back distance + is not too large. + """ + + 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) + + fmodel.set( + layout_x=LAYOUT_X, + layout_y=LAYOUT_Y, + wind_directions=WIND_DIRECTIONS, + wind_speeds=WIND_SPEEDS, + turbulence_intensities=TURBULENCE_INTENSITIES + ) + fmodel.set_operation_model("mixed") + + # Disable the middle turbine in all wind conditions, run optimization, and extract results + fmodel.set(disable_turbines=[[False, True, False]]*4) + yaw_opt = YawOptimizationGeometric( + fmodel, + minimum_yaw_angle=0.0, + maximum_yaw_angle=MAXIMUM_YAW_ANGLE + ) + df_opt = yaw_opt.optimize() + yaw_angles_opt_disabled = df_opt.loc[3, "yaw_angles_opt"] + + # Set up a new wind farm with the middle turbine removed + fmodel = FlorisModel(sample_inputs_fixture.core) + fmodel.set( + layout_x=np.array(LAYOUT_X)[[0, 2]], + layout_y=np.array(LAYOUT_Y)[[0, 2]], + wind_directions=WIND_DIRECTIONS, + wind_speeds=WIND_SPEEDS, + turbulence_intensities=TURBULENCE_INTENSITIES + ) + fmodel.set_operation_model("cosine-loss") + yaw_opt = YawOptimizationGeometric( + fmodel, + minimum_yaw_angle=0.0, + maximum_yaw_angle=MAXIMUM_YAW_ANGLE + ) + df_opt = yaw_opt.optimize() + yaw_angles_opt_removed = df_opt.loc[3, "yaw_angles_opt"] + + assert np.allclose(yaw_angles_opt_disabled[[0, 2]], yaw_angles_opt_removed) diff --git a/tests/serial_refine_unit_test.py b/tests/serial_refine_unit_test.py new file mode 100644 index 000000000..cfda030a7 --- /dev/null +++ b/tests/serial_refine_unit_test.py @@ -0,0 +1,107 @@ + +import numpy as np +import pandas as pd + +from floris import FlorisModel +from floris.optimization.yaw_optimization.yaw_optimizer_sr import YawOptimizationSR + + +DEBUG = False +VELOCITY_MODEL = "gauss" +DEFLECTION_MODEL = "gauss" + +# Inputs for basic yaw optimizations +WIND_DIRECTIONS = [0.0, 90.0, 180.0, 270.0] +WIND_SPEEDS = [8.0] * 4 +TURBULENCE_INTENSITIES = [0.06] * 4 +LAYOUT_X = [0.0, 600.0, 1200.0] +LAYOUT_Y = [0.0, 0.0, 0.0] +MAXIMUM_YAW_ANGLE = 25.0 + +def test_basic_optimization(sample_inputs_fixture): + """ + The Serial Refine (SR) method optimizes yaw angles based on a sequential, iterative yaw + optimization scheme. This test checks basic properties of the optimization result. + """ + 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) + + fmodel.set( + layout_x=LAYOUT_X, + layout_y=LAYOUT_Y, + wind_directions=WIND_DIRECTIONS, + wind_speeds=WIND_SPEEDS, + turbulence_intensities=TURBULENCE_INTENSITIES + ) + fmodel.set_operation_model("cosine-loss") + + yaw_opt = YawOptimizationSR(fmodel, minimum_yaw_angle=0.0, maximum_yaw_angle=MAXIMUM_YAW_ANGLE) + df_opt = yaw_opt.optimize() + + # Unaligned conditions + assert np.allclose(df_opt.loc[0, "yaw_angles_opt"], 0.0) + assert np.allclose(df_opt.loc[2, "yaw_angles_opt"], 0.0) + + # Check aligned conditions + # Check maximum and minimum are respected + assert (df_opt.loc[1, "yaw_angles_opt"] <= MAXIMUM_YAW_ANGLE).all() + assert (df_opt.loc[3, "yaw_angles_opt"] <= MAXIMUM_YAW_ANGLE).all() + assert (df_opt.loc[1, "yaw_angles_opt"] >= 0.0).all() + assert (df_opt.loc[3, "yaw_angles_opt"] >= 0.0).all() + + # Check 90.0 and 270.0 are symmetric + assert np.allclose(df_opt.loc[1, "yaw_angles_opt"], np.flip(df_opt.loc[3, "yaw_angles_opt"])) + + # Check last turbine's angles are zero at 270.0 + assert np.allclose(df_opt.loc[3, "yaw_angles_opt"][-1], 0.0) + + # Check that optimizer reports a power improvement + assert (df_opt["farm_power_opt"] >= df_opt["farm_power_baseline"]).all() + +def test_disabled_turbines(sample_inputs_fixture): + """ + Tests SR when some turbines are disabled and checks that the results are equivalent to removing + those turbines from the wind farm. Need a tight layout to ensure that the front-to-back distance + is not too large. + """ + + 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) + + fmodel.set( + layout_x=LAYOUT_X, + layout_y=LAYOUT_Y, + wind_directions=WIND_DIRECTIONS, + wind_speeds=WIND_SPEEDS, + turbulence_intensities=TURBULENCE_INTENSITIES + ) + fmodel.set_operation_model("mixed") + + # Disable the middle turbine in all wind conditions, run optimization, and extract results + fmodel.set(disable_turbines=[[False, True, False]]*4) + yaw_opt = YawOptimizationSR(fmodel, minimum_yaw_angle=0.0, maximum_yaw_angle=MAXIMUM_YAW_ANGLE) + df_opt = yaw_opt.optimize() + yaw_angles_opt_disabled = df_opt.loc[3, "yaw_angles_opt"] + farm_power_opt_disabled = df_opt.loc[3, "farm_power_opt"] + + # Set up a new wind farm with the middle turbine removed + fmodel = FlorisModel(sample_inputs_fixture.core) + fmodel.set( + layout_x=np.array(LAYOUT_X)[[0, 2]], + layout_y=np.array(LAYOUT_Y)[[0, 2]], + wind_directions=WIND_DIRECTIONS, + wind_speeds=WIND_SPEEDS, + turbulence_intensities=TURBULENCE_INTENSITIES + ) + fmodel.set_operation_model("cosine-loss") + yaw_opt = YawOptimizationSR(fmodel, minimum_yaw_angle=0.0, maximum_yaw_angle=MAXIMUM_YAW_ANGLE) + df_opt = yaw_opt.optimize() + yaw_angles_opt_removed = df_opt.loc[3, "yaw_angles_opt"] + farm_power_opt_removed = df_opt.loc[3, "farm_power_opt"] + + assert np.allclose(yaw_angles_opt_disabled[[0, 2]], yaw_angles_opt_removed) + assert np.allclose(farm_power_opt_disabled, farm_power_opt_removed) From dff48cdeee700a64513870a7bd4cc79827b32b17 Mon Sep 17 00:00:00 2001 From: paulf81 Date: Wed, 20 Nov 2024 10:26:16 -0700 Subject: [PATCH 4/9] Move all metadata to pyproject.toml (#1026) --- .github/workflows/python-publish.yml | 4 +- docs/dev_guide.md | 4 +- floris/__init__.py | 4 +- floris/version.py | 1 - pyproject.toml | 65 ++++++++++++++++++++ setup.py | 89 ---------------------------- 6 files changed, 71 insertions(+), 96 deletions(-) delete mode 100644 floris/version.py delete mode 100644 setup.py diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index ead77afce..9281f86c0 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -20,11 +20,11 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install setuptools wheel twine + pip install build twine - name: Build and publish env: TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} run: | - python setup.py sdist bdist_wheel + python -m build twine upload dist/* diff --git a/docs/dev_guide.md b/docs/dev_guide.md index 4a7c5dd76..a4a08e195 100644 --- a/docs/dev_guide.md +++ b/docs/dev_guide.md @@ -210,7 +210,7 @@ is located at `floris/.github/workflows/continuous-integration-workflow.yaml`. The online documentation is built with Jupyter Book which uses Sphinx as a framework. It is automatically built and hosted by GitHub, but it can also be compiled locally. Additional dependencies are required -for the documentation, and they are listed in the `EXTRAS` of `setup.py`. +for the documentation, and they are listed in the `project.optional-dependencies` of `pyproject.toml`. The commands to build the docs are given below. After successfully compiling, a file should be located at ``docs/_build/html/index.html``. This file can be opened in any browser. @@ -246,7 +246,7 @@ Be sure to complete each step in the sequence as described. with a commit message such as "Update version to vN.M". The version number must be updated in the following two files: - [floris/README.md](https://github.com/NREL/floris/blob/main/README.md) - - [floris/floris/version.py](https://github.com/NREL/floris/blob/main/floris/version.py) + - [pyproject.toml](https://github.com/NREL/floris/blob/main/pyproject.toml) Note that a `.0` version number is left off meaning that valid versions are `v3`, `v3.1`, `v3.1.1`, etc. diff --git a/floris/__init__.py b/floris/__init__.py index d97bb24eb..717ea3175 100644 --- a/floris/__init__.py +++ b/floris/__init__.py @@ -1,9 +1,9 @@ +from importlib.metadata import version from pathlib import Path -with open(Path(__file__).parent / "version.py") as _version_file: - __version__ = _version_file.read().strip() +__version__ = version("floris") from .floris_model import FlorisModel diff --git a/floris/version.py b/floris/version.py deleted file mode 100644 index bf77d5496..000000000 --- a/floris/version.py +++ /dev/null @@ -1 +0,0 @@ -4.2 diff --git a/pyproject.toml b/pyproject.toml index 330c5a2d8..dea224866 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,6 +2,71 @@ requires = ["setuptools >= 40.6.0", "wheel"] build-backend = "setuptools.build_meta" +[project] +name = "floris" +version = "4.2" +description = "A controls-oriented engineering wake model." +readme = "README.md" +requires-python = ">=3.8" +authors = [ + { name = "Rafael Mudafort", email = "rafael.mudafort@nrel.gov" }, + { name = "Paul Fleming", email = "paul.fleming@nrel.gov" }, + { name = "Michael (Misha) Sinner", email = "Michael.Sinner@nrel.gov" }, + { name = "Eric Simley", email = "Eric.Simley@nrel.gov" }, + { name = "Christopher Bay", email = "Christopher.Bay@nrel.gov" }, +] +license = { file = "LICENSE.txt" } +keywords = ["floris"] +classifiers = [ + "License :: OSI Approved :: BSD License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy" +] +dependencies = [ + "attrs", + "pyyaml~=6.0", + "numexpr~=2.0", + "numpy~=1.20", + "scipy~=1.1", + "matplotlib~=3.0", + "pandas~=2.0", + "shapely~=2.0", + "coloredlogs~=15.0", + "pathos~=0.3", +] + +[project.optional-dependencies] +docs = [ + "jupyter-book", + "sphinx-book-theme", + "sphinx-autodoc-typehints", + "sphinxcontrib-autoyaml", + "sphinxcontrib.mermaid", +] +develop = [ + "pytest", + "pre-commit", + "ruff", + "isort" +] + +[tool.setuptools.packages.find] +include = ["floris*"] + +[tool.setuptools.package-data] +floris = [ + "turbine_library/*.yaml", + "core/wake_velocity/turbopark_lookup_table.mat" +] + +[project.urls] +Homepage = "https://github.com/NREL/floris" +Documentation = "https://nrel.github.io/floris/" [coverage.run] # Coverage.py configuration file diff --git a/setup.py b/setup.py deleted file mode 100644 index 7e3a11be4..000000000 --- a/setup.py +++ /dev/null @@ -1,89 +0,0 @@ - -from pathlib import Path - -from setuptools import find_packages, setup - - -# Package meta-data. -NAME = "FLORIS" -DESCRIPTION = "A controls-oriented engineering wake model." -URL = "https://github.com/NREL/FLORIS" -EMAIL = "rafael.mudafort@nrel.gov" -AUTHOR = "NREL National Wind Technology Center" -REQUIRES_PYTHON = ">=3.8.0" - -# What packages are required for this module to be executed? -REQUIRED = [ - # simulation - "attrs", - "pyyaml~=6.0", - "numexpr~=2.0", - "numpy~=1.20", - "scipy~=1.1", - - # tools - "matplotlib~=3.0", - "pandas~=2.0", - "shapely~=2.0", - - # utilities - "coloredlogs~=15.0", - "pathos~=0.3", -] - -# What packages are optional? -# To use: -# pip install -e ".[docs,develop]" install both sets of extras in editable install -# pip install -e ".[develop]" installs only developer packages in editable install -# pip install "floris[develop]" installs developer packages in non-editable install -EXTRAS = { - "docs": { - "jupyter-book", - "sphinx-book-theme", - "sphinx-autodoc-typehints", - "sphinxcontrib-autoyaml", - "sphinxcontrib.mermaid", - }, - "develop": { - "pytest", - "pre-commit", - "ruff", - "isort", - }, -} - -ROOT = Path(__file__).parent -with open(ROOT / "floris" / "version.py") as version_file: - VERSION = version_file.read().strip() - -setup( - name=NAME, - version=VERSION, - description=DESCRIPTION, - long_description=DESCRIPTION, - long_description_content_type="text/markdown", - author=AUTHOR, - author_email=EMAIL, - python_requires=REQUIRES_PYTHON, - url=URL, - packages=find_packages(exclude=["tests", "*.tests", "*.tests.*", "tests.*"]), - package_data={ - 'floris': ['turbine_library/*.yaml', 'core/wake_velocity/turbopark_lookup_table.mat'] - }, - install_requires=REQUIRED, - extras_require=EXTRAS, - include_package_data=True, - license_files = ('LICENSE.txt',), - classifiers=[ - # Trove classifiers - # Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers - "License :: OSI Approved :: BSD License", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: Implementation :: CPython", - "Programming Language :: Python :: Implementation :: PyPy" - ], -) From 93a9cde65c1d3b950e0975b4025530d735b36951 Mon Sep 17 00:00:00 2001 From: misi9170 <39596329+misi9170@users.noreply.github.com> Date: Wed, 20 Nov 2024 12:27:54 -0500 Subject: [PATCH 5/9] Add try: except to create legend instead of colorbar (#1028) --- floris/wind_data.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/floris/wind_data.py b/floris/wind_data.py index b470fe515..3d49dec4b 100644 --- a/floris/wind_data.py +++ b/floris/wind_data.py @@ -779,8 +779,12 @@ def plot( ) # Configure the plot - ax.figure.colorbar(sm_ws, ax=ax, **legend_kwargs) - ax.figure.tight_layout() + try: + ax.figure.colorbar(sm_ws, ax=ax, **legend_kwargs) + ax.figure.tight_layout() + except TypeError: + ax.legend(reversed(rects), ws_bins, **legend_kwargs) + ax.figure.get_children()[-1].remove() # Remove the empty colorbar ax.set_theta_direction(-1) ax.set_theta_offset(np.pi / 2.0) ax.set_theta_zero_location("N") @@ -1822,8 +1826,12 @@ def plot( ) # Configure the plot - ax.figure.colorbar(sm_wv, ax=ax, **legend_kwargs) - ax.figure.tight_layout() + try: + ax.figure.colorbar(sm_wv, ax=ax, **legend_kwargs) + ax.figure.tight_layout() + except TypeError: + ax.legend(reversed(rects), var_bins, **legend_kwargs) + ax.figure.get_children()[-1].remove() # Remove the empty colorbar ax.set_theta_direction(-1) ax.set_theta_offset(np.pi / 2.0) ax.set_theta_zero_location("N") From d961a74e414afc83fd5d111edd4ccc870748ebd1 Mon Sep 17 00:00:00 2001 From: paulf81 Date: Thu, 21 Nov 2024 13:39:36 -0700 Subject: [PATCH 6/9] Fix docs (#1034) * pin autoyaml * build with 3.10 * minor test fix * restore 3.13 * Temporary; force docs build without upload * Revert temporary forced docs build. --------- Co-authored-by: misi9170 --- pyproject.toml | 2 +- tests/wind_data_integration_test.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index dea224866..295756d84 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,7 @@ docs = [ "jupyter-book", "sphinx-book-theme", "sphinx-autodoc-typehints", - "sphinxcontrib-autoyaml", + "sphinxcontrib-autoyaml==1.1.1", "sphinxcontrib.mermaid", ] develop = [ diff --git a/tests/wind_data_integration_test.py b/tests/wind_data_integration_test.py index 02d2a7ced..d3ca57438 100644 --- a/tests/wind_data_integration_test.py +++ b/tests/wind_data_integration_test.py @@ -893,6 +893,7 @@ def test_wind_ti_rose_aggregate(): np.testing.assert_allclose(wind_rose.wind_speeds, wind_rose_2.wind_speeds) np.testing.assert_allclose(wind_rose.turbulence_intensities, wind_rose_2.turbulence_intensities) + wind_rose_2 = copy.deepcopy(wind_rose) wind_rose_2.downsample(ti_step=0.04, inplace=True) np.testing.assert_allclose( wind_rose_aggregate.turbulence_intensities, wind_rose_2.turbulence_intensities From 0c07a6857245f0e634c8a2bf7cbd7b89a046281e Mon Sep 17 00:00:00 2001 From: misi9170 <39596329+misi9170@users.noreply.github.com> Date: Wed, 27 Nov 2024 12:15:18 -0500 Subject: [PATCH 7/9] Add warnings about dropping support for python 3.8 (#1041) * Add statement about supporting active python versions and warnings regarding dropping 3.8 support * Try formatting warning differently. * Simpler formatting for warning. --- README.md | 3 +++ docs/index.md | 4 ++++ docs/installation.md | 7 ++++++- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 28954775a..91df13982 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,9 @@ the conversation in [GitHub Discussions](https://github.com/NREL/floris/discussi ## Installation +**WARNING:** +Support for python version 3.8 will be dropped in FLORIS v4.3. See [Installation documentation](https://nrel.github.io/floris/installation.html#installation) for details. + **If upgrading from a previous version, it is recommended to install FLORIS v4 into a new virtual environment**. If you intend to use [pyOptSparse](https://mdolab-pyoptsparse.readthedocs-hosted.com/en/latest/) with FLORIS, it is recommended to install that package first before installing FLORIS. diff --git a/docs/index.md b/docs/index.md index 202627695..3bd73151d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -8,6 +8,10 @@ is highly encouraged. If you are interested in using FLORIS to conduct studies of a wind farm or extending FLORIS to include your own wake model, please join the conversation in [GitHub Discussions](https://github.com/NREL/floris/discussions/)! +```{note} +Support for python version 3.8 will be dropped in FLORIS v4.3. See {ref}`installation` for details. +``` + ## Quick Start FLORIS is a Python package run on the command line typically by providing diff --git a/docs/installation.md b/docs/installation.md index 4a06260e6..0e1c22f9d 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -7,11 +7,16 @@ The following sections detail how download and install FLORIS for each use case. (requirements)= ## Requirements -FLORIS is intended to be used with Python 3.8 and up, and it is highly recommended that users +FLORIS is a python package. FLORIS is intended to work with all [active versions of python](https://devguide.python.org/versions/). Support will drop for python versions once they reach end-of-life. +It is highly recommended that users work within a virtual environment for both working with and working on FLORIS, to maintain a clean and sandboxed environment. The simplest way to get started with virtual environments is through [conda](https://docs.conda.io/en/latest/miniconda.html). +```{warning} +Support for python version 3.8 will be dropped in FLORIS v4.3. +``` + Installing into a Python environment that contains a previous version of FLORIS may cause conflicts. If you intend to use [pyOptSparse](https://mdolab-pyoptsparse.readthedocs-hosted.com/en/latest/) with FLORIS, it is recommended to install that package first before installing FLORIS. From d0c48130b63e10ea0131f19eb708ae41d326c2c4 Mon Sep 17 00:00:00 2001 From: misi9170 <39596329+misi9170@users.noreply.github.com> Date: Wed, 27 Nov 2024 12:21:51 -0500 Subject: [PATCH 8/9] [BUGFIX] Fix docs build 2 (#1036) * remove pin in sphinxcontrib-autoyaml * Temporarily force deploy pages action. * Revert temporary changes to deploy-pages.yaml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 295756d84..dea224866 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,7 @@ docs = [ "jupyter-book", "sphinx-book-theme", "sphinx-autodoc-typehints", - "sphinxcontrib-autoyaml==1.1.1", + "sphinxcontrib-autoyaml", "sphinxcontrib.mermaid", ] develop = [ From f6b94950bb08eb823e319a48d0a15d01f98462e9 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Wed, 27 Nov 2024 10:34:05 -0700 Subject: [PATCH 9/9] Update version to v4.2.1 --- README.md | 4 ++-- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 91df13982..16fa26752 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ FLORIS is a controls-focused wind farm simulation software incorporating steady-state engineering wake models into a performance-focused Python framework. It has been in active development at NREL since 2013 and the latest -release is [FLORIS v4.1.1](https://github.com/NREL/floris/releases/latest). +release is [FLORIS v4.2.1](https://github.com/NREL/floris/releases/latest). Online documentation is available at https://nrel.github.io/floris. The software is in active development and engagement with the development team @@ -82,7 +82,7 @@ PACKAGE CONTENTS wind_data VERSION - 4.2 + 4.2.1 FILE ~/floris/floris/__init__.py diff --git a/pyproject.toml b/pyproject.toml index dea224866..a90ef6012 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "floris" -version = "4.2" +version = "4.2.1" description = "A controls-oriented engineering wake model." readme = "README.md" requires-python = ">=3.8"