Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions floris/core/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ def __attrs_post_init__(self) -> None:
grid_resolution=self.solver["flow_field_grid_points"],
x1_bounds=self.solver["flow_field_bounds"][0],
x2_bounds=self.solver["flow_field_bounds"][1],
keep_inertial_frame=self.solver.get("keep_inertial_frame", False),
)
else:
raise ValueError(
Expand Down
47 changes: 34 additions & 13 deletions floris/core/grid.py
Original file line number Diff line number Diff line change
Expand Up @@ -515,11 +515,14 @@ class FlowFieldPlanarGrid(Grid):
planar direction. Must be 2 components for resolution in the x and y directions.
The z direction is set to 3 planes at -10.0, 0.0, and +10.0 relative to the
`planar_coordinate`.
keep_inertial_frame (:py:obj:`bool`, optional): If True, coordinates are kept in the
inertial frame (not rotated relative to wind direction). Defaults to False.
"""
normal_vector: str = field()
planar_coordinate: float = field()
x1_bounds: tuple = field(default=None)
x2_bounds: tuple = field(default=None)
keep_inertial_frame: bool = field(default=False)
x_center_of_rotation: NDArrayFloat = field(init=False)
y_center_of_rotation: NDArrayFloat = field(init=False)
sorted_indices: NDArrayInt = field(init=False)
Expand All @@ -541,10 +544,21 @@ def set_grid(self) -> None:
Then, create the grid based on this wind-from-left orientation
"""
# These are the rotated coordinates of the wind turbines based on the wind direction
x, y, z, self.x_center_of_rotation, self.y_center_of_rotation = rotate_coordinates_rel_west(
self.wind_directions,
self.turbine_coordinates
)
# Unless keep_inertial_frame is True, in which case we keep original coordinates
if not self.keep_inertial_frame:
x, y, z, self.x_center_of_rotation, self.y_center_of_rotation = rotate_coordinates_rel_west(
self.wind_directions,
self.turbine_coordinates
)
else:
# Use coordinates directly without rotation
x_coordinates, y_coordinates, z_coordinates = self.turbine_coordinates.T
x = np.ones((len(self.wind_directions), 1)) * x_coordinates
y = np.ones((len(self.wind_directions), 1)) * y_coordinates
z = np.ones((len(self.wind_directions), 1)) * z_coordinates
self.x_center_of_rotation = (np.min(x_coordinates) + np.max(x_coordinates)) / 2
self.y_center_of_rotation = (np.min(y_coordinates) + np.max(y_coordinates)) / 2

max_diameter = np.max(self.turbine_diameters)

if self.normal_vector == "z": # Rules of thumb for horizontal plane
Expand Down Expand Up @@ -607,15 +621,22 @@ def set_grid(self) -> None:
self.z_sorted = z_points[None, :, :, :]

# Now calculate grid coordinates in original frame (from 270 deg perspective)
self.x_sorted_inertial_frame, self.y_sorted_inertial_frame, self.z_sorted_inertial_frame = \
reverse_rotate_coordinates_rel_west(
wind_directions=self.wind_directions,
grid_x=self.x_sorted,
grid_y=self.y_sorted,
grid_z=self.z_sorted,
x_center_of_rotation=self.x_center_of_rotation,
y_center_of_rotation=self.y_center_of_rotation,
)
# Skip this if keep_inertial_frame is True since we're already in the inertial frame
if not self.keep_inertial_frame:
self.x_sorted_inertial_frame, self.y_sorted_inertial_frame, self.z_sorted_inertial_frame = \
reverse_rotate_coordinates_rel_west(
wind_directions=self.wind_directions,
grid_x=self.x_sorted,
grid_y=self.y_sorted,
grid_z=self.z_sorted,
x_center_of_rotation=self.x_center_of_rotation,
y_center_of_rotation=self.y_center_of_rotation,
)
else:
# If we're keeping inertial frame, sorted and inertial are the same
self.x_sorted_inertial_frame = self.x_sorted.copy()
self.y_sorted_inertial_frame = self.y_sorted.copy()
self.z_sorted_inertial_frame = self.z_sorted.copy()

@define
class PointsGrid(Grid):
Expand Down
132 changes: 132 additions & 0 deletions floris/floris_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
Any,
List,
Optional,
Tuple,
Union,
Dict,
)

