def _filter_met(met, start, num_hours): # the passed-in met is a reference to the fires_manager's met, so copy it met = copy.deepcopy(met) if not met: # return `met` in case it's a dict and dict is expected downstream return met # limit met to only what's needed to cover dispersion window end = start + datetime.timedelta(hours=num_hours) # Note: we don't store the parsed first and last hour values # because they aren't used outside of this method, and they'd # just have to be dumped back to string values when bsp exits logging.debug('Determinig met files needed for dispersion') met_files = met.pop('files', []) met["files"] = [] for m in met_files: if (parse_datetime(m['first_hour']) <= end and parse_datetime(m['last_hour']) >= start): met["files"].append(m) else: logging.debug('Dropping met file %s - not needed for dispersion', m["file"]) return met
def _within_time_range(self, active_area): """ Note that all times in the activity objects (activity 'start' and 'end' times, as well as timeprofile and hourly_frp keys) are already in local time. Also note that, though unlikely, the locations (specified points and/or perimeter) in the active area could have different utc offsets. """ if active_area.get('start') and active_area.get('end'): utc_offsets = set( [self._get_utc_offset(loc) for loc in active_area.locations]) # convert to datetime objects in place active_area['start'] = parse_datetime(active_area.get('start'), 'start') active_area['end'] = parse_datetime(active_area.get('end'), 'end') is_within = False for utc_offset in utc_offsets: # the activity object's 'start' and 'end' will be in local time; # convert them to UTC to compare with start/end query parameters utc_start = active_area['start'] - utc_offset utc_end = active_area['end'] - utc_offset is_within = is_within or ( (not self._start or utc_end >= self._start) and (not self._end or utc_start <= self._end)) return is_within return False # not necessary, but makes code more readable
def _get_profiler(hourly_fractions, fire, active_area): tw = parse_datetimes(active_area, 'start', 'end') # Use FepsTimeProfiler for Rx fires and StaticTimeProfiler for WF, # Unless custom hourly_fractions are specified, in which case # Static Time Profiler is used for all fires. # If ignition_start and ignition_end aren't specified for Rx fires, # FepsTimeProfiler will assume 9am-12pm # TODO: add config setting to use FEPS for Rx even if custom # hourly_fractions are specified (or the converse - i.e. alwys use # FEPS for rx and add setting to turn on use of hourly_fractions, # if specified, for Rx) if fire.type == 'rx' and not hourly_fractions: ig_start = active_area.get('ignition_start') and parse_datetime( active_area['ignition_start'], k='ignition_start') ig_end = active_area.get('ignition_end') and parse_datetime( active_area['ignition_end'], k='ignition_end') # TODO: pass in duff_fuel_load, total_above_ground_consumption, # total_below_ground_consumption, moisture_category, # relative_humidity, wind_speed, and duff_moisture_content, # if defined? return FepsTimeProfiler(tw['start'], tw['end'], local_ignition_start_time=ig_start, local_ignition_end_time=ig_end, fire_type=FireType.RX) else: return StaticTimeProfiler(tw['start'], tw['end'], hourly_fractions=hourly_fractions)
def __init__(self, **config): self._config = config # start and end times, to use in filtering activity windows self._start = self._config.get('start') self._end = self._config.get('end') self._start = self._start and parse_datetime(self._start, 'start') self._end = self._end and parse_datetime(self._end, 'end') if self._start and self._end and self._start > self._end: raise BlueSkyConfigurationError(self.START_AFTER_END_ERROR_MSG)
def setup(self): # with open(FSV2_INPUT_FILENAME) as f: # self._input = json.loads(f.read()) with open(self.LOADED_FILENAME) as f: self._expected_output = json.loads(f.read()) # convert timestamps to datetime objects for f in self._expected_output: for a in f.get('activity', []): for aa in a.get('active_areas', []): aa['start'] = parse_datetime(aa['start']) aa['end'] = parse_datetime(aa['end'])
def _load_expected_output(self, filename): # with open(FSV2_INPUT_FILENAME) as f: # self._input = json.loads(f.read()) with open(filename) as f: expected = json.loads(f.read()) expected.sort(key=lambda f: f['id']) # convert timestamps to datetime objects for f in expected: for a in f.get('activity', []): for aa in a.get('active_areas', []): aa['start'] = parse_datetime(aa['start']) aa['end'] = parse_datetime(aa['end']) # Note: timeprofile keys are expected to be strings return expected
def _within_time_range(self, a): utc_offset = self._get_utc_offset(a) if a.get('start') and a.get('end'): # convert to datetime objects in place a['start'] = parse_datetime(a.get('start'), 'start') a['end'] = parse_datetime(a.get('end'), 'end') # the activity object's 'start' and 'end' will be in local time; # convert them to UTC to compare with start/end query parameters utc_start = a['start'] - utc_offset utc_end = a['end'] - utc_offset return ((not self._start or utc_end >= self._start) and (not self._end or utc_start <= self._end)) return False # not necessary, but makes code more readable
def _get_profiler(hourly_fractions, fire, active_area): tw = parse_datetimes(active_area, 'start', 'end') # Use FepsTimeProfiler for Rx fires and StaticTimeProfiler for WF, # Unless custom hourly_fractions are specified, in which case # Static Time Profiler is used for all fires. # If ignition_start and ignition_end aren't specified for Rx fires, # FepsTimeProfiler will assume 9am-12pm # TODO: add config setting to use FEPS for Rx even if custom # hourly_fractions are specified (or the converse - i.e. alwys use # FEPS for rx and add setting to turn on use of hourly_fractions, # if specified, for Rx) if fire.type == 'rx' and not hourly_fractions: ig_start = active_area.get('ignition_start') and parse_datetime( active_area['ignition_start'], k='ignition_start') ig_end = active_area.get('ignition_end') and parse_datetime( active_area['ignition_end'], k='ignition_end') # TODO: pass in duff_fuel_load, total_above_ground_consumption, # total_below_ground_consumption, moisture_category, # relative_humidity, wind_speed, and duff_moisture_content, # if defined? return FepsTimeProfiler(tw['start'], tw['end'], local_ignition_start_time=ig_start, local_ignition_end_time=ig_end, fire_type=FireType.RX) else: model_name = Config().get("timeprofile", "model").lower() if model_name == "ubc-bsf-feps": wfrtConfig = Config().get('timeprofile', 'ubc-bsf-feps') working_dir = wfrtConfig.get('working_dir') delete_if_no_error = wfrtConfig.get( 'delete_working_dir_if_no_error') with osutils.create_working_dir( working_dir=working_dir, delete_if_no_error=delete_if_no_error) as wdir: fire_working_dir = os.path.join( wdir, "feps-timeprofile-{}".format(fire.id)) if not os.path.exists(fire_working_dir): os.makedirs(fire_working_dir) return ubcbsffeps.UbcBsfFEPSTimeProfiler( active_area, fire_working_dir, wfrtConfig) else: return StaticTimeProfiler(tw['start'], tw['end'], hourly_fractions=hourly_fractions)
def _get_time_window(): start = Config().get('trajectories', 'start') if not start: raise BlueSkyConfigurationError("Missing trajectories 'start' time") num_hours = Config().get('trajectories', 'num_hours') return parse_datetime(start), num_hours
def __init__(self, init_time, **config): #, log_level): self.enabled = config and config.get('enabled') if self.enabled: # parse to datetime if necessary and then format appropriately self.init_time = parse_datetime(init_time).strftime('%Y%m%d%H') # TODO: error handling to catch undefined fields self.sl = statuslogging.StatusLogger(config.get('api_endpoint'), config.get('api_key'), config.get('api_secret'), config.get('process')) self.domain = config.get('domain')
def _f(fire, working_dir): # TODO: create and change to working directory here (per fire), # above (one working dir per all fires), or below (per activity # window)...or just let plumerise create temp workingdir (as # it's currently doing? for aa in fire.active_areas: start = aa.get('start') if not start: raise ValueError(MISSING_START_TIME_ERROR_MSG) start = datetimeutils.parse_datetime(aa.get('start'), 'start') if not aa.get('timeprofile'): raise ValueError(MISSING_TIMEPROFILE_ERROR_MSG) for loc in aa.locations: if not loc.get('consumption', {}).get('summary'): raise ValueError(MISSING_CONSUMPTION_ERROR_MSG) # Fill in missing sunrise / sunset if any([ loc.get(k) is None for k in ('sunrise_hour', 'sunset_hour') ]): # default: UTC utc_offset = datetimeutils.parse_utc_offset( loc.get('utc_offset', 0.0)) # Use NOAA-standard sunrise/sunset calculations latlng = locationutils.LatLng(loc) s = sun.Sun(lat=latlng.latitude, lng=latlng.longitude) d = start.date() # just set them both, even if one is already set loc["sunrise_hour"] = s.sunrise_hr(d, utc_offset) loc["sunset_hour"] = s.sunset_hr(d, utc_offset) fire_working_dir = _get_fire_working_dir(fire, working_dir) plumerise_data = pr.compute(aa['timeprofile'], loc['consumption']['summary'], loc, working_dir=fire_working_dir) loc['plumerise'] = plumerise_data['hours'] if config.get("load_heat"): if 'fuelbeds' not in loc: raise ValueError( "Fuelbeds should exist before loading heat in plumerise" ) loc["fuelbeds"][0]["heat"] = _loadHeat( fire_working_dir)
def start(self): """Returns start of initial activity window Doesn't memoize, in case activity windows are added/removed/modified """ # consider only activity windows with start times activity = [a for a in self.get('activity', []) if a.get('start')] if activity: activity = sorted(activity, key=lambda a: a['start']) # record utc offset of initial activity window, in case # start_utc is being called self.__utc_offset = activity[0].get('location', {}).get('utc_offset') return datetimeutils.parse_datetime(activity[0]['start'], 'start')
def end(self): """Returns end of final activity window Doesn't memoize, in case activity windows are added/removed/modified TODO: take into account possibility of activity objects having different utc offsets. (It's an extreme edge case where one start/end string is gt/lt another when utc offset is ignored but not when utc offset is considered, so this isn't a high priority) """ # consider only activie areas with end times active_areas = [a for a in self.active_areas if a.get('end')] if active_areas: active_areas = sorted(active_areas, key=lambda a: a['end']) # record utc offset of initial active area, in case # start_utc is being called self.__utc_offset = active_areas[-1].get('utc_offset') return datetimeutils.parse_datetime(active_areas[-1]['end'], 'end')
def _f(fire): # TODO: create and change to working directory here (per fire), # above (one working dir per all fires), or below (per activity # window)...or just let plumerise create temp workingdir (as # it's currently doing? for a in fire.activity: if not a.get('consumption', {}).get('summary'): raise ValueError("Missing fire activity consumption data " "required for FEPS plumerise") # Fill in missing sunrise / sunset if any([a['location'].get(k) is None for k in ('sunrise_hour', 'sunset_hour')]): start = datetimeutils.parse_datetime(a['start'], 'start') if not start: raise ValueError("Missing fire activity start time " "required by FEPS plumerise") # default: UTC utc_offset = datetimeutils.parse_utc_offset( a['location'].get('utc_offset', 0.0)) # Use NOAA-standard sunrise/sunset calculations latlng = locationutils.LatLng(a['location']) s = sun.Sun(lat=latlng.latitude, lng=latlng.longitude) d = start.date() # just set them both, even if one is already set a['location']["sunrise_hour"] = s.sunrise_hr(d, utc_offset) a['location']["sunset_hour"] = s.sunset_hr(d, utc_offset) if not a.get('timeprofile'): raise ValueError("Missing timeprofile data required for " "computing FEPS plumerise") plumerise_data = pr.compute(a['timeprofile'], a['consumption']['summary'], a['location'], working_dir=_get_working_dir(fire)) a['plumerise'] = plumerise_data['hours']