conftest.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  1. from __future__ import annotations
  2. from datetime import timedelta
  3. import pytest
  4. from timely_beliefs.sensors.func_store.knowledge_horizons import at_date
  5. import pandas as pd
  6. from sqlalchemy import select
  7. from flexmeasures.data.models.generic_assets import GenericAsset, GenericAssetType
  8. from flexmeasures.data.models.planning.utils import initialize_index
  9. from flexmeasures.data.models.time_series import Sensor, TimedBelief
  10. from flexmeasures.utils.unit_utils import ur
  11. @pytest.fixture(params=["appsi_highs", "cbc"])
  12. def app_with_each_solver(app, request):
  13. """Set up the app config to run with different solvers.
  14. A test that uses this fixture runs all of its test cases with HiGHS and then again with Cbc.
  15. """
  16. original_solver = app.config["FLEXMEASURES_LP_SOLVER"]
  17. app.config["FLEXMEASURES_LP_SOLVER"] = request.param
  18. yield app
  19. # Restore original config setting for the solver
  20. app.config["FLEXMEASURES_LP_SOLVER"] = original_solver
  21. @pytest.fixture(scope="module", autouse=True)
  22. def setup_planning_test_data(db, add_market_prices, add_charging_station_assets):
  23. """
  24. Set up data for all planning tests.
  25. """
  26. print("Setting up data for planning tests on %s" % db.engine)
  27. return add_charging_station_assets
  28. @pytest.fixture(scope="module")
  29. def create_test_tariffs(db, setup_accounts, setup_sources) -> dict[str, Sensor]:
  30. """Create a fixed consumption tariff and a fixed feed-in tariff that is lower."""
  31. market_type = GenericAssetType(
  32. name="tariff market",
  33. )
  34. db.session.add(market_type)
  35. contract = GenericAsset(
  36. name="supply contract",
  37. generic_asset_type=market_type,
  38. owner=setup_accounts["Supplier"],
  39. )
  40. db.session.add(contract)
  41. consumption_price_sensor = Sensor(
  42. name="fixed consumption tariff",
  43. generic_asset=contract,
  44. event_resolution=timedelta(hours=24 * 365),
  45. unit="EUR/MWh",
  46. knowledge_horizon=(at_date, {"knowledge_time": "2014-11-01T00:00+01:00"}),
  47. )
  48. db.session.add(consumption_price_sensor)
  49. production_price_sensor = Sensor(
  50. name="fixed feed-in tariff",
  51. generic_asset=contract,
  52. event_resolution=timedelta(hours=24 * 365),
  53. unit="EUR/MWh",
  54. knowledge_horizon=(at_date, {"knowledge_time": "2014-11-01T00:00+01:00"}),
  55. )
  56. db.session.add(production_price_sensor)
  57. # Add prices
  58. consumption_price = TimedBelief(
  59. event_start="2015-01-01T00:00+01:00",
  60. belief_time="2014-11-01T00:00+01:00", # publication date
  61. event_value=300 * 1.21,
  62. source=setup_sources["Seita"],
  63. sensor=consumption_price_sensor,
  64. )
  65. db.session.add(consumption_price)
  66. production_price = TimedBelief(
  67. event_start="2015-01-01T00:00+01:00",
  68. belief_time="2014-11-01T00:00+01:00", # publication date
  69. event_value=300,
  70. source=setup_sources["Seita"],
  71. sensor=production_price_sensor,
  72. )
  73. db.session.add(production_price)
  74. db.session.flush() # make sure that prices are assigned to price sensors
  75. return {
  76. "consumption_price_sensor": consumption_price_sensor,
  77. "production_price_sensor": production_price_sensor,
  78. }
  79. @pytest.fixture(scope="module")
  80. def building(db, setup_accounts, setup_markets) -> GenericAsset:
  81. """
  82. Set up a building.
  83. """
  84. building_type = db.session.execute(
  85. select(GenericAssetType).filter_by(name="building")
  86. ).scalar_one_or_none()
  87. if not building_type:
  88. # create_test_battery_assets might have created it already
  89. building_type = GenericAssetType(name="battery")
  90. db.session.add(building_type)
  91. building = GenericAsset(
  92. name="building",
  93. generic_asset_type=building_type,
  94. owner=setup_accounts["Prosumer"],
  95. flex_context={
  96. "site-power-capacity": "2 MVA",
  97. "consumption-price": {"sensor": setup_markets["epex_da"].id},
  98. },
  99. )
  100. db.session.add(building)
  101. return building
  102. @pytest.fixture(scope="module")
  103. def flexible_devices(db, building) -> dict[str, Sensor]:
  104. """
  105. Set up power sensors for flexible devices:
  106. - A battery
  107. - A Charge Point (todo)
  108. """
  109. battery_sensor = Sensor(
  110. name="battery power sensor",
  111. generic_asset=building,
  112. event_resolution=timedelta(minutes=15),
  113. attributes=dict(
  114. capacity_in_mw=2,
  115. max_soc_in_mwh=5,
  116. min_soc_in_mwh=0,
  117. ),
  118. unit="MW",
  119. )
  120. db.session.add(battery_sensor)
  121. return {
  122. battery_sensor.name: battery_sensor,
  123. }
  124. @pytest.fixture(scope="module")
  125. def inflexible_devices(db, building) -> dict[str, Sensor]:
  126. """
  127. Set up power sensors for inflexible devices:
  128. - A PV panel
  129. - Residual building demand
  130. """
  131. pv_sensor = Sensor(
  132. name="PV power sensor",
  133. generic_asset=building,
  134. event_resolution=timedelta(hours=1),
  135. unit="MW",
  136. attributes={"capacity_in_mw": 2},
  137. )
  138. db.session.add(pv_sensor)
  139. residual_demand_sensor = Sensor(
  140. name="residual demand power sensor",
  141. generic_asset=building,
  142. event_resolution=timedelta(hours=1),
  143. unit="kW",
  144. attributes={"capacity_in_mw": 2},
  145. )
  146. db.session.add(residual_demand_sensor)
  147. return {
  148. pv_sensor.name: pv_sensor,
  149. residual_demand_sensor.name: residual_demand_sensor,
  150. }
  151. @pytest.fixture(scope="module")
  152. def add_inflexible_device_forecasts(
  153. db, inflexible_devices, setup_sources
  154. ) -> dict[Sensor, list[int | float]]:
  155. """
  156. Set up inflexible devices and forecasts.
  157. """
  158. # 2 days of test data
  159. time_slots = initialize_index(
  160. start=pd.Timestamp("2015-01-01").tz_localize("Europe/Amsterdam"),
  161. end=pd.Timestamp("2015-01-03").tz_localize("Europe/Amsterdam"),
  162. resolution="15min",
  163. )
  164. # PV (8 hours at zero capacity, 8 hours at 90% capacity, and again 8 hours at zero capacity)
  165. headroom = 0.1 # 90% of nominal capacity
  166. pv_sensor = inflexible_devices["PV power sensor"]
  167. capacity = pv_sensor.get_attribute("capacity_in_mw")
  168. pv_values = (
  169. [0] * (8 * 4) + [(1 - headroom) * capacity] * (8 * 4) + [0] * (8 * 4)
  170. ) * (len(time_slots) // (24 * 4))
  171. add_as_beliefs(db, pv_sensor, pv_values, time_slots, setup_sources["Seita"])
  172. # Residual demand (1 MW = 1000 kW continuously)
  173. residual_demand_sensor = inflexible_devices["residual demand power sensor"]
  174. residual_demand_values = [-1000] * len(time_slots)
  175. add_as_beliefs(
  176. db,
  177. residual_demand_sensor,
  178. residual_demand_values,
  179. time_slots,
  180. setup_sources["Seita"],
  181. )
  182. return {
  183. pv_sensor: pv_values,
  184. residual_demand_sensor: residual_demand_values,
  185. }
  186. @pytest.fixture(scope="module")
  187. def process(db, building, setup_sources) -> dict[str, Sensor]:
  188. """
  189. Set up a process sensor where the output of the optimization is stored.
  190. """
  191. _process = Sensor(
  192. name="Process",
  193. generic_asset=building,
  194. event_resolution=timedelta(hours=1),
  195. unit="kWh",
  196. )
  197. db.session.add(_process)
  198. return _process
  199. @pytest.fixture(scope="module")
  200. def efficiency_sensors(db, add_battery_assets, setup_sources) -> dict[str, Sensor]:
  201. battery = add_battery_assets["Test battery"]
  202. sensors = {}
  203. sensor_specs = [("efficiency", timedelta(minutes=15), 90)]
  204. for name, resolution, value in sensor_specs:
  205. # 1 days of test data
  206. time_slots = initialize_index(
  207. start=pd.Timestamp("2015-01-01").tz_localize("Europe/Amsterdam"),
  208. end=pd.Timestamp("2015-01-02").tz_localize("Europe/Amsterdam"),
  209. resolution=resolution,
  210. )
  211. efficiency_sensor = Sensor(
  212. name=name,
  213. unit="%",
  214. event_resolution=resolution,
  215. generic_asset=battery,
  216. )
  217. db.session.add(efficiency_sensor)
  218. db.session.flush()
  219. steps_in_hour = int(timedelta(hours=1) / resolution)
  220. efficiency = [value] * len(time_slots)
  221. add_as_beliefs(
  222. db,
  223. efficiency_sensor,
  224. efficiency[:-steps_in_hour],
  225. time_slots[:-steps_in_hour],
  226. setup_sources["Seita"],
  227. )
  228. sensors[name] = efficiency_sensor
  229. return sensors
  230. @pytest.fixture(scope="module")
  231. def add_stock_delta(db, add_battery_assets, setup_sources) -> dict[str, Sensor]:
  232. """
  233. Different usage forecast sensors are defined:
  234. - "delta fails": the usage forecast exceeds the maximum power.
  235. - "delta": the usage forecast can be fulfilled just right. This coincides with the schedule resolution.
  236. - "delta hourly": the event resolution is changed to test that the schedule is still feasible.
  237. This has a greater resolution.
  238. - "delta 5min": the event resolution is reduced even more. This sensor has a resolution smaller than that used
  239. for the scheduler.
  240. """
  241. battery = add_battery_assets["Test battery"]
  242. capacity = battery.get_attribute(
  243. "capacity_in_mw",
  244. ur.Quantity(battery.get_attribute("site-power-capacity")).to("MW").magnitude,
  245. )
  246. sensors = {}
  247. sensor_specs = [
  248. ("delta fails", timedelta(minutes=15), capacity * 1.2),
  249. ("delta", timedelta(minutes=15), capacity),
  250. ("delta hourly", timedelta(hours=1), capacity),
  251. ("delta 5min", timedelta(minutes=5), capacity),
  252. ]
  253. for name, resolution, value in sensor_specs:
  254. # 1 days of test data
  255. time_slots = initialize_index(
  256. start=pd.Timestamp("2015-01-01").tz_localize("Europe/Amsterdam"),
  257. end=pd.Timestamp("2015-01-02").tz_localize("Europe/Amsterdam"),
  258. resolution=resolution,
  259. )
  260. stock_delta_sensor = Sensor(
  261. name=name,
  262. unit="MW",
  263. event_resolution=resolution,
  264. generic_asset=battery,
  265. )
  266. db.session.add(stock_delta_sensor)
  267. db.session.flush()
  268. stock_gain = [value] * len(time_slots)
  269. add_as_beliefs(
  270. db,
  271. stock_delta_sensor,
  272. stock_gain,
  273. time_slots,
  274. setup_sources["Seita"],
  275. )
  276. sensors[name] = stock_delta_sensor
  277. return sensors
  278. @pytest.fixture(scope="module")
  279. def add_storage_efficiency(db, add_battery_assets, setup_sources) -> dict[str, Sensor]:
  280. """
  281. Fixture to add storage efficiency sensors and their beliefs to the database.
  282. This fixture creates several storage efficiency sensors with different characteristics
  283. and attaches them to a test battery asset.
  284. The sensor specifications include:
  285. - "storage efficiency 90%" with 15-minute resolution and 90% efficiency.
  286. - "storage efficiency 110%" with 15-minute resolution and 110% efficiency.
  287. - "storage efficiency negative" with 15-minute resolution and -90% efficiency.
  288. - "storage efficiency hourly" with 1-hour resolution and 90% efficiency.
  289. The function creates a day's worth of test data for each sensor starting from
  290. January 1, 2015.
  291. """
  292. battery = add_battery_assets["Test battery"]
  293. sensors = {}
  294. sensor_specs = [
  295. ("storage efficiency 90%", timedelta(minutes=15), 90),
  296. ("storage efficiency 110%", timedelta(minutes=15), 110),
  297. ("storage efficiency negative", timedelta(minutes=15), -90),
  298. ("storage efficiency hourly", timedelta(hours=1), 90),
  299. ]
  300. for name, resolution, value in sensor_specs:
  301. # 1 days of test data
  302. time_slots = initialize_index(
  303. start=pd.Timestamp("2015-01-01").tz_localize("Europe/Amsterdam"),
  304. end=pd.Timestamp("2015-01-02").tz_localize("Europe/Amsterdam"),
  305. resolution=resolution,
  306. )
  307. storage_efficiency_sensor = Sensor(
  308. name=name,
  309. unit="%",
  310. event_resolution=resolution,
  311. generic_asset=battery,
  312. )
  313. db.session.add(storage_efficiency_sensor)
  314. db.session.flush()
  315. efficiency_values = [value] * len(time_slots)
  316. add_as_beliefs(
  317. db,
  318. storage_efficiency_sensor,
  319. efficiency_values,
  320. time_slots,
  321. setup_sources["Seita"],
  322. )
  323. sensors[name] = storage_efficiency_sensor
  324. return sensors
  325. @pytest.fixture(scope="module")
  326. def add_soc_targets(db, add_battery_assets, setup_sources) -> dict[str, Sensor]:
  327. """
  328. Fixture to add storage SOC targets as sensors and their beliefs to the database.
  329. The function creates a single event at 14:00 + offset with a value of 0.5.
  330. """
  331. battery = add_battery_assets["Test battery"]
  332. soc_value = 0.5
  333. soc_datetime = pd.Timestamp("2015-01-01T14:00:00", tz="Europe/Amsterdam")
  334. sensors = {}
  335. sensor_specs = [
  336. # name, resolution, offset from the resolution tick
  337. ("soc-targets (1h)", timedelta(minutes=60), timedelta(minutes=0)),
  338. ("soc-targets (15min)", timedelta(minutes=15), timedelta(minutes=0)),
  339. ("soc-targets (15min lagged)", timedelta(minutes=15), timedelta(minutes=5)),
  340. ("soc-targets (5min)", timedelta(minutes=5), timedelta(minutes=0)),
  341. ("soc-targets (instantaneous)", timedelta(minutes=0), timedelta(minutes=0)),
  342. ]
  343. for name, resolution, offset in sensor_specs:
  344. storage_constraint_sensor = Sensor(
  345. name=name,
  346. unit="MWh",
  347. event_resolution=resolution,
  348. generic_asset=battery,
  349. )
  350. db.session.add(storage_constraint_sensor)
  351. db.session.flush()
  352. belief = TimedBelief(
  353. event_start=soc_datetime + offset,
  354. belief_horizon=timedelta(hours=100),
  355. event_value=soc_value,
  356. source=setup_sources["Seita"],
  357. sensor=storage_constraint_sensor,
  358. )
  359. db.session.add(belief)
  360. sensors[name] = storage_constraint_sensor
  361. return sensors
  362. def add_as_beliefs(db, sensor, values, time_slots, source):
  363. beliefs = [
  364. TimedBelief(
  365. event_start=dt,
  366. belief_time=time_slots[0],
  367. event_value=val,
  368. source=source,
  369. sensor=sensor,
  370. )
  371. for dt, val in zip(time_slots, values)
  372. ]
  373. db.session.add_all(beliefs)