Example #1
0
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
Example #2
0
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
Example #3
0
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")
Example #4
0
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
Example #5
0
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
Example #6
0
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")
Example #7
0
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