From cc48217d903be6750dba333931d404058c8e5fc7 Mon Sep 17 00:00:00 2001 From: Chris Date: Mon, 28 Sep 2020 23:32:26 -0400 Subject: [PATCH] feat(weather): Update AMY components to parse new NOAA format This commit updates the AMY component to parse the new NOAA files from https://www.ncei.noaa.gov/access/search/data-search/global-hourly --- .../json/DF_Import_NOAA_File.json | 10 +- .../json/DF_Import_NOAA_Staion_Location.json | 8 +- .../src/DF Import NOAA File.py | 160 ++++++++---------- .../src/DF Import NOAA Staion Location.py | 58 +++---- .../user_objects/DF Import NOAA File.ghuser | Bin 7575 -> 7518 bytes .../DF Import NOAA Staion Location.ghuser | Bin 4426 -> 4311 bytes 6 files changed, 105 insertions(+), 131 deletions(-) diff --git a/dragonfly_grasshopper/json/DF_Import_NOAA_File.json b/dragonfly_grasshopper/json/DF_Import_NOAA_File.json index e90efb3..6fcfbaa 100644 --- a/dragonfly_grasshopper/json/DF_Import_NOAA_File.json +++ b/dragonfly_grasshopper/json/DF_Import_NOAA_File.json @@ -1,5 +1,5 @@ { - "version": "0.1.1", + "version": "0.2.0", "nickname": "ImportNOAA", "outputs": [ [ @@ -72,14 +72,14 @@ { "access": "item", "name": "_noaa_file", - "description": "The path to a .txt file of annual data obtained from the NOAA\ndatabase on your system as a string.", + "description": "The path to a .csv file of annual data obtained from the NOAA\ndatabase on your system as a string.", "type": "string", "default": null }, { "access": "item", "name": "_timestep_", - "description": "The timestep at which the data collections should be output.\nDefault is 1 but this can be set as high as 60 to ensure that all data\nfrom the .txt file is imported.", + "description": "The timestep at which the data collections should be output.\nDefault is 1 but this can be set as high as 60 to ensure that all data\nfrom the .csv file is imported.", "type": "int", "default": null }, @@ -92,8 +92,8 @@ } ], "subcategory": "4 :: AlternativeWeather", - "code": "\nimport os\nimport csv\n\ntry:\n from ladybug.dt import DateTime\n from ladybug.analysisperiod import AnalysisPeriod\n from ladybug.header import Header\n from ladybug.datacollection import HourlyDiscontinuousCollection, HourlyContinuousCollection\n from ladybug.datatype.temperature import DryBulbTemperature, DewPointTemperature\n from ladybug.datatype.speed import WindSpeed\n from ladybug.datatype.angle import WindDirection\n from ladybug.datatype.fraction import TotalSkyCover\n from ladybug.datatype.pressure import AtmosphericStationPressure\n from ladybug.datatype.distance import Visibility, CeilingHeight\n from ladybug.datatype.generic import GenericType\nexcept ImportError as e:\n raise ImportError('\\nFailed to import ladybug:\\n\\t{}'.format(e))\n\ntry:\n from ladybug_{{cad}}.{{plugin}} import all_required_inputs\nexcept ImportError as e:\n raise ImportError('\\nFailed to import ladybug_{{cad}}:\\n\\t{}'.format(e))\n\n\n# dictionary that converts from sky cover codes to tenths of sky cover.\nsky_codes_dict = {\n 'CLR': 0,\n 'SCT': 3,\n 'BKN': 7,\n 'OVC': 10,\n 'OBS': 10,\n 'POB': 5\n }\n\n\ndef build_collection(values, dates, data_type, unit):\n \"\"\"Build a data collection from raw noaa data and process it to the timestep.\"\"\"\n \n if values == []:\n return None\n \n # convert date codes into datetimes.\n datetimes = [DateTime(int(dat[4:6]), int(dat[6:8]), int(dat[8:10]),\n int(dat[10:12])) for dat in dates]\n \n # make a discontinuous cata collection\n data_header = Header(data_type, unit, AnalysisPeriod())\n data_init = HourlyDiscontinuousCollection(data_header, values, datetimes)\n data_final = data_init.validate_analysis_period()\n \n # cull out unwanted timesteps.\n if _timestep_:\n data_final.convert_to_culled_timestep(_timestep_)\n else:\n data_final.convert_to_culled_timestep(1)\n \n return data_final\n\n\nif all_required_inputs(ghenv.Component) and _run:\n # check that the file exists.\n assert os.path.isfile(_noaa_file), 'Cannot find file at {}.'.format(_noaa_file)\n \n # empty lists to be filled with data\n all_years = []\n header_txt = []\n db_t = []\n db_t_dates = []\n dp_t = []\n dp_t_dates = []\n ws = []\n ws_dates = []\n wd = []\n wd_dates = []\n sc = []\n sc_dates = []\n ap = []\n ap_dates = []\n slp = []\n slp_dates = []\n vis = []\n vis_dates = []\n ceil = []\n ceil_dates = []\n \n # pull relevant data out of the file\n with open(_noaa_file) as csv_file:\n csv_reader = csv.reader(csv_file, delimiter=' ', skipinitialspace=True)\n next(csv_reader) # Skip header row.\n for row in csv_reader:\n all_years.append(int(row[2][:4]))\n if row[3] != '***':\n wd.append(float(row[3]))\n wd_dates.append(row[2])\n if row[4] != '***':\n ws.append(float(row[4]))\n ws_dates.append(row[2])\n if row[6] != '***':\n ceil.append(float(row[6]))\n ceil_dates.append(row[2])\n if row[7] != '***':\n sc.append(row[7])\n sc_dates.append(row[2])\n if row[11] != '****':\n vis.append(float(row[11]))\n vis_dates.append(row[2])\n if row[21] != '****':\n db_t.append(float(row[21]))\n db_t_dates.append(row[2])\n if row[22] != '****':\n dp_t.append(float(row[22]))\n dp_t_dates.append(row[2])\n if row[25] != '******':\n ap.append(float(row[25]))\n ap_dates.append(row[2])\n if row[23] != '******':\n slp.append(float(row[23]))\n slp_dates.append(row[2])\n \n # check that all years in the file are the same.\n yr1 = all_years[0]\n for yr in all_years:\n assert yr == yr1, 'Not all of the data in the file is from the same ' \\\n 'year. {} != {}'.format(yr1, yr)\n data_header = Header(GenericType('Years', 'yr'), 'yr', AnalysisPeriod())\n model_year = HourlyContinuousCollection(data_header, [yr1] * 8760)\n \n # perform conversions\n sc = [sky_codes_dict[cov] for cov in sc] # sky cover codes to values\n ceil = [c * 100 for c in ceil] # hundreds of feet to feet\n \n # build data collections from the imported values\n dry_bulb_temp = build_collection(db_t, db_t_dates, DryBulbTemperature(), 'F')\n dew_point_temp = build_collection(dp_t, dp_t_dates, DewPointTemperature(), 'F')\n wind_speed = build_collection(ws, ws_dates, WindSpeed(), 'mph')\n wind_direction = build_collection(wd, wd_dates, WindDirection(), 'degrees')\n total_sky_cover = build_collection(sc, sc_dates, TotalSkyCover(), 'tenths')\n visibility = build_collection(vis, vis_dates, Visibility(), 'mi')\n ceiling_height = build_collection(ceil, ceil_dates, CeilingHeight(), 'ft')\n \n # deal with available atmospheric pressure data\n if ap != []:\n ap = [press / 1000 for press in ap] # pressure mbar to bar\n atmos_pressure = build_collection(ap, ap_dates, AtmosphericStationPressure(), 'bar')\n else:\n slp = [press / 1000 for press in slp] # pressure mbar to bar\n atmos_pressure = build_collection(slp, slp_dates, AtmosphericStationPressure(), 'bar')\n \n # convert all units to SI.\n if dry_bulb_temp is not None:\n dry_bulb_temp.convert_to_unit('C')\n if dew_point_temp is not None:\n dew_point_temp.convert_to_unit('C')\n if wind_speed is not None:\n wind_speed.convert_to_unit('m/s')\n if visibility is not None:\n visibility.convert_to_unit('km')\n if ceiling_height is not None:\n ceiling_height.convert_to_unit('m')\n if atmos_pressure is not None:\n atmos_pressure.convert_to_unit('Pa')", + "code": "\nimport os\nimport csv\n\ntry:\n from ladybug.dt import DateTime\n from ladybug.analysisperiod import AnalysisPeriod\n from ladybug.header import Header\n from ladybug.datacollection import HourlyDiscontinuousCollection, HourlyContinuousCollection\n from ladybug.datatype.temperature import DryBulbTemperature, DewPointTemperature\n from ladybug.datatype.speed import WindSpeed\n from ladybug.datatype.angle import WindDirection\n from ladybug.datatype.fraction import TotalSkyCover\n from ladybug.datatype.pressure import AtmosphericStationPressure\n from ladybug.datatype.distance import Visibility, CeilingHeight\n from ladybug.datatype.generic import GenericType\nexcept ImportError as e:\n raise ImportError('\\nFailed to import ladybug:\\n\\t{}'.format(e))\n\ntry:\n from ladybug_{{cad}}.{{plugin}} import all_required_inputs\nexcept ImportError as e:\n raise ImportError('\\nFailed to import ladybug_{{cad}}:\\n\\t{}'.format(e))\n\n\ndef build_collection(values, dates, data_type, unit):\n \"\"\"Build a data collection from raw noaa data and process it to the timestep.\"\"\"\n if values == []:\n return None\n \n # convert date codes into datetimes.\n datetimes = [DateTime(int(dat[5:7]), int(dat[8:10]), int(dat[11:13]),\n int(dat[14:16])) for dat in dates]\n \n # make a discontinuous cata collection\n data_header = Header(data_type, unit, AnalysisPeriod())\n data_init = HourlyDiscontinuousCollection(data_header, values, datetimes)\n data_final = data_init.validate_analysis_period()\n \n # cull out unwanted timesteps.\n if _timestep_:\n data_final.convert_to_culled_timestep(_timestep_)\n else:\n data_final.convert_to_culled_timestep(1)\n \n return data_final\n\n\nif all_required_inputs(ghenv.Component) and _run:\n # check that the file exists.\n assert os.path.isfile(_noaa_file), 'Cannot find file at {}.'.format(_noaa_file)\n\n # empty lists to be filled with data\n all_years = []\n header_txt = []\n db_t = []\n db_t_dates = []\n dp_t = []\n dp_t_dates = []\n ws = []\n ws_dates = []\n wd = []\n wd_dates = []\n sc = []\n sc_dates = []\n slp = []\n slp_dates = []\n vis = []\n vis_dates = []\n ceil = []\n ceil_dates = []\n\n # pull relevant data out of the file\n with open(_noaa_file) as csv_file:\n csv_reader = csv.reader(csv_file, delimiter=',', skipinitialspace=True)\n\n # find the column with total sky cover if it exists\n header = csv_reader.next() # get header row\n sc_col = None\n for i, colname in enumerate(header):\n if colname == 'GF1':\n sc_col = i\n\n for row in csv_reader:\n # parse the dates and the years\n date_row = row[1]\n all_years.append(int(date_row[:4]))\n\n # parse the wind information\n wind_info = row[10].split(',')\n if wind_info[0] != '999':\n wd.append(float(wind_info[0]))\n wd_dates.append(date_row)\n if wind_info[3] != '9999':\n ws.append(float(wind_info[3]) / 10)\n ws_dates.append(date_row)\n\n # parse the ceiling height information\n ceil_info = row[11].split(',')\n if ceil_info[0] != '99999':\n ceil.append(float(ceil_info[0]))\n ceil_dates.append(date_row)\n\n # parse the visibility information\n vis_info = row[12].split(',')\n if vis_info[0] != '999999':\n vis.append(float(vis_info[0]) / 1000)\n vis_dates.append(date_row)\n\n # parse the dry bulb and dew point information\n temp_info = row[13].split(',')\n if temp_info[0] != '+9999':\n db_t.append(float(temp_info[0]) / 10)\n db_t_dates.append(date_row)\n dwpt_info = row[14].split(',')\n if dwpt_info[0] != '+9999':\n dp_t.append(float(dwpt_info[0]) / 10)\n dp_t_dates.append(date_row)\n\n # parse the pressure information\n slp_info = row[15].split(',')\n if slp_info[0] != '99999':\n slp.append(float(slp_info[0]) * 10)\n slp_dates.append(date_row)\n\n # parse the sky cover info if it exists\n if sc_col is not None and row[sc_col] != '':\n sc_info = row[sc_col].split(',')\n sc_oktas = int(sc_info[0])\n sc_tenths = sc_oktas * (10 / 8) if sc_oktas != 9 else 10\n sc.append(sc_tenths)\n sc_dates.append(date_row)\n\n # check that all years in the file are the same.\n yr1 = all_years[0]\n for yr in all_years:\n assert yr == yr1, 'Not all of the data in the file is from the same ' \\\n 'year. {} != {}'.format(yr1, yr)\n data_header = Header(GenericType('Years', 'yr'), 'yr', AnalysisPeriod())\n model_year = HourlyContinuousCollection(data_header, [yr1] * 8760)\n\n # build data collections from the imported values\n dry_bulb_temp = build_collection(db_t, db_t_dates, DryBulbTemperature(), 'C')\n dew_point_temp = build_collection(dp_t, dp_t_dates, DewPointTemperature(), 'C')\n wind_speed = build_collection(ws, ws_dates, WindSpeed(), 'm/s')\n wind_direction = build_collection(wd, wd_dates, WindDirection(), 'degrees')\n ceiling_height = build_collection(ceil, ceil_dates, CeilingHeight(), 'm')\n visibility = build_collection(vis, vis_dates, Visibility(), 'km')\n atmos_pressure = build_collection(slp, slp_dates, AtmosphericStationPressure(), 'bar')\n total_sky_cover = build_collection(sc, sc_dates, TotalSkyCover(), 'tenths')\n", "category": "Dragonfly", "name": "DF Import NOAA File", - "description": "Import climate data from a .txt file of annual data obtained from the National\nOceanic and Atmospheric Administration (NOAA) database. The database can be\naccessed here:\nhttps://gis.ncdc.noaa.gov/maps/ncei/cdo/hourly\n-" + "description": "Import climate data from a .csv file of annual data obtained from the National\nOceanic and Atmospheric Administration (NOAA) database. The database can be\naccessed here:\nhttps://gis.ncdc.noaa.gov/maps/ncei/cdo/hourly\n-" } \ No newline at end of file diff --git a/dragonfly_grasshopper/json/DF_Import_NOAA_Staion_Location.json b/dragonfly_grasshopper/json/DF_Import_NOAA_Staion_Location.json index 9b9e94d..2b92936 100644 --- a/dragonfly_grasshopper/json/DF_Import_NOAA_Staion_Location.json +++ b/dragonfly_grasshopper/json/DF_Import_NOAA_Staion_Location.json @@ -1,5 +1,5 @@ { - "version": "0.1.1", + "version": "0.2.0", "nickname": "ImportStaion", "outputs": [ [ @@ -16,7 +16,7 @@ { "access": "item", "name": "_station_file", - "description": "The path to a .txt file of NOAA station location data\non your system as a string.", + "description": "The path to a .csv file of NOAA station location data\non your system as a string.", "type": "string", "default": null }, @@ -29,8 +29,8 @@ } ], "subcategory": "4 :: AlternativeWeather", - "code": "\nimport os\n\ntry:\n from ladybug.datatype.distance import Distance\n from ladybug.location import Location\nexcept ImportError as e:\n raise ImportError('\\nFailed to import ladybug:\\n\\t{}'.format(e))\n\ntry:\n from ladybug_{{cad}}.{{plugin}} import all_required_inputs\nexcept ImportError as e:\n raise ImportError('\\nFailed to import ladybug_{{cad}}:\\n\\t{}'.format(e))\n\n\nif all_required_inputs(ghenv.Component):\n # check that the file exists.\n assert os.path.isfile(_station_file), 'Cannot find file at {}.'.format(_station_file)\n \n with open(_station_file) as station_file:\n station_file.readline() # Skip header row\n \n # get the pattern of data within the file\n char_pattern = station_file.readline().strip().split(' ')\n data_line = station_file.readline()\n data_list = []\n total_char = 0\n for pattern in char_pattern:\n data_list.append(data_line[total_char:total_char + len(pattern)])\n total_char += len(pattern) + 1\n \n # parse all of the info from the file\n station_id, wban_id = data_list[0].split(' ')\n station_name = data_list[1].strip()\n country = data_list[2].strip()\n state = data_list[3].strip()\n latitude = float(data_list[4])\n longitude = float(data_list[5])\n elevation = float(data_list[6])\n \n elevation = Distance().to_unit([elevation], 'm', 'ft')[0] # convert to meters\n \n # estimate or parse time zone.\n if time_zone_:\n assert -12 <= time_zone_ <= 12, ' time_zone_ must be between -12 and '\\\n ' 12. Got {}.'.format(time_zone_)\n time_zone = time_zone_\n else:\n time_zone = int((longitude / 180) * 12)\n \n # build the location object\n location = Location(city=station_name,\n state=state,\n country=country,\n latitude=latitude,\n longitude=longitude,\n time_zone=time_zone,\n elevation=elevation,\n station_id=station_id,\n source='NCDC')", + "code": "\nimport os\n\ntry:\n from ladybug.datatype.distance import Distance\n from ladybug.location import Location\nexcept ImportError as e:\n raise ImportError('\\nFailed to import ladybug:\\n\\t{}'.format(e))\n\ntry:\n from ladybug_{{cad}}.{{plugin}} import all_required_inputs\nexcept ImportError as e:\n raise ImportError('\\nFailed to import ladybug_{{cad}}:\\n\\t{}'.format(e))\n\n\nif all_required_inputs(ghenv.Component):\n # check that the file exists.\n assert os.path.isfile(_station_file), 'Cannot find file at {}.'.format(_station_file)\n \n with open(_station_file) as station_file:\n station_file.readline() # Skip header row\n\n # get the pattern of data within the file\n dat_line = station_file.readline().strip().split(',')\n\n # parse all of the info from the file\n station_id = dat_line[0].replace('\"', '')\n city = dat_line[6].replace('\"', '')\n latitude = float(dat_line[3].replace('\"', ''))\n longitude = float(dat_line[4].replace('\"', ''))\n elevation = float(dat_line[5].replace('\"', ''))\n elevation = Distance().to_unit([elevation], 'm', 'ft')[0] # convert to meters\n\n # estimate or parse time zone.\n if time_zone_:\n assert -12 <= time_zone_ <= 14, ' time_zone_ must be between -12 and '\\\n ' 14. Got {}.'.format(time_zone_)\n time_zone = time_zone_\n else:\n time_zone = int((longitude / 180) * 12)\n\n # build the location object\n location = Location(\n city=city, latitude=latitude, longitude=longitude,\n time_zone=time_zone, elevation=elevation,\n station_id=station_id, source='NCDC')\n", "category": "Dragonfly", "name": "DF Import NOAA Staion Location", - "description": "Import station location from a .txt file of station information obtained from the\nNational Oceanic and Atmospheric Administration (NOAA) database. The database can\nbe accessed here:\nhttps://gis.ncdc.noaa.gov/maps/ncei/cdo/hourly\n-" + "description": "Import station location from a .csv file of station information obtained from the\nNational Oceanic and Atmospheric Administration (NOAA) database. The database can\nbe accessed here:\nhttps://gis.ncdc.noaa.gov/maps/ncei/cdo/hourly\n-" } \ No newline at end of file diff --git a/dragonfly_grasshopper/src/DF Import NOAA File.py b/dragonfly_grasshopper/src/DF Import NOAA File.py index d4ea60b..923227c 100644 --- a/dragonfly_grasshopper/src/DF Import NOAA File.py +++ b/dragonfly_grasshopper/src/DF Import NOAA File.py @@ -8,20 +8,20 @@ # @license GPL-3.0+ """ -Import climate data from a .txt file of annual data obtained from the National +Import climate data from a .csv file of annual data obtained from the National Oceanic and Atmospheric Administration (NOAA) database. The database can be accessed here: https://gis.ncdc.noaa.gov/maps/ncei/cdo/hourly - Args: - _noaa_file: The path to a .txt file of annual data obtained from the NOAA + _noaa_file: The path to a .csv file of annual data obtained from the NOAA database on your system as a string. _timestep_: The timestep at which the data collections should be output. Default is 1 but this can be set as high as 60 to ensure that all data - from the .txt file is imported. + from the .csv file is imported. _run: Set to True to run the component and import the data. - + Returns: dry_bulb_temp: The houlry dry bulb temperature, in C. Note that this is a full numeric field (i.e. 23.6) and not an integer @@ -58,7 +58,7 @@ ghenv.Component.Name = "DF Import NOAA File" ghenv.Component.NickName = 'ImportNOAA' -ghenv.Component.Message = '0.1.1' +ghenv.Component.Message = '0.2.0' ghenv.Component.Category = "Dragonfly" ghenv.Component.SubCategory = '4 :: AlternativeWeather' ghenv.Component.AdditionalHelpFromDocStrings = "3" @@ -87,26 +87,14 @@ raise ImportError('\nFailed to import ladybug_rhino:\n\t{}'.format(e)) -# dictionary that converts from sky cover codes to tenths of sky cover. -sky_codes_dict = { - 'CLR': 0, - 'SCT': 3, - 'BKN': 7, - 'OVC': 10, - 'OBS': 10, - 'POB': 5 - } - - def build_collection(values, dates, data_type, unit): """Build a data collection from raw noaa data and process it to the timestep.""" - if values == []: return None # convert date codes into datetimes. - datetimes = [DateTime(int(dat[4:6]), int(dat[6:8]), int(dat[8:10]), - int(dat[10:12])) for dat in dates] + datetimes = [DateTime(int(dat[5:7]), int(dat[8:10]), int(dat[11:13]), + int(dat[14:16])) for dat in dates] # make a discontinuous cata collection data_header = Header(data_type, unit, AnalysisPeriod()) @@ -125,7 +113,7 @@ def build_collection(values, dates, data_type, unit): if all_required_inputs(ghenv.Component) and _run: # check that the file exists. assert os.path.isfile(_noaa_file), 'Cannot find file at {}.'.format(_noaa_file) - + # empty lists to be filled with data all_years = [] header_txt = [] @@ -139,49 +127,74 @@ def build_collection(values, dates, data_type, unit): wd_dates = [] sc = [] sc_dates = [] - ap = [] - ap_dates = [] slp = [] slp_dates = [] vis = [] vis_dates = [] ceil = [] ceil_dates = [] - + # pull relevant data out of the file with open(_noaa_file) as csv_file: - csv_reader = csv.reader(csv_file, delimiter=' ', skipinitialspace=True) - next(csv_reader) # Skip header row. + csv_reader = csv.reader(csv_file, delimiter=',', skipinitialspace=True) + + # find the column with total sky cover if it exists + header = csv_reader.next() # get header row + sc_col = None + for i, colname in enumerate(header): + if colname == 'GF1': + sc_col = i + for row in csv_reader: - all_years.append(int(row[2][:4])) - if row[3] != '***': - wd.append(float(row[3])) - wd_dates.append(row[2]) - if row[4] != '***': - ws.append(float(row[4])) - ws_dates.append(row[2]) - if row[6] != '***': - ceil.append(float(row[6])) - ceil_dates.append(row[2]) - if row[7] != '***': - sc.append(row[7]) - sc_dates.append(row[2]) - if row[11] != '****': - vis.append(float(row[11])) - vis_dates.append(row[2]) - if row[21] != '****': - db_t.append(float(row[21])) - db_t_dates.append(row[2]) - if row[22] != '****': - dp_t.append(float(row[22])) - dp_t_dates.append(row[2]) - if row[25] != '******': - ap.append(float(row[25])) - ap_dates.append(row[2]) - if row[23] != '******': - slp.append(float(row[23])) - slp_dates.append(row[2]) - + # parse the dates and the years + date_row = row[1] + all_years.append(int(date_row[:4])) + + # parse the wind information + wind_info = row[10].split(',') + if wind_info[0] != '999': + wd.append(float(wind_info[0])) + wd_dates.append(date_row) + if wind_info[3] != '9999': + ws.append(float(wind_info[3]) / 10) + ws_dates.append(date_row) + + # parse the ceiling height information + ceil_info = row[11].split(',') + if ceil_info[0] != '99999': + ceil.append(float(ceil_info[0])) + ceil_dates.append(date_row) + + # parse the visibility information + vis_info = row[12].split(',') + if vis_info[0] != '999999': + vis.append(float(vis_info[0]) / 1000) + vis_dates.append(date_row) + + # parse the dry bulb and dew point information + temp_info = row[13].split(',') + if temp_info[0] != '+9999': + db_t.append(float(temp_info[0]) / 10) + db_t_dates.append(date_row) + dwpt_info = row[14].split(',') + if dwpt_info[0] != '+9999': + dp_t.append(float(dwpt_info[0]) / 10) + dp_t_dates.append(date_row) + + # parse the pressure information + slp_info = row[15].split(',') + if slp_info[0] != '99999': + slp.append(float(slp_info[0]) * 10) + slp_dates.append(date_row) + + # parse the sky cover info if it exists + if sc_col is not None and row[sc_col] != '': + sc_info = row[sc_col].split(',') + sc_oktas = int(sc_info[0]) + sc_tenths = sc_oktas * (10 / 8) if sc_oktas != 9 else 10 + sc.append(sc_tenths) + sc_dates.append(date_row) + # check that all years in the file are the same. yr1 = all_years[0] for yr in all_years: @@ -189,38 +202,13 @@ def build_collection(values, dates, data_type, unit): 'year. {} != {}'.format(yr1, yr) data_header = Header(GenericType('Years', 'yr'), 'yr', AnalysisPeriod()) model_year = HourlyContinuousCollection(data_header, [yr1] * 8760) - - # perform conversions - sc = [sky_codes_dict[cov] for cov in sc] # sky cover codes to values - ceil = [c * 100 for c in ceil] # hundreds of feet to feet - + # build data collections from the imported values - dry_bulb_temp = build_collection(db_t, db_t_dates, DryBulbTemperature(), 'F') - dew_point_temp = build_collection(dp_t, dp_t_dates, DewPointTemperature(), 'F') - wind_speed = build_collection(ws, ws_dates, WindSpeed(), 'mph') + dry_bulb_temp = build_collection(db_t, db_t_dates, DryBulbTemperature(), 'C') + dew_point_temp = build_collection(dp_t, dp_t_dates, DewPointTemperature(), 'C') + wind_speed = build_collection(ws, ws_dates, WindSpeed(), 'm/s') wind_direction = build_collection(wd, wd_dates, WindDirection(), 'degrees') + ceiling_height = build_collection(ceil, ceil_dates, CeilingHeight(), 'm') + visibility = build_collection(vis, vis_dates, Visibility(), 'km') + atmos_pressure = build_collection(slp, slp_dates, AtmosphericStationPressure(), 'bar') total_sky_cover = build_collection(sc, sc_dates, TotalSkyCover(), 'tenths') - visibility = build_collection(vis, vis_dates, Visibility(), 'mi') - ceiling_height = build_collection(ceil, ceil_dates, CeilingHeight(), 'ft') - - # deal with available atmospheric pressure data - if ap != []: - ap = [press / 1000 for press in ap] # pressure mbar to bar - atmos_pressure = build_collection(ap, ap_dates, AtmosphericStationPressure(), 'bar') - else: - slp = [press / 1000 for press in slp] # pressure mbar to bar - atmos_pressure = build_collection(slp, slp_dates, AtmosphericStationPressure(), 'bar') - - # convert all units to SI. - if dry_bulb_temp is not None: - dry_bulb_temp.convert_to_unit('C') - if dew_point_temp is not None: - dew_point_temp.convert_to_unit('C') - if wind_speed is not None: - wind_speed.convert_to_unit('m/s') - if visibility is not None: - visibility.convert_to_unit('km') - if ceiling_height is not None: - ceiling_height.convert_to_unit('m') - if atmos_pressure is not None: - atmos_pressure.convert_to_unit('Pa') \ No newline at end of file diff --git a/dragonfly_grasshopper/src/DF Import NOAA Staion Location.py b/dragonfly_grasshopper/src/DF Import NOAA Staion Location.py index 7093e4a..fb73814 100644 --- a/dragonfly_grasshopper/src/DF Import NOAA Staion Location.py +++ b/dragonfly_grasshopper/src/DF Import NOAA Staion Location.py @@ -8,27 +8,27 @@ # @license GPL-3.0+ """ -Import station location from a .txt file of station information obtained from the +Import station location from a .csv file of station information obtained from the National Oceanic and Atmospheric Administration (NOAA) database. The database can be accessed here: https://gis.ncdc.noaa.gov/maps/ncei/cdo/hourly - Args: - _station_file: The path to a .txt file of NOAA station location data + _station_file: The path to a .csv file of NOAA station location data on your system as a string. times_zone_: Optional time zone for the station. If blank, a default time zone will be estimated from the longitude. - + Returns: location: A Ladybug Location object describing the location data in the NOAA station information file. """ -ghenv.Component.Name = "DF Import NOAA Staion Location" +ghenv.Component.Name = 'DF Import NOAA Staion Location' ghenv.Component.NickName = 'ImportStaion' -ghenv.Component.Message = '0.1.1' -ghenv.Component.Category = "Dragonfly" +ghenv.Component.Message = '0.2.0' +ghenv.Component.Category = 'Dragonfly' ghenv.Component.SubCategory = '4 :: AlternativeWeather' ghenv.Component.AdditionalHelpFromDocStrings = "3" @@ -52,42 +52,28 @@ with open(_station_file) as station_file: station_file.readline() # Skip header row - + # get the pattern of data within the file - char_pattern = station_file.readline().strip().split(' ') - data_line = station_file.readline() - data_list = [] - total_char = 0 - for pattern in char_pattern: - data_list.append(data_line[total_char:total_char + len(pattern)]) - total_char += len(pattern) + 1 - + dat_line = station_file.readline().strip().split(',') + # parse all of the info from the file - station_id, wban_id = data_list[0].split(' ') - station_name = data_list[1].strip() - country = data_list[2].strip() - state = data_list[3].strip() - latitude = float(data_list[4]) - longitude = float(data_list[5]) - elevation = float(data_list[6]) - + station_id = dat_line[0].replace('"', '') + city = dat_line[6].replace('"', '') + latitude = float(dat_line[3].replace('"', '')) + longitude = float(dat_line[4].replace('"', '')) + elevation = float(dat_line[5].replace('"', '')) elevation = Distance().to_unit([elevation], 'm', 'ft')[0] # convert to meters - + # estimate or parse time zone. if time_zone_: - assert -12 <= time_zone_ <= 12, ' time_zone_ must be between -12 and '\ - ' 12. Got {}.'.format(time_zone_) + assert -12 <= time_zone_ <= 14, ' time_zone_ must be between -12 and '\ + ' 14. Got {}.'.format(time_zone_) time_zone = time_zone_ else: time_zone = int((longitude / 180) * 12) - + # build the location object - location = Location(city=station_name, - state=state, - country=country, - latitude=latitude, - longitude=longitude, - time_zone=time_zone, - elevation=elevation, - station_id=station_id, - source='NCDC') \ No newline at end of file + location = Location( + city=city, latitude=latitude, longitude=longitude, + time_zone=time_zone, elevation=elevation, + station_id=station_id, source='NCDC') diff --git a/dragonfly_grasshopper/user_objects/DF Import NOAA File.ghuser b/dragonfly_grasshopper/user_objects/DF Import NOAA File.ghuser index 76b08825168d711666130ee414ced79661328ed6..f3cc9f790b24413cd70ef45722ef0fd5bea80308 100644 GIT binary patch literal 7518 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 literal 7575 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 FzW`nzkZ=G1 literal 4426 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 QeRc4F8^fVkM>zVw0Em{YnE(I)