Skip to content

Commit

Permalink
Merge pull request #364 from jaredthomas68/updatefloris-hopp
Browse files Browse the repository at this point in the history
Update FLORIS Version
  • Loading branch information
johnjasa authored Oct 16, 2024
2 parents 8dafb0d + a6982a3 commit d874402
Show file tree
Hide file tree
Showing 7 changed files with 63 additions and 49 deletions.
11 changes: 6 additions & 5 deletions hopp/simulation/technologies/sites/site_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
import matplotlib.pyplot as plt
import numpy as np
from numpy.typing import NDArray
from shapely.geometry import Polygon, MultiPolygon, Point
from shapely.geometry import Polygon, MultiPolygon, Point, shape
from shapely.geometry.base import BaseGeometry
from shapely.ops import transform
from shapely.validation import make_valid
from shapely import make_valid
from fastkml import kml, KML
import pyproj
import utm
Expand Down Expand Up @@ -259,7 +259,8 @@ def kml_read(filepath):
valid_region = None
for pm in placemarks:
if "boundary" in pm.name.lower():
valid_region = make_valid(pm.geometry)
shapely_object = shape(pm.geometry)
valid_region = make_valid(shapely_object)
lon, lat = valid_region.centroid.x, valid_region.centroid.y
if project is None:
zone_num = utm.from_latlon(lat, lon)[2]
Expand All @@ -272,9 +273,9 @@ def kml_read(filepath):
for pm in placemarks:
if 'exclusion' in pm.name.lower():
try:
valid_region = valid_region.difference(transform(project, pm.geometry.buffer(0)))
valid_region = valid_region.difference(transform(project, shape(pm.geometry.buffer(0))))
except:
valid_region = valid_region.difference(transform(project, make_valid(pm.geometry)))
valid_region = valid_region.difference(transform(project, make_valid(shape(pm.geometry))))
return k, valid_region, lat, lon

@staticmethod
Expand Down
43 changes: 33 additions & 10 deletions hopp/simulation/technologies/wind/floris.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# tools to add floris to the hybrid simulation class
from attrs import define, field
from dataclasses import dataclass, asdict
import csv
from typing import TYPE_CHECKING, Tuple
import numpy as np

from floris.tools import FlorisInterface
from floris import FlorisModel, TimeSeries

from hopp.simulation.base import BaseClass
from hopp.simulation.technologies.sites import SiteInfo
Expand All @@ -20,8 +21,10 @@ class Floris(BaseClass):
site: SiteInfo = field()
config: "WindConfig" = field()

_operational_losses: float = field(init=False)
_timestep: Tuple[int, int] = field(init=False)
fi: FlorisInterface = field(init=False)
annual_energy_pre_curtailment_ac: float = field(init=False)
fi: FlorisModel = field(init=False)

def __attrs_post_init__(self):
# floris_input_file = resource_file_converter(self.config["simulation_input_file"])
Expand All @@ -34,8 +37,9 @@ def __attrs_post_init__(self):

# the above change is a temporary patch to bridge to refactor floris

self.fi = FlorisInterface(floris_input_file)
self.fi = FlorisModel(floris_input_file)
self._timestep = self.config.timestep
self._operational_losses = self.config.operational_losses

self.wind_resource_data = self.site.wind_resource.data
self.speeds, self.wind_dirs = self.parse_resource_data()
Expand All @@ -52,7 +56,8 @@ def __attrs_post_init__(self):
self.wind_farm_yCoordinates = self.fi.layout_y
self.nTurbs = len(self.wind_farm_xCoordinates)
self.turb_rating = self.config.turbine_rating_kw
self.wind_turbine_rotor_diameter = self.fi.floris.farm.rotor_diameters[0]

self.wind_turbine_rotor_diameter = self.fi.core.farm.rotor_diameters[0]
self.system_capacity = self.nTurbs * self.turb_rating

# turbine power curve (array of kW power outputs)
Expand Down Expand Up @@ -121,15 +126,33 @@ def execute(self, project_life):
power_turbines = np.zeros((self.nTurbs, 8760))
power_farm = np.zeros(8760)

self.fi.reinitialize(wind_speeds=self.speeds[self.start_idx:self.end_idx], wind_directions=self.wind_dirs[self.start_idx:self.end_idx], time_series=True)
self.fi.calculate_wake()
time_series = TimeSeries(
wind_directions=self.wind_dirs[self.start_idx:self.end_idx],
wind_speeds=self.speeds[self.start_idx:self.end_idx],
turbulence_intensities=self.fi.core.flow_field.turbulence_intensities[0]
)

self.fi.set(wind_data=time_series)
self.fi.run()

power_turbines[:, self.start_idx:self.end_idx] = self.fi.get_turbine_powers().reshape((self.nTurbs, self.end_idx - self.start_idx))
power_farm[self.start_idx:self.end_idx] = self.fi.get_farm_power().reshape((self.end_idx - self.start_idx))

