test_scheduling_simultaneous.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. import pytest
  2. import numpy as np
  3. import pandas as pd
  4. from flexmeasures.data.services.scheduling import create_simultaneous_scheduling_job
  5. from flexmeasures.data.tests.utils import work_on_rq
  6. from flexmeasures.data.models.time_series import Sensor
  7. @pytest.mark.parametrize("use_heterogeneous_resolutions", [True, False])
  8. def test_create_simultaneous_jobs(
  9. db, app, flex_description_sequential, smart_building, use_heterogeneous_resolutions
  10. ):
  11. assets, sensors, _ = smart_building
  12. queue = app.queues["scheduling"]
  13. start = pd.Timestamp("2015-01-03").tz_localize("Europe/Amsterdam")
  14. end = pd.Timestamp("2015-01-04").tz_localize("Europe/Amsterdam")
  15. scheduler_specs = {
  16. "module": "flexmeasures.data.models.planning.storage",
  17. "class": "StorageScheduler",
  18. }
  19. flex_description_sequential["start"] = start
  20. flex_description_sequential["end"] = end
  21. if use_heterogeneous_resolutions:
  22. flex_description_sequential["flex_model"][1]["sensor"] = sensors[
  23. "Test Battery 1h"
  24. ]
  25. job = create_simultaneous_scheduling_job(
  26. asset=assets["Test Site"],
  27. scheduler_specs=scheduler_specs,
  28. enqueue=True,
  29. **flex_description_sequential,
  30. )
  31. # The EV is scheduled firstly.
  32. assert job.kwargs["asset_or_sensor"] == {
  33. "id": assets["Test Site"].id,
  34. "class": "GenericAsset",
  35. }
  36. # It uses the inflexible-device-sensors that are defined in the flex-context, exclusively.
  37. assert job.kwargs["flex_context"]["inflexible-device-sensors"] == [
  38. sensors["Test Solar"].id,
  39. sensors["Test Building"].id,
  40. ]
  41. ev_power = sensors["Test EV"].search_beliefs()
  42. battery_power = sensors["Test Battery"].search_beliefs()
  43. assert ev_power.empty
  44. assert battery_power.empty
  45. # work tasks
  46. work_on_rq(queue)
  47. # check that the jobs complete successfully
  48. job.perform()
  49. assert job.get_status() == "finished"
  50. # Get power values
  51. ev_power = sensors["Test EV"].search_beliefs()
  52. assert ev_power.sources.unique()[0].model == "StorageScheduler"
  53. ev_power = ev_power.droplevel([1, 2, 3])
  54. if use_heterogeneous_resolutions:
  55. battery_power = sensors["Test Battery 1h"].search_beliefs()
  56. assert len(battery_power) == 24
  57. else:
  58. battery_power = sensors["Test Battery"].search_beliefs()
  59. assert len(battery_power) == 96
  60. assert battery_power.sources.unique()[0].model == "StorageScheduler"
  61. battery_power = battery_power.droplevel([1, 2, 3])
  62. start_charging = start + pd.Timedelta(hours=8)
  63. end_charging = start + pd.Timedelta(hours=10) - sensors["Test EV"].event_resolution
  64. # Check schedules
  65. assert (
  66. ev_power.loc[start_charging:end_charging] != -0.005
  67. ).values.any(), "no charging at full device power capacity (5 kW) expected"
  68. for target_no in (1, 2, 3):
  69. non_zero_target = flex_description_sequential["flex_model"][0][
  70. "sensor_flex_model"
  71. ]["soc-targets"][target_no]
  72. # NB: assumes perfect conversion and storage efficiencies
  73. np.testing.assert_approx_equal(
  74. # head(-1) because ev_power is indexed by event start and target datetime corresponds to event end
  75. # minus ev_power because ev_power uses negative values for consumption
  76. -ev_power[: non_zero_target["datetime"]].head(-1).sum()[0] / 4,
  77. non_zero_target["value"],
  78. )
  79. # Get price data
  80. price_sensor_id = flex_description_sequential["flex_context"][
  81. "consumption-price-sensor"
  82. ]
  83. price_sensor = db.session.get(Sensor, price_sensor_id)
  84. prices = price_sensor.search_beliefs(
  85. event_starts_after=start - pd.Timedelta(hours=1), event_ends_before=end
  86. )
  87. prices = prices.droplevel([1, 2, 3])
  88. prices.index = prices.index.tz_convert("Europe/Amsterdam")
  89. # Calculate costs
  90. ev_costs = (-ev_power.resample("1h").mean() * prices).sum().item()
  91. battery_costs = (-battery_power.resample("1h").mean() * prices).sum().item()
  92. total_cost = ev_costs + battery_costs
  93. # Define expected costs based on resolution
  94. expected_ev_costs = 2.2375
  95. expected_battery_costs = -5.515
  96. expected_total_cost = -3.2775
  97. # Check costs
  98. assert (
  99. round(total_cost, 4) == expected_total_cost
  100. ), f"Total costs should be €{expected_total_cost}, got €{total_cost}"
  101. assert (
  102. round(ev_costs, 4) == expected_ev_costs
  103. ), f"EV costs should be €{expected_ev_costs}, got €{ev_costs}"
  104. assert (
  105. round(battery_costs, 4) == expected_battery_costs
  106. ), f"Battery costs should be €{expected_battery_costs}, got €{battery_costs}"