diff --git a/examples/CSP_PV_Battery_Analysis/simulation_init.py b/examples/CSP_PV_Battery_Analysis/simulation_init.py index ac993158f..613973861 100644 --- a/examples/CSP_PV_Battery_Analysis/simulation_init.py +++ b/examples/CSP_PV_Battery_Analysis/simulation_init.py @@ -82,8 +82,7 @@ def init_hybrid_plant(techs_in_sim: list, is_test: bool = False, ud_techs: dict "lon": -116.7830, "elev": 561, "tz": 1, - "no_wind": True - } + } solar_file = example_root + "02_weather_data/daggett_ca_34.865371_-116.783023_psmv3_60_tmy.csv" prices_file = example_root + "03_cost_load_price_data/constant_norm_prices.csv" desired_schedule_file = example_root + "03_cost_load_price_data/desired_schedule_normalized.csv" @@ -98,7 +97,8 @@ def init_hybrid_plant(techs_in_sim: list, is_test: bool = False, ud_techs: dict site = SiteInfo(site_data, solar_resource_file=solar_file, grid_resource_file=prices_file, - desired_schedule=desired_schedule + desired_schedule=desired_schedule, + wind=False ) # Load in system costs diff --git a/examples/offshore-hybrid/wind-h2.py b/examples/offshore-hybrid/wind-h2.py index 386a5cb98..b8f84de9a 100644 --- a/examples/offshore-hybrid/wind-h2.py +++ b/examples/offshore-hybrid/wind-h2.py @@ -160,14 +160,14 @@ def setup_hopp(plant_config, turbine_config, wind_resource, orbit_project, flori hopp_site_input_data["lat"] = plant_config["project_location"]["lat"] hopp_site_input_data["lon"] = plant_config["project_location"]["lon"] hopp_site_input_data["year"] = plant_config["wind_resource_year"] - hopp_site_input_data["no_solar"] = not plant_config["project_parameters"]["solar"] + solar = plant_config["project_parameters"]["solar"] # set desired schedule based on electrolyzer capacity desired_schedule = [plant_config["electrolyzer"]["rating"]]*8760 desired_schedule = [] # generate HOPP SiteInfo class instance - hopp_site = SiteInfo(hopp_site_input_data, hub_height=turbine_config["hub_height"], desired_schedule=desired_schedule) + hopp_site = SiteInfo(hopp_site_input_data, hub_height=turbine_config["hub_height"], desired_schedule=desired_schedule, solar=solar) # replace wind data with previously downloaded and adjusted wind data hopp_site.wind_resource = wind_resource diff --git a/hopp/eco/hopp_mgmt.py b/hopp/eco/hopp_mgmt.py index b94ab60af..97f301e25 100644 --- a/hopp/eco/hopp_mgmt.py +++ b/hopp/eco/hopp_mgmt.py @@ -21,8 +21,8 @@ def setup_hopp( hopp_site_input_data["lat"] = plant_config["project_location"]["lat"] hopp_site_input_data["lon"] = plant_config["project_location"]["lon"] hopp_site_input_data["year"] = plant_config["wind_resource_year"] - hopp_site_input_data["no_wind"] = not plant_config["project_parameters"]["wind"] - hopp_site_input_data["no_solar"] = not plant_config["project_parameters"]["solar"] + wind = plant_config["project_parameters"]["wind"] + solar = plant_config["project_parameters"]["solar"] # set desired schedule based on electrolyzer capacity desired_schedule = [plant_config["electrolyzer"]["rating"]] * 8760 @@ -33,6 +33,8 @@ def setup_hopp( hopp_site_input_data, hub_height=turbine_config["hub_height"], desired_schedule=desired_schedule, + wind=wind, + solar=solar ) # replace wind data with previously downloaded and adjusted wind data diff --git a/hopp/simulation/technologies/sites/site_info.py b/hopp/simulation/technologies/sites/site_info.py index 9cf501808..776545fd1 100644 --- a/hopp/simulation/technologies/sites/site_info.py +++ b/hopp/simulation/technologies/sites/site_info.py @@ -1,19 +1,31 @@ +from typing import Optional, Union +from pathlib import Path + +from attrs import define, field import matplotlib.pyplot as plt -from shapely.geometry import * -from shapely.geometry.base import * -from shapely.validation import make_valid -from fastkml import kml +import numpy as np +from numpy.typing import NDArray +from shapely.geometry import Polygon, MultiPolygon, Point +from shapely.geometry.base import BaseGeometry from shapely.ops import transform +from shapely.validation import make_valid +from fastkml import kml, KML import pyproj import utm -from hopp.simulation.technologies.resource.solar_resource import SolarResource -from hopp.simulation.technologies.resource.wind_resource import WindResource -from hopp.simulation.technologies.resource.elec_prices import ElectricityPrices +from hopp.simulation.technologies.resource import ( + SolarResource, + WindResource, + ElectricityPrices +) from hopp.simulation.technologies.layout.plot_tools import plot_shape from hopp.utilities.log import hybrid_logger as logger from hopp.utilities.keys import set_nrel_key_dot_env - +from hopp.type_dec import ( + hopp_array_converter as converter, NDArrayFloat, resource_file_converter, + hopp_float_type +) +from hopp.simulation.base import BaseClass def plot_site(verts, plt_style, labels): for i in range(len(verts)): @@ -25,140 +37,121 @@ def plot_site(verts, plt_style, labels): plt.grid() - -class SiteInfo: +@define +class SiteInfo(BaseClass): """ - Site specific information + Represents site-specific information needed by the hybrid simulation class and layout optimization. - Attributes - ---------- - data : dict - dictionary of initialization data - lat : float - site latitude [decimal degrees] - long : float - site longitude [decimal degrees] - vertices : np.array - site boundary vertices [m] - polygon : shapely.geometry.polygon - site polygon - valid_region : shapely.geometry.polygon - `tidy` site polygon - solar_resource : :class:`hybrid.resource.SolarResource` - class containing solar resource data - wind_resource : :class:`hybrid.resource.WindResource` - class containing wind resource data - elec_prices : :class:`hybrid.resource.ElectricityPrices` - Class containing electricity prices - n_timesteps : int - Number of timesteps in resource data - n_periods_per_day : int - Number of time periods per day - interval : int - Number of minutes per time interval - urdb_label : string - `Link Utility Rate DataBase `_ label for REopt runs - capacity_hours : list - Boolean list where ``True`` if the hour counts for capacity payments, ``False`` otherwise - desired_schedule : list - Absolute desired load profile [MWe] - follow_desired_schedule : boolean - ``True`` if a desired schedule was provided, ``False`` otherwise + Args: + data (dict): Dictionary containing site-specific information. + solar_resource_file (Union[Path, str], optional): Path to solar resource file. Defaults to "". + wind_resource_file (Union[Path, str], optional): Path to wind resource file. Defaults to "". + grid_resource_file (Union[Path, str], optional): Path to grid pricing data file. Defaults to "". + hub_height (float, optional): Turbine hub height for resource download in meters. Defaults to 97.0. + capacity_hours (:obj:`NDArray`, optional): Boolean list indicating hours for capacity payments. Defaults to []. + desired_schedule (:obj:`NDArray`, optional): Absolute desired load profile in MWe. Defaults to []. + solar (bool, optional): Whether to set solar data for this site. Defaults to True. + wind (bool, optional): Whether to set wind data for this site. Defaults to True. """ + # User provided + data: dict + solar_resource_file: Union[Path, str] = field(default="", converter=resource_file_converter) + wind_resource_file: Union[Path, str] = field(default="", converter=resource_file_converter) + grid_resource_file: Union[Path, str] = field(default="", converter=resource_file_converter) + hub_height: hopp_float_type = field(default=97., converter=hopp_float_type) + capacity_hours: NDArray = field(default=[], converter=converter(bool)) + desired_schedule: NDArrayFloat = field(default=[], converter=converter()) + solar: bool = field(default=True) + wind: bool = field(default=True) - def __init__(self, data, - solar_resource_file="", - wind_resource_file="", - grid_resource_file="", - hub_height=97, - capacity_hours=[], - desired_schedule=[]): - """ - Site specific information required by the hybrid simulation class and layout optimization. - - :param data: dict, containing the following keys: - - #. ``lat``: float, latitude [decimal degrees] - #. ``lon``: float, longitude [decimal degrees] - #. ``year``: int, year used to pull solar and/or wind resource data. If not provided, default is 2012 [-] - #. ``elev``: float (optional), elevation (metadata purposes only) [m] - #. ``tz``: int (optional), timezone code (metadata purposes only) [-] - #. ``no_solar``: bool (optional), if ``True`` solar data download for site is skipped, otherwise solar resource is downloaded from NSRDB - #. ``no_wind``: bool (optional), if ``True`` wind data download for site is skipped, otherwise wind resource is downloaded from wind-toolkit - #. ``site_boundaries``: dict (optional), with the following keys: - - * ``verts``: list of list [x,y], site boundary vertices [m] - * ``verts_simple``: list of list [x,y], simple site boundary vertices [m] - - #. ``kml_file``: string (optional), filepath to KML with "Boundary" and "Exclusion" Placemarks - #. ``urdb_label``: string (optional), `Link Utility Rate DataBase `_ label for REopt runs + # Set in post init hook + n_timesteps: int = field(init=False, default=None) + lat: hopp_float_type = field(init=False) + lon: hopp_float_type = field(init=False) + year: int = field(init=False, default=2012) + tz: Optional[int] = field(init=False, default=None) + solar_resource: Optional[SolarResource] = field(init=False, default=None) + wind_resource: Optional[WindResource] = field(init=False, default=None) + elec_prices: Optional[ElectricityPrices] = field(init=False, default=None) + n_periods_per_day: int = field(init=False) + interval: int = field(init=False) + follow_desired_schedule: bool = field(init=False) + polygon: Union[Polygon, BaseGeometry] = field(init=False) + vertices: NDArrayFloat = field(init=False) + kml_data: Optional[KML] = field(init=False, default=None) - .. TODO: Can we get rid of verts_simple and simplify site_boundaries + # .. TODO: Can we get rid of verts_simple and simplify site_boundaries - :param solar_resource_file: string, location (path) and filename of solar resource file (if not downloading from NSRDB) - :param wind_resource_file: string, location (path) and filename of wind resource file (if not downloading from wind-toolkit) - :param grid_resource_file: string, location (path) and filename of grid pricing data - :param hub_height: int (default = 97), turbine hub height for resource download [m] - :param capacity_hours: list of booleans, (8760 length) ``True`` if the hour counts for capacity payments, ``False`` otherwise - :param desired_schedule: list of floats, (8760 length) absolute desired load profile [MWe] + def __attrs_post_init__(self): + """ + The following are set in this post init hook: + lat (numpy.float64): Site latitude in decimal degrees. + lon (numpy.float64): Site longitude in decimal degrees. + tz (int, optional): Timezone code for metadata purposes only. Defaults to None. + vertices (:obj:`NDArray`): Site boundary vertices in meters. + polygon (:obj:`shapely.geometry.polygon.Polygon`): Site polygon. + valid_region (:obj:`shapely.geometry.polygon.Polygon`): Tidy site polygon. + solar_resource (:obj:`hopp.simulation.technologies.resource.SolarResource`): Class containing solar resource data. + wind_resource (:obj:`hopp.simulation.technologies.resource.WindResource`): Class containing wind resource data. + elec_prices (:obj:`hopp.simulation.technologies.resource.ElectricityPrices`): Class containing electricity prices. + n_timesteps (int): Number of timesteps in resource data. + n_periods_per_day (int): Number of time periods per day. + interval (int): Number of minutes per time interval. + urdb_label (str): Link to `Utility Rate DataBase `_ label for REopt runs. + follow_desired_schedule (bool): Indicates if a desired schedule was provided. Defaults to False. """ set_nrel_key_dot_env() - self.data = data + + data = self.data if 'site_boundaries' in data: self.vertices = np.array([np.array(v) for v in data['site_boundaries']['verts']]) - self.polygon: Polygon = Polygon(self.vertices) + self.polygon = Polygon(self.vertices) self.polygon = self.polygon.buffer(1e-8) if 'kml_file' in data: self.kml_data, self.polygon, data['lat'], data['lon'] = self.kml_read(data['kml_file']) self.polygon = self.polygon.buffer(1e-8) + if 'lat' not in data or 'lon' not in data: raise ValueError("SiteInfo requires lat and lon") self.lat = data['lat'] self.lon = data['lon'] - self.n_timesteps = None + if 'year' not in data: data['year'] = 2012 + if 'tz' in data: + self.tz = data['tz'] - if 'no_solar' not in data: - data['no_solar'] = False - - if not data['no_solar']: - self.solar_resource = SolarResource(data['lat'], data['lon'], data['year'], filepath=solar_resource_file) + if self.solar: + self.solar_resource = SolarResource(data['lat'], data['lon'], data['year'], filepath=self.solar_resource_file) self.n_timesteps = len(self.solar_resource.data['gh']) // 8760 * 8760 - if 'no_wind' not in data: - data['no_wind'] = False - - if not data['no_wind']: + if self.wind: # TODO: allow hub height to be used as an optimization variable - self.wind_resource = WindResource(data['lat'], data['lon'], data['year'], wind_turbine_hub_ht=hub_height, - filepath=wind_resource_file) + self.wind_resource = WindResource(data['lat'], data['lon'], data['year'], wind_turbine_hub_ht=self.hub_height, + filepath=self.wind_resource_file) n_timesteps = len(self.wind_resource.data['data']) // 8760 * 8760 if self.n_timesteps is None: self.n_timesteps = n_timesteps elif self.n_timesteps != n_timesteps: raise ValueError(f"Wind resource timesteps of {n_timesteps} different than other resource timesteps of {self.n_timesteps}") - self.elec_prices = ElectricityPrices(data['lat'], data['lon'], data['year'], filepath=grid_resource_file) + self.elec_prices = ElectricityPrices(data['lat'], data['lon'], data['year'], filepath=self.grid_resource_file) self.n_periods_per_day = self.n_timesteps // 365 # TODO: Does not handle leap years well self.interval = int((60*24)/self.n_periods_per_day) self.urdb_label = data['urdb_label'] if 'urdb_label' in data.keys() else None - if len(capacity_hours) == self.n_timesteps: - self.capacity_hours = capacity_hours - else: - self.capacity_hours = [False] * self.n_timesteps + if len(self.capacity_hours) != self.n_timesteps: + self.capacity_hours = np.array([False] * self.n_timesteps) # Desired load schedule for the system to dispatch against - self.desired_schedule = desired_schedule - self.follow_desired_schedule = len(desired_schedule) == self.n_timesteps - if len(desired_schedule) > 0 and len(desired_schedule) != self.n_timesteps: + self.follow_desired_schedule = len(self.desired_schedule) == self.n_timesteps + if len(self.desired_schedule) > 0 and len(self.desired_schedule) != self.n_timesteps: raise ValueError('The provided desired schedule does not match length of the simulation horizon.') - # FIXME: this a hack - if 'no_wind' in data and data["no_wind"]: + + if not self.wind: logger.info("Set up SiteInfo with solar resource files: {}".format(self.solar_resource.filename)) - elif 'no_solar' in data and data["no_solar"]: + elif not self.solar: logger.info("Set up SiteInfo with wind resource files: {}".format(self.wind_resource.filename)) else: logger.info( @@ -220,6 +213,9 @@ def plot(self, return figure, axes def kml_write(self, filepath, turb_coords=None, solar_region=None, wind_radius=200): + if self.kml_data is None: + raise AttributeError("No KML data to write.") + if turb_coords is not None: turb_coords = np.atleast_2d(turb_coords) for n, (x, y) in enumerate(turb_coords): diff --git a/hopp/to_organize/hopp_tools_steel.py b/hopp/to_organize/hopp_tools_steel.py index 04f80043f..6f15cf0d2 100644 --- a/hopp/to_organize/hopp_tools_steel.py +++ b/hopp/to_organize/hopp_tools_steel.py @@ -114,11 +114,10 @@ def set_site_info(hopp_dict, site_df, sample_site): lon = float(lon) sample_site['lat'] = lat sample_site['lon'] = lon - sample_site['no_solar'] = False # if solar_size_mw>0: - # sample_site['no_solar'] = False + # sample_site['solar'] = True # else: - # sample_site['no_solar'] = True + # sample_site['solar'] = False hopp_dict.add('Configuration', {'sample_site': sample_site}) diff --git a/hopp/tools/dispatch/csp_pv_battery_plot.py b/hopp/tools/dispatch/csp_pv_battery_plot.py index 4b03dcb5b..00551c471 100644 --- a/hopp/tools/dispatch/csp_pv_battery_plot.py +++ b/hopp/tools/dispatch/csp_pv_battery_plot.py @@ -65,8 +65,7 @@ def init_hybrid_plant(): "elev": 641, "year": 2012, "tz": -8, - "no_wind": True - } + } root = "C:/Users/WHamilt2/Documents/Projects/HOPP/CSP_PV_battery_dispatch_plots/" solar_file = root + "34.865371_-116.783023_psmv3_60_tmy.csv" @@ -84,7 +83,8 @@ def init_hybrid_plant(): site = SiteInfo(site_data, solar_resource_file=solar_file, grid_resource_file=prices_file, - desired_schedule=desired_schedule + desired_schedule=desired_schedule, + wind=False ) technologies = {'tower': { diff --git a/hopp/tools/hopp_tools.py b/hopp/tools/hopp_tools.py index 28606ff86..756f67add 100644 --- a/hopp/tools/hopp_tools.py +++ b/hopp/tools/hopp_tools.py @@ -39,7 +39,6 @@ def set_site_info(site_df, sample_site): lon = float(lon) sample_site['lat'] = lat sample_site['lon'] = lon - sample_site['no_solar'] = False return site_df, sample_site diff --git a/hopp/type_dec.py b/hopp/type_dec.py index 43f7f67f1..634599402 100644 --- a/hopp/type_dec.py +++ b/hopp/type_dec.py @@ -21,11 +21,11 @@ from hopp.utilities.log import hybrid_logger as logger - ### Define general data types used throughout hopp_path = Path(__file__).parent.parent hopp_float_type = np.float64 +hopp_int_type = np.int_ NDArrayFloat = npt.NDArray[hopp_float_type] NDArrayInt = npt.NDArray[np.int_] @@ -35,17 +35,42 @@ ### Custom callables for attrs objects and functions -def hopp_array_converter(data: Iterable) -> np.ndarray: - try: - a = np.array(data, dtype=hopp_float_type) - except TypeError as e: - raise TypeError(e.args[0] + f". Data given: {data}") - return a +def hopp_array_converter(dtype: Any = hopp_float_type) -> Callable: + """ + Returns a converter function for `attrs` fields to convert data into a numpy array of a specified dtype. + This function is primarily used to ensure that data provided to an `attrs` class is converted to the + appropriate numpy array type. + + Args: + dtype (Any, optional): The desired data type for the numpy array. Defaults to `hopp_float_type`. + + Returns: + Callable: A converter function that takes an iterable and returns it as a numpy array of the specified dtype. + + Raises: + TypeError: If the provided data cannot be converted to the desired numpy dtype. + + Examples: + >>> converter = hopp_array_converter() + >>> converter([1.0, 2.0, 3.0]) + array([1., 2., 3.]) + >>> converter = hopp_array_converter(dtype=np.int32) + >>> converter([1, 2, 3]) + array([1, 2, 3], dtype=int32) + """ + def converter(data: Iterable): + try: + a = np.array(data, dtype=dtype) + except TypeError as e: + raise TypeError(e.args[0] + f". Data given: {data}") + return a + + return converter -def resource_file_converter(resource_file: str) -> None: - # If the default value of an empty string is supplied, just pass through the default +def resource_file_converter(resource_file: str) -> Union[Path, str]: + # If the default value of an empty string is supplied, return empty path obj if resource_file == "": - return resource_file + return "" # Check the path relative to the hopp directory for the resource file and return if it exists resource_file_path = str(hopp_path / resource_file) diff --git a/tests/hopp/hopp_tools_test.py b/tests/hopp/hopp_tools_test.py index 41eb14ca4..fbf3e8bd0 100644 --- a/tests/hopp/hopp_tools_test.py +++ b/tests/hopp/hopp_tools_test.py @@ -35,7 +35,7 @@ def set_site_info(xl, turbine_model, site_location, sample_site): lon = float(lon) sample_site['lat'] = lat sample_site['lon'] = lon - sample_site['no_solar'] = True + sample_site['solar'] = False return site_df, sample_site diff --git a/tests/hopp/test_layout.py b/tests/hopp/test_layout.py index d799e836b..f8b994ec1 100644 --- a/tests/hopp/test_layout.py +++ b/tests/hopp/test_layout.py @@ -205,37 +205,6 @@ def test_hybrid_layout_solar_only(site): assert buffer_region[i] == pytest.approx(expected_buffer_region[i], 1e-3) -def test_kml_file_read(): - filepath = Path(__file__).absolute().parent / "layout_example.kml" - site_data = {'kml_file': filepath} - solar_resource_file = Path(__file__).absolute().parent.parent.parent / "resource_files" / "solar" / "35.2018863_-101.945027_psmv3_60_2012.csv" - wind_resource_file = Path(__file__).absolute().parent.parent.parent / "resource_files" / "wind" / "35.2018863_-101.945027_windtoolkit_2012_60min_80m_100m.srw" - site = SiteInfo(site_data, solar_resource_file=solar_resource_file, wind_resource_file=wind_resource_file) - site.plot() - assert np.array_equal(np.round(site.polygon.bounds), [ 681175., 4944970., 686386., 4949064.]) - assert site.polygon.area * 3.86102e-7 == pytest.approx(2.3393, abs=0.01) # m2 to mi2 - - -def test_kml_file_append(): - filepath = Path(__file__).absolute().parent / "layout_example.kml" - site_data = {'kml_file': filepath} - solar_resource_file = Path(__file__).absolute().parent.parent.parent / "resource_files" / "solar" / "35.2018863_-101.945027_psmv3_60_2012.csv" - wind_resource_file = Path(__file__).absolute().parent.parent.parent / "resource_files" / "wind" / "35.2018863_-101.945027_windtoolkit_2012_60min_80m_100m.srw" - site = SiteInfo(site_data, solar_resource_file=solar_resource_file, wind_resource_file=wind_resource_file) - - x = site.polygon.centroid.x - y = site.polygon.centroid.y - turb_coords = [x - 500, y - 500] - solar_region = Polygon(((x, y), (x, y + 5000), (x + 5000, y), (x + 5000, y + 5000))) - - filepath_new = Path(__file__).absolute().parent / "layout_example2.kml" - site.kml_write(filepath_new, turb_coords, solar_region) - assert filepath_new.exists() - k, valid_region, lat, lon = SiteInfo.kml_read(filepath) - assert valid_region.area > 0 - os.remove(filepath_new) - - def test_system_electrical_sizing(site): target_solar_kw = 1e5 target_dc_ac_ratio = 1.34 diff --git a/tests/hopp/test_site_info.py b/tests/hopp/test_site_info.py new file mode 100644 index 000000000..7f1fdff31 --- /dev/null +++ b/tests/hopp/test_site_info.py @@ -0,0 +1,159 @@ +import os +import copy +from pathlib import Path + +import pytest +from pytest import fixture +from shapely.geometry import Polygon +import numpy as np +from numpy.testing import assert_array_equal + +from hopp.simulation.technologies.sites import SiteInfo, flatirons_site +from hopp import ROOT_DIR + +solar_resource_file = os.path.join( + ROOT_DIR.parent, "resource_files", "solar", + "35.2018863_-101.945027_psmv3_60_2012.csv" +) +wind_resource_file = os.path.join( + ROOT_DIR.parent, "resource_files", "wind", + "35.2018863_-101.945027_windtoolkit_2012_60min_80m_100m.srw" +) +grid_resource_file = os.path.join( + ROOT_DIR.parent, "resource_files", "grid", + "pricing-data-2015-IronMtn-002_factors.csv" +) +kml_filepath = Path(__file__).absolute().parent / "layout_example.kml" + + +@fixture +def site(): + return SiteInfo( + flatirons_site, + solar_resource_file=solar_resource_file, + wind_resource_file=wind_resource_file, + grid_resource_file=grid_resource_file + ) + + +def test_site_init(site): + """Site should initialize properly.""" + assert site is not None + + # data + assert site.lat == flatirons_site["lat"] + assert site.lon == flatirons_site["lon"] + assert site.year == flatirons_site["year"] + assert site.tz == flatirons_site["tz"] + assert site.urdb_label == flatirons_site["urdb_label"] + + # resources + assert site.solar_resource is not None + assert site.wind_resource is not None + assert site.elec_prices is not None + + # time periods + assert site.n_timesteps == 8760 + assert site.n_periods_per_day == 24 + assert site.interval == 60 + assert_array_equal(site.capacity_hours, [False] * site.n_timesteps) + assert_array_equal(site.desired_schedule, []) + + # polygon + assert site.polygon is not None + assert site.vertices is not None + + # unset + assert site.kml_data is None + + +def test_site_init_kml_read(): + """Should initialize via kml file.""" + site = SiteInfo({"kml_file": kml_filepath}, solar_resource_file=solar_resource_file, wind_resource_file=wind_resource_file) + + assert site.kml_data is not None + assert site.polygon is not None + + +def test_site_init_missing_coords(): + """Should fail if lat/lon missing.""" + data = copy.deepcopy(flatirons_site) + del data["lat"] + del data["lon"] + + with pytest.raises(ValueError): + SiteInfo(data) + + data["lat"] = flatirons_site["lat"] + + # should still fail because lon is missing + with pytest.raises(ValueError): + SiteInfo(data) + + +def test_site_init_improper_schedule(): + """Should fail if the desired schedule mismatches the number of timesteps.""" + data = copy.deepcopy(flatirons_site) + + with pytest.raises(ValueError): + SiteInfo( + data, + solar_resource_file=solar_resource_file, + wind_resource_file=wind_resource_file, + grid_resource_file=grid_resource_file, + desired_schedule=np.array([1]) + ) + + +def test_site_init_no_wind(): + """Should initialize without pulling wind data.""" + data = copy.deepcopy(flatirons_site) + + site = SiteInfo( + data, + solar_resource_file=solar_resource_file, + wind_resource_file=wind_resource_file, + grid_resource_file=grid_resource_file, + wind=False + ) + + assert site.wind_resource is None + + +def test_site_init_no_solar(): + """Should initialize without pulling wind data.""" + data = copy.deepcopy(flatirons_site) + + site = SiteInfo( + data, + solar_resource_file=solar_resource_file, + wind_resource_file=wind_resource_file, + grid_resource_file=grid_resource_file, + solar=False + ) + + assert site.solar_resource is None + + +def test_site_kml_file_read(): + site_data = {'kml_file': kml_filepath} + site = SiteInfo(site_data, solar_resource_file=solar_resource_file, wind_resource_file=wind_resource_file) + assert np.array_equal(np.round(site.polygon.bounds), [ 681175., 4944970., 686386., 4949064.]) + assert site.polygon.area * 3.86102e-7 == pytest.approx(2.3393, abs=0.01) # m2 to mi2 + + +def test_site_kml_file_append(): + site_data = {'kml_file': kml_filepath} + site = SiteInfo(site_data, solar_resource_file=solar_resource_file, wind_resource_file=wind_resource_file) + + x = site.polygon.centroid.x + y = site.polygon.centroid.y + turb_coords = [x - 500, y - 500] + solar_region = Polygon(((x, y), (x, y + 5000), (x + 5000, y), (x + 5000, y + 5000))) + + filepath_new = Path(__file__).absolute().parent / "layout_example2.kml" + site.kml_write(filepath_new, turb_coords, solar_region) + assert filepath_new.exists() + k, valid_region, lat, lon = SiteInfo.kml_read(kml_filepath) + assert valid_region.area > 0 + os.remove(filepath_new) \ No newline at end of file