# Adding losses from PySAM defaults (excluding turbine and wake losses)
self.gen = power_farm *((100 - 12.83)/100) / 1000
# self.gen = power_farm / 1000
self.annual_energy = np.sum(self.gen)
print('Wind annual energy: ', self.annual_energy)
self.gen = power_farm * ((100 - self._operational_losses)/100) / 1000 # kW

self.annual_energy = np.sum(self.gen) # kWh
self.capacity_factor = np.sum(self.gen) / (8760 * self.system_capacity) * 100
self.turb_powers = power_turbines * (100 - self._operational_losses) / 100 / 1000 # kW
self.turb_velocities = self.fi.turbine_average_velocities
self.annual_energy_pre_curtailment_ac = self.annual_energy

def export(self):
"""
Return all the floris system configuration in a dictionary for the financial model
"""
config = {
'system_capacity': self.system_capacity,
'annual_energy': self.annual_energy,
}
return config
7 changes: 4 additions & 3 deletions hopp/simulation/technologies/wind/wind_plant.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from hopp.simulation.base import BaseClass
from hopp.type_dec import resource_file_converter
from hopp.utilities import load_yaml
from hopp.utilities.validators import gt_zero, contains
from hopp.utilities.validators import gt_zero, contains, range_val
from hopp.simulation.technologies.wind.floris import Floris
from hopp.simulation.technologies.power_source import PowerSource
from hopp.simulation.technologies.sites import SiteInfo
Expand All @@ -35,6 +35,7 @@ class WindConfig(BaseClass):
layout_params: layout configuration
rating_range_kw: allowable kw range of turbines, default is 1000 - 3000 kW
floris_config: Floris configuration, only used if `model_name` == 'floris'
operational_losses: total percentage losses in addition to wake losses, defaults based on PySAM (only used for Floris model)
timestep: Timestep (required for floris runs, otherwise optional)
fin_model: Optional financial model. Can be any of the following:
Expand All @@ -43,7 +44,6 @@ class WindConfig(BaseClass):
- a dict representing a `CustomFinancialModel`
- an object representing a `CustomFinancialModel` or `Singleowner.Singleowner` instance
"""
num_turbines: int = field(validator=gt_zero)
turbine_rating_kw: float = field(validator=gt_zero)
Expand All @@ -55,6 +55,7 @@ class WindConfig(BaseClass):
model_input_file: Optional[str] = field(default=None)
rating_range_kw: Tuple[int, int] = field(default=(1000, 3000))
floris_config: Optional[Union[dict, str, Path]] = field(default=None)
operational_losses: float = field(default = 12.83, validator=range_val(0, 100))
timestep: Optional[Tuple[int, int]] = field(default=None)
fin_model: Optional[Union[dict, FinancialModelType]] = field(default=None)

Expand Down Expand Up @@ -134,7 +135,7 @@ def __attrs_post_init__(self):
self._system_model.Turbine.wind_turbine_hub_ht = self.config.hub_height
if self.config.rotor_diameter is not None:
self.rotor_diameter = self.config.rotor_diameter

@property
def wake_model(self) -> str:
try:
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ dependencies = [
"Pyomo>=6.1.2",
"diskcache",
"fastkml",
"floris<=3.6",
"floris>=4.0",
"future",
"global_land_mask",
"humpday",
Expand Down Expand Up @@ -46,7 +46,7 @@ dependencies = [
"scikit-learn",
"scikit-optimize",
"scipy",
"shapely>=1.8.5,<2.0.0",
"shapely",
"setuptools",
"timezonefinder",
"urllib3",
Expand Down
21 changes: 6 additions & 15 deletions tests/hopp/inputs/floris_config.yaml
Original file line number Diff line number Diff line change
@@ -1,57 +1,49 @@
name: Gauss
description: Three turbines using Gauss model
floris_version: v3.0.0

logging:
console:
enable: false
level: WARNING
file:
enable: false
level: WARNING

solver:
type: turbine_grid
turbine_grid_points: 1

farm:
layout_x:
- 0.0
- 1841.0
- 3682.0
- 5523.0

layout_y:
- 0.0
- 0.0
- 0.0
- 0.0

turbine_type:
- nrel_5MW

flow_field:
air_density: 1.225
reference_wind_height: -1 # -1 is code for use the hub height
turbulence_intensity: 0.06
reference_wind_height: -1
wind_directions:
- 270.0
wind_shear: 0.33
wind_speeds:
- 8.0
wind_veer: 0.0

turbulence_intensities:
- 0.06
wake:
model_strings:
combination_model: sosfs # TODO followup on why not using linear free-stream superposition? - ask Chris Bay and Gen S.
combination_model: sosfs
deflection_model: gauss
turbulence_model: crespo_hernandez
velocity_model: gauss

enable_secondary_steering: false
enable_yaw_added_recovery: false
enable_transverse_velocities: false

wake_deflection_parameters:
gauss:
ad: 0.0
Expand All @@ -65,7 +57,6 @@ wake:
ad: 0.0
bd: 0.0
kd: 0.05

wake_velocity_parameters:
cc:
a_s: 0.179367259
Expand All @@ -83,10 +74,10 @@ wake:
kb: 0.004
jensen:
we: 0.05

wake_turbulence_parameters:
crespo_hernandez:
initial: 0.1
constant: 0.5
ai: 0.8
downstream: -0.32
downstream: -0.32
enable_active_wake_mixing: false
8 changes: 4 additions & 4 deletions tests/hopp/test_hybrid.py
Original file line number Diff line number Diff line change
Expand Up @@ -430,13 +430,13 @@ def test_hybrid_wind_only_floris(hybrid_config, subtests):
cf = hybrid_plant.capacity_factors

with subtests.test("wind aep"):
assert aeps.wind == approx(72776380, 1e-3)
assert aeps.wind == approx(74149945, 1e-3)
with subtests.test("hybrid aep"):
assert aeps.hybrid == approx(67118441, 1e-3)
assert aeps.hybrid == approx(68271657, 1e-3)
with subtests.test("wind npv"):
assert npvs.wind == approx(3231853, 1e-3)
assert npvs.wind == approx(3592293, 1e-3)
with subtests.test("hybrid npv"):
assert npvs.hybrid == approx(1804831, 1e-3)
assert npvs.hybrid == approx(2108687, 1e-3)

def test_hybrid_pv_only(hybrid_config):
technologies = hybrid_config["technologies"]
Expand Down
18 changes: 8 additions & 10 deletions tests/hopp/test_wind.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import math

import PySAM.Windpower as windpower
from floris import FlorisModel, TimeSeries

from hopp.simulation.technologies.wind.wind_plant import WindPlant, WindConfig
from tests.hopp.utils import create_default_site_info
Expand Down Expand Up @@ -43,7 +44,7 @@ def site():
35.75, 36, 36.25, 36.5, 36.75, 37, 37.25, 37.5, 37.75, 38, 38.25, 38.5, 38.75, 39, 39.25, 39.5, 39.75, 40)


def test_wind_powercurve():
def test_wind_powercurve_pysam():
model = windpower.default("WindpowerSingleowner")
model.Turbine.wind_turbine_rotor_diameter = 75

Expand All @@ -67,7 +68,7 @@ def test_wind_powercurve():
assert all([a == b for a, b in zip(powercurve_truth, powercurve_calc)])


def test_changing_n_turbines(site):
def test_changing_n_turbines_pysam(site):
# test with gridded layout
config = WindConfig.from_dict({'num_turbines': 10, "turbine_rating_kw": 2000})
model = WindPlant(site, config=config)
Expand All @@ -80,7 +81,7 @@ def test_changing_n_turbines(site):
# test with row layout


def test_changing_rotor_diam_recalc(site):
def test_changing_rotor_diam_recalc_pysam(site):
config = WindConfig.from_dict({'num_turbines': 10, "turbine_rating_kw": 2000})
model = WindPlant(site, config=config)
assert model.system_capacity_kw == 20000
Expand All @@ -91,7 +92,7 @@ def test_changing_rotor_diam_recalc(site):
assert model.turb_rating == 2000, "new rating different when rotor diameter is " + str(d)


def test_changing_turbine_rating(site):
def test_changing_turbine_rating_pysam(site):
# powercurve scaling
config = WindConfig.from_dict({'num_turbines': 24, "turbine_rating_kw": 2000})
model = WindPlant(site, config=config)
Expand All @@ -101,7 +102,7 @@ def test_changing_turbine_rating(site):
assert model.system_capacity_kw == model.turb_rating * n_turbs, "system size error when rating is " + str(n)


def test_changing_powercurve(site):
def test_changing_powercurve_pysam(site):
# with power curve recalculation requires diameter changes
config = WindConfig.from_dict({'num_turbines': 24, "turbine_rating_kw": 2000})
model = WindPlant(site, config=config)
Expand All @@ -114,7 +115,7 @@ def test_changing_powercurve(site):
assert model.system_capacity_kw == pytest.approx(model.turb_rating * n_turbs, 0.1), "size error when rating is " + str(n)


def test_changing_system_capacity(site):
def test_changing_system_capacity_pysam(site):
# adjust number of turbines, system capacity won't be exactly as requested
config = WindConfig.from_dict({'num_turbines': 20, "turbine_rating_kw": 1000})
model = WindPlant(site, config=config)
Expand Down Expand Up @@ -156,7 +157,6 @@ def test_changing_turbine_rating_floris(site):
for n in range(1000, 3000, 150):
model.turb_rating = n
assert model.system_capacity_kw == model.turb_rating * n_turbs, "system size error when rating is " + str(n)

def test_changing_system_capacity_floris(site):
floris_config_path = (
ROOT_DIR.parent / "tests" / "hopp" / "inputs" / "floris_config.yaml"
Expand All @@ -173,6 +173,4 @@ def test_changing_system_capacity_floris(site):
model = WindPlant(site, config=config)
for n in range(40000, 60000, 1000):
model.system_capacity_by_rating(n)
assert model.system_capacity_kw == pytest.approx(n)


assert model.system_capacity_kw == pytest.approx(n)

0 comments on commit d874402

Please sign in to comment.