def _process_row(self, row): fire_id = row.get("id") or str(uuid.uuid4()) if fire_id not in self._fires: self._fires[fire_id] = Fire({ "id": fire_id, "event_of": {}, "specified_points_by_date": defaultdict(lambda: []) }) start, utc_offset = self._parse_date_time(row["date_time"]) if not start: raise ValueError("Fire location missing time information") sp = { loc_attr: f(row.get(csv_key)) for csv_key, loc_attr, f in LOCATION_FIELDS } if utc_offset: sp['utc_offset'] = utc_offset self._fires[fire_id]['specified_points_by_date'][start].append(sp) # event and type could have been set when the Fire object was # instantiated, but checking amd setting here allow the fields to # be set even if only subsequent rows for the fire have them defined if row.get("event_id"): self._fires[fire_id]["event_of"]["id"] = row["event_id"] if row.get("event_url"): self._fires[fire_id]["event_of"]["url"] = row["event_url"] if row.get("type"): self._fires[fire_id]["type"] = row["type"].lower()
def generate_dummy_fire(model_start, num_hours, grid_params): """Returns dummy fire formatted like """ logging.info("Generating dummy fire for HYSPLIT") f = Fire( # let fire autogenerate id area=1, latitude=grid_params['center_latitude'], longitude=grid_params['center_longitude'], # TODO: look up offset from lat, lng, and model_start utc_offset=0, # since plumerise and timeprofile will have utc keys plumerise={}, timeprofile={}, emissions={ p: {e: DUMMY_EMISSIONS_VALUE for e in DUMMY_EMISSIONS} for p in PHASES }) for hour in range(num_hours): dt = model_start + datetime.timedelta(hours=hour) dt = dt.strftime('%Y-%m-%dT%H:%M:%S') f['plumerise'][dt] = DUMMY_PLUMERISE_HOUR f['timeprofile'][dt] = dummy_timeprofile_hour(num_hours) return f
def generate_dummy_fire(model_start, num_hours, grid_params): """Returns dummy fire formatted like """ logging.info("Generating dummy fire for HYSPLIT") f = Fire( is_dummy=True, # let fire autogenerate id area=1, latitude=grid_params['center_latitude'], longitude=grid_params['center_longitude'], # TODO: look up offset from lat, lng, and model_start utc_offset=0, # since plumerise and timeprofile will have utc keys plumerise={}, timeprofiled_emissions={}, timeprofiled_area={}) hourly_area = 1.0 / float(num_hours) hourly_timeprofiled_emissions = dummy_timeprofiled_emissions_hour() for hour in range(num_hours): dt = model_start + datetime.timedelta(hours=hour) dt = dt.strftime('%Y-%m-%dT%H:%M:%S') f['plumerise'][dt] = DUMMY_PLUMERISE_HOUR f['timeprofiled_area'][dt] = hourly_area f['timeprofiled_emissions'][dt] = hourly_timeprofiled_emissions return f
def test_two_fires(self): fires = [ Fire({ 'activity': [{ "location": { "area": 10 }, "fuelbeds": [{ "fccs_id": "1", "pct": 30 }, { "fccs_id": "2", "pct": 70 }] }] }), Fire({ 'activity': [{ "location": { "area": 5 }, "fuelbeds": [{ "fccs_id": "2", "pct": 10 }, { "fccs_id": "3", "pct": 90 }] }] }) ] expected_summary = [{ "fccs_id": "1", "pct": 20 }, { "fccs_id": "2", "pct": 50 }, { "fccs_id": "3", "pct": 30 }] summary = fuelbeds.summarize(fires) assert summary == expected_summary
def load(self): data = self._load() self._save_copy(data) fires = self._marshal(data) # cast each fire to Fire object, in case child class # did override _marshal but didn't return Fire objects? # TODO: support config setting 'skip_failures' fires = [Fire(f) for f in fires] fires = self._prune(fires) return fires
def test_two_fires(self): fires = [ Fire({ 'activity':[{ "active_areas":[{ "specified_points": [{ "area": 10, "lat": 45, "lng": -118, "fuelbeds":[ {"fccs_id": "1", "pct": 30}, {"fccs_id": "2", "pct": 70} ] }] }] }] }), Fire({ 'activity':[{ "active_areas":[{ "specified_points": [{ "area": 5, "lat": 44, "lng": -117, "fuelbeds":[ {"fccs_id": "2", "pct": 10}, {"fccs_id": "3", "pct": 90} ] }] }] }] }) ] expected_summary = [ {"fccs_id": "1", "pct": 20}, {"fccs_id": "2", "pct": 50}, {"fccs_id": "3", "pct": 30} ] summary = fuelbeds.summarize(fires) assert summary == expected_summary
def load(self): data = self._load() self._save_copy(data) fires = [] for fire in self._marshal(data): with skip_failures(self._skip_failures): # cast each fire to Fire object, in case child class # did override _marshal but didn't return Fire objects? fire = Fire(fire) self._prune_activity(fire) if fire['activity']: fires.append(fire) return fires
def test_merge_three(self, monkeypatch): monkeypatch.setattr(uuid, 'uuid4', lambda: '1234abcd') expected = Fire({ "id": "1234abcd", "original_fire_ids": {"bbb", "ddd", "eee"}, "meta": {'foo': 'bar', 'bar': 'CONFLICT'}, "start": datetime.datetime(2015,8,4,17,0,0), "end": datetime.datetime(2015,8,4,23,0,0), # TODO: fill in .... }) actual = self.merger._merge_fires([ copy.deepcopy(self.FIRE_1), copy.deepcopy(self.FIRE_2), copy.deepcopy(self.FIRE_3) ])
def test_merge_two(self, monkeypatch): monkeypatch.setattr(uuid, 'uuid4', lambda: '1234abcd') expected = Fire({ "id": "1234abcd", "original_fire_ids": ["bbb", "ddd", "eee"], "meta": {'foo': 'bar', 'bar': 'baz'}, "start": datetime.datetime(2015,8,4,17,0,0), "end": datetime.datetime(2015,8,4,19,0,0), "area": 270.0, "latitude": 47.5, "longitude": -121.6, "utc_offset": -7.0, "plumerise": { "2015-08-04T17:00:00": { # "emission_fractions": [0.4, 0.2, 0.2, 0.2], # "emission_fractions": [0.1, 0.3, 0.4,0.2], "emission_fractions": [0.34, 0.22, 0.4, 0.04], "heights": [90.0, 192.5, 295.0, 397.5, 500.0], # need to write out how the result is calculated so that # we have the same rounding error in expected as we have # in actual "smolder_fraction": (0.05*10 + 0.06*2.5) / 12.5 # == 0.052 }, "2015-08-04T18:00:00": { "emission_fractions": [0.5, 0.2, 0.2, 0.1], "heights": [300, 350, 400, 425, 450], "smolder_fraction": 0.05 } }, "timeprofiled_area": { "2015-08-04T17:00:00": 32.0, "2015-08-04T18:00:00": 30.0 }, "timeprofiled_emissions": { "2015-08-04T17:00:00": {"CO": 23.0, "PM2.5": 12.5}, "2015-08-04T18:00:00": {"CO": 3.0, "PM2.5": 5.0} }, "consumption": { "flaming": 1511, "residual": 1549, "smoldering": 1317, "total": 4427 }, "heat": 3000000.0 }) actual = self.merger._merge_fires([ copy.deepcopy(self.FIRE_1), copy.deepcopy(self.FIRE_2) ]) assert actual == expected
def _marshal(self, data): # TODO: support config setting 'skip_failures' # v2 didn't initially have version specified in the output data if not data.get('version') or data['version'].startswith("2."): func = Blueskyv4_0To4_1().marshal elif data['version'].startswith("3."): # Nothing needs be done; just return fires func = lambda fires: [Fire(f) for f in fires] else: raise NotImplementedError( "Support for FireSpider " "version %s not implemented", data['version']) return func(data.get('data', []))
def _add_location(self, fire, aa, loc, activity_fields, utc_offset, loc_num): if any([not loc.get(f) for f in activity_fields]): raise ValueError("Each active area must have {} in " "order to compute {} dispersion".format( ','.join(activity_fields), self.__class__.__name__)) if any([not fb.get('emissions') for fb in loc['fuelbeds']]): raise ValueError( "Missing emissions data required for computing dispersion") heat = self._get_heat(fire, loc) plumerise, timeprofile = self._get_plumerise_and_timeprofile( loc, utc_offset) emissions = self._get_emissions(loc) timeprofiled_emissions = self._get_timeprofiled_emissions( timeprofile, emissions) timeprofiled_area = { dt: e.get('area_fraction') * loc['area'] for dt, e in timeprofile.items() } # consumption = datautils.sum_nested_data( # [fb.get("consumption", {}) for fb in a['fuelbeds']], 'summary', 'total') consumption = loc['consumption']['summary'] latlng = locationutils.LatLng(loc) f = Fire( id="{}-{}".format(fire.id, loc_num), # See note above, in _merge_two_fires, about why # original_fire_ids is a set instead of scalar original_fire_ids=set([fire.id]), meta=fire.get('meta', {}), start=aa['start'], end=aa['end'], area=loc['area'], latitude=latlng.latitude, longitude=latlng.longitude, utc_offset=utc_offset, plumerise=plumerise, timeprofiled_emissions=timeprofiled_emissions, timeprofiled_area=timeprofiled_area, consumption=consumption) if heat: f['heat'] = heat self._fires.append(f)
def test_contiguous_time_windows(self, monkeypatch): monkeypatch.setattr(uuid, 'uuid4', lambda: '1234abcd') original_fire_1 = copy.deepcopy(self.FIRE_1) original_fire_contiguous_time_windows = copy.deepcopy(self.FIRE_CONTIGUOUS_TIME_WINDOWS) # *should* be merged merged_fires = firemerge.FireMerger().merge([self.FIRE_1, self.FIRE_CONTIGUOUS_TIME_WINDOWS]) expected_merged_fires = [ Fire({ "id": "1234abcd", "original_fire_ids": {"SF11C14225236095807750"}, "meta": {'foo': 'bar', 'bar': 'asdasd'}, "start": datetime.datetime(2015,8,4,17,0,0), "end": datetime.datetime(2015,8,4,21,0,0), "area": 220.0, "latitude": 47.41, "longitude": -121.41, "utc_offset": -7.0, "plumerise": { "2015-08-04T17:00:00": PLUMERISE_HOUR, "2015-08-04T18:00:00": EMPTY_PLUMERISE_HOUR, "2015-08-04T19:00:00": PLUMERISE_HOUR, "2015-08-04T20:00:00": EMPTY_PLUMERISE_HOUR }, "timeprofiled_area": { "2015-08-04T17:00:00": 12.0, "2015-08-04T18:00:00": 0.0, "2015-08-04T19:00:00": 10.0, "2015-08-04T20:00:00": 0.0 }, "timeprofiled_emissions": { "2015-08-04T17:00:00": {"CO": 0.0, "PM2.5": 4.0}, # == 5.0 * 0.2 + 10.0 * 0.1 + 20.0 * 0.1 "2015-08-04T18:00:00": {"CO": 0.0, 'PM2.5': 0.0}, "2015-08-04T19:00:00": {"CO": 0.0, "PM2.5": 5.0}, # == 10.0 * 0.2 + 10.0 * 0.1 + 20.0 * 0.1 "2015-08-04T20:00:00": {"CO": 0.0, 'PM2.5': 0.0} }, "consumption": {k: 2*v for k,v in CONSUMPTION['summary'].items()}, "heat": 4000000.0 }) ] assert len(merged_fires) == len(expected_merged_fires) assert merged_fires == expected_merged_fires # make sure input fire wasn't modified assert self.FIRE_1 == original_fire_1 assert self.FIRE_CONTIGUOUS_TIME_WINDOWS == original_fire_contiguous_time_windows
def _merge_two_fires(self, f_merged, f): new_f_merged = Fire( # We'll let the new fire be assigned a new id # It's possible, but not likely, that locations from different # fires will get merged together. This set of original fire # ids isn't currently used other than in log messages, but # could be used in tranching original_fire_ids=f_merged.original_fire_ids.union( f.original_fire_ids), # we know at this point that their meta dicts don't conflict meta=dict(f_merged.meta, **f.meta), # there may be a gap between f_merged['end'] and f['start'] # but no subsequent fires will be in that gap, since # fires were sorted by 'start' # Note: we need to use f_merged['start'] instead of f_merged.start # because the Fire model has special property 'start' that # returns the first start time of all active_areas in the fire's # activity, and since we're not using nested activity here, # f_merged.start returns 'None' rather than the actual value # set in _add_location start=f_merged['start'], # end will only be used when merging fires # Note: see note about 'start', above end=f['end'], area=f_merged.area + f.area, # f_merged and f have the same lat,lng (o.w. they wouldn't # be merged) latitude=f_merged.latitude, longitude=f_merged.longitude, # the offsets could be different, but only if on DST transition # TODO: Should we worry about this? If so, we should add same # utc offset to criteria for deciding to merge or not utc_offset=f_merged.utc_offset, plumerise=self._merge_hourly_data(f_merged.plumerise, f.plumerise, f['start']), timeprofiled_area=self._merge_hourly_data( f_merged.timeprofiled_area, f.timeprofiled_area, f['start']), timeprofiled_emissions=self._merge_hourly_data( f_merged.timeprofiled_emissions, f.timeprofiled_emissions, f['start']), consumption=self._sum_data(f_merged.consumption, f.consumption)) if 'heat' in f_merged or 'heat' in f: new_f_merged['heat'] = f_merged.get('heat', 0.0) + f.get( 'heat', 0.0) return new_f_merged
def test_one_fire(self): fires = [ Fire({ 'activity': [{ "location": { "area": 10 }, "fuelbeds": [{ "fccs_id": "1", "pct": 40 }, { "fccs_id": "2", "pct": 60 }] }] }) ] summary = fuelbeds.summarize(fires) assert summary == fires[0]['activity'][0]['fuelbeds']
def _merge_fires(self, fires): if len(fires) == 1: return fires[0] logging.debug("Merging plumes of %s fires in for hysplit", len(fires)) latLng = self._get_centroid(fires) new_f_merged = Fire( # We'll let the new fire be assigned a new id # It's possible, but not likely, that locations from different # fires will get merged together. This set of original fire # ids isn't currently used other than in log messages, but # could be used in tranching original_fire_ids=sorted( list(set.union(*[f.original_fire_ids for f in fires]))), meta=self._merge_meta(fires), # Note: we need to use f['start'] instead of f.start # because the Fire model has special property 'start' that # returns the first start time of all active_areas in the fire's # activity, and since we're not using nested activity here, # f.start returns 'None' rather than the actual value # set in _add_location start=min([f['start'] for f in fires]), # end will only be used when merging fires # Note: see note about 'start', above end=max([f['end'] for f in fires]), area=sum([f.area for f in fires]), latitude=latLng.latitude, longitude=latLng.longitude, # the offsets could be different, but just take first # TODO: look up utc offset of centroid potision utc_offset=fires[0].utc_offset, plumerise=self._merge_plumerise(fires), timeprofiled_area=self._sum_data(fires, 'timeprofiled_area'), timeprofiled_emissions=self._sum_data(fires, 'timeprofiled_emissions'), consumption=self._sum_data(fires, 'consumption')) heat_values = [f['heat'] for f in fires if f.get('heat') is not None] if heat_values: new_f_merged['heat'] = sum(heat_values) return new_f_merged
def test_one_fire(self): fires = [ Fire({ 'activity':[{ "active_areas":[{ "specified_points": [{ "area": 10, "lat": 45, "lng": -118, "fuelbeds":[ {"fccs_id": "1", "pct": 40}, {"fccs_id": "2", "pct": 60} ] }] }] }] }) ] expected_summary = [ {"fccs_id": "1", "pct": 40}, {"fccs_id": "2", "pct": 60} ] summary = fuelbeds.summarize(fires) assert summary == expected_summary
class TestPlumeMerger_AggregatePlumeriseHour(BaseTestPlumeMerger): FIRE_1 = Fire({ "plumerise": { "2015-08-04T17:00:00": { "emission_fractions": [0.4, 0.2, 0.2, 0.2], "heights": [90, 250, 300, 325, 350], "smolder_fraction": 0.05 } }, "timeprofiled_emissions": { "2015-08-04T17:00:00": {"CO": 22.0, "PM2.5": 10.0} } }) FIRE_2 = Fire({ "plumerise": { "2015-08-04T17:00:00": { "emission_fractions": [0.1,0.3,0.4,0.2], "heights": [100,200,300,400,500], "smolder_fraction": 0.06 }, "2015-08-04T18:00:00": { "emission_fractions": [0.5, 0.2, 0.2, 0.1], "heights": [300, 350, 400, 425, 450], "smolder_fraction": 0.05 } }, "timeprofiled_emissions": { "2015-08-04T17:00:00": {"CO": 1.0, "PM2.5": 2.5}, "2015-08-04T18:00:00": {"CO": 3.0, "PM2.5": 5.0}, } }) # Note: no need to test one fire, since _aggregate_plumerise_hour # will never get called with one fire def test_two_fires_no_merge(self): expected = ( [(325, 2.5), (375, 1.0), (412.5, 1.0), (437.5, 0.5)], 300, 450, 0.25, 5.0 ) fires = [ copy.deepcopy(self.FIRE_1), copy.deepcopy(self.FIRE_2) ] actual = self.merger._aggregate_plumerise_hour( fires, "2015-08-04T18:00:00") assert expected == actual def test_two_fires_w_merge(self): expected = ( [(150, 0.25), (170, 4), (250, 0.75), (275, 2), (312.5, 2), (337.5, 2), (350, 1.0), (450, 0.5)], 90, 500, 0.5+0.15, 12.5 ) fires = [ copy.deepcopy(self.FIRE_1), copy.deepcopy(self.FIRE_2) ] assert expected == self.merger._aggregate_plumerise_hour( fires, "2015-08-04T17:00:00")
def _process_row(self, row): fire = Fire({ "id": row.get("id") or str(uuid.uuid4()), "event_of": {}, # Though unlikely, it's possible that the points in # a single fire span multiple time zones "specified_points_by_date_n_offset": defaultdict(lambda: defaultdict(lambda: [])) }) start, utc_offset = self._parse_date_time(row["date_time"]) if not start: raise ValueError("Fire location missing time information") sp = { loc_attr: f(row.get(csv_key)) for csv_key, loc_attr, f in LOCATION_FIELDS } if self._omit_nulls: sp = {k: v for k, v in sp.items() if v is not None} fire['specified_points_by_date_n_offset'][start][utc_offset].append(sp) # event and type could have been set when the Fire object was # instantiated, but checking amd setting here allow the fields to # be set even if only subsequent rows for the fire have them defined if row.get("event_id"): fire["event_of"]["id"] = row["event_id"] if row.get("event_url"): fire["event_of"]["url"] = row["event_url"] if row.get("type"): fire["type"] = row["type"].lower() # Add consumption data if present and flag active. # This was implemented for the Canadian version of the SmartFire system. # It is important to note that this consumption marshalling was done with only the Canadian format # in mind. If consumption is added to the input of the US system, further changes maybe required. if self._load_consumption: flaming = 0 smold = 0 resid = 0 duff = 0 if row.get("consumption_flaming") is not None: flaming = get_optional_float(row.get("consumption_flaming")) if row.get("consumption_smoldering") is not None: smold = get_optional_float(row.get("consumption_smoldering")) if row.get("consumption_residual") is not None: resid = get_optional_float(row.get("consumption_residual")) if row.get("consumption_duff") is not None: duff = get_optional_float(row.get("consumption_duff")) total_cons = flaming + smold + resid + duff area = sp.get('area') or 1 consumption = { "flaming": [area * flaming], "residual": [area * resid], "smoldering": [area * smold], "duff": [area * duff], "total": [area * total_cons] } self._consumption_values[fire["id"]] = consumption # TODO: other marshaling return fire
Fire({ 'source': 'GOES-16', 'type': "wf", "activity": [{ "active_areas": [{ "start": "2018-06-27T00:00:00", "end": "2018-06-28T00:00:00", "ignition_start": "2018-06-27T09:00:00", "ignition_end": "2018-06-28T11:00:00", "utc_offset": "-04:00", 'slope': 5, 'windspeed': 5, 'rain_days': 10, 'moisture_10hr': 50, 'fm_type': "MEAS-Th", 'moisture_1khr': 50, 'moisture_duff': 50, 'moisture_litter': 30, 'canopy_consumption_pct': 0, 'shrub_blackened_pct': 50, 'pile_blackened_pct': 0, "specified_points": [{ 'area': 47.20000000000001, 'lat': 50.632, 'lng': -71.362, # timeprofile should be ignored and replcaced when running # FRP emissions "timeprofile": { "2018-06-27T17:00:00": { 'area_fraction': 0.75, 'flaming': 0.75, 'smoldering': 0.3, 'residual': 0.0 }, "2018-06-27T20:00:00": { 'area_fraction': 0.25, 'flaming': 0.25, 'smoldering': 0.7, 'residual': 1.0 } }, # Hourly FRP is used for FRP emissions "hourly_frp": { "2018-06-27T10:00:00": 55.4, "2018-06-27T11:00:00": 66, "2018-06-27T12:00:00": 78, "2018-06-27T13:00:00": 83, "2018-06-27T18:00:00": 82, "2018-06-27T19:00:00": 66, "2018-06-27T20:00:00": 52.5 }, #55.4 + 66 + 78 + 83 + 82 + 66 + 52.5 = 482.9 "frp": 482.9, }] }] }] }),
def marshal_fire(self, fire): # TODO: break this method up into multiple methods fire = copy.deepcopy(fire) activity = fire.pop('activity', None) or fire.pop('growth', []) fire['activity'] = [] for old_a in activity: loc = old_a.pop('location', {}) lat = loc.pop('latitude', None) lng = loc.pop('longitude', None) area = loc.pop('area', None) geojson = loc.pop('geojson', None) aa_template = dict(loc) new_a = {'active_areas': []} loc_template = {} for k in EXTRA_LOC_FIELDS: val = old_a.pop(k, None) if val: loc_template[k] = val for k in TO_COPY_UP: val = old_a.pop(k, None) if val: loc_template[k] = val new_a[k] = val aa_template[k] = val aa_template.update(**old_a) if lat is not None and lng is not None: aa = copy.deepcopy(aa_template) aa["specified_points"] = [ dict(loc_template, lat=lat, lng=lng, area=area) ] new_a["active_areas"].append(aa) elif geojson: if geojson['type'] == 'Polygon': aa = copy.deepcopy(aa_template) aa["perimeter"] = dict(loc_template, polygon=geojson['coordinates'][0], area=area) new_a["active_areas"].append(aa) elif geojson['type'] == 'MultiPolygon': for p in geojson['coordinates']: aa = copy.deepcopy(aa_template) aa["perimeter"] = dict(loc_template, polygon=p[0], area=area) new_a['active_areas'].append(aa) elif geojson['type'] == 'MultiPoint' and area: aa = copy.deepcopy(aa_template) num_points = len(geojson['coordinates']) aa["specified_points"] = [ dict(loc_template,lat=p[1],lng=p[0], area=area / num_points) for p in geojson['coordinates'] ] new_a['active_areas'].append(aa) elif geojson['type'] == 'Point' and area: aa = copy.deepcopy(aa_template) aa["specified_points"] = [ dict(loc_template,lat=geojson['coordinates'][1], lng=geojson['coordinates'][0], area=area) ] new_a['active_areas'].append(aa) else: raise ValueError("Can't convert fire: %s", fire) else: raise ValueError("Can't convert fire: %s", fire) fire['activity'].append(new_a) return Fire(fire)
def __init__(self, fires): self.fires = [Fire(f) for f in fires]
def test_one_fire_no_event(self): fire = Fire({ "fuel_type": "natural", "id": "SF11C14225236095807750", "type": "wildfire", "activity": [{ "active_areas": [{ "start": "2015-08-04T18:00:00", "end": "2015-08-05T18:00:00", "utc_offset": "-06:00", "specified_points": [{ "lat": 35.0, "lng": -96.2, "area": 99 }] }, { "start": "2015-08-04T17:00:00", "end": "2015-08-05T17:00:00", "utc_offset": "-07:00", "canopy_consumption_pct": 23.3, "min_wind": 34, "specified_points": [{ "lat": 30.0, "lng": -116.2, "area": 102 }, { "area": 120.0, "rain_days": 8, "slope": 20.0, "snow_month": 5, "sunrise_hour": 4, "sunset_hour": 19, "consumption": { "summary": { "flaming": 1311.2071801109494, "residual": 1449.3962581338644, "smoldering": 1267.0712004277434, "total": 4027.6746386725567 } }, "fuelbeds": [{ "emissions": { "flaming": { "PM2.5": [9.545588271207714] }, "residual": { "PM2.5": [24.10635856528243] }, "smoldering": { "PM2.5": [21.073928205514225] }, "total": { "PM2.5": [54.725875042004375] } }, "fccs_id": "9", "heat": { "flaming": [20979314881.77519], "residual": [23190340130.141827], "smoldering": [20273139206.843895], "total": [64442794218.7609] }, "pct": 100.0 }], "lat": 47.41, "lng": -121.41 }], "state": "WA" }], }] }) writer = firescsvs.FiresCsvsWriter('/foo') fires_fields, events_fields = writer._collect_csv_fields([fire]) expected_fires_fields = [{ "area": 99, "canopy_consumption_pct": '', 'ch4': '', 'co': '', 'co2': '', 'consumption_flaming': '', 'consumption_residual': '', 'consumption_smoldering': '', 'consumption_total': '', 'country': '', 'county': '', 'date_time': '20150804', 'elevation': '', 'event_id': '', 'event_name': '', 'fccs_number': '', 'heat': '', 'id': 'SF11C14225236095807750', "latitude": 35.0, "longitude": -96.2, 'max_humid': '', 'max_temp': '', 'max_temp_hour': '', 'max_wind': '', 'max_wind_aloft': '', 'min_humid': '', 'min_temp': '', 'min_temp_hour': '', 'min_wind': '', 'min_wind_aloft': '', 'moisture_100hr': '', 'moisture_10hr': '', 'moisture_1hr': '', 'moisture_1khr': '', 'moisture_duff': '', 'moisture_live': '', 'nh3': '', 'nox': '', 'pm10': '', 'pm2.5': '', 'rain_days': '', 'slope': '', 'snow_month': '', 'so2': '', 'state': '', 'sunrise_hour': '', 'sunset_hour': '', 'type': 'WF', "utc_offset": "-06:00", 'voc': '' }, { "area": 102, "canopy_consumption_pct": 23.3, 'ch4': '', 'co': '', 'co2': '', 'consumption_flaming': '', 'consumption_residual': '', 'consumption_smoldering': '', 'consumption_total': '', 'country': '', 'county': '', 'date_time': '20150804', 'elevation': '', 'event_id': '', 'event_name': '', 'fccs_number': '', 'heat': '', 'id': 'SF11C14225236095807750', "latitude": 30.0, "longitude": -116.2, 'max_humid': '', 'max_temp': '', 'max_temp_hour': '', 'max_wind': '', 'max_wind_aloft': '', 'min_humid': '', 'min_temp': '', 'min_temp_hour': '', "min_wind": 34, 'min_wind_aloft': '', 'moisture_100hr': '', 'moisture_10hr': '', 'moisture_1hr': '', 'moisture_1khr': '', 'moisture_duff': '', 'moisture_live': '', 'nh3': '', 'nox': '', 'pm10': '', 'pm2.5': '', 'rain_days': '', 'slope': '', 'snow_month': '', 'so2': '', 'state': 'WA', 'sunrise_hour': '', 'sunset_hour': '', 'type': 'WF', "utc_offset": "-07:00", 'voc': '' }, { 'area': 120.0, "canopy_consumption_pct": 23.3, 'ch4': '', 'co': '', 'co2': '', 'consumption_flaming': 1311.2071801109494, 'consumption_residual': 1449.3962581338644, 'consumption_smoldering': 1267.0712004277434, 'consumption_total': 4027.6746386725567, 'country': '', 'county': '', 'date_time': '20150804', 'elevation': '', 'event_id': '', 'event_name': '', 'fccs_number': '9', 'heat': 64442794218.7609, 'id': 'SF11C14225236095807750', 'latitude': 47.41, 'longitude': -121.41, 'max_humid': '', 'max_temp': '', 'max_temp_hour': '', 'max_wind': '', 'max_wind_aloft': '', 'min_humid': '', 'min_temp': '', 'min_temp_hour': '', 'min_wind': 34, 'min_wind_aloft': '', 'moisture_100hr': '', 'moisture_10hr': '', 'moisture_1hr': '', 'moisture_1khr': '', 'moisture_duff': '', 'moisture_live': '', 'nh3': '', 'nox': '', 'pm10': '', 'pm2.5': 54.725875042004375, 'rain_days': 8.0, 'slope': 20.0, 'snow_month': 5.0, 'so2': '', 'state': 'WA', 'sunrise_hour': 4.0, 'sunset_hour': 19.0, 'type': 'WF', 'utc_offset': '-07:00', 'voc': '' }] assert len(fires_fields) == len(expected_fires_fields) for i in range(len(fires_fields)): assert fires_fields[i].keys() == expected_fires_fields[i].keys() for k in fires_fields[i]: assert fires_fields[i][k] == expected_fires_fields[i][ k], "{} differs".format(k) expected_events_fields = {} assert events_fields == expected_events_fields
"""Unit tests for bluesky.modules.findmetdata""" __author__ = "Joel Dubowy" import copy import datetime from py.test import raises from bluesky.config import Config from bluesky.exceptions import BlueSkyConfigurationError from bluesky.models.fires import FiresManager, Fire from bluesky.modules import findmetdata FIRE_NO_ACTIVITY = Fire({"id": "SF11C14225236095807750"}) FIRE_1 = Fire({ "activity": [{ "active_areas": [ { "start": "2015-01-20T17:00:00", "end": "2015-01-21T17:00:00", "ecoregion": "southern", "utc_offset": "-09:00", "specified_points": [{ "lat": 45, "lng": -119, }] }, { "pct": 40, "start": "2015-01-20T17:00:00", # SAME TIME WINDOW
"""Unit tests for bluesky.modules.findmetdata""" __author__ = "Joel Dubowy" import datetime from py.test import raises from met.arl import arlprofiler from bluesky.config import defaults from bluesky.exceptions import BlueSkyConfigurationError from bluesky.models.fires import FiresManager, Fire from bluesky.modules import localmet FIRE_NO_ACTIVITY = Fire({"id": "SF11C14225236095807750"}) FIRE = Fire({ "activity": [{ "active_areas": [ { "start": "2015-01-20T17:00:00", "end": "2015-01-21T17:00:00", "ecoregion": "southern", "utc_offset": "-07:00", "specified_points": [{ "lat": 45, "lng": -119, "area": 123 }] }, {
def test_all(self, monkeypatch): monkeypatch.setattr(uuid, 'uuid4', lambda: '1234abcd') original_fire_1 = copy.deepcopy(self.FIRE_1) original_overlapping_time_windows = copy.deepcopy(self.FIRE_OVERLAPPING_TIME_WINDOWS) original_fire_contiguous_time_windows = copy.deepcopy(self.FIRE_CONTIGUOUS_TIME_WINDOWS) original_fire_non_contiguous_time_windows = copy.deepcopy(self.FIRE_NON_CONTIGUOUS_TIME_WINDOWS) original_fire_conflicting_meta = copy.deepcopy(self.FIRE_CONFLICTING_META) original_fire_different_lat_lng = copy.deepcopy(self.FIRE_DIFFERENT_LAT_LNG) merged_fires = firemerge.FireMerger().merge([ self.FIRE_1, self.FIRE_OVERLAPPING_TIME_WINDOWS, self.FIRE_CONTIGUOUS_TIME_WINDOWS, self.FIRE_NON_CONTIGUOUS_TIME_WINDOWS, self.FIRE_CONFLICTING_META, self.FIRE_DIFFERENT_LAT_LNG ]) expected_merged_fires = [ # FIRE_1 merged with FIRE_CONTIGUOUS_TIME_WINDOWS Fire({ "id": "1234abcd", "original_fire_ids": {"SF11C14225236095807750"}, "meta": {'foo': 'bar', 'bar': 'asdasd'}, "start": datetime.datetime(2015,8,4,17,0,0), "end": datetime.datetime(2015,8,4,21,0,0), "area": 220.0, "latitude": 47.41, "longitude": -121.41, "utc_offset": -7.0, "plumerise": { "2015-08-04T17:00:00": PLUMERISE_HOUR, "2015-08-04T18:00:00": EMPTY_PLUMERISE_HOUR, "2015-08-04T19:00:00": PLUMERISE_HOUR, "2015-08-04T20:00:00": EMPTY_PLUMERISE_HOUR }, "timeprofiled_area": { "2015-08-04T17:00:00": 12.0, "2015-08-04T18:00:00": 0.0, "2015-08-04T19:00:00": 10.0, "2015-08-04T20:00:00": 0.0 }, "timeprofiled_emissions": { "2015-08-04T17:00:00": {"CO": 0.0, "PM2.5": 4.0}, # == 5.0 * 0.2 + 10.0 * 0.1 + 20.0 * 0.1 "2015-08-04T18:00:00": {"CO": 0.0, 'PM2.5': 0.0}, "2015-08-04T19:00:00": {"CO": 0.0, "PM2.5": 5.0}, # == 10.0 * 0.2 + 10.0 * 0.1 + 20.0 * 0.1 "2015-08-04T20:00:00": {"CO": 0.0, 'PM2.5': 0.0} }, "consumption": {k: 2*v for k,v in CONSUMPTION['summary'].items()}, "heat": 4000000.0 }), # FIRE_OVERLAPPING_TIME_WINDOWS merged with # FIRE_NON_CONTIGUOUS_TIME_WINDOWS Fire({ "id": "1234abcd", "original_fire_ids": {"SF11C14225236095807750"}, "meta": {'foo': 'bar', 'bar': 'sdf'}, "start": datetime.datetime(2015,8,4,18,0,0), "end": datetime.datetime(2015,8,4,22,0,0), "area": 240.0, "latitude": 47.41, "longitude": -121.41, "utc_offset": -7.0, "plumerise": { "2015-08-04T18:00:00": PLUMERISE_HOUR, "2015-08-04T19:00:00": EMPTY_PLUMERISE_HOUR, "2015-08-04T20:00:00": PLUMERISE_HOUR, "2015-08-04T21:00:00": EMPTY_PLUMERISE_HOUR }, "timeprofiled_area": { "2015-08-04T18:00:00": 12.0, "2015-08-04T19:00:00": 0.0, "2015-08-04T20:00:00": 12.0, "2015-08-04T21:00:00": 0.0 }, "timeprofiled_emissions": { "2015-08-04T18:00:00": {"CO": 0.0, "PM2.5": 4.0}, "2015-08-04T19:00:00": {"CO": 0.0, 'PM2.5': 0.0}, "2015-08-04T20:00:00": {"CO": 0.0, "PM2.5": 4.0}, "2015-08-04T21:00:00": {"CO": 0.0, 'PM2.5': 0.0} }, "consumption": {k: 2*v for k,v in CONSUMPTION['summary'].items()}, "heat": 6000000.0 }), self.FIRE_CONFLICTING_META, self.FIRE_DIFFERENT_LAT_LNG ] assert len(merged_fires) == len(expected_merged_fires) assert merged_fires == expected_merged_fires # make sure input fire wasn't modified assert self.FIRE_1 == original_fire_1 assert self.FIRE_OVERLAPPING_TIME_WINDOWS == original_overlapping_time_windows assert self.FIRE_CONTIGUOUS_TIME_WINDOWS == original_fire_contiguous_time_windows assert self.FIRE_NON_CONTIGUOUS_TIME_WINDOWS == original_fire_non_contiguous_time_windows assert self.FIRE_CONFLICTING_META == original_fire_conflicting_meta assert self.FIRE_DIFFERENT_LAT_LNG == original_fire_different_lat_lng
fire = Fire({ 'type': "rx", "activity": [ { "active_areas": [ { "start": "2018-06-27T00:00:00", "end": "2018-06-28T00:00:00", "ignition_start": "2018-06-27T09:00:00", "ignition_end": "2018-06-28T11:00:00", "utc_offset": "-07:00", "ecoregion": "western", 'slope': 5, 'windspeed': 5, 'rain_days': 10, 'moisture_10hr': 50, 'fm_type': "MEAS-Th", 'moisture_1khr': 50, 'moisture_duff': 50, 'moisture_litter': 30, 'canopy_consumption_pct': 0, 'shrub_blackened_pct': 50, 'pile_blackened_pct': 0, "specified_points": [ { 'area': 50.4, 'lat': 45.632, 'lng': -120.362, "fuelbeds": [ { "fccs_id": "52", "pct": 100.0 } ] } ] } ] } ] })
def test_bucket_fires(self): fires = [ # The following two fires will be in their own buckets, even # though they're within what would be a grid cell, because they're # outside the merging boundary Fire({'id': '1', 'latitude': 32.1, 'longitude': -100.1}), Fire({'id': '1.5', 'latitude': 32.2, 'longitude': -100.2}), Fire({'id': '2', 'latitude': 36.22, 'longitude': -111.1}), Fire({'id': '3', 'latitude': 32.2, 'longitude': -110}), Fire({'id': '4', 'latitude': 32.2, 'longitude': -111.2}), Fire({'id': '5', 'latitude': 36.1, 'longitude': -111.4}), Fire({'id': '6', 'latitude': 36.4, 'longitude': -111.3}) ] expected = [ [ Fire({'id': '5', 'latitude': 36.1, 'longitude': -111.4}), Fire({'id': '6', 'latitude': 36.4, 'longitude': -111.3}), Fire({'id': '2', 'latitude': 36.22, 'longitude': -111.1}) ], [Fire({'id': '4', 'latitude': 32.2, 'longitude': -111.2})], [Fire({'id': '3', 'latitude': 32.2, 'longitude': -110})], [Fire({'id': '1.5', 'latitude': 32.2, 'longitude': -100.2})], [Fire({'id': '1', 'latitude': 32.1, 'longitude': -100.1})] ] actual = self.merger._bucket_fires(fires) # sort to compare for a in actual: a.sort(key=lambda f: f.longitude) actual.sort(key=lambda fl: fl[0].longitude) assert actual == expected
import datetime import os import tempfile import uuid from py.test import raises from plumerise import sev, feps from pyairfire import osutils from bluesky.config import Config, defaults from bluesky.exceptions import BlueSkyConfigurationError from bluesky.models.fires import FiresManager, Fire from bluesky.models import activity from bluesky.modules import plumerise FIRE_NO_ACTIVITY = Fire({"id": "SF11C14225236095807750"}) FIRE_MISSING_LOCATION_AREA = Fire({ "activity": [{ "active_areas": [{ "start": "2015-01-20T17:00:00", "end": "2015-01-21T17:00:00", "ecoregion": "southern", "utc_offset": "-07:00", "specified_points": [{ "lat": 45, "lng": -119 }] }] }] })
class TestPlumeMerger_MergeFires(BaseTestPlumeMerger): FIRE_1 = Fire({ "id": "aaa", "original_fire_ids": {"bbb"}, "meta": {'foo': 'bar'}, "start": datetime.datetime(2015,8,4,17,0,0), "end": datetime.datetime(2015,8,4,18,0,0), "area": 120.0, "latitude": 47.4, "longitude": -121.5, "utc_offset": -7.0, "plumerise": { "2015-08-04T17:00:00": { "emission_fractions": [0.4, 0.2, 0.2, 0.2], "heights": [90, 250, 300, 325, 350], "smolder_fraction": 0.05 } }, "timeprofiled_area": { "2015-08-04T17:00:00": 12.0 }, "timeprofiled_emissions": { "2015-08-04T17:00:00": {"CO": 22.0, "PM2.5": 10.0} }, "consumption": { "flaming": 1311, "residual": 1449, "smoldering": 1267, "total": 4027 }, "heat": 1000000.0 }) FIRE_2 = Fire({ "id": "ccc", "original_fire_ids": {"ddd", "eee"}, "meta": {'foo': 'bar', 'bar': 'baz'}, "start": datetime.datetime(2015,8,4,17,0,0), "end": datetime.datetime(2015,8,4,19,0,0), "area": 150.0, "latitude": 47.6, "longitude": -121.7, "utc_offset": -7.0, "plumerise": { "2015-08-04T17:00:00": { "emission_fractions": [0.1,0.3,0.4,0.2], "heights": [100,200,300,400,500], "smolder_fraction": 0.06 }, "2015-08-04T18:00:00": { "emission_fractions": [0.5, 0.2, 0.2, 0.1], "heights": [300, 350, 400, 425, 450], "smolder_fraction": 0.05 } }, "timeprofiled_area": { "2015-08-04T17:00:00": 20.0, "2015-08-04T18:00:00": 30.0 }, "timeprofiled_emissions": { "2015-08-04T17:00:00": {"CO": 1.0, "PM2.5": 2.5}, "2015-08-04T18:00:00": {"CO": 3.0, "PM2.5": 5.0} }, "consumption": { "flaming": 200, "residual": 100, "smoldering": 50, "total": 400 }, "heat": 2000000.0 }) FIRE_3 = Fire({ "id": "ccc", "original_fire_ids": {"ddd", "eee"}, "meta": {'foo': 'bar', 'bar': 'CONFLICT'}, "start": datetime.datetime(2015,8,4,22,0,0), "end": datetime.datetime(2015,8,4,23,0,0), "area": 150.0, "latitude": 47.6, "longitude": -121.7, "utc_offset": -7.0, "plumerise": { "2015-08-04T22:00:00": { "emission_fractions": [0.2, 0.2, 0.2, 0.4], "heights": [111, 222, 333, 444, 555], "smolder_fraction": 0.05 } }, "timeprofiled_area": { "2015-08-04T22:00:00": 20.0 }, "timeprofiled_emissions": { "2015-08-04T22:00:00": {"CO": 1.0, "PM2.5": 4.0} }, "consumption": { "flaming": 100, "residual": 2, "smoldering": 33, "total": 135 }, "heat": 2000.0 }) def setup(self): # The config won't come into play in these tests, since we're # calling _merge_fires directly self.merger = firemerge.PlumeMerger({ "grid": { "spacing": 0.5, "boundary": { "sw": { "lat": 30, "lng": -110 }, "ne": { "lat": 40, "lng": -120 } } } }) def test_merge_one(self): assert self.FIRE_1 == self.merger._merge_fires([ copy.deepcopy(self.FIRE_1) ]) def test_merge_two(self, monkeypatch): monkeypatch.setattr(uuid, 'uuid4', lambda: '1234abcd') expected = Fire({ "id": "1234abcd", "original_fire_ids": ["bbb", "ddd", "eee"], "meta": {'foo': 'bar', 'bar': 'baz'}, "start": datetime.datetime(2015,8,4,17,0,0), "end": datetime.datetime(2015,8,4,19,0,0), "area": 270.0, "latitude": 47.5, "longitude": -121.6, "utc_offset": -7.0, "plumerise": { "2015-08-04T17:00:00": { # "emission_fractions": [0.4, 0.2, 0.2, 0.2], # "emission_fractions": [0.1, 0.3, 0.4,0.2], "emission_fractions": [0.34, 0.22, 0.4, 0.04], "heights": [90.0, 192.5, 295.0, 397.5, 500.0], # need to write out how the result is calculated so that # we have the same rounding error in expected as we have # in actual "smolder_fraction": (0.05*10 + 0.06*2.5) / 12.5 # == 0.052 }, "2015-08-04T18:00:00": { "emission_fractions": [0.5, 0.2, 0.2, 0.1], "heights": [300, 350, 400, 425, 450], "smolder_fraction": 0.05 } }, "timeprofiled_area": { "2015-08-04T17:00:00": 32.0, "2015-08-04T18:00:00": 30.0 }, "timeprofiled_emissions": { "2015-08-04T17:00:00": {"CO": 23.0, "PM2.5": 12.5}, "2015-08-04T18:00:00": {"CO": 3.0, "PM2.5": 5.0} }, "consumption": { "flaming": 1511, "residual": 1549, "smoldering": 1317, "total": 4427 }, "heat": 3000000.0 }) actual = self.merger._merge_fires([ copy.deepcopy(self.FIRE_1), copy.deepcopy(self.FIRE_2) ]) assert actual == expected def test_merge_three(self, monkeypatch): monkeypatch.setattr(uuid, 'uuid4', lambda: '1234abcd') expected = Fire({ "id": "1234abcd", "original_fire_ids": {"bbb", "ddd", "eee"}, "meta": {'foo': 'bar', 'bar': 'CONFLICT'}, "start": datetime.datetime(2015,8,4,17,0,0), "end": datetime.datetime(2015,8,4,23,0,0), # TODO: fill in .... }) actual = self.merger._merge_fires([ copy.deepcopy(self.FIRE_1), copy.deepcopy(self.FIRE_2), copy.deepcopy(self.FIRE_3) ])
class TestFireMerger(object): FIRE_1 = Fire({ "id": "SF11C14225236095807750-0", "original_fire_ids": {"SF11C14225236095807750"}, "meta": {'foo': 'bar'}, "start": datetime.datetime(2015,8,4,17,0,0), "end": datetime.datetime(2015,8,4,19,0,0), "area": 120.0, "latitude": 47.41, "longitude": -121.41, "utc_offset": -7.0, "plumerise": { "2015-08-04T17:00:00": PLUMERISE_HOUR, "2015-08-04T18:00:00": EMPTY_PLUMERISE_HOUR }, "timeprofiled_area": { "2015-08-04T17:00:00": 12.0, "2015-08-04T18:00:00": 0.0 }, "timeprofiled_emissions": { "2015-08-04T17:00:00": {"CO": 0.0, "PM2.5": 4.0}, # == 5.0 * 0.2 + 10.0 * 0.1 + 20.0 * 0.1 "2015-08-04T18:00:00": {"CO": 0.0, 'PM2.5': 0.0} }, "consumption": CONSUMPTION['summary'], "heat": 1000000.0 }) # no conflicting meta, same location, but overlapping time window FIRE_OVERLAPPING_TIME_WINDOWS = Fire({ "id": "SF11C14225236095807750-0", "original_fire_ids": {"SF11C14225236095807750"}, "meta": {'foo': 'bar'}, "start": datetime.datetime(2015,8,4,18,0,0), "end": datetime.datetime(2015,8,4,20,0,0), "area": 120.0, "latitude": 47.41, "longitude": -121.41, "utc_offset": -7.0, "plumerise": { "2015-08-04T18:00:00": PLUMERISE_HOUR, "2015-08-04T19:00:00": EMPTY_PLUMERISE_HOUR }, "timeprofiled_area": { "2015-08-04T18:00:00": 12.0, "2015-08-04T19:00:00": 0.0 }, "timeprofiled_emissions": { "2015-08-04T18:00:00": {"CO": 0.0, "PM2.5": 4.0}, # == 5.0 * 0.2 + 10.0 * 0.1 + 20.0 * 0.1 "2015-08-04T19:00:00": {"CO": 0.0, 'PM2.5': 0.0} }, "consumption": CONSUMPTION['summary'], "heat": 2000000.0 }) # contiguous time windows, no conflicting meta, same location FIRE_CONTIGUOUS_TIME_WINDOWS = Fire({ "id": "SF11C14225236095807750-0", "original_fire_ids": {"SF11C14225236095807750"}, "meta": {'foo': 'bar', 'bar': 'asdasd'}, "start": datetime.datetime(2015,8,4,19,0,0), "end": datetime.datetime(2015,8,4,21,0,0), "area": 100.0, "latitude": 47.41, "longitude": -121.41, "utc_offset": -7.0, "plumerise": { "2015-08-04T19:00:00": PLUMERISE_HOUR, "2015-08-04T20:00:00": EMPTY_PLUMERISE_HOUR }, "timeprofiled_area": { "2015-08-04T19:00:00": 10.0, "2015-08-04T20:00:00": 0.0 }, "timeprofiled_emissions": { "2015-08-04T19:00:00": {"CO": 0.0, "PM2.5": 5.0}, # == 10.0 * 0.2 + 10.0 * 0.1 + 20.0 * 0.1 "2015-08-04T20:00:00": {"CO": 0.0, 'PM2.5': 0.0} }, "consumption": CONSUMPTION['summary'], "heat": 3000000.0 }) # non contiguous time windows, no conflicting meta, same location FIRE_NON_CONTIGUOUS_TIME_WINDOWS = Fire({ "id": "SF11C14225236095807750-0", "original_fire_ids": {"SF11C14225236095807750"}, "meta": {'foo': 'bar', 'bar': 'sdf'}, "start": datetime.datetime(2015,8,4,20,0,0), "end": datetime.datetime(2015,8,4,22,0,0), "area": 120.0, "latitude": 47.41, "longitude": -121.41, "utc_offset": -7.0, "plumerise": { "2015-08-04T20:00:00": PLUMERISE_HOUR, "2015-08-04T21:00:00": EMPTY_PLUMERISE_HOUR }, "timeprofiled_area": { "2015-08-04T20:00:00": 12.0, "2015-08-04T21:00:00": 0.0 }, "timeprofiled_emissions": { "2015-08-04T20:00:00": {"CO": 0.0, "PM2.5": 4.0}, # == 5.0 * 0.2 + 10.0 * 0.1 + 20.0 * 0.1 "2015-08-04T21:00:00": {"CO": 0.0, 'PM2.5': 0.0} }, "consumption": CONSUMPTION['summary'], "heat": 4000000.0 }) FIRE_CONFLICTING_META = Fire({ "id": "SF11C14225236095807750-0", "original_fire_ids": {"SF11C14225236095807750"}, "meta": {'foo': 'baz'}, "start": datetime.datetime(2015,8,4,20,0,0), "end": datetime.datetime(2015,8,4,22,0,0), "area": 120.0, "latitude": 47.41, "longitude": -121.41, "utc_offset": -7.0, "plumerise": { "2015-08-04T20:00:00": PLUMERISE_HOUR, "2015-08-04T21:00:00": EMPTY_PLUMERISE_HOUR }, "timeprofiled_area": { "2015-08-04T20:00:00": 12.0, "2015-08-04T21:00:00": 0.0 }, "timeprofiled_emissions": { "2015-08-04T20:00:00": {"CO": 0.0, "PM2.5": 4.0}, # == 5.0 * 0.2 + 10.0 * 0.1 + 20.0 * 0.1 "2015-08-04T21:00:00": {"CO": 0.0, 'PM2.5': 0.0} }, "consumption": CONSUMPTION['summary'], "heat": 5000000.0 }) FIRE_DIFFERENT_LAT_LNG = Fire({ "id": "SF11C14225236095807750-0", "original_fire_ids": {"SF11C14225236095807750"}, "meta": {}, "start": datetime.datetime(2015,8,4,20,0,0), "end": datetime.datetime(2015,8,4,22,0,0), "area": 120.0, "latitude": 47.0, "longitude": -121.0, "utc_offset": -7.0, "plumerise": { "2015-08-04T20:00:00": PLUMERISE_HOUR, "2015-08-04T21:00:00": EMPTY_PLUMERISE_HOUR }, "timeprofiled_area": { "2015-08-04T20:00:00": 12.0, "2015-08-04T21:00:00": 0.0 }, "timeprofiled_emissions": { "2015-08-04T20:00:00": {"CO": 0.0, "PM2.5": 4.0}, # == 5.0 * 0.2 + 10.0 * 0.1 + 20.0 * 0.1 "2015-08-04T21:00:00": {"CO": 0.0, 'PM2.5': 0.0} }, "consumption": CONSUMPTION['summary'], "heat": 6000000.0 }) # def setup(self): # pass ## Cases that do *not* merge def test_one_fire(self): original_fire_1 = copy.deepcopy(self.FIRE_1) merged_fires = firemerge.FireMerger().merge([self.FIRE_1]) assert len(merged_fires) == 1 assert merged_fires == [self.FIRE_1] # make sure input fire wasn't modified assert self.FIRE_1 == original_fire_1 def test_differenent_lat_lng(self): original_fire_1 = copy.deepcopy(self.FIRE_1) original_fire_different_lat_lng = copy.deepcopy(self.FIRE_DIFFERENT_LAT_LNG) # shouldn't be merged merged_fires = firemerge.FireMerger().merge([self.FIRE_1, self.FIRE_DIFFERENT_LAT_LNG]) assert len(merged_fires) == 2 assert merged_fires == [self.FIRE_1, self.FIRE_DIFFERENT_LAT_LNG] # make sure input fire wasn't modified assert self.FIRE_1 == original_fire_1 assert self.FIRE_DIFFERENT_LAT_LNG == original_fire_different_lat_lng def test_overlapping_time_windows(self): original_fire_1 = copy.deepcopy(self.FIRE_1) original_overlapping_time_windows = copy.deepcopy(self.FIRE_OVERLAPPING_TIME_WINDOWS) # shouldn't be merged merged_fires = firemerge.FireMerger().merge([self.FIRE_1, self.FIRE_OVERLAPPING_TIME_WINDOWS]) assert len(merged_fires) == 2 assert merged_fires == [self.FIRE_1, self.FIRE_OVERLAPPING_TIME_WINDOWS] # make sure input fire wasn't modified assert self.FIRE_1 == original_fire_1 assert self.FIRE_OVERLAPPING_TIME_WINDOWS == original_overlapping_time_windows def test_conflicting_meta(self): original_fire_1 = copy.deepcopy(self.FIRE_1) original_fire_conflicting_meta = copy.deepcopy(self.FIRE_CONFLICTING_META) # shouldn't be merged merged_fires = firemerge.FireMerger().merge([self.FIRE_1, self.FIRE_CONFLICTING_META]) assert len(merged_fires) == 2 assert merged_fires == [self.FIRE_1, self.FIRE_CONFLICTING_META] # make sure input fire wasn't modified assert self.FIRE_1 == original_fire_1 assert self.FIRE_CONFLICTING_META == original_fire_conflicting_meta ## Cases that merge def test_non_contiguous_time_windows(self, monkeypatch): monkeypatch.setattr(uuid, 'uuid4', lambda: '1234abcd') original_fire_1 = copy.deepcopy(self.FIRE_1) original_fire_non_contiguous_time_windows = copy.deepcopy(self.FIRE_NON_CONTIGUOUS_TIME_WINDOWS) # *should* be merged merged_fires = firemerge.FireMerger().merge([self.FIRE_1, self.FIRE_NON_CONTIGUOUS_TIME_WINDOWS]) expected_merged_fires = [ Fire({ "id": "1234abcd", "original_fire_ids": {"SF11C14225236095807750"}, "meta": {'foo': 'bar', 'bar': 'sdf'}, "start": datetime.datetime(2015,8,4,17,0,0), "end": datetime.datetime(2015,8,4,22,0,0), "area": 240.0, "latitude": 47.41, "longitude": -121.41, "utc_offset": -7.0, "plumerise": { "2015-08-04T17:00:00": PLUMERISE_HOUR, "2015-08-04T18:00:00": EMPTY_PLUMERISE_HOUR, "2015-08-04T20:00:00": PLUMERISE_HOUR, "2015-08-04T21:00:00": EMPTY_PLUMERISE_HOUR }, "timeprofiled_area": { "2015-08-04T17:00:00": 12.0, "2015-08-04T18:00:00": 0.0, "2015-08-04T20:00:00": 12.0, "2015-08-04T21:00:00": 0.0 }, "timeprofiled_emissions": { "2015-08-04T17:00:00": {"CO": 0.0, "PM2.5": 4.0}, # == 5.0 * 0.2 + 10.0 * 0.1 + 20.0 * 0.1 "2015-08-04T18:00:00": {"CO": 0.0, 'PM2.5': 0.0}, "2015-08-04T20:00:00": {"CO": 0.0, "PM2.5": 4.0}, # == 5.0 * 0.2 + 10.0 * 0.1 + 20.0 * 0.1 "2015-08-04T21:00:00": {"CO": 0.0, 'PM2.5': 0.0} }, "consumption": {k: 2*v for k,v in CONSUMPTION['summary'].items()}, "heat": 5000000.0 }) ] assert len(merged_fires) == len(expected_merged_fires) assert merged_fires == expected_merged_fires # make sure input fire wasn't modified assert self.FIRE_1 == original_fire_1 assert self.FIRE_NON_CONTIGUOUS_TIME_WINDOWS == original_fire_non_contiguous_time_windows def test_contiguous_time_windows(self, monkeypatch): monkeypatch.setattr(uuid, 'uuid4', lambda: '1234abcd') original_fire_1 = copy.deepcopy(self.FIRE_1) original_fire_contiguous_time_windows = copy.deepcopy(self.FIRE_CONTIGUOUS_TIME_WINDOWS) # *should* be merged merged_fires = firemerge.FireMerger().merge([self.FIRE_1, self.FIRE_CONTIGUOUS_TIME_WINDOWS]) expected_merged_fires = [ Fire({ "id": "1234abcd", "original_fire_ids": {"SF11C14225236095807750"}, "meta": {'foo': 'bar', 'bar': 'asdasd'}, "start": datetime.datetime(2015,8,4,17,0,0), "end": datetime.datetime(2015,8,4,21,0,0), "area": 220.0, "latitude": 47.41, "longitude": -121.41, "utc_offset": -7.0, "plumerise": { "2015-08-04T17:00:00": PLUMERISE_HOUR, "2015-08-04T18:00:00": EMPTY_PLUMERISE_HOUR, "2015-08-04T19:00:00": PLUMERISE_HOUR, "2015-08-04T20:00:00": EMPTY_PLUMERISE_HOUR }, "timeprofiled_area": { "2015-08-04T17:00:00": 12.0, "2015-08-04T18:00:00": 0.0, "2015-08-04T19:00:00": 10.0, "2015-08-04T20:00:00": 0.0 }, "timeprofiled_emissions": { "2015-08-04T17:00:00": {"CO": 0.0, "PM2.5": 4.0}, # == 5.0 * 0.2 + 10.0 * 0.1 + 20.0 * 0.1 "2015-08-04T18:00:00": {"CO": 0.0, 'PM2.5': 0.0}, "2015-08-04T19:00:00": {"CO": 0.0, "PM2.5": 5.0}, # == 10.0 * 0.2 + 10.0 * 0.1 + 20.0 * 0.1 "2015-08-04T20:00:00": {"CO": 0.0, 'PM2.5': 0.0} }, "consumption": {k: 2*v for k,v in CONSUMPTION['summary'].items()}, "heat": 4000000.0 }) ] assert len(merged_fires) == len(expected_merged_fires) assert merged_fires == expected_merged_fires # make sure input fire wasn't modified assert self.FIRE_1 == original_fire_1 assert self.FIRE_CONTIGUOUS_TIME_WINDOWS == original_fire_contiguous_time_windows def test_all(self, monkeypatch): monkeypatch.setattr(uuid, 'uuid4', lambda: '1234abcd') original_fire_1 = copy.deepcopy(self.FIRE_1) original_overlapping_time_windows = copy.deepcopy(self.FIRE_OVERLAPPING_TIME_WINDOWS) original_fire_contiguous_time_windows = copy.deepcopy(self.FIRE_CONTIGUOUS_TIME_WINDOWS) original_fire_non_contiguous_time_windows = copy.deepcopy(self.FIRE_NON_CONTIGUOUS_TIME_WINDOWS) original_fire_conflicting_meta = copy.deepcopy(self.FIRE_CONFLICTING_META) original_fire_different_lat_lng = copy.deepcopy(self.FIRE_DIFFERENT_LAT_LNG) merged_fires = firemerge.FireMerger().merge([ self.FIRE_1, self.FIRE_OVERLAPPING_TIME_WINDOWS, self.FIRE_CONTIGUOUS_TIME_WINDOWS, self.FIRE_NON_CONTIGUOUS_TIME_WINDOWS, self.FIRE_CONFLICTING_META, self.FIRE_DIFFERENT_LAT_LNG ]) expected_merged_fires = [ # FIRE_1 merged with FIRE_CONTIGUOUS_TIME_WINDOWS Fire({ "id": "1234abcd", "original_fire_ids": {"SF11C14225236095807750"}, "meta": {'foo': 'bar', 'bar': 'asdasd'}, "start": datetime.datetime(2015,8,4,17,0,0), "end": datetime.datetime(2015,8,4,21,0,0), "area": 220.0, "latitude": 47.41, "longitude": -121.41, "utc_offset": -7.0, "plumerise": { "2015-08-04T17:00:00": PLUMERISE_HOUR, "2015-08-04T18:00:00": EMPTY_PLUMERISE_HOUR, "2015-08-04T19:00:00": PLUMERISE_HOUR, "2015-08-04T20:00:00": EMPTY_PLUMERISE_HOUR }, "timeprofiled_area": { "2015-08-04T17:00:00": 12.0, "2015-08-04T18:00:00": 0.0, "2015-08-04T19:00:00": 10.0, "2015-08-04T20:00:00": 0.0 }, "timeprofiled_emissions": { "2015-08-04T17:00:00": {"CO": 0.0, "PM2.5": 4.0}, # == 5.0 * 0.2 + 10.0 * 0.1 + 20.0 * 0.1 "2015-08-04T18:00:00": {"CO": 0.0, 'PM2.5': 0.0}, "2015-08-04T19:00:00": {"CO": 0.0, "PM2.5": 5.0}, # == 10.0 * 0.2 + 10.0 * 0.1 + 20.0 * 0.1 "2015-08-04T20:00:00": {"CO": 0.0, 'PM2.5': 0.0} }, "consumption": {k: 2*v for k,v in CONSUMPTION['summary'].items()}, "heat": 4000000.0 }), # FIRE_OVERLAPPING_TIME_WINDOWS merged with # FIRE_NON_CONTIGUOUS_TIME_WINDOWS Fire({ "id": "1234abcd", "original_fire_ids": {"SF11C14225236095807750"}, "meta": {'foo': 'bar', 'bar': 'sdf'}, "start": datetime.datetime(2015,8,4,18,0,0), "end": datetime.datetime(2015,8,4,22,0,0), "area": 240.0, "latitude": 47.41, "longitude": -121.41, "utc_offset": -7.0, "plumerise": { "2015-08-04T18:00:00": PLUMERISE_HOUR, "2015-08-04T19:00:00": EMPTY_PLUMERISE_HOUR, "2015-08-04T20:00:00": PLUMERISE_HOUR, "2015-08-04T21:00:00": EMPTY_PLUMERISE_HOUR }, "timeprofiled_area": { "2015-08-04T18:00:00": 12.0, "2015-08-04T19:00:00": 0.0, "2015-08-04T20:00:00": 12.0, "2015-08-04T21:00:00": 0.0 }, "timeprofiled_emissions": { "2015-08-04T18:00:00": {"CO": 0.0, "PM2.5": 4.0}, "2015-08-04T19:00:00": {"CO": 0.0, 'PM2.5': 0.0}, "2015-08-04T20:00:00": {"CO": 0.0, "PM2.5": 4.0}, "2015-08-04T21:00:00": {"CO": 0.0, 'PM2.5': 0.0} }, "consumption": {k: 2*v for k,v in CONSUMPTION['summary'].items()}, "heat": 6000000.0 }), self.FIRE_CONFLICTING_META, self.FIRE_DIFFERENT_LAT_LNG ] assert len(merged_fires) == len(expected_merged_fires) assert merged_fires == expected_merged_fires # make sure input fire wasn't modified assert self.FIRE_1 == original_fire_1 assert self.FIRE_OVERLAPPING_TIME_WINDOWS == original_overlapping_time_windows assert self.FIRE_CONTIGUOUS_TIME_WINDOWS == original_fire_contiguous_time_windows assert self.FIRE_NON_CONTIGUOUS_TIME_WINDOWS == original_fire_non_contiguous_time_windows assert self.FIRE_CONFLICTING_META == original_fire_conflicting_meta assert self.FIRE_DIFFERENT_LAT_LNG == original_fire_different_lat_lng