|
| 1 | +import csv, os |
| 2 | +from pathlib import Path |
| 3 | +from typing import Union, Optional, List |
| 4 | +import pandas as pd |
| 5 | +import urllib.parse |
| 6 | + |
| 7 | +from attrs import define, field |
| 8 | + |
| 9 | +from hopp.utilities.keys import get_developer_nrel_gov_key, get_developer_nrel_gov_email |
| 10 | +from hopp.utilities.validators import range_val |
| 11 | +from hopp.simulation.technologies.resource.resource import Resource |
| 12 | +from hopp import ROOT_DIR |
| 13 | +from hopp.tools.resource.pysam_wind_tools import combine_wind_files |
| 14 | + |
| 15 | +AK_BASE_URL = "https://developer.nrel.gov/api/wind-toolkit/v2/wind/wtk-alaska-v1-0-0-download.csv?" |
| 16 | + |
| 17 | +@define |
| 18 | +class AlaskaWindData(Resource): |
| 19 | + #: latitude corresponding to location for wind resource data |
| 20 | + lat: float = field() |
| 21 | + #: longitude corresponding to location for wind resource data |
| 22 | + lon: float = field() |
| 23 | + #: year for resource data. must be between 2018 and 2020 |
| 24 | + year: int = field(validator=range_val(2018, 2020)) |
| 25 | + |
| 26 | + #: the hub-height for wind resource data (meters) |
| 27 | + hub_height_meters: float = field(validator=range_val(10.0, 1000.0)) |
| 28 | + |
| 29 | + #: filepath to resource_files directory. Defaults to ROOT_DIR/"simulation"/"resource_files". |
| 30 | + path_resource: Optional[Union[str, Path]] = field(default = ROOT_DIR / "simulation" / "resource_files") |
| 31 | + #: file path of resource file to load or download |
| 32 | + filename: Optional[Union[str, Path]] = field(default = None) |
| 33 | + #: Make an API call even if there's an existing file. Defaults to False. |
| 34 | + use_api: Optional[bool] = field(default = False) |
| 35 | + #: dictionary of preloaded and formatted wind resource data. Defaults to None. |
| 36 | + resource_data: Optional[dict] = field(default = None) |
| 37 | + |
| 38 | + #: dictionary of heights and filenames to download from Wind Toolkit |
| 39 | + file_resource_heights: dict = field(default = None) |
| 40 | + |
| 41 | + #: list of heights that wind resource data is available for downloading (meters) |
| 42 | + allowed_hub_height_meters: list[int] = [10, 20, 40, 60, 80, 100, 120, 140, 160, 180, 200, 250, 300, 500, 1000] |
| 43 | + |
| 44 | + |
| 45 | + def __attrs_post_init__(self): |
| 46 | + super().__init__(self.lat, self.lon, self.year) |
| 47 | + |
| 48 | + # if resource_data is input as a dictionary then set_data |
| 49 | + if isinstance(self.resource_data,dict): |
| 50 | + self.data = self.resource_data |
| 51 | + return |
| 52 | + |
| 53 | + # if resource_data is not provided, download or load resource data |
| 54 | + if isinstance(self.path_resource,str): |
| 55 | + self.path_resource = Path(self.path_resource).resolve() |
| 56 | + if self.path_resource.parts[-1]!="wind": |
| 57 | + self.path_resource = self.path_resource / 'wind' |
| 58 | + |
| 59 | + if self.filename is None: |
| 60 | + self.calculate_heights_to_download() |
| 61 | + |
| 62 | + self.check_download_dir() |
| 63 | + |
| 64 | + if not os.path.isfile(self.filename) or self.use_api: |
| 65 | + self.download_resource() |
| 66 | + |
| 67 | + self.format_data() |
| 68 | + |
| 69 | + def calculate_heights_to_download(self): |
| 70 | + """ |
| 71 | + Given the system hub height, and the available hubheights from WindToolkit, |
| 72 | + determine which heights to download to bracket the hub height |
| 73 | + """ |
| 74 | + hub_height_meters = self.hub_height_meters |
| 75 | + |
| 76 | + # evaluate hub height, determine what heights to download |
| 77 | + heights = [hub_height_meters] |
| 78 | + if hub_height_meters not in self.allowed_hub_height_meters: |
| 79 | + height_low = self.allowed_hub_height_meters[0] |
| 80 | + height_high = self.allowed_hub_height_meters[-1] |
| 81 | + for h in self.allowed_hub_height_meters: |
| 82 | + if h < hub_height_meters: |
| 83 | + height_low = h |
| 84 | + elif h > hub_height_meters: |
| 85 | + height_high = h |
| 86 | + break |
| 87 | + heights[0] = height_low |
| 88 | + heights.append(height_high) |
| 89 | + |
| 90 | + filename_base = f"{self.latitude}_{self.longitude}_WTK_Alaksa_{self.year}_{self.interval}min" |
| 91 | + file_resource_full = filename_base |
| 92 | + file_resource_heights = dict() |
| 93 | + |
| 94 | + for h in heights: |
| 95 | + h_int = int(h) |
| 96 | + file_resource_heights[h_int] = self.path_resource/(filename_base + f'_{h_int}m.csv') |
| 97 | + file_resource_full += f'_{h_int}m' |
| 98 | + file_resource_full += ".csv" |
| 99 | + |
| 100 | + self.file_resource_heights = file_resource_heights |
| 101 | + self.filename = self.path_resource / file_resource_full |
| 102 | + |
| 103 | + def update_height(self, hub_height_meters): |
| 104 | + """Update hub-height and corresponding attributes. |
| 105 | + Also updates ``file_resource_heights`` and ``filename``. |
| 106 | +
|
| 107 | + Args: |
| 108 | + hub_height_meters (float): hub-height for wind resource data (meters) |
| 109 | + """ |
| 110 | + self.hub_height_meters = hub_height_meters |
| 111 | + self.calculate_heights_to_download() |
| 112 | + |
| 113 | + def download_resource(self): |
| 114 | + success = False |
| 115 | + |
| 116 | + base_attributs = ["temperature","windspeed","winddirection"] |
| 117 | + attributes = ["pressure_100m"] |
| 118 | + for height, f in self.file_resource_heights.items(): |
| 119 | + attributes += [f"{a}_{height}m" for a in base_attributs] |
| 120 | + |
| 121 | + attributes_str = ",".join(k for k in attributes) |
| 122 | + input_data = { |
| 123 | + 'attributes': attributes_str, |
| 124 | + 'interval': self.interval, |
| 125 | + 'api_key': get_developer_nrel_gov_key(), |
| 126 | + 'email': get_developer_nrel_gov_email(), |
| 127 | + 'names': [str(self.year)], |
| 128 | + 'wkt': f"POINT({self.longitude} {self.latitude})" |
| 129 | + } |
| 130 | + url = AK_BASE_URL + urllib.parse.urlencode(input_data, True) |
| 131 | + success = self.call_api(url, filename=self.filename) |
| 132 | + |
| 133 | + if not success: |
| 134 | + raise ValueError('Unable to download wind data') |
| 135 | + |
| 136 | + return success |
| 137 | + |
| 138 | + def format_data(self): |
| 139 | + """ |
| 140 | + Format as 'wind_resource_data' dictionary for use in PySAM. |
| 141 | + """ |
| 142 | + if not os.path.isfile(self.filename): |
| 143 | + raise FileNotFoundError(f"{self.filename} does not exist. Try `download_resource` first.") |
| 144 | + |
| 145 | + self.data = self.filename |
| 146 | + |
| 147 | + @Resource.data.setter |
| 148 | + def data(self, data_info): |
| 149 | + """ |
| 150 | + Sets the wind resource data to a dictionary in SAM Wind format. |
| 151 | + """ |
| 152 | + if isinstance(data_info,dict): |
| 153 | + self._data = data_info |
| 154 | + if isinstance(data_info,(str, Path)): |
| 155 | + resource_heights = [k for k in self.file_resource_heights.keys()] |
| 156 | + self._data = combine_wind_files(str(data_info),resource_heights) |
0 commit comments