From 47cbb29083bd6134d4315739753d82fefebf9c52 Mon Sep 17 00:00:00 2001 From: Rafael M Mudafort Date: Wed, 5 Feb 2025 10:09:30 -0600 Subject: [PATCH] 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)