Skip to content

Commit

Permalink
Support relative paths in YAML configuration files (NREL#739)
Browse files Browse the repository at this point in the history
* account for using the method from a system call

* add internal library as farm object

* add relative file capability to FI for convenience in example running

* update Farm and multi dim turb to work with library paths

* change cp-ct curve reference location

* fix misspelling

* clean up library switch logic

* update docstrings

* remove notebook handling from the examples CI workflow

* fix example 18 to rely on the internal turbine library directly
  • Loading branch information
RHammond2 authored Nov 28, 2023
1 parent bc4fd28 commit 34a7e0c
Show file tree
Hide file tree
Showing 7 changed files with 90 additions and 41 deletions.
21 changes: 0 additions & 21 deletions .github/workflows/check-working-examples.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,27 +53,6 @@ jobs:
fi
done
# Run all Jupyter notebooks
for i in *.ipynb; do
# Convert this notebook to a Python script
if ! jupyter nbconvert --to script $i; then
# On conversion error, report and go to the next notebook
error_results="${error_results}"$'\n'" - Error converting ${i} to Python script"
continue
fi
# Get the basename of the notebook since the converted script will have the same basename
script_name=`basename $i .ipynb`
# Run the converted script
if ! python "${script_name}.py"; then
error_results="${error_results}"$'\n'" - ${i}"
error_found=1
fi
done
if [[ $error_found ]]; then
echo "${error_results}"
fi
Expand Down
15 changes: 8 additions & 7 deletions examples/18_check_turbine.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# See https://floris.readthedocs.io for documentation


import os
from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np
Expand All @@ -38,12 +38,13 @@
# Apply wind speeds
fi.reinitialize(wind_speeds=ws_array)

# Get a list of available turbine models
turbines = os.listdir('../floris/turbine_library')
turbines = [t for t in turbines if 'yaml' in t]
turbines = [t.strip('.yaml') for t in turbines]
# Remove multi-dimensional Cp/Ct turbine definitions as they require different handling
turbines = [i for i in turbines if ('multi_dim' not in i)]
# Get a list of available turbine models provided through FLORIS, and remove
# multi-dimensional Cp/Ct turbine definitions as they require different handling
turbines = [
t.stem
for t in fi.floris.farm.internal_turbine_library.iterdir()
if t.suffix == ".yaml" and ("multi_dim" not in t.stem)
]

# Declare a set of figures for comparing cp and ct across models
fig_cp_ct, axarr_cp_ct = plt.subplots(2,1,sharex=True,figsize=(10,10))
Expand Down
31 changes: 25 additions & 6 deletions floris/simulation/farm.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,18 @@ class Farm(BaseClass):
Wake, FlowField) and packages everything into the appropriate data
type. Farm should also be used as an entry point to probe objects
for generating output.
Args:
layout_x (NDArrayFloat): A sequence of x-axis locations for the turbines that can be
converted to a 1-D :py:obj:`numpy.ndarray`.
layout_y (NDArrayFloat): A sequence of y-axis locations for the turbines that can be
converted to a 1-D :py:obj:`numpy.ndarray`.
turbine_type (list[dict | str]): A list of turbine definition dictionaries, or string
references to the filename of the turbine type in either the FLORIS-provided turbine
library (.../floris/turbine_library/), or a user-provided
:py:attr:`turbine_library_path`.
turbine_library_path (:obj:`str`): Either an absolute file path to the turbine library, or a
path relative to the file that is running the analysis.
"""

layout_x: NDArrayFloat = field(converter=floris_array_converter)
Expand Down Expand Up @@ -97,6 +109,8 @@ class Farm(BaseClass):
correct_cp_ct_for_tilt: NDArrayFloat = field(init=False, default=[])
correct_cp_ct_for_tilt_sorted: NDArrayFloat = field(init=False, default=[])

internal_turbine_library: Path = field(init=False, default=default_turbine_library_path)

def __attrs_post_init__(self) -> None:
# Turbine definitions can be supplied in three ways:
# - A string selecting a turbine in the floris turbine library
Expand Down Expand Up @@ -128,7 +142,7 @@ def __attrs_post_init__(self) -> None:
continue

# Check if the file exists in the internal and/or external library
internal_fn = (default_turbine_library_path / t).with_suffix(".yaml")
internal_fn = (self.internal_turbine_library / t).with_suffix(".yaml")
external_fn = (self.turbine_library_path / t).with_suffix(".yaml")
in_internal = internal_fn.exists()
in_external = external_fn.exists()
Expand Down Expand Up @@ -248,11 +262,16 @@ def construct_turbine_correct_cp_ct_for_tilt(self):
)

def construct_turbine_map(self):
if 'multi_dimensional_cp_ct' in self.turbine_definitions[0].keys() \
and self.turbine_definitions[0]['multi_dimensional_cp_ct'] is True:
self.turbine_map = [
TurbineMultiDimensional.from_dict(turb) for turb in self.turbine_definitions
]
multi_key = "multi_dimensional_cp_ct"
if multi_key in self.turbine_definitions[0] and self.turbine_definitions[0][multi_key]:
self.turbine_map = []
for turb in self.turbine_definitions:
_turb = {**turb, **{"turbine_library_path": self.internal_turbine_library}}
try:
self.turbine_map.append(TurbineMultiDimensional.from_dict(_turb))
except FileNotFoundError:
_turb["turbine_library_path"] = self.turbine_library_path
self.turbine_map.append(TurbineMultiDimensional.from_dict(_turb))
else:
self.turbine_map = [Turbine.from_dict(turb) for turb in self.turbine_definitions]

Expand Down
20 changes: 19 additions & 1 deletion floris/simulation/turbine_multi_dim.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@

import copy
from collections.abc import Iterable
from pathlib import Path

import attrs
import numpy as np
import pandas as pd
from attrs import define, field
Expand Down Expand Up @@ -422,10 +424,22 @@ class TurbineMultiDimensional(Turbine):
the width/height of the grid of points on the rotor as a ratio of
the rotor radius.
Defaults to 0.5.
power_thrust_data_file (:py:obj:`str`): The path and name of the file containing the
multidimensional power thrust curve. The path may be an absolute location or a relative
path to where FLORIS is being run.
multi_dimensional_cp_ct (:py:obj:`bool`, optional): Indicates if the turbine definition is
single dimensional (False) or multidimensional (True).
turbine_library_path (:py:obj:`pathlib.Path`, optional): The
:py:attr:`Farm.turbine_library_path` or :py:attr:`Farm.internal_turbine_library_path`,
whichever is being used to load turbine definitions.
Defaults to the current file location.
"""

