From 7a7a829e5b096c60bf706c35797dd29dbd772c82 Mon Sep 17 00:00:00 2001 From: misi9170 <39596329+misi9170@users.noreply.github.com> Date: Mon, 2 Dec 2024 14:29:31 -0500 Subject: [PATCH 01/17] Remove support for python 3.8 (#1042) * requires-python >= 3.9; update classifiers list. * Remove python 3.8 from testing and example checks. --- .github/workflows/check-working-examples.yaml | 2 +- .github/workflows/continuous-integration-workflow.yaml | 2 +- pyproject.toml | 6 ++++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/check-working-examples.yaml b/.github/workflows/check-working-examples.yaml index 7580ae3b5..28dd11e07 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.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["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 0035cdb0e..69bc7f857 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", "3.12", "3.13"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] os: [ubuntu-latest] #, macos-latest, windows-latest] fail-fast: False env: diff --git a/pyproject.toml b/pyproject.toml index a90ef6012..da2c52ad0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "floris" version = "4.2.1" description = "A controls-oriented engineering wake model." readme = "README.md" -requires-python = ">=3.8" +requires-python = ">=3.9" authors = [ { name = "Rafael Mudafort", email = "rafael.mudafort@nrel.gov" }, { name = "Paul Fleming", email = "paul.fleming@nrel.gov" }, @@ -21,9 +21,11 @@ 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 :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy" ] From cf05538ff8893e59864a8d2c72fb8df9f7fbcb0c Mon Sep 17 00:00:00 2001 From: paulf81 Date: Mon, 2 Dec 2024 13:23:55 -0700 Subject: [PATCH 02/17] add versions (#1035) Co-authored-by: misi9170 --- pyproject.toml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index da2c52ad0..79785ff6d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,17 +44,17 @@ dependencies = [ [project.optional-dependencies] docs = [ - "jupyter-book", - "sphinx-book-theme", - "sphinx-autodoc-typehints", - "sphinxcontrib-autoyaml", - "sphinxcontrib.mermaid", + "jupyter-book~=1.0", + "sphinx-book-theme~=1.0", + "sphinx-autodoc-typehints~=2.0", + "sphinxcontrib-autoyaml~=1.0", + "sphinxcontrib.mermaid~=1.0", ] develop = [ - "pytest", - "pre-commit", - "ruff", - "isort" + "pytest~=8.0", + "pre-commit~=4.0", + "ruff~=0.7", + "isort~=5.0" ] [tool.setuptools.packages.find] From a3620f5b35c45491b41a0f64101a975051c059e6 Mon Sep 17 00:00:00 2001 From: Rafael M Mudafort Date: Mon, 9 Dec 2024 10:15:08 -0600 Subject: [PATCH 03/17] Improve error check for FlorisModel merge function (#1044) * Add test showing missing error * Fix error handling --- floris/floris_model.py | 15 ++++++++++---- tests/floris_model_integration_test.py | 27 ++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/floris/floris_model.py b/floris/floris_model.py index 83610e993..f521b5faf 100644 --- a/floris/floris_model.py +++ b/floris/floris_model.py @@ -1745,9 +1745,16 @@ def reinitialize(self, **_): @staticmethod def merge_floris_models(fmodel_list, reference_wind_height=None): - """Merge a list of FlorisModel objects into a single FlorisModel object. Note that it uses - the very first object specified in fmodel_list to build upon, + """Merge a list of FlorisModel objects into a single FlorisModel object. + Note that it uses the first object specified in fmodel_list to build upon, so it uses those wake model parameters, air density, and so on. + Currently, this function supports merging the following components of the FLORIS inputs: + - farm + - layout_x + - layout_y + - turbine_type + - flow_field + - reference_wind_height Args: fmodel_list (list): Array-like of FlorisModel objects. @@ -1764,8 +1771,8 @@ def merge_floris_models(fmodel_list, reference_wind_height=None): or general solver settings. """ - if not isinstance(fmodel_list[0], FlorisModel): - raise ValueError( + if not all( type(fm) == FlorisModel for fm in fmodel_list ): + raise TypeError( "Incompatible input specified. fmodel_list must be a list of FlorisModel objects." ) diff --git a/tests/floris_model_integration_test.py b/tests/floris_model_integration_test.py index 88a242c56..eabe764db 100644 --- a/tests/floris_model_integration_test.py +++ b/tests/floris_model_integration_test.py @@ -827,3 +827,30 @@ def test_reference_wind_height_methods(caplog): turbine_type=["nrel_5MW", "iea_15MW"] ) fmodel.assign_hub_height_to_ref_height() # Shouldn't allow due to multiple turbine types + +def test_merge_floris_models(): + + # Check that the merge function extends the data as expected + fmodel1 = FlorisModel(configuration=YAML_INPUT) + fmodel1.set( + layout_x=[0, 1000], + layout_y=[0, 0] + ) + fmodel2 = FlorisModel(configuration=YAML_INPUT) + fmodel2.set( + layout_x=[2000, 3000], + layout_y=[0, 0] + ) + + merged_fmodel = FlorisModel.merge_floris_models([fmodel1, fmodel2]) + assert merged_fmodel.n_turbines == 4 + + # Check that this model will run without error + merged_fmodel.run() + + # Verify error handling + + ## Input list with incorrect types + fmodel_list = [fmodel1, "not a floris model"] + with pytest.raises(TypeError): + merged_fmodel = FlorisModel.merge_floris_models(fmodel_list) From a14193b55ff7be1afa5bdcaea5fe3d41eba29918 Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 20 Dec 2024 16:05:46 -0700 Subject: [PATCH 04/17] Change py_pi to token --- .github/workflows/python-publish.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 9281f86c0..6f956d1ab 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -23,8 +23,8 @@ jobs: pip install build twine - name: Build and publish env: - TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} - TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} run: | python -m build twine upload dist/* From e6bbd6e1cb1854a1ae992b878a1b75b94398d5dc Mon Sep 17 00:00:00 2001 From: misi9170 <39596329+misi9170@users.noreply.github.com> Date: Wed, 5 Feb 2025 11:06:38 -0500 Subject: [PATCH 05/17] Add parallel solve for `sample_flow_at_points` (#1059) * Running sample_flow_at_points in all interface options. * Reducing code repetitions. * Reduce code repetition in run() call. * Add test for sample_flow_at_points --- floris/par_floris_model.py | 165 +++++++++++++++++++--------- tests/par_floris_model_unit_test.py | 26 +++++ 2 files changed, 139 insertions(+), 52 deletions(-) diff --git a/floris/par_floris_model.py b/floris/par_floris_model.py index bfb60b300..8c3e5dce8 100644 --- a/floris/par_floris_model.py +++ b/floris/par_floris_model.py @@ -8,6 +8,9 @@ from floris.core import State from floris.floris_model import FlorisModel +from floris.type_dec import ( + NDArrayFloat, +) class ParFlorisModel(FlorisModel): @@ -123,81 +126,111 @@ def run(self) -> None: t0 = timerpc() super().run() t1 = timerpc() - elif self.interface == "multiprocessing": + self._print_timings(t0, t1, None, None) + else: t0 = timerpc() self.core.initialize_domain() parallel_run_inputs = self._preprocessing() t1 = timerpc() - if self.return_turbine_powers_only: - with self._PoolExecutor(self.max_workers) as p: - self._turbine_powers_split = p.starmap( - _parallel_run_powers_only, + if self.interface == "multiprocessing": + if self.return_turbine_powers_only: + with self._PoolExecutor(self.max_workers) as p: + self._turbine_powers_split = p.starmap( + _parallel_run_powers_only, + parallel_run_inputs + ) + else: + with self._PoolExecutor(self.max_workers) as p: + self._fmodels_split = p.starmap(_parallel_run, parallel_run_inputs) + elif self.interface == "pathos": + if self.return_turbine_powers_only: + self._turbine_powers_split = self.pathos_pool.map( + _parallel_run_powers_only_map, parallel_run_inputs ) - else: - with self._PoolExecutor(self.max_workers) as p: - self._fmodels_split = p.starmap(_parallel_run, parallel_run_inputs) + else: + self._fmodels_split = self.pathos_pool.map( + _parallel_run_map, + parallel_run_inputs + ) + elif self.interface == "concurrent": + if self.return_turbine_powers_only: + with self._PoolExecutor(self.max_workers) as p: + self._turbine_powers_split = p.map( + _parallel_run_powers_only_map, + parallel_run_inputs + ) + self._turbine_powers_split = list(self._turbine_powers_split) + else: + with self._PoolExecutor(self.max_workers) as p: + self._fmodels_split = p.map( + _parallel_run_map, + parallel_run_inputs + ) + self._fmodels_split = list(self._fmodels_split) t2 = timerpc() self._postprocessing() self.core.farm.finalize(self.core.grid.unsorted_indices) self.core.state = State.USED t3 = timerpc() - elif self.interface == "pathos": + self._print_timings(t0, t1, t2, t3) + + def sample_flow_at_points(self, x: NDArrayFloat, y: NDArrayFloat, z: NDArrayFloat): + """ + Sample the flow field at specified points. + + Args: + x: The x-coordinates of the points. + y: The y-coordinates of the points. + z: The z-coordinates of the points. + + Returns: + NDArrayFloat: The wind speeds at the specified points. + """ + if self.return_turbine_powers_only: + raise NotImplementedError( + "Sampling flow at points is not supported when " + "return_turbine_powers_only is set to True on ParFlorisModel." + ) + + if self.interface is None: t0 = timerpc() - self.core.initialize_domain() - parallel_run_inputs = self._preprocessing() + sampled_wind_speeds = super().sample_flow_at_points(x, y, z) t1 = timerpc() - if self.return_turbine_powers_only: - self._turbine_powers_split = self.pathos_pool.map( - _parallel_run_powers_only_map, - parallel_run_inputs - ) - else: - self._fmodels_split = self.pathos_pool.map( - _parallel_run_map, - parallel_run_inputs - ) - t2 = timerpc() - self._postprocessing() - self.core.farm.finalize(self.core.grid.unsorted_indices) - self.core.state = State.USED - t3 = timerpc() - elif self.interface == "concurrent": + self._print_timings(t0, t1, None, None) + else: t0 = timerpc() self.core.initialize_domain() parallel_run_inputs = self._preprocessing() + parallel_sample_flow_at_points_inputs = [ + (fmodel_dict, control_setpoints, x, y, z) + for fmodel_dict, control_setpoints in parallel_run_inputs + ] t1 = timerpc() - if self.return_turbine_powers_only: + if self.interface == "multiprocessing": with self._PoolExecutor(self.max_workers) as p: - self._turbine_powers_split = p.map( - _parallel_run_powers_only_map, - parallel_run_inputs + sampled_wind_speeds_p = p.starmap( + _parallel_sample_flow_at_points, + parallel_sample_flow_at_points_inputs ) - self._turbine_powers_split = list(self._turbine_powers_split) - else: + elif self.interface == "pathos": + sampled_wind_speeds_p = self.pathos_pool.map( + _parallel_sample_flow_at_points_map, + parallel_sample_flow_at_points_inputs + ) + elif self.interface == "concurrent": with self._PoolExecutor(self.max_workers) as p: - self._fmodels_split = p.map( - _parallel_run_map, - parallel_run_inputs + sampled_wind_speeds_p = p.map( + _parallel_sample_flow_at_points_map, + parallel_sample_flow_at_points_inputs ) - self._fmodels_split = list(self._fmodels_split) + sampled_wind_speeds_p = list(sampled_wind_speeds_p) t2 = timerpc() - self._postprocessing() - self.core.farm.finalize(self.core.grid.unsorted_indices) - self.core.state = State.USED + sampled_wind_speeds = np.concatenate(sampled_wind_speeds_p, axis=0) t3 = timerpc() - if self.print_timings: - print("===============================================================================") - if self.interface is None: - print(f"Total time spent for serial calculation (interface=None): {t1 - t0:.3f} s") - else: - print( - "Total time spent for parallel calculation " - f"({self.max_workers} workers): {t3-t0:.3f} s" - ) - print(f" Time spent in parallel preprocessing: {t1-t0:.3f} s") - print(f" Time spent in parallel loop execution: {t2-t1:.3f} s.") - print(f" Time spent in parallel postprocessing: {t3-t2:.3f} s") + self._print_timings(t0, t1, t2, t3) + + return sampled_wind_speeds def _preprocessing(self): """ @@ -278,6 +311,23 @@ def _postprocessing(self): axis=0 ) + def _print_timings(self, t0, t1, t2, t3): + """ + Print the timings for the parallel execution. + """ + if self.print_timings: + print("===============================================================================") + if self.interface is None: + print(f"Total time spent for serial calculation (interface=None): {t1 - t0:.3f} s") + else: + print( + "Total time spent for parallel calculation " + f"({self.max_workers} workers): {t3-t0:.3f} s" + ) + print(f" Time spent in parallel preprocessing: {t1-t0:.3f} s") + print(f" Time spent in parallel loop execution: {t2-t1:.3f} s.") + print(f" Time spent in parallel postprocessing: {t3-t2:.3f} s") + def _get_turbine_powers(self): """ Calculates the power at each turbine in the wind farm. @@ -364,3 +414,14 @@ def _parallel_run_powers_only_map(x): Wrapper for unpacking inputs to _parallel_run_powers_only() for use with map(). """ return _parallel_run_powers_only(*x) + +def _parallel_sample_flow_at_points(fmodel_dict, set_kwargs, x, y, z): + fmodel = FlorisModel(fmodel_dict) + fmodel.set(**set_kwargs) + return fmodel.sample_flow_at_points(x, y, z) + +def _parallel_sample_flow_at_points_map(x): + """ + Wrapper for unpacking inputs to _parallel_sample_flow_at_points() for use with map(). + """ + return _parallel_sample_flow_at_points(*x) diff --git a/tests/par_floris_model_unit_test.py b/tests/par_floris_model_unit_test.py index 9e56e4d8c..9f5669a3c 100644 --- a/tests/par_floris_model_unit_test.py +++ b/tests/par_floris_model_unit_test.py @@ -286,3 +286,29 @@ def test_control_setpoints(sample_inputs_fixture): assert powers_fmodel.shape == powers_pfmodel.shape assert np.allclose(powers_fmodel, powers_pfmodel) + +def test_sample_flow_at_points(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 + + fmodel = FlorisModel(sample_inputs_fixture.core) + + wind_speeds = np.array([8.0, 8.0, 9.0]) + wind_directions = np.array([270.0, 270.0, 270.0]) + fmodel.set( + wind_directions=wind_speeds.flatten(), + wind_speeds=wind_directions.flatten(), + turbulence_intensities=0.06 * np.ones_like(wind_speeds.flatten()), + ) + + x_test = np.array([500.0, 750.0, 1000.0, 1250.0, 1500.0]) + y_test = np.array([0.0, 0.0, 0.0, 0.0, 0.0]) + z_test = np.array([90.0, 90.0, 90.0, 90.0, 90.0]) + + ws_base = fmodel.sample_flow_at_points(x_test, y_test, z_test) + + for interface in ["multiprocessing", "pathos", "concurrent"]: + pfmodel = ParFlorisModel(fmodel, max_workers=2, interface=interface) + ws_test = pfmodel.sample_flow_at_points(x_test, y_test, z_test) + assert np.allclose(ws_base, ws_test) From 47cbb29083bd6134d4315739753d82fefebf9c52 Mon Sep 17 00:00:00 2001 From: Rafael M Mudafort Date: Wed, 5 Feb 2025 10:09:30 -0600 Subject: [PATCH 06/17] Support initializing FLORIS with default values (#1040) * Support initializing with defaults * Add docs for using FLORIS as a library * Fix defaults data types * Require setting the atmospheric conditions * Store defaults in a YAML file Allows for using this file in other areas of the software * Add a function to display the input configs * Add back print_dict as a wrapper for show_config to avoid breaking user interface changes. --------- Co-authored-by: misi9170 --- docs/advanced_concepts.ipynb | 136 ++++++++++++++++++++++++- floris/default_inputs.yaml | 106 +++++++++++++++++++ floris/floris_model.py | 31 +++++- pyproject.toml | 28 ++--- tests/floris_model_integration_test.py | 19 +++- 5 files changed, 301 insertions(+), 19 deletions(-) create mode 100644 floris/default_inputs.yaml diff --git a/docs/advanced_concepts.ipynb b/docs/advanced_concepts.ipynb index 45fb9c017..023ca3466 100644 --- a/docs/advanced_concepts.ipynb +++ b/docs/advanced_concepts.ipynb @@ -95,16 +95,148 @@ "plt.show()" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## FLORIS as a library\n", + "\n", + "FLORIS is commonly used as a library in other software packages.\n", + "In cases where the calling-code will create inputs for FLORIS rather than require them from the\n", + "user, it can be helpful to initialize the FLORIS model with default inputs and then\n", + "change them in code.\n", + "In this case, the following workflow is recommended." + ] + }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": {}, "outputs": [], - "source": [] + "source": [ + "import floris\n", + "\n", + "# Initialize FLORIS with defaults\n", + "fmodel = floris.FlorisModel(\"defaults\")\n", + "\n", + "# Within the calling-code's setup step, update FLORIS as needed\n", + "fmodel.set(\n", + " wind_directions=[i for i in range(10)],\n", + " wind_speeds=[5 + i for i in range(10)],\n", + " turbulence_intensities=[i for i in range(10)],\n", + " # turbine_library_path=\"path/to/turbine_library\", # Shown here for reference\n", + " # turbine_type=[\"my_turbine\"]\n", + ")\n", + "\n", + "# Within the calling code's computation, run FLORIS\n", + "fmodel.run()" + ] }, { "cell_type": "markdown", "metadata": {}, + "source": [ + "Alternatively, the calling-code can import the FLORIS default inputs as a Python dictionary\n", + "and modify them directly before initializing the FLORIS model.\n", + "This is especially helpful when the calling-code will modify a parameter that isn't\n", + "supported by the `FlorisModel.set(...)` command.\n", + "In particular, the wake model parameters are not directly accessible, so these can be updated\n", + "externally, as shown below.\n", + "Note that the `FlorisModel.get_defaults()` function returns a deep copy of the default inputs,\n", + "so these can be modified directly without side effects." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "solver\n", + " type\n", + " turbine_grid\n", + " turbine_grid_points\n", + " 3\n", + "wake\n", + " model_strings\n", + " combination_model\n", + " sosfs\n", + " deflection_model\n", + " gauss\n", + " turbulence_model\n", + " crespo_hernandez\n", + " velocity_model\n", + " jensen\n", + "farm\n", + " layout_x\n", + " [0.0]\n", + " layout_y\n", + " [0.0]\n", + " turbine_type\n", + " ['nrel_5MW']\n", + " turbine_library_path\n", + " /Users/rmudafor/Development/floris/floris/turbine_library\n", + "flow_field\n", + " wind_speeds\n", + " [5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0]\n", + " wind_directions\n", + " [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]\n", + " wind_veer\n", + " 0.0\n", + " wind_shear\n", + " 0.12\n", + " air_density\n", + " 1.225\n", + " turbulence_intensities\n", + " [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]\n", + " reference_wind_height\n", + " 90.0\n", + "name\n", + " GCH\n", + "description\n", + " Default initialization: Gauss-Curl hybrid model (GCH)\n", + "floris_version\n", + " v4\n" + ] + } + ], + "source": [ + "import floris\n", + "\n", + "# Retrieve the default parameters\n", + "fdefaults = floris.FlorisModel.get_defaults()\n", + "\n", + "# Update wake model parameters\n", + "fdefaults[\"wake\"][\"model_strings\"][\"velocity_model\"] = \"jensen\"\n", + "fdefaults[\"wake\"][\"wake_velocity_parameters\"][\"jensen\"][\"we\"] = 0.05\n", + "\n", + "# Initialize FLORIS with modified parameters\n", + "fmodel = floris.FlorisModel(configuration=fdefaults)\n", + "\n", + "# Within the calling-code's setup step, update FLORIS as needed\n", + "fmodel.set(\n", + " wind_directions=[i for i in range(10)],\n", + " wind_speeds=[5 + i for i in range(10)],\n", + " turbulence_intensities=[i for i in range(10)],\n", + " # turbine_library_path=\"path/to/turbine_library\", # Shown here for reference\n", + " # turbine_type=[\"my_turbine\"]\n", + ")\n", + "\n", + "# Verify settings are correct\n", + "fmodel.show_config() # Shows truncated set of inputs; show all with fmodel.show_config(full=True)\n", + "\n", + "# Within the calling code's computation, run FLORIS\n", + "fmodel.run()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [] } ], diff --git a/floris/default_inputs.yaml b/floris/default_inputs.yaml new file mode 100644 index 000000000..67e199559 --- /dev/null +++ b/floris/default_inputs.yaml @@ -0,0 +1,106 @@ + +name: GCH +description: "Default initialization: Gauss-Curl hybrid model (GCH)" +floris_version: v4 + +logging: + console: + enable: true + level: WARNING + file: + enable: false + level: WARNING + +solver: + type: turbine_grid + turbine_grid_points: 3 + +farm: + layout_x: + - 0.0 + layout_y: + - 0.0 + turbine_type: + - nrel_5MW + +flow_field: + air_density: 1.225 + reference_wind_height: -1 + turbulence_intensities: [] + wind_directions: [] + wind_shear: 0.12 + wind_speeds: [] + wind_veer: 0.0 + +wake: + model_strings: + combination_model: sosfs + deflection_model: gauss + turbulence_model: crespo_hernandez + velocity_model: gauss + + enable_secondary_steering: true + enable_yaw_added_recovery: true + enable_transverse_velocities: true + enable_active_wake_mixing: false + + wake_deflection_parameters: + gauss: + ad: 0.0 + alpha: 0.58 + bd: 0.0 + beta: 0.077 + dm: 1.0 + ka: 0.38 + kb: 0.004 + jimenez: + ad: 0.0 + bd: 0.0 + kd: 0.05 + empirical_gauss: + horizontal_deflection_gain_D: 3.0 + vertical_deflection_gain_D: -1 + deflection_rate: 22 + mixing_gain_deflection: 0.0 + yaw_added_mixing_gain: 0.0 + + wake_velocity_parameters: + gauss: + alpha: 0.58 + beta: 0.077 + ka: 0.38 + kb: 0.004 + jensen: + we: 0.05 + cc: + a_s: 0.179367259 + b_s: 0.0118889215 + c_s1: 0.0563691592 + c_s2: 0.13290157 + a_f: 3.11 + b_f: -0.68 + c_f: 2.41 + alpha_mod: 1.0 + turbopark: + A: 0.04 + sigma_max_rel: 4.0 + turboparkgauss: + A: 0.04 + include_mirror_wake: True + empirical_gauss: + wake_expansion_rates: [0.023, 0.008] + breakpoints_D: [10] + sigma_0_D: 0.28 + smoothing_length_D: 2.0 + mixing_gain_velocity: 2.0 + awc_wake_exp: 1.2 + awc_wake_denominator: 400 + + wake_turbulence_parameters: + crespo_hernandez: + initial: 0.1 + constant: 0.5 + ai: 0.8 + downstream: -0.32 + wake_induced_mixing: + atmospheric_ti_gain: 0.0 diff --git a/floris/floris_model.py b/floris/floris_model.py index f521b5faf..edf987081 100644 --- a/floris/floris_model.py +++ b/floris/floris_model.py @@ -33,6 +33,7 @@ NDArrayStr, ) from floris.utilities import ( + load_yaml, nested_get, nested_set, print_nested_dict, @@ -63,7 +64,15 @@ class FlorisModel(LoggingManager): - **logging**: See `floris.simulation.core.Core` for more details. """ + @staticmethod + def get_defaults() -> dict: + return copy.deepcopy(load_yaml(Path(__file__).parent / "default_inputs.yaml")) + def __init__(self, configuration: dict | str | Path): + + if configuration == "defaults": + configuration = FlorisModel.get_defaults() + self.configuration = configuration if isinstance(self.configuration, (str, Path)): @@ -1627,11 +1636,29 @@ def get_turbine_layout(self, z=False): else: return xcoords, ycoords - def print_dict(self) -> None: + def show_config(self, full=False) -> None: """Print the FlorisModel dictionary. """ - print_nested_dict(self.core.as_dict()) + config_dict = self.core.as_dict() + if not full: + del config_dict["logging"] + del config_dict["wake"]["enable_secondary_steering"] + del config_dict["wake"]["enable_yaw_added_recovery"] + del config_dict["wake"]["enable_transverse_velocities"] + del config_dict["wake"]["enable_active_wake_mixing"] + del config_dict["wake"]["wake_deflection_parameters"] + del config_dict["wake"]["wake_velocity_parameters"] + del config_dict["wake"]["wake_turbulence_parameters"] + print_nested_dict(config_dict) + def print_dict(self) -> None: + """Print the FlorisModel dictionary. + """ + self.logger.warning( + "The print_dict() method has been deprecated." + " Please use the show_config() method instead." + ) + self.show_config(full=True) ### Properties diff --git a/pyproject.toml b/pyproject.toml index e64d5cc37..23fe7aead 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,8 +9,8 @@ description = "A controls-oriented engineering wake model." readme = "README.md" requires-python = ">=3.9" authors = [ - { name = "Rafael Mudafort", email = "rafael.mudafort@nrel.gov" }, - { name = "Paul Fleming", email = "paul.fleming@nrel.gov" }, + { 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" }, @@ -18,16 +18,16 @@ authors = [ license = { file = "LICENSE.txt" } keywords = ["floris"] classifiers = [ - "License :: OSI Approved :: BSD License", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "Programming Language :: Python :: Implementation :: CPython", - "Programming Language :: Python :: Implementation :: PyPy" + "License :: OSI Approved :: BSD License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy" ] dependencies = [ "attrs", @@ -63,7 +63,8 @@ include = ["floris*"] [tool.setuptools.package-data] floris = [ "turbine_library/*.yaml", - "core/wake_velocity/turbopark_lookup_table.mat" + "core/wake_velocity/turbopark_lookup_table.mat", + "default_inputs.yaml" ] [project.urls] @@ -76,7 +77,6 @@ Documentation = "https://nrel.github.io/floris/" branch = true source = "floris/*" omit = [ - "setup.py", "tests/*" ] diff --git a/tests/floris_model_integration_test.py b/tests/floris_model_integration_test.py index eabe764db..3ba94b955 100644 --- a/tests/floris_model_integration_test.py +++ b/tests/floris_model_integration_test.py @@ -17,6 +17,24 @@ YAML_INPUT = TEST_DATA / "input_full.yaml" +def test_default_init(): + # Test getting the default dict + defaults = FlorisModel.get_defaults() + fmodel1 = FlorisModel(defaults) + assert isinstance(fmodel1, FlorisModel) + + # Test that there are no side effects from changing the default dict + defaults2 = FlorisModel.get_defaults() + defaults2["farm"]["layout_x"] = [0, 1000] + defaults2["farm"]["layout_y"] = [0, 0] + fmodel2 = FlorisModel(defaults2) + assert fmodel2.core.as_dict() != FlorisModel(defaults).core.as_dict() + + # Test using the "default" string + # This checks that the init works and that the default dictionary hasn't changed + fmodel3 = FlorisModel("defaults") + assert fmodel1.core.as_dict() == fmodel3.core.as_dict() + def test_read_yaml(): fmodel = FlorisModel(configuration=YAML_INPUT) assert isinstance(fmodel, FlorisModel) @@ -488,7 +506,6 @@ def test_expected_farm_value_regression(): expected_farm_value = fmodel.get_expected_farm_value() assert np.allclose(expected_farm_value,75108001.05154414 , atol=1e-1) - def test_get_farm_avp(caplog): fmodel = FlorisModel(configuration=YAML_INPUT) From 3afc053bb67e27f050843d6dfa1677bcf951afb5 Mon Sep 17 00:00:00 2001 From: misi9170 <39596329+misi9170@users.noreply.github.com> Date: Wed, 5 Feb 2025 13:00:46 -0500 Subject: [PATCH 07/17] Enable `solve_for_points()` with CC model (#1058) * Specify input arrays as 4d in full_flow_cc_solver * Move dimension expansion to definition for consistency. * Remove TODO check. * Use sorted awc modes settings * Fix TI-field average array dims in CC & TurbOPark * Update comments and warning to point users to turboparkgauss model. --------- Co-authored-by: Rafael M Mudafort --- .../002_extract_wind_speed_at_points.py | 4 ++-- floris/core/core.py | 8 +++++--- floris/core/solver.py | 16 ++++++++-------- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/examples/examples_get_flow/002_extract_wind_speed_at_points.py b/examples/examples_get_flow/002_extract_wind_speed_at_points.py index aaf086f4b..663124140 100644 --- a/examples/examples_get_flow/002_extract_wind_speed_at_points.py +++ b/examples/examples_get_flow/002_extract_wind_speed_at_points.py @@ -20,8 +20,8 @@ # User options -# FLORIS model to use (limited to Gauss/GCH, Jensen, and empirical Gauss) -floris_model = "gch" # Try "gch", "jensen", "emgauss" +# FLORIS model to use (legacy Turbopark not available) +floris_model = "gch" # Try "gch", "cc", "jensen", "emgauss", "turboparkgauss" # Option to try different met mast locations met_mast_option = 0 # Try 0, 1, 2, 3 diff --git a/floris/core/core.py b/floris/core/core.py index d12005ba5..857b90fa4 100644 --- a/floris/core/core.py +++ b/floris/core/core.py @@ -252,13 +252,15 @@ def solve_for_points(self, x, y, z): vel_model = self.wake.model_strings["velocity_model"] - if vel_model == "cc" or vel_model == "turbopark": + if vel_model == "turbopark": raise NotImplementedError( - "solve_for_points is currently only available with the "+\ - "gauss, jensen, and empirical_gauss models." + "solve_for_points is not available for the legacy \'turbopark\' model. " + "However, it is available for \'turboparkgauss\'." ) elif vel_model == "empirical_gauss": full_flow_empirical_gauss_solver(self.farm, self.flow_field, field_grid, self.wake) + elif vel_model == "cc": + full_flow_cc_solver(self.farm, self.flow_field, field_grid, self.wake) else: full_flow_sequential_solver(self.farm, self.flow_field, field_grid, self.wake) diff --git a/floris/core/solver.py b/floris/core/solver.py index a7c3d8796..679a97c4a 100644 --- a/floris/core/solver.py +++ b/floris/core/solver.py @@ -694,7 +694,7 @@ def cc_solver( flow_field.turbulence_intensity_field_sorted_avg = np.mean( turbine_turbulence_intensity, axis=(2,3) - ) + )[:, :, None, None] def full_flow_cc_solver( @@ -774,7 +774,7 @@ def full_flow_cc_solver( yaw_angles=turbine_grid_farm.yaw_angles_sorted, tilt_angles=turbine_grid_farm.tilt_angles_sorted, power_setpoints=turbine_grid_farm.power_setpoints_sorted, - awc_modes=turbine_grid_farm.awc_modes, + awc_modes=turbine_grid_farm.awc_modes_sorted, awc_amplitudes=turbine_grid_farm.awc_amplitudes_sorted, thrust_coefficient_functions=turbine_grid_farm.turbine_thrust_coefficient_functions, tilt_interps=turbine_grid_farm.turbine_tilt_interps, @@ -794,7 +794,7 @@ def full_flow_cc_solver( yaw_angles=turbine_grid_farm.yaw_angles_sorted, tilt_angles=turbine_grid_farm.tilt_angles_sorted, power_setpoints=turbine_grid_farm.power_setpoints_sorted, - awc_modes=turbine_grid_farm.awc_modes, + awc_modes=turbine_grid_farm.awc_modes_sorted, awc_amplitudes=turbine_grid_farm.awc_amplitudes_sorted, axial_induction_functions=turbine_grid_farm.turbine_axial_induction_functions, tilt_interps=turbine_grid_farm.turbine_tilt_interps, @@ -937,7 +937,7 @@ def turbopark_solver( yaw_angles=farm.yaw_angles_sorted, tilt_angles=farm.tilt_angles_sorted, power_setpoints=farm.power_setpoints_sorted, - awc_modes=farm.awc_modes, + awc_modes=farm.awc_modes_sorted, awc_amplitudes=farm.awc_amplitudes_sorted, thrust_coefficient_functions=farm.turbine_thrust_coefficient_functions, tilt_interps=farm.turbine_tilt_interps, @@ -956,7 +956,7 @@ def turbopark_solver( yaw_angles=farm.yaw_angles_sorted, tilt_angles=farm.tilt_angles_sorted, power_setpoints=farm.power_setpoints_sorted, - awc_modes=farm.awc_modes, + awc_modes=farm.awc_modes_sorted, awc_amplitudes=farm.awc_amplitudes_sorted, thrust_coefficient_functions=farm.turbine_thrust_coefficient_functions, tilt_interps=farm.turbine_tilt_interps, @@ -978,7 +978,7 @@ def turbopark_solver( yaw_angles=farm.yaw_angles_sorted, tilt_angles=farm.tilt_angles_sorted, power_setpoints=farm.power_setpoints_sorted, - awc_modes=farm.awc_modes, + awc_modes=farm.awc_modes_sorted, awc_amplitudes=farm.awc_amplitudes_sorted, axial_induction_functions=farm.turbine_axial_induction_functions, tilt_interps=farm.turbine_tilt_interps, @@ -1027,7 +1027,7 @@ def turbopark_solver( yaw_angles=farm.yaw_angles_sorted, tilt_angles=farm.tilt_angles_sorted, power_setpoints=farm.power_setpoints_sorted, - awc_modes=farm.awc_modes, + awc_modes=farm.awc_modes_sorted, awc_amplitudes=farm.awc_amplitudes_sorted, thrust_coefficient_functions=farm.turbine_thrust_coefficient_functions, tilt_interps=farm.turbine_tilt_interps, @@ -1122,7 +1122,7 @@ def turbopark_solver( flow_field.turbulence_intensity_field_sorted_avg = np.mean( turbine_turbulence_intensity, axis=(2, 3) - ) + )[:, :, None, None] def full_flow_turbopark_solver( From f48e897adcb8459a1364632f5bface74bd99da68 Mon Sep 17 00:00:00 2001 From: Rafael M Mudafort Date: Thu, 6 Feb 2025 16:11:07 -0600 Subject: [PATCH 08/17] [BUGFIX] Allow empty arrays in export as dict (#1061) * Test error * Allow empty arrays in export as dict --- floris/type_dec.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/floris/type_dec.py b/floris/type_dec.py index 319a09917..7798c59e9 100644 --- a/floris/type_dec.py +++ b/floris/type_dec.py @@ -106,9 +106,16 @@ def _attr_floris_filter(inst: Attribute, value: Any) -> bool: return False if value is None: return False - if isinstance(value, np.ndarray): - if value.size == 0: - return False + + # This is removed to support initializing FLORIS with default values: + # - defaults added in https://github.com/NREL/floris/pull/1040 + # - bug fix in https://github.com/NREL/floris/pull/1061 + # When Core is exported to a dict in _reinitialize, this filter removes empty arrays. + # For init with defaults, this results in FlowField losing the wind speed, wind direction and TI + # arrays if they weren't provided in the .set function. + # if isinstance(value, np.ndarray): + # if value.size == 0: + # return False return True def iter_validator(iter_type, item_types: Union[Any, Tuple[Any]]) -> Callable: From a97ef085f73b35e2e36a9befdb362188293d391f Mon Sep 17 00:00:00 2001 From: paulf81 Date: Fri, 7 Feb 2025 14:11:59 -0700 Subject: [PATCH 09/17] Add automatic benchmarking (#1062) --- .github/workflows/deploy-pages.yaml | 53 +++++++-- benchmarks/bench.py | 163 ++++++++++++++++++++++++++++ docs/code_quality.ipynb | 18 +++ pyproject.toml | 4 +- 4 files changed, 229 insertions(+), 9 deletions(-) create mode 100644 benchmarks/bench.py diff --git a/.github/workflows/deploy-pages.yaml b/.github/workflows/deploy-pages.yaml index 708ba1930..ed124f339 100644 --- a/.github/workflows/deploy-pages.yaml +++ b/.github/workflows/deploy-pages.yaml @@ -4,8 +4,9 @@ on: push: branches: - develop - paths: - - docs/** + + + workflow_dispatch: # Allows manual triggering of the workflow # This job installs dependencies, builds the book, and pushes it to `gh-pages` jobs: @@ -22,7 +23,7 @@ jobs: - name: Install dependencies run: | - pip install -e ".[docs]" + pip install -e ".[docs, develop]" # Make a copy of the examples folder within the docs folder - name: Copy examples to docs @@ -35,12 +36,8 @@ jobs: - name: Convert examples to notebooks working-directory: ${{runner.workspace}}/floris/docs/examples/ run: | - # Print the working directory pwd - - # Show the contents ls - python _convert_examples_to_notebooks.py # Build the book @@ -51,7 +48,47 @@ jobs: # Push the book's HTML to github-pages - name: GitHub Pages action - uses: peaceiris/actions-gh-pages@v3.6.1 + uses: peaceiris/actions-gh-pages@v4.0.0 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./docs/_build/html + + - name: Run benchmark + working-directory: ${{runner.workspace}}/floris/ + run: | + ls -lah + cd benchmarks + pytest bench.py --benchmark-json output.json + + # Store benchmark result and create the benchmark pages + # Update the index.html and data.js files in the + # dev/bench folder of the benches branch + # dev/bench is the default folder for pytest-benchmark + - name: Store benchmark result + uses: benchmark-action/github-action-benchmark@v1 + with: + name: Python Benchmark with pytest-benchmark + tool: 'pytest' + output-file-path: benchmarks/output.json + github-token: ${{ secrets.GITHUB_TOKEN }} + auto-push: true + gh-pages-branch: benches + + # Add bench mark files to the gh-pages branch + - name: Add benchmark files to gh-pages + working-directory: ${{runner.workspace}}/floris/ + run: | + ls -lah + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git fetch origin benches + git checkout benches + rsync -av dev/bench /tmp/bench + git fetch origin gh-pages + git checkout gh-pages + mkdir -p dev + rsync -av /tmp/bench/ dev/ + ls -lah dev/bench + git add dev + git commit -m "Add bench folder to gh-pages" + git push origin gh-pages diff --git a/benchmarks/bench.py b/benchmarks/bench.py new file mode 100644 index 000000000..f4730dfff --- /dev/null +++ b/benchmarks/bench.py @@ -0,0 +1,163 @@ +from pathlib import Path + +import numpy as np +import pytest + +from floris import ( + FlorisModel, + TimeSeries, +) +from floris.core.turbine.operation_models import POWER_SETPOINT_DEFAULT +from floris.heterogeneous_map import HeterogeneousMap + + +N_Conditions = 100 + +# These tests are run automatically by pytest-benchmark. The benchmark +# object is passed to the test function. + +def test_timing_small_farm_set(benchmark): + """Timing test for setting up a small farm""" + fmodel = FlorisModel(configuration="defaults") + wind_directions = np.linspace(0, 360, N_Conditions) + wind_speeds = np.ones(N_Conditions) * 8 + turbulence_intensities = np.ones(N_Conditions) * 0.06 + + benchmark( + fmodel.set, + wind_directions=wind_directions, + wind_speeds=wind_speeds, + turbulence_intensities=turbulence_intensities, + layout_x=np.linspace(0, 1000, 3), + layout_y=np.linspace(0, 1000, 3), + ) + + +def test_timing_small_farm_run(benchmark): + """Timing test for running a small farm""" + fmodel = FlorisModel(configuration="defaults") + wind_directions = np.linspace(0, 360, N_Conditions) + wind_speeds = np.ones(N_Conditions) * 8 + turbulence_intensities = np.ones(N_Conditions) * 0.06 + + fmodel.set( + wind_directions=wind_directions, + wind_speeds=wind_speeds, + turbulence_intensities=turbulence_intensities, + layout_x=np.linspace(0, 1000, 3), + layout_y=np.linspace(0, 1000, 3), + ) + + benchmark(fmodel.run) + + +def test_timing_large_farm_set(benchmark): + """Timing test for setting up a large farm""" + fmodel = FlorisModel(configuration="defaults") + wind_directions = np.linspace(0, 360, N_Conditions) + wind_speeds = np.ones(N_Conditions) * 8 + turbulence_intensities = np.ones(N_Conditions) * 0.06 + + benchmark( + fmodel.set, + wind_directions=wind_directions, + wind_speeds=wind_speeds, + turbulence_intensities=turbulence_intensities, + layout_x=np.linspace(0, 10000, 100), + layout_y=np.linspace(0, 10000, 100), + ) + + +def test_timing_large_farm_run(benchmark): + """Timing test for running a large farm""" + fmodel = FlorisModel(configuration="defaults") + wind_directions = np.linspace(0, 360, N_Conditions) + wind_speeds = np.ones(N_Conditions) * 8 + turbulence_intensities = np.ones(N_Conditions) * 0.06 + + fmodel.set( + wind_directions=wind_directions, + wind_speeds=wind_speeds, + turbulence_intensities=turbulence_intensities, + layout_x=np.linspace(0, 10000, 100), + layout_y=np.linspace(0, 10000, 100), + ) + + benchmark(fmodel.run) + + +def test_timing_het_set(benchmark): + """Timing test for setting up a farm with a heterogeneous map""" + + # The side of the flow which is accelerated reverses for east versus west + het_map = HeterogeneousMap( + x=np.array([0.0, 0.0, 500.0, 500.0]), + y=np.array([0.0, 500.0, 0.0, 500.0]), + speed_multipliers=np.array( + [ + [1.0, 2.0, 1.0, 2.0], # Top accelerated + [2.0, 1.0, 2.0, 1.0], # Bottom accelerated + ] + ), + wind_directions=np.array([270.0, 90.0]), + wind_speeds=np.array([8.0, 8.0]), + ) + + # Get the FLORIS model + fmodel = FlorisModel(configuration="defaults") + + time_series = TimeSeries( + wind_directions=np.linspace(0, 360, N_Conditions), + wind_speeds=8.0, + turbulence_intensities=0.06, + heterogeneous_map=het_map, + ) + + # Set the model to a turbines perpendicular to + # east/west flow with 0 turbine closer to bottom and + # turbine 1 closer to top + benchmark( + fmodel.set, + wind_data=time_series, + layout_x=[250.0, 250.0], + layout_y=[100.0, 400.0], + ) + + +def test_timing_het_run(benchmark): + """Timing test for running a farm with a heterogeneous map""" + + # The side of the flow which is accelerated reverses for east versus west + het_map = HeterogeneousMap( + x=np.array([0.0, 0.0, 500.0, 500.0]), + y=np.array([0.0, 500.0, 0.0, 500.0]), + speed_multipliers=np.array( + [ + [1.0, 2.0, 1.0, 2.0], # Top accelerated + [2.0, 1.0, 2.0, 1.0], # Bottom accelerated + ] + ), + wind_directions=np.array([270.0, 90.0]), + wind_speeds=np.array([8.0, 8.0]), + ) + + # Get the FLORIS model + fmodel = FlorisModel(configuration="defaults") + + time_series = TimeSeries( + wind_directions=np.linspace(0, 360, N_Conditions), + wind_speeds=8.0, + turbulence_intensities=0.06, + heterogeneous_map=het_map, + ) + + # Set the model to a turbines perpendicular to + # east/west flow with 0 turbine closer to bottom and + # turbine 1 closer to top + fmodel.set( + wind_data=time_series, + layout_x=[250.0, 250.0], + layout_y=[100.0, 400.0], + ) + + benchmark(fmodel.run) diff --git a/docs/code_quality.ipynb b/docs/code_quality.ipynb index fc7fa9374..080580b7f 100644 --- a/docs/code_quality.ipynb +++ b/docs/code_quality.ipynb @@ -19,6 +19,24 @@ "Coming soon! -->" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Live Code Quality Metrics\n", + "\n", + "Live code quality metrics are computed when develop branch is updated. [Live Chart](https://nrel.github.io/floris/dev/bench/). Note these charts are computed within github actions and are somewhat noisy" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Code Quality Metrics Computed on Super Computer\n", + "\n", + "Code quality metrics computed manually on kestrel are shown below." + ] + }, { "cell_type": "code", "execution_count": 1, diff --git a/pyproject.toml b/pyproject.toml index 23fe7aead..530ba5024 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,6 +52,7 @@ docs = [ ] develop = [ "pytest~=8.0", + "pytest-benchmark~=5.1", "pre-commit~=4.0", "ruff~=0.7", "isort~=5.0" @@ -77,7 +78,8 @@ Documentation = "https://nrel.github.io/floris/" branch = true source = "floris/*" omit = [ - "tests/*" + "tests/*", + "benchmarks/*" ] From cdbbcb9a6dee72472c14addd59367cafbecf8396 Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 7 Feb 2025 14:43:16 -0700 Subject: [PATCH 10/17] Add git stash step --- .github/workflows/deploy-pages.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/deploy-pages.yaml b/.github/workflows/deploy-pages.yaml index ed124f339..9ddc3e9e0 100644 --- a/.github/workflows/deploy-pages.yaml +++ b/.github/workflows/deploy-pages.yaml @@ -53,6 +53,12 @@ jobs: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./docs/_build/html + # Stash changes before benchmark action + - name: Stash changes + working-directory: ${{runner.workspace}}/floris/ + run: | + git stash + - name: Run benchmark working-directory: ${{runner.workspace}}/floris/ run: | From 33ffc738e3d5816a5386b8b1fe6888b9a183c60e Mon Sep 17 00:00:00 2001 From: paulf81 Date: Fri, 7 Feb 2025 18:19:27 -0700 Subject: [PATCH 11/17] Update numpy req to 2 (#1051) * Update numpy req to 2 * Add warnings/notes to documentation. * Trailing whitespace... --------- Co-authored-by: misi9170 --- docs/index.md | 2 ++ docs/installation.md | 2 ++ pyproject.toml | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 3bd73151d..7ceeb237b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -10,6 +10,8 @@ the conversation in [GitHub Discussions](https://github.com/NREL/floris/discussi ```{note} Support for python version 3.8 will be dropped in FLORIS v4.3. See {ref}`installation` for details. + +FLORIS v4.3 will also move to requiring `numpy` version 2. See the [numpy documentation for details](https://numpy.org/doc/stable/numpy_2_0_migration_guide.html). ``` ## Quick Start diff --git a/docs/installation.md b/docs/installation.md index 0e1c22f9d..60e18dc70 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -15,6 +15,8 @@ and sandboxed environment. The simplest way to get started with virtual environm ```{warning} Support for python version 3.8 will be dropped in FLORIS v4.3. + +FLORIS v4.3 will also require `numpy` version 2. See the [numpy documentation for details](https://numpy.org/doc/stable/numpy_2_0_migration_guide.html). ``` Installing into a Python environment that contains a previous version of FLORIS may cause conflicts. diff --git a/pyproject.toml b/pyproject.toml index 530ba5024..efa2606ca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ dependencies = [ "attrs", "pyyaml~=6.0", "numexpr~=2.0", - "numpy~=1.20", + "numpy~=2.0", "scipy~=1.1", "matplotlib~=3.0", "pandas~=2.0", From f3f42ed8b8c08abe6ef6fa4aa4b0a93afa0f55ec Mon Sep 17 00:00:00 2001 From: Bart Doekemeijer Date: Tue, 11 Feb 2025 20:35:07 +0100 Subject: [PATCH 12/17] Reduce computation time massively in large het_map objects (#1024) * Reduce computation time massively in large het_map objects by avoiding the recalculation of the delaunay triangulation for every findex * Import copy library * Enforce appropriate shape for interpolant object * ruff formatting * bugfix * Add a test of applying a het map * Add convert to array * Add convert to array * Add to new tests * Clean up * Add comments and rename variables for clarity. * Change FlowField.het_map to be a numpy array rather than a list. --------- Co-authored-by: Paul Co-authored-by: misi9170 --- floris/core/flow_field.py | 44 ++++++--- tests/heterogeneous_map_integration_test.py | 101 ++++++++++++++++++++ 2 files changed, 133 insertions(+), 12 deletions(-) diff --git a/floris/core/flow_field.py b/floris/core/flow_field.py index 19b488518..037227d4f 100644 --- a/floris/core/flow_field.py +++ b/floris/core/flow_field.py @@ -1,6 +1,8 @@ from __future__ import annotations +import copy + import attrs import matplotlib.path as mpltPath import numpy as np @@ -16,6 +18,7 @@ from floris.type_dec import ( floris_array_converter, NDArrayFloat, + NDArrayObject, ) @@ -41,7 +44,7 @@ class FlowField(BaseClass): u: NDArrayFloat = field(init=False, factory=lambda: np.array([])) v: NDArrayFloat = field(init=False, factory=lambda: np.array([])) w: NDArrayFloat = field(init=False, factory=lambda: np.array([])) - het_map: list = field(init=False, default=None) + het_map: NDArrayObject = field(init=False, default=None) dudz_initial_sorted: NDArrayFloat = field(init=False, factory=lambda: np.array([])) turbulence_intensity_field: NDArrayFloat = field(init=False, factory=lambda: np.array([])) @@ -281,29 +284,46 @@ def generate_heterogeneous_wind_map(self): - **y**: A list of y locations at which the speed up factors are defined. - **z** (optional): A list of z locations at which the speed up factors are defined. """ - speed_multipliers = self.heterogeneous_inflow_config['speed_multipliers'] + speed_multipliers = np.array(self.heterogeneous_inflow_config['speed_multipliers']) x = self.heterogeneous_inflow_config['x'] y = self.heterogeneous_inflow_config['y'] z = self.heterogeneous_inflow_config['z'] + # Declare an empty list to store interpolants by findex + interps_f = np.empty(self.n_findex, dtype=object) if z is not None: # Compute the 3-dimensional interpolants for each wind direction # Linear interpolation is used for points within the user-defined area of values, - # while the freestream wind speed is used for points outside that region - in_region = [ - self.interpolate_multiplier_xyz(x, y, z, multiplier, fill_value=1.0) - for multiplier in speed_multipliers - ] + # while the freestream wind speed is used for points outside that region. + + # Because the (x,y,z) points are the same for each findex, we create the triangulation + # once and then overwrite the values for each findex. + + # Create triangulation using zeroth findex + interp_3d = self.interpolate_multiplier_xyz( + x, y, z, speed_multipliers[0], fill_value=1.0 + ) + # Copy the interpolant for each findex and overwrite the values + for findex in range(self.n_findex): + interp_3d.values = speed_multipliers[findex, :].reshape(-1, 1) + interps_f[findex] = copy.deepcopy(interp_3d) + else: # Compute the 2-dimensional interpolants for each wind direction # Linear interpolation is used for points within the user-defined area of values, # while the freestream wind speed is used for points outside that region - in_region = [ - self.interpolate_multiplier_xy(x, y, multiplier, fill_value=1.0) - for multiplier in speed_multipliers - ] - self.het_map = in_region + # Because the (x,y) points are the same for each findex, we create the triangulation + # once and then overwrite the values for each findex. + + # Create triangulation using zeroth findex + interp_2d = self.interpolate_multiplier_xy(x, y, speed_multipliers[0], fill_value=1.0) + # Copy the interpolant for each findex and overwrite the values + for findex in range(self.n_findex): + interp_2d.values = speed_multipliers[findex, :].reshape(-1, 1) + interps_f[findex] = copy.deepcopy(interp_2d) + + self.het_map = interps_f @staticmethod def interpolate_multiplier_xy(x: NDArrayFloat, diff --git a/tests/heterogeneous_map_integration_test.py b/tests/heterogeneous_map_integration_test.py index 630e1c72e..c8ea43f83 100644 --- a/tests/heterogeneous_map_integration_test.py +++ b/tests/heterogeneous_map_integration_test.py @@ -394,3 +394,104 @@ def test_3d_het_and_shear(): wind_directions=[270.0], wind_speeds=[wind_speed] ), ) + + +def test_run_2d_het_map(): + # Define a 2D het map and confirm the results are as expected + # when applied to FLORIS + + # The side of the flow which is accelerated reverses for east versus west + het_map = HeterogeneousMap( + x=np.array([0.0, 0.0, 500.0, 500.0]), + y=np.array([0.0, 500.0, 0.0, 500.0]), + speed_multipliers=np.array( + [ + [1.0, 2.0, 1.0, 2.0], # Top accelerated + [2.0, 1.0, 2.0, 1.0], # Bottom accelerated + ] + ), + wind_directions=np.array([270.0, 90.0]), + wind_speeds=np.array([8.0, 8.0]), + ) + + # Get the FLORIS model + fmodel = FlorisModel(configuration=YAML_INPUT) + + from floris import TimeSeries + + time_series = TimeSeries( + wind_directions=np.array([270.0, 90.0]), + wind_speeds=8.0, + turbulence_intensities=0.06, + heterogeneous_map=het_map, + ) + + # Set the model to a turbines perpinducluar to + # east/west flow with 0 turbine closer to bottom and + # turbine 1 closer to top + fmodel.set( + wind_data=time_series, + layout_x=[250.0, 250.0], + layout_y=[100.0, 400.0], + ) + + # Run the model + fmodel.run() + + # Get the turbine powers + powers = fmodel.get_turbine_powers() + + # Assert that in the first condition, turbine 1 has higher power + assert powers[0, 1] > powers[0, 0] + + # Assert that in the second condition, turbine 0 has higher power + assert powers[1, 0] > powers[1, 1] + + # Assert that the power of turbine 1 equals in the first condition + # equals the power of turbine 0 in the second condition + assert powers[0, 1] == powers[1, 0] + + +def test_het_config(): + + # Test that setting FLORIS with a heterogeneous inflow configuration + # works as expected and consistent with previous results + + # Get the FLORIS model + fmodel = FlorisModel(configuration=YAML_INPUT) + + # Change the layout to a 4 turbine layout in a box + fmodel.set(layout_x=[0, 0, 500.0, 500.0], layout_y=[0, 500.0, 0, 500.0]) + + # Set FLORIS to run for a single condition + fmodel.set(wind_speeds=[8.0], wind_directions=[270.0], turbulence_intensities=[0.06]) + + # Define the speed-ups of the heterogeneous inflow, and their locations. + # Note that heterogeneity is only applied within the bounds of the points defined in the + # heterogeneous_inflow_config dictionary. In this case, set the inflow to be 1.25x the ambient + # wind speed for the upper turbines at y = 500m. + speed_ups = [[1.0, 1.25, 1.0, 1.25]] # Note speed-ups has dimensions of n_findex X n_points + x_locs = [-500.0, -500.0, 1000.0, 1000.0] + y_locs = [-500.0, 1000.0, -500.0, 1000.0] + + # Create the configuration dictionary to be used for the heterogeneous inflow. + heterogeneous_inflow_config = { + "speed_multipliers": speed_ups, + "x": x_locs, + "y": y_locs, + } + + # Set the heterogeneous inflow configuration + fmodel.set(heterogeneous_inflow_config=heterogeneous_inflow_config) + + # Run the FLORIS simulation + fmodel.run() + + turbine_powers = fmodel.get_turbine_powers() / 1000.0 + + # Test that the turbine powers are consistent with previous implementation + # 2248.2, 2800.1, 466.2, 601.5 before the change + # Using almost equal assert + assert np.allclose( + turbine_powers, np.array([[2248.2, 2800.0, 466.2, 601.4]]), atol=1.0, + ) From a3a5b6eaf0255c91241fa10f593600fb0882ef36 Mon Sep 17 00:00:00 2001 From: paulf81 Date: Tue, 11 Feb 2025 16:02:53 -0700 Subject: [PATCH 13/17] Update ruff versions (#1063) --- .pre-commit-config.yaml | 2 +- floris/floris_model.py | 2 +- pyproject.toml | 19 ++++++++++--------- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 72c92a9df..7f4d4dc60 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - id: mixed-line-ending - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.0.241 + rev: v0.9.6 hooks: - id: ruff diff --git a/floris/floris_model.py b/floris/floris_model.py index edf987081..4bb926688 100644 --- a/floris/floris_model.py +++ b/floris/floris_model.py @@ -1798,7 +1798,7 @@ def merge_floris_models(fmodel_list, reference_wind_height=None): or general solver settings. """ - if not all( type(fm) == FlorisModel for fm in fmodel_list ): + if not all( type(fm) is FlorisModel for fm in fmodel_list ): raise TypeError( "Incompatible input specified. fmodel_list must be a list of FlorisModel objects." ) diff --git a/pyproject.toml b/pyproject.toml index efa2606ca..e39a55af4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,7 +54,7 @@ develop = [ "pytest~=8.0", "pytest-benchmark~=5.1", "pre-commit~=4.0", - "ruff~=0.7", + "ruff~=0.9", "isort~=5.0" ] @@ -141,20 +141,20 @@ filterwarnings = [ [tool.ruff] src = ["floris", "tests"] line-length = 100 -target-version = "py310" +target-version = "py313" # See https://github.com/charliermarsh/ruff#supported-rules # for rules included and matching to prefix. -select = ["F", "E", "W", "C4", ] #"T20", "I" +lint.select = ["F", "E", "W", "C4", ] #"T20", "I" # I - isort is not fully implemented in ruff so there is not parity. Consider disabling I. # F401 unused-import: Ignore until all used isort flags are adopted in ruff -ignore = ["F401"] +lint.ignore = ["F401"] # Allow autofix for all enabled rules (when `--fix`) is provided. # fixable = ["A", "B", "C", "D", "E", "F"] -fixable = ["F", "E", "W", "C4"] #"T20", "I" -unfixable = [] +lint.fixable = ["F", "E", "W", "C4"] #"T20", "I" +lint.unfixable = [] # Exclude a variety of commonly ignored directories. exclude = [ @@ -178,12 +178,13 @@ exclude = [ "dist", "node_modules", "venv", + "docs", ] # Allow unused variables when underscore-prefixed. -dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" +lint.dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" -[tool.ruff.per-file-ignores] +[tool.ruff.lint.per-file-ignores] # F841 unused-variable: ignore since this file uses numexpr and many variables look unused "floris/core/wake_deflection/jimenez.py" = ["F841"] "floris/core/wake_turbulence/crespo_hernandez.py" = ["F841"] @@ -198,7 +199,7 @@ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" # import dependencies "floris/core/__init__.py" = ["I001"] -[tool.ruff.isort] +[tool.ruff.lint.isort] combine-as-imports = true known-first-party = ["floris"] order-by-type = false From fd1b36d06f5c1dbfca97dd9acbb586dfaf729ade Mon Sep 17 00:00:00 2001 From: misi9170 <39596329+misi9170@users.noreply.github.com> Date: Fri, 21 Feb 2025 09:33:11 -0700 Subject: [PATCH 14/17] [BUGFIX] Dimension update in `LayoutOptimizationBase` (#1067) * Add test for unusual limits * Update index to reflect new 4D arrays in Floris v4. * isort. * Typo fix on axis labels. --- .../002_opt_yaw_single_ws_uncertain.py | 2 +- .../yaw_optimization/yaw_optimization_base.py | 10 +-- tests/yaw_optimization_integration_test.py | 66 +++++++++++++++++++ 3 files changed, 72 insertions(+), 6 deletions(-) create mode 100644 tests/yaw_optimization_integration_test.py diff --git a/examples/examples_control_optimization/002_opt_yaw_single_ws_uncertain.py b/examples/examples_control_optimization/002_opt_yaw_single_ws_uncertain.py index 4b9ceda1e..1f6c80881 100644 --- a/examples/examples_control_optimization/002_opt_yaw_single_ws_uncertain.py +++ b/examples/examples_control_optimization/002_opt_yaw_single_ws_uncertain.py @@ -76,7 +76,7 @@ color="r", marker="x", ) - ax.set_ylabel("Yaw Offset (deg") + ax.set_ylabel("Yaw Offset T{0:03d} (deg)".format(tindex)) ax.legend() ax.grid(True) diff --git a/floris/optimization/yaw_optimization/yaw_optimization_base.py b/floris/optimization/yaw_optimization/yaw_optimization_base.py index 358ecc40f..5dab7fe30 100644 --- a/floris/optimization/yaw_optimization/yaw_optimization_base.py +++ b/floris/optimization/yaw_optimization/yaw_optimization_base.py @@ -260,14 +260,14 @@ def _reduce_control_problem(self): # Find bounds closest to 0.0 deg combined_bounds = np.concatenate( ( - np.expand_dims(minimum_yaw_angle_subset, axis=3), - np.expand_dims(maximum_yaw_angle_subset, axis=3) + np.expand_dims(minimum_yaw_angle_subset, axis=2), + np.expand_dims(maximum_yaw_angle_subset, axis=2) ), - axis=3 + axis=2 ) # Overwrite all values that are not allowed to be 0.0 with bound value closest to zero - ids_closest = np.expand_dims(np.argmin(np.abs(combined_bounds), axis=3), axis=3) - yaw_mb = np.squeeze(np.take_along_axis(combined_bounds, ids_closest, axis=3)) + ids_closest = np.expand_dims(np.argmin(np.abs(combined_bounds), axis=2), axis=2) + yaw_mb = np.squeeze(np.take_along_axis(combined_bounds, ids_closest, axis=2), axis=2) yaw_angles_template_subset[idx] = yaw_mb[idx] # Save all subset variables to self diff --git a/tests/yaw_optimization_integration_test.py b/tests/yaw_optimization_integration_test.py new file mode 100644 index 000000000..a0c3011fc --- /dev/null +++ b/tests/yaw_optimization_integration_test.py @@ -0,0 +1,66 @@ +import numpy as np +import pandas as pd +import pytest + +from floris import FlorisModel +from floris.optimization.yaw_optimization.yaw_optimizer_sr import YawOptimizationSR + + +DEBUG = False +VELOCITY_MODEL = "gauss" +DEFLECTION_MODEL = "gauss" + +def test_yaw_optimization_limits(sample_inputs_fixture): + """ + The Serial Refine (SR) method optimizes yaw angles based on a sequential, iterative yaw + optimization scheme. This test compares the optimization results from the SR method for + a simple farm with a simple wind rose to stored baseline 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) + wd_array = np.arange(0.0, 360.0, 90.0) + ws_array = 8.0 * np.ones_like(wd_array) + ti_array = 0.1 * np.ones_like(wd_array) + + D = 126.0 # Rotor diameter for the NREL 5 MW + fmodel.set( + layout_x=[0.0, 5 * D, 10 * D], + layout_y=[0.0, 0.0, 0.0], + wind_directions=wd_array, + wind_speeds=ws_array, + turbulence_intensities=ti_array, + ) + + # Asymmetric limits + yaw_opt = YawOptimizationSR( + fmodel, + minimum_yaw_angle=-10.0, + maximum_yaw_angle=20.0, + ) + yaw_opt.optimize() + + # Strictly positive limits + yaw_opt = YawOptimizationSR( + fmodel, + minimum_yaw_angle=5.0, + maximum_yaw_angle=20.0, + ) + yaw_opt.optimize() + + # Strictly negative limits + yaw_opt = YawOptimizationSR( + fmodel, + minimum_yaw_angle=-20.0, + maximum_yaw_angle=-5.0, + ) + yaw_opt.optimize() + + # Infeasible limits + with pytest.raises(ValueError): + yaw_opt = YawOptimizationSR( + fmodel, + minimum_yaw_angle=20.0, + maximum_yaw_angle=5.0, + ) From 9f6795d29a2bbf536f7cefd5bfa575117a9226ac Mon Sep 17 00:00:00 2001 From: misi9170 Date: Mon, 3 Mar 2025 17:10:39 -0700 Subject: [PATCH 15/17] Update version to v4.3 --- README.md | 4 ++-- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 78d74ddaf..dd6843ab9 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.2.2](https://github.com/NREL/floris/releases/latest). +release is [FLORIS v4.3](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.2 + 4.3 FILE ~/floris/floris/__init__.py diff --git a/pyproject.toml b/pyproject.toml index e39a55af4..b0684812b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "floris" -version = "4.2.2" +version = "4.3" description = "A controls-oriented engineering wake model." readme = "README.md" requires-python = ">=3.9" From e77566324e5d83ab9caa7589056da722b5acad63 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Tue, 4 Mar 2025 09:41:06 -0700 Subject: [PATCH 16/17] Update docs notes on Floris support and numpy requirements. --- docs/index.md | 4 +--- docs/installation.md | 6 ++---- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/docs/index.md b/docs/index.md index 7ceeb237b..4fae76ca7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -9,9 +9,7 @@ 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. - -FLORIS v4.3 will also move to requiring `numpy` version 2. See the [numpy documentation for details](https://numpy.org/doc/stable/numpy_2_0_migration_guide.html). +Support for python version 3.8 was dropped in FLORIS v4.3. See {ref}`installation` for details. FLORIS v4.3 also made the move to requiring `numpy` version 2. See the [numpy documentation for details](https://numpy.org/doc/stable/numpy_2_0_migration_guide.html). ``` ## Quick Start diff --git a/docs/installation.md b/docs/installation.md index 60e18dc70..8c17c01bc 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -13,10 +13,8 @@ work within a virtual environment for both working with and working on FLORIS, t 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. - -FLORIS v4.3 will also require `numpy` version 2. See the [numpy documentation for details](https://numpy.org/doc/stable/numpy_2_0_migration_guide.html). +```{note} +Support for python version 3.8 was dropped in FLORIS v4.3. See {ref}`installation` for details. FLORIS v4.3 also made the move to requiring `numpy` version 2. See the [numpy documentation for details](https://numpy.org/doc/stable/numpy_2_0_migration_guide.html). ``` Installing into a Python environment that contains a previous version of FLORIS may cause conflicts. From ba63a9a57fd278d072849a19ff1b6f523b722a5a Mon Sep 17 00:00:00 2001 From: misi9170 Date: Tue, 4 Mar 2025 10:00:15 -0700 Subject: [PATCH 17/17] Remove warning from readme. --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index dd6843ab9..80a7e9afd 100644 --- a/README.md +++ b/README.md @@ -13,9 +13,6 @@ 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.