def test_battery_solver_day_2(): epex_da = Market.query.filter(Market.name == "epex_da").one_or_none() battery = Asset.query.filter(Asset.name == "Test battery").one_or_none() start = as_server_time(datetime(2015, 1, 2)) end = as_server_time(datetime(2015, 1, 3)) resolution = timedelta(minutes=15) soc_at_start = battery.soc_in_mwh schedule = schedule_battery(battery, epex_da, start, end, resolution, soc_at_start) soc_schedule = integrate_time_series(schedule, soc_at_start, decimal_precision=6) with pd.option_context("display.max_rows", None, "display.max_columns", 3): print(soc_schedule) # Check if constraints were met assert min(schedule.values) >= battery.capacity_in_mw * -1 assert max(schedule.values) <= battery.capacity_in_mw for soc in soc_schedule.values: assert soc >= battery.min_soc_in_mwh assert soc <= battery.max_soc_in_mwh # Check whether the resulting soc schedule follows our expectations for 8 expensive, 8 cheap and 8 expensive hours assert (soc_schedule.iloc[-1] == battery.min_soc_in_mwh ) # Battery sold out at the end of its planning horizon assert (soc_schedule.loc[start + timedelta(hours=8)] == battery.min_soc_in_mwh ) # Sell what you begin with assert (soc_schedule.loc[start + timedelta(hours=16)] == battery.max_soc_in_mwh ) # Buy what you can to sell later
def test_battery_solver_day_1(): epex_da = Market.query.filter(Market.name == "epex_da").one_or_none() battery = Asset.query.filter(Asset.name == "Test battery").one_or_none() start = as_server_time(datetime(2015, 1, 1)) end = as_server_time(datetime(2015, 1, 2)) resolution = timedelta(minutes=15) soc_at_start = battery.soc_in_mwh schedule = schedule_battery(battery, epex_da, start, end, resolution, soc_at_start) soc_schedule = integrate_time_series(schedule, soc_at_start, decimal_precision=6) with pd.option_context("display.max_rows", None, "display.max_columns", 3): print(soc_schedule) # Check if constraints were met assert min(schedule.values) >= battery.capacity_in_mw * -1 assert max(schedule.values) <= battery.capacity_in_mw for soc in soc_schedule.values: assert soc >= battery.min_soc_in_mwh assert soc <= battery.max_soc_in_mwh
def test_battery_solver_day_1(add_battery_assets): epex_da = Sensor.query.filter(Sensor.name == "epex_da").one_or_none() battery = Sensor.query.filter(Sensor.name == "Test battery").one_or_none() assert Sensor.query.get(battery.get_attribute("market_id")) == epex_da start = as_server_time(datetime(2015, 1, 1)) end = as_server_time(datetime(2015, 1, 2)) resolution = timedelta(minutes=15) soc_at_start = battery.get_attribute("soc_in_mwh") schedule = schedule_battery(battery, start, end, resolution, soc_at_start) soc_schedule = integrate_time_series(schedule, soc_at_start, decimal_precision=6) with pd.option_context("display.max_rows", None, "display.max_columns", 3): print(soc_schedule) # Check if constraints were met assert (min(schedule.values) >= battery.get_attribute("capacity_in_mw") * -1 - TOLERANCE) assert max(schedule.values) <= battery.get_attribute("capacity_in_mw") for soc in soc_schedule.values: assert soc >= battery.get_attribute("min_soc_in_mwh") assert soc <= battery.get_attribute("max_soc_in_mwh")
def make_schedule( sensor_id: int, start: datetime, end: datetime, belief_time: datetime, resolution: timedelta, soc_at_start: Optional[float] = None, soc_targets: Optional[pd.Series] = None, soc_min: Optional[float] = None, soc_max: Optional[float] = None, roundtrip_efficiency: Optional[float] = None, price_sensor: Optional[Sensor] = None, ) -> bool: """Preferably, a starting soc is given. Otherwise, we try to retrieve the current state of charge from the asset (if that is the valid one at the start). Otherwise, we set the starting soc to 0 (some assets don't use the concept of a state of charge, and without soc targets and limits the starting soc doesn't matter). """ # https://docs.sqlalchemy.org/en/13/faq/connections.html#how-do-i-use-engines-connections-sessions-with-python-multiprocessing-or-os-fork db.engine.dispose() rq_job = get_current_job() # find sensor sensor = Sensor.query.filter_by(id=sensor_id).one_or_none() if rq_job: click.echo("Running Scheduling Job %s: %s, from %s to %s" % (rq_job.id, sensor, start, end)) if soc_at_start is None: if (start == sensor.get_attribute("soc_datetime") and sensor.get_attribute("soc_in_mwh") is not None): soc_at_start = sensor.get_attribute("soc_in_mwh") else: soc_at_start = 0 if soc_targets is None: soc_targets = pd.Series(np.nan, index=pd.date_range(start, end, freq=resolution, closed="right")) if sensor.generic_asset.generic_asset_type.name == "battery": consumption_schedule = schedule_battery( sensor, start, end, resolution, soc_at_start, soc_targets, soc_min, soc_max, roundtrip_efficiency, price_sensor=price_sensor, ) elif sensor.generic_asset.generic_asset_type.name in ( "one-way_evse", "two-way_evse", ): consumption_schedule = schedule_charging_station( sensor, start, end, resolution, soc_at_start, soc_targets, soc_min, soc_max, roundtrip_efficiency, price_sensor=price_sensor, ) else: raise ValueError( "Scheduling is not (yet) supported for asset type %s." % sensor.generic_asset.generic_asset_type) data_source = get_data_source( data_source_name="Seita", data_source_type="scheduling script", ) if rq_job: click.echo("Job %s made schedule." % rq_job.id) ts_value_schedule = [ TimedBelief( event_start=dt, belief_time=belief_time, event_value=-value, sensor=sensor, source=data_source, ) for dt, value in consumption_schedule.items() ] # For consumption schedules, positive values denote consumption. For the db, consumption is negative bdf = tb.BeliefsDataFrame(ts_value_schedule) save_to_db(bdf) db.session.commit() return True
def get_device_message_response(generic_asset_name_groups, duration): unit = "MW" min_planning_horizon = timedelta( hours=24 ) # user can request a shorter planning, but the scheduler takes into account at least this horizon planning_horizon = min( max(min_planning_horizon, duration), current_app.config.get("FLEXMEASURES_PLANNING_HORIZON"), ) if not has_assets(): current_app.logger.info("User doesn't seem to have any assets.") value_groups = [] new_event_groups = [] for event_group in generic_asset_name_groups: for event in event_group: # Parse the entity address try: ea = parse_entity_address(event, entity_type="event", fm_scheme="fm0") except EntityAddressException as eae: return invalid_domain(str(eae)) sensor_id = ea["asset_id"] event_id = ea["event_id"] event_type = ea["event_type"] # Look for the Sensor object sensor = Sensor.query.filter(Sensor.id == sensor_id).one_or_none() if sensor is None or not can_access_asset(sensor): current_app.logger.warning( "Cannot identify sensor given the event %s." % event) return unrecognized_connection_group() if sensor.generic_asset.generic_asset_type.name != "battery": return invalid_domain( "API version 1.2 only supports device messages for batteries. " "Sensor ID:%s does not belong to a battery." % sensor_id) if event_type != "soc" or event_id != sensor.generic_asset.get_attribute( "soc_udi_event_id"): return unrecognized_event(event_id, event_type) start = datetime.fromisoformat( sensor.generic_asset.get_attribute("soc_datetime")) resolution = sensor.event_resolution # Schedule the asset try: schedule = schedule_battery( sensor, start, start + planning_horizon, resolution, soc_at_start=sensor.generic_asset.get_attribute( "soc_in_mwh"), prefer_charging_sooner=False, ) except UnknownPricesException: return unknown_prices() except UnknownMarketException: return invalid_market() else: # Update the planning window start = schedule.index[0] duration = min(duration, schedule.index[-1] + resolution - start) schedule = schedule[start:start + duration - resolution] value_groups.append(schedule.tolist()) new_event_groups.append(event) response = groups_to_dict(new_event_groups, value_groups, generic_asset_type_name="event") response["start"] = isodate.datetime_isoformat(start) response["duration"] = isodate.duration_isoformat(duration) response["unit"] = unit d, s = request_processed() return dict(**response, **d), s
def test_battery_solver_day_2(add_battery_assets, roundtrip_efficiency: float): """Check battery scheduling results for day 2, which is set up with 8 expensive, then 8 cheap, then again 8 expensive hours. If efficiency losses aren't too bad, we expect the scheduler to: - completely discharge within the first 8 hours - completely charge within the next 8 hours - completely discharge within the last 8 hours If efficiency losses are bad, the price difference is not worth cycling the battery, and so we expect the scheduler to only: - completely discharge within the last 8 hours """ epex_da = Sensor.query.filter(Sensor.name == "epex_da").one_or_none() battery = Sensor.query.filter(Sensor.name == "Test battery").one_or_none() assert Sensor.query.get(battery.get_attribute("market_id")) == epex_da start = as_server_time(datetime(2015, 1, 2)) end = as_server_time(datetime(2015, 1, 3)) resolution = timedelta(minutes=15) soc_at_start = battery.get_attribute("soc_in_mwh") soc_min = 0.5 soc_max = 4.5 schedule = schedule_battery( battery, start, end, resolution, soc_at_start, soc_min=soc_min, soc_max=soc_max, roundtrip_efficiency=roundtrip_efficiency, ) soc_schedule = integrate_time_series(schedule, soc_at_start, decimal_precision=6) with pd.option_context("display.max_rows", None, "display.max_columns", 3): print(soc_schedule) # Check if constraints were met assert min(schedule.values) >= battery.get_attribute("capacity_in_mw") * -1 assert max( schedule.values) <= battery.get_attribute("capacity_in_mw") + TOLERANCE for soc in soc_schedule.values: assert soc >= max(soc_min, battery.get_attribute("min_soc_in_mwh")) assert soc <= battery.get_attribute("max_soc_in_mwh") # Check whether the resulting soc schedule follows our expectations for 8 expensive, 8 cheap and 8 expensive hours assert soc_schedule.iloc[-1] == max( soc_min, battery.get_attribute("min_soc_in_mwh") ) # Battery sold out at the end of its planning horizon # As long as the roundtrip efficiency isn't too bad (I haven't computed the actual switch point) if roundtrip_efficiency > 0.9: assert soc_schedule.loc[start + timedelta(hours=8)] == max( soc_min, battery.get_attribute( "min_soc_in_mwh")) # Sell what you begin with assert soc_schedule.loc[start + timedelta(hours=16)] == min( soc_max, battery.get_attribute( "max_soc_in_mwh")) # Buy what you can to sell later else: # If the roundtrip efficiency is poor, best to stand idle assert soc_schedule.loc[start + timedelta( hours=8)] == battery.get_attribute("soc_in_mwh") assert soc_schedule.loc[start + timedelta( hours=16)] == battery.get_attribute("soc_in_mwh")
def make_schedule( asset_id: int, start: datetime, end: datetime, belief_time: datetime, resolution: timedelta, soc_at_start: Optional[float] = None, soc_targets: Optional[pd.Series] = None, ) -> bool: """Preferably, a starting soc is given. Otherwise, we try to retrieve the current state of charge from the asset (if that is the valid one at the start). Otherwise, we set the starting soc to 0 (some assets don't use the concept of a state of charge, and without soc targets and limits the starting soc doesn't matter). """ # https://docs.sqlalchemy.org/en/13/faq/connections.html#how-do-i-use-engines-connections-sessions-with-python-multiprocessing-or-os-fork db.engine.dispose() rq_job = get_current_job() # find asset asset = Asset.query.filter_by(id=asset_id).one_or_none() click.echo( "Running Scheduling Job %s: %s, from %s to %s" % (rq_job.id, asset, start, end) ) if soc_at_start is None: if start == asset.soc_datetime and asset.soc_in_mwh is not None: soc_at_start = asset.soc_in_mwh else: soc_at_start = 0 if soc_targets is None: soc_targets = pd.Series( np.nan, index=pd.date_range(start, end, freq=resolution, closed="right") ) if asset.asset_type_name == "battery": consumption_schedule = schedule_battery( asset, asset.market, start, end, resolution, soc_at_start, soc_targets ) elif asset.asset_type_name in ( "one-way_evse", "two-way_evse", ): consumption_schedule = schedule_charging_station( asset, asset.market, start, end, resolution, soc_at_start, soc_targets ) else: raise ValueError( "Scheduling is not supported for asset type %s." % asset.asset_type ) data_source = get_data_source( data_source_name="Seita", data_source_type="scheduling script", ) click.echo("Job %s made schedule." % rq_job.id) ts_value_schedule = [ Power( datetime=dt, horizon=dt.astimezone(pytz.utc) - belief_time.astimezone(pytz.utc), value=-value, asset_id=asset_id, data_source_id=data_source.id, ) for dt, value in consumption_schedule.items() ] # For consumption schedules, positive values denote consumption. For the db, consumption is negative try: save_to_session(ts_value_schedule) except IntegrityError as e: current_app.logger.warning(e) click.echo("Rolling back due to IntegrityError") db.session.rollback() if current_app.config.get("FLEXMEASURES_MODE", "") == "play": click.echo("Saving again, with overwrite=True") save_to_session(ts_value_schedule, overwrite=True) db.session.commit() return True