import numpy as np
Expand Down Expand Up @@ -34,6 +37,7 @@
nested_get,
nested_set,
print_nested_dict,
update_nested_dict,
)
from floris.wind_data import (
TimeSeries,
Expand Down Expand Up @@ -1107,6 +1111,7 @@ def calculate_cross_plane(
y_bounds=None,
z_bounds=None,
findex_for_viz=None,
keep_inertial_frame=False,
):
"""
Shortcut method to instantiate a :py:class:`~.tools.cut_plane.CutPlane`
Expand All @@ -1124,6 +1129,8 @@ def calculate_cross_plane(
z_bounds (tuple, optional): Limits of output array (in m).
Defaults to None.
finder_for_viz (int, optional): Index of the condition to visualize.
keep_inertial_frame (bool, optional): If True, coordinates are kept in the
inertial frame (not rotated relative to wind direction). Defaults to False.
Returns:
:py:class:`~.tools.cut_plane.CutPlane`: containing values
of y, z, u, v, w
Expand All @@ -1145,6 +1152,7 @@ def calculate_cross_plane(
"planar_coordinate": downstream_dist,
"flow_field_grid_points": [y_resolution, z_resolution],
"flow_field_bounds": [y_bounds, z_bounds],
"keep_inertial_frame": keep_inertial_frame,
}
fmodel_viz.set_for_viz(findex_for_viz, solver_settings)

Expand Down Expand Up @@ -1172,6 +1180,7 @@ def calculate_horizontal_plane(
x_bounds=None,
y_bounds=None,
findex_for_viz=None,
keep_inertial_frame=False,
):
"""
Shortcut method to instantiate a :py:class:`~.tools.cut_plane.CutPlane`
Expand All @@ -1189,6 +1198,8 @@ def calculate_horizontal_plane(
y_bounds (tuple, optional): Limits of output array (in m).
Defaults to None.
findex_for_viz (int, optional): Index of the condition to visualize.
keep_inertial_frame (bool, optional): If True, coordinates are kept in the
inertial frame (not rotated relative to wind direction). Defaults to False.

Returns:
:py:class:`~.tools.cut_plane.CutPlane`: containing values
Expand All @@ -1211,6 +1222,7 @@ def calculate_horizontal_plane(
"planar_coordinate": height,
"flow_field_grid_points": [x_resolution, y_resolution],
"flow_field_bounds": [x_bounds, y_bounds],
"keep_inertial_frame": keep_inertial_frame,
}
fmodel_viz.set_for_viz(findex_for_viz, solver_settings)

Expand Down Expand Up @@ -1243,6 +1255,7 @@ def calculate_y_plane(
x_bounds=None,
z_bounds=None,
findex_for_viz=None,
keep_inertial_frame=False,
):
"""
Shortcut method to instantiate a :py:class:`~.tools.cut_plane.CutPlane`
Expand All @@ -1261,6 +1274,8 @@ def calculate_y_plane(
Defaults to None.
findex_for_viz (int, optional): Index of the condition to visualize.
Defaults to 0.
keep_inertial_frame (bool, optional): If True, coordinates are kept in the
inertial frame (not rotated relative to wind direction). Defaults to False.

Returns:
:py:class:`~.tools.cut_plane.CutPlane`: containing values
Expand All @@ -1283,6 +1298,7 @@ def calculate_y_plane(
"planar_coordinate": crossstream_dist,
"flow_field_grid_points": [x_resolution, z_resolution],
"flow_field_bounds": [x_bounds, z_bounds],
"keep_inertial_frame": keep_inertial_frame,
}
fmodel_viz.set_for_viz(findex_for_viz, solver_settings)

Expand Down Expand Up @@ -1897,3 +1913,119 @@ def merge_floris_models(fmodel_list, reference_wind_height=None):
)

return fmodel_merged


#############################################################################

def _reset_windio_metadata(self, category: List[str] = None) -> None:
METADATA_FILEDS = ['wind_farm', 'wind_resource']

if not hasattr(self, "_windio_metadata"):
category = None

if category is None:
self._windio_metadata = {k: {} for k in METADATA_FILEDS}
else:
self._windio_metadata[category] = {}

@staticmethod
def from_windio( wind_energy_system: Path | Dict = None,
wind_resource: Path | Dict = None,
wind_farm: Path | Dict = None,
model_attrs: Path | Dict = None,
) -> 'FlorisModel':

from .read_windio import load_windio_input

if (wind_energy_system is not None):
wind_energy_system = load_windio_input(wind_energy_system)

def _wes_or_input(input: Path | Dict, input_name: str, wes_nested_key_paths: Tuple[str]) -> Dict:
if (input is not None):
print(f"{input_name} from wind_energy_system will be ignored.")
data_dict = load_windio_input(input)
data_dict['_context'] = f".{input_name}"
return data_dict

if (wind_energy_system is not None):
data_dict = nested_get(wind_energy_system, wes_nested_key_paths)
data_dict['_context'] = f".wind_energy_system.{'.'.join(wes_nested_key_paths)}"
return data_dict

raise ValueError(f"Either {input_name} or wind_energy_system must be provided.")

wind_farm_windio = _wes_or_input(wind_farm,
"wind_farm",
("wind_farm",))
wind_data_windio = _wes_or_input(wind_resource,
"wind_resource",
("site", "energy_resource", "wind_resource"))
model_attrs_windio = _wes_or_input(model_attrs,
"attributes",
("attributes",))

# Start with defaults
default_param = load_windio_input(Path(__file__).parent / "default_inputs.yaml")

# Wake model parameters are set once and for all
from .read_windio import read_wake_model
wake_model_floris = read_wake_model(model_attrs_windio)
update_nested_dict(default_param, wake_model_floris)

# Instantiate FlorisModel
fmodel = FlorisModel(default_param)
fmodel._reset_windio_metadata()

# Reconstruct description
fmodel_name = "FlorisModel from windio"

if (wind_energy_system is not None):
fmodel_name = wind_energy_system['name']

fmodel_name += f" ({wind_farm_windio['name']})"

fmodel.core.name = fmodel_name
fmodel.core.description = fmodel_name

# Set the various components from windio data
fmodel.set_farm_from_windio(wind_farm_windio)
fmodel.set_wind_data_from_windio(wind_data_windio)

return fmodel

def set_wind_data_from_windio(self, wind_resource_windio: Dict) -> None:
from .read_windio import read_wind_resource, load_windio_input
self._reset_windio_metadata(category="wind_resource")

wind_resource_windio = load_windio_input(wind_resource_windio)
wind_data_floris = read_wind_resource(wind_resource_windio)

self._reset_windio_metadata(category="wind_resource")
self._windio_metadata["wind_resource"] = wind_data_floris.pop('_metadata', {})

# Handle special cases
disable = wind_data_floris.pop('_disable', None)
dict_disable = {'disable_turbines': disable} if disable is not None else {}
if (disable is not None):
self.set_operation_model("mixed")
else:
self.set_operation_model("simple")

self.set(
**wind_data_floris,
**dict_disable
)

def set_farm_from_windio(self, wind_farm_windio: Dict) -> None:
from .read_windio import read_wind_farm, load_windio_input
self._reset_windio_metadata(category="wind_farm")

wind_farm_windio = load_windio_input(wind_farm_windio)
wind_farm_floris = read_wind_farm(wind_farm_windio, logger=self.logger)

self._reset_windio_metadata(category="wind_farm")
self._windio_metadata["wind_farm"] = wind_farm_floris.pop('_metadata', {})

# Set reference height to mean hub height
hub_heights = [t["hub_height"] for t in wind_farm_floris["turbine_type"]]
self.set(**wind_farm_floris, reference_wind_height=np.mean(hub_heights))
5 changes: 5 additions & 0 deletions floris/read_windio/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from .utils import load_windio_input, TrackedDict

from .read_wind_resource import read_wind_resource
from .read_wind_farm import read_wind_farm
from .read_wake_model import read_wake_model
Loading