power_thrust_data_file: str = field(default=None)
multi_dimensional_cp_ct: bool = field(default=False)
turbine_library_path: Path = field(
default=Path(".").resolve(),
validator=attrs.validators.instance_of(Path)
)

# rloc: float = float_attrib() # TODO: goes here or on the Grid?
# use_points_on_perimeter: bool = bool_attrib()
Expand All @@ -447,6 +461,10 @@ class TurbineMultiDimensional(Turbine):
# self.use_points_on_perimeter = False

def __attrs_post_init__(self) -> None:
# Solidify the data file path and name
self.power_thrust_data_file = (
self.turbine_library_path / self.power_thrust_data_file
).resolve()

# Read in the multi-dimensional data supplied by the user.
df = pd.read_csv(self.power_thrust_data_file)
Expand Down
12 changes: 11 additions & 1 deletion floris/tools/floris_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

from __future__ import annotations

import inspect
from pathlib import Path

import numpy as np
Expand Down Expand Up @@ -55,7 +56,16 @@ def __init__(self, configuration: dict | str | Path):
self.configuration = configuration

if isinstance(self.configuration, (str, Path)):
self.floris = Floris.from_file(self.configuration)
try:
self.floris = Floris.from_file(self.configuration)
except FileNotFoundError:
# If the file cannot be found, then attempt the configuration path relative to the
# file location from which FlorisInterface was attempted to be run. If successful,
# update self.configuration to an aboslute, working file path and name.
base_fn = Path(inspect.stack()[-1].filename).resolve().parent
config = (base_fn / self.configuration).resolve()
self.floris = Floris.from_file(config)
self.configuration = config

elif isinstance(self.configuration, dict):
self.floris = Floris.from_dict(self.configuration)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ TSR: 8.0
ref_density_cp_ct: 1.225
ref_tilt_cp_ct: 6.0
multi_dimensional_cp_ct: True
power_thrust_data_file: '../floris/turbine_library/iea_15MW_multi_dim_Tp_Hs.csv'
power_thrust_data_file: 'iea_15MW_multi_dim_Tp_Hs.csv'
floating_tilt_table:
tilt:
- 5.747296314800103
Expand Down
30 changes: 26 additions & 4 deletions floris/type_dec.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from __future__ import annotations

import copy
import inspect
from pathlib import Path
from typing import (
Any,
Expand Down Expand Up @@ -91,6 +92,8 @@ def convert_to_path(fn: str | Path) -> Path:
fn (str | Path): The user input file path or file name.
Raises:
FileExistsError: Raised if :py:attr:`fn` is not able to be found as an absolute path, nor as
a relative path.
TypeError: Raised if :py:attr:`fn` is neither a :py:obj:`str`, nor a :py:obj:`pathlib.Path`.
Returns:
Expand All @@ -99,11 +102,30 @@ def convert_to_path(fn: str | Path) -> Path:
if isinstance(fn, str):
fn = Path(fn)

# Get the base path from where the analysis script was run to determine the relative
# path from which `fn` might be based. [1] is where a direct call to this function will be
# located (e.g., testing via pytest), and [-1] is where a direct call to the function via an
# analysis script will be located (e.g., running an example).
base_fn_script = Path(inspect.stack()[-1].filename).resolve().parent
base_fn_sys = Path(inspect.stack()[1].filename).resolve().parent

if isinstance(fn, Path):
fn.resolve()
else:
raise TypeError(f"The passed input: {fn} could not be converted to a pathlib.Path object")
return fn
absolute_fn = fn.resolve()
relative_fn_script = (base_fn_script / fn).resolve()
relative_fn_sys = (base_fn_sys / fn).resolve()
if absolute_fn.is_dir():
return absolute_fn
if relative_fn_script.is_dir():
return relative_fn_script
if relative_fn_sys.is_dir():
return relative_fn_sys
raise FileExistsError(
f"{fn} could not be found as either a\n"
f" - relative file path from a script: {relative_fn_script}\n"
f" - relative file path from a system location: {relative_fn_sys}\n"
f" - or absolute file path: {absolute_fn}"
)
raise TypeError(f"The passed input: {fn} could not be converted to a pathlib.Path object")


@define
Expand Down

0 comments on commit 34a7e0c

Please sign in to comment.