linear_optimization.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630
  1. from __future__ import annotations
  2. from flask import current_app
  3. import pandas as pd
  4. import numpy as np
  5. from pandas.tseries.frequencies import to_offset
  6. from pyomo.core import (
  7. ConcreteModel,
  8. Var,
  9. RangeSet,
  10. Set,
  11. Param,
  12. Reals,
  13. NonNegativeReals,
  14. NonPositiveReals,
  15. Binary,
  16. Constraint,
  17. Objective,
  18. minimize,
  19. )
  20. from pyomo.environ import UnknownSolver # noqa F401
  21. from pyomo.environ import value
  22. from pyomo.opt import SolverFactory, SolverResults
  23. from flexmeasures.data.models.planning import (
  24. Commitment,
  25. FlowCommitment,
  26. StockCommitment,
  27. )
  28. from flexmeasures.data.models.planning.utils import initialize_series, initialize_df
  29. from flexmeasures.utils.calculations import apply_stock_changes_and_losses
  30. infinity = float("inf")
  31. def device_scheduler( # noqa C901
  32. device_constraints: list[pd.DataFrame],
  33. ems_constraints: pd.DataFrame,
  34. commitment_quantities: list[pd.Series] | None = None,
  35. commitment_downwards_deviation_price: list[pd.Series] | list[float] | None = None,
  36. commitment_upwards_deviation_price: list[pd.Series] | list[float] | None = None,
  37. commitments: list[pd.DataFrame] | list[Commitment] | None = None,
  38. initial_stock: float | list[float] = 0,
  39. ) -> tuple[list[pd.Series], float, SolverResults, ConcreteModel]:
  40. """This generic device scheduler is able to handle an EMS with multiple devices,
  41. with various types of constraints on the EMS level and on the device level,
  42. and with multiple market commitments on the EMS level.
  43. A typical example is a house with many devices.
  44. The commitments are assumed to be with regard to the flow of energy to the device (positive for consumption,
  45. negative for production). The solver minimises the costs of deviating from the commitments.
  46. :param device_constraints: Device constraints are on a device level. Handled constraints (listed by column name):
  47. max: maximum stock assuming an initial stock of zero (e.g. in MWh or boxes)
  48. min: minimum stock assuming an initial stock of zero
  49. equal: exact amount of stock (we do this by clamping min and max)
  50. efficiency: amount of stock left at the next datetime (the rest is lost)
  51. derivative max: maximum flow (e.g. in MW or boxes/h)
  52. derivative min: minimum flow
  53. derivative equals: exact amount of flow (we do this by clamping derivative min and derivative max)
  54. derivative down efficiency: conversion efficiency of flow out of a device (flow out : stock decrease)
  55. derivative up efficiency: conversion efficiency of flow into a device (stock increase : flow in)
  56. stock delta: predefined stock delta to apply to the storage device. Positive values cause an increase and negative values a decrease
  57. :param ems_constraints: EMS constraints are on an EMS level. Handled constraints (listed by column name):
  58. derivative max: maximum flow
  59. derivative min: minimum flow
  60. :param commitments: Commitments are on an EMS level by default. Handled parameters (listed by column name):
  61. quantity: for example, 5.5
  62. downwards deviation price: 10.1
  63. upwards deviation price: 10.2
  64. group: 1 (defaults to the enumerate time step j)
  65. device: 0 (corresponds to device d; if not set, commitment is on an EMS level)
  66. :param initial_stock: initial stock for each device. Use a list with the same number of devices as device_constraints,
  67. or use a single value to set the initial stock to be the same for all devices.
  68. Potentially deprecated arguments:
  69. commitment_quantities: amounts of flow specified in commitments (both previously ordered and newly requested)
  70. - e.g. in MW or boxes/h
  71. commitment_downwards_deviation_price: penalty for downwards deviations of the flow
  72. - e.g. in EUR/MW or EUR/(boxes/h)
  73. - either a single value (same value for each flow value) or a Series (different value for each flow value)
  74. commitment_upwards_deviation_price: penalty for upwards deviations of the flow
  75. Separate costs for each commitment are stored in a dictionary under `model.commitment_costs` (indexed by commitment).
  76. All Series and DataFrames should have the same resolution.
  77. For now, we pass in the various constraints and prices as separate variables, from which we make a MultiIndex
  78. DataFrame. Later we could pass in a MultiIndex DataFrame directly.
  79. """
  80. model = ConcreteModel()
  81. # If the EMS has no devices, don't bother
  82. if len(device_constraints) == 0:
  83. return [], 0, SolverResults(), model
  84. # Get timing from first device
  85. start = device_constraints[0].index.to_pydatetime()[0]
  86. # Workaround for https://github.com/pandas-dev/pandas/issues/53643. Was: resolution = pd.to_timedelta(device_constraints[0].index.freq)
  87. resolution = pd.to_timedelta(device_constraints[0].index.freq).to_pytimedelta()
  88. end = device_constraints[0].index.to_pydatetime()[-1] + resolution
  89. # Move commitments from old structure to new
  90. if commitments is None:
  91. commitments = []
  92. else:
  93. commitments = [
  94. c.to_frame() if isinstance(c, Commitment) else c for c in commitments
  95. ]
  96. if commitment_quantities is not None:
  97. for quantity, down, up in zip(
  98. commitment_quantities,
  99. commitment_downwards_deviation_price,
  100. commitment_upwards_deviation_price,
  101. ):
  102. # Turn prices per commitment into prices per commitment flow
  103. if all(isinstance(price, float) for price in down) or isinstance(
  104. down, float
  105. ):
  106. down = initialize_series(down, start, end, resolution)
  107. if all(isinstance(price, float) for price in up) or isinstance(up, float):
  108. up = initialize_series(up, start, end, resolution)
  109. group = initialize_series(list(range(len(down))), start, end, resolution)
  110. df = initialize_df(
  111. ["quantity", "downwards deviation price", "upwards deviation price"],
  112. start,
  113. end,
  114. resolution,
  115. )
  116. df["quantity"] = quantity
  117. df["downwards deviation price"] = down
  118. df["upwards deviation price"] = up
  119. df["group"] = group
  120. commitments.append(df)
  121. # Check if commitments have the same time window and resolution as the constraints
  122. for commitment in commitments:
  123. start_c = commitment.index.to_pydatetime()[0]
  124. resolution_c = pd.to_timedelta(commitment.index.freq)
  125. end_c = commitment.index.to_pydatetime()[-1] + resolution
  126. if not (start_c == start and end_c == end):
  127. raise Exception(
  128. "Not implemented for different time windows.\n(%s,%s)\n(%s,%s)"
  129. % (start, end, start_c, end_c)
  130. )
  131. if resolution_c != resolution:
  132. raise Exception(
  133. "Not implemented for different resolutions.\n%s\n%s"
  134. % (resolution, resolution_c)
  135. )
  136. def convert_commitments_to_subcommitments(
  137. dfs: list[pd.DataFrame],
  138. ) -> tuple[list[pd.DataFrame], dict[int, int]]:
  139. """Transform commitments, each specifying a group for each time step, to sub-commitments, one per group.
  140. 'Groups' are a commitment concept (grouping time slots of a commitment),
  141. making it possible that deviations/breaches can be accounted for properly within this group
  142. (e.g. highest breach per calendar month defines the penalty).
  143. Here, we define sub-commitments, by separating commitments by group and by direction of deviation (up, down).
  144. We also enumerate the time steps in a new column "j".
  145. For example, given contracts A and B (represented by 2 DataFrames), each with 3 groups,
  146. we return (sub)commitments A1, A2, A3, B1, B2 and B3,
  147. where A,B,C is the enumerated contract and 1,2,3 is the enumerated group.
  148. """
  149. commitment_mapping = {}
  150. sub_commitments = []
  151. for c, df in enumerate(dfs):
  152. # Make sure each commitment has "device" (default NaN) and "class" (default FlowCommitment) columns
  153. if "device" not in df.columns:
  154. df["device"] = np.nan
  155. if "class" not in df.columns:
  156. df["class"] = FlowCommitment
  157. df["j"] = range(len(df.index))
  158. groups = list(df["group"].unique())
  159. for group in groups:
  160. sub_commitment = df[df["group"] == group].drop(columns=["group"])
  161. # Catch non-uniqueness
  162. if len(sub_commitment["upwards deviation price"].unique()) > 1:
  163. raise ValueError(
  164. "Commitment groups cannot have non-unique upwards deviation prices."
  165. )
  166. if len(sub_commitment["downwards deviation price"].unique()) > 1:
  167. raise ValueError(
  168. "Commitment groups cannot have non-unique downwards deviation prices."
  169. )
  170. if len(sub_commitment) == 1:
  171. commitment_mapping[len(sub_commitments)] = c
  172. sub_commitments.append(sub_commitment)
  173. else:
  174. down_commitment = sub_commitment.copy().drop(
  175. columns="upwards deviation price"
  176. )
  177. up_commitment = sub_commitment.copy().drop(
  178. columns="downwards deviation price"
  179. )
  180. commitment_mapping[len(sub_commitments)] = c
  181. commitment_mapping[len(sub_commitments) + 1] = c
  182. sub_commitments.extend([down_commitment, up_commitment])
  183. return sub_commitments, commitment_mapping
  184. commitments, commitment_mapping = convert_commitments_to_subcommitments(commitments)
  185. bigM_columns = ["derivative max", "derivative min", "derivative equals"]
  186. # Compute a good value for M
  187. M = np.nanmax([np.nanmax(d[bigM_columns].abs()) for d in device_constraints])
  188. # M has to be 1 MW, at least
  189. M = max(M, 1)
  190. for d in range(len(device_constraints)):
  191. if "stock delta" not in device_constraints[d].columns:
  192. device_constraints[d]["stock delta"] = 0
  193. else:
  194. device_constraints[d]["stock delta"] = (
  195. device_constraints[d]["stock delta"].astype(float).fillna(0)
  196. )
  197. # Add indices for devices (d), datetimes (j) and commitments (c)
  198. model.d = RangeSet(0, len(device_constraints) - 1, doc="Set of devices")
  199. model.j = RangeSet(
  200. 0, len(device_constraints[0].index.to_pydatetime()) - 1, doc="Set of datetimes"
  201. )
  202. model.c = RangeSet(0, len(commitments) - 1, doc="Set of commitments")
  203. # Add 2D indices for commitment datetimes (cj)
  204. def commitments_init(m):
  205. return ((c, j) for c in m.c for j in commitments[c]["j"])
  206. model.cj = Set(dimen=2, initialize=commitments_init)
  207. # Add parameters
  208. def price_down_select(m, c):
  209. if "downwards deviation price" not in commitments[c].columns:
  210. return 0
  211. price = commitments[c]["downwards deviation price"].iloc[0]
  212. if np.isnan(price):
  213. return 0
  214. return price
  215. def price_up_select(m, c):
  216. if "upwards deviation price" not in commitments[c].columns:
  217. return 0
  218. price = commitments[c]["upwards deviation price"].iloc[0]
  219. if np.isnan(price):
  220. return 0
  221. return price
  222. def commitment_quantity_select(m, c, j):
  223. quantity = commitments[c][commitments[c]["j"] == j]["quantity"].values[0]
  224. if np.isnan(quantity):
  225. return -infinity
  226. return quantity
  227. def device_max_select(m, d, j):
  228. min_v = device_constraints[d]["min"].iloc[j]
  229. max_v = device_constraints[d]["max"].iloc[j]
  230. equal_v = device_constraints[d]["equals"].iloc[j]
  231. if np.isnan(max_v) and np.isnan(equal_v):
  232. return infinity
  233. else:
  234. if not np.isnan(equal_v):
  235. # make min_v < equal_v
  236. equal_v = np.nanmax([equal_v, min_v])
  237. return np.nanmin([max_v, equal_v])
  238. def device_min_select(m, d, j):
  239. min_v = device_constraints[d]["min"].iloc[j]
  240. max_v = device_constraints[d]["max"].iloc[j]
  241. equal_v = device_constraints[d]["equals"].iloc[j]
  242. if np.isnan(min_v) and np.isnan(equal_v):
  243. return -infinity
  244. else:
  245. if not np.isnan(equal_v):
  246. # make equal_v <= max_v
  247. equal_v = np.nanmin([equal_v, max_v])
  248. return np.nanmax([min_v, equal_v])
  249. def device_derivative_max_select(m, d, j):
  250. max_v = device_constraints[d]["derivative max"].iloc[j]
  251. equal_v = device_constraints[d]["derivative equals"].iloc[j]
  252. if np.isnan(max_v) and np.isnan(equal_v):
  253. return infinity
  254. else:
  255. return np.nanmin([max_v, equal_v])
  256. def device_derivative_min_select(m, d, j):
  257. min_v = device_constraints[d]["derivative min"].iloc[j]
  258. equal_v = device_constraints[d]["derivative equals"].iloc[j]
  259. if np.isnan(min_v) and np.isnan(equal_v):
  260. return -infinity
  261. else:
  262. return np.nanmax([min_v, equal_v])
  263. def ems_derivative_max_select(m, j):
  264. v = ems_constraints["derivative max"].iloc[j]
  265. if np.isnan(v):
  266. return infinity
  267. else:
  268. return v
  269. def ems_derivative_min_select(m, j):
  270. v = ems_constraints["derivative min"].iloc[j]
  271. if np.isnan(v):
  272. return -infinity
  273. else:
  274. return v
  275. def device_efficiency(m, d, j):
  276. """Assume perfect efficiency if no efficiency information is available."""
  277. try:
  278. eff = device_constraints[d]["efficiency"].iloc[j]
  279. except KeyError:
  280. return 1
  281. if np.isnan(eff):
  282. return 1
  283. return eff
  284. def device_derivative_down_efficiency(m, d, j):
  285. """Assume perfect efficiency if no efficiency information is available."""
  286. try:
  287. eff = device_constraints[d]["derivative down efficiency"].iloc[j]
  288. except KeyError:
  289. return 1
  290. if np.isnan(eff):
  291. return 1
  292. return eff
  293. def device_derivative_up_efficiency(m, d, j):
  294. """Assume perfect efficiency if no efficiency information is available."""
  295. try:
  296. eff = device_constraints[d]["derivative up efficiency"].iloc[j]
  297. except KeyError:
  298. return 1
  299. if np.isnan(eff):
  300. return 1
  301. return eff
  302. def device_stock_delta(m, d, j):
  303. return device_constraints[d]["stock delta"].iloc[j]
  304. model.up_price = Param(model.c, initialize=price_up_select)
  305. model.down_price = Param(model.c, initialize=price_down_select)
  306. model.commitment_quantity = Param(
  307. model.cj, domain=Reals, initialize=commitment_quantity_select
  308. )
  309. model.device_max = Param(model.d, model.j, initialize=device_max_select)
  310. model.device_min = Param(model.d, model.j, initialize=device_min_select)
  311. model.device_derivative_max = Param(
  312. model.d, model.j, initialize=device_derivative_max_select
  313. )
  314. model.device_derivative_min = Param(
  315. model.d, model.j, initialize=device_derivative_min_select
  316. )
  317. model.ems_derivative_max = Param(model.j, initialize=ems_derivative_max_select)
  318. model.ems_derivative_min = Param(model.j, initialize=ems_derivative_min_select)
  319. model.device_efficiency = Param(model.d, model.j, initialize=device_efficiency)
  320. model.device_derivative_down_efficiency = Param(
  321. model.d, model.j, initialize=device_derivative_down_efficiency
  322. )
  323. model.device_derivative_up_efficiency = Param(
  324. model.d, model.j, initialize=device_derivative_up_efficiency
  325. )
  326. model.stock_delta = Param(model.d, model.j, initialize=device_stock_delta)
  327. # Add variables
  328. model.ems_power = Var(model.d, model.j, domain=Reals, initialize=0)
  329. model.device_power_down = Var(
  330. model.d, model.j, domain=NonPositiveReals, initialize=0
  331. )
  332. model.device_power_up = Var(model.d, model.j, domain=NonNegativeReals, initialize=0)
  333. model.device_power_sign = Var(model.d, model.j, domain=Binary, initialize=0)
  334. model.commitment_downwards_deviation = Var(
  335. model.c,
  336. domain=NonPositiveReals,
  337. initialize=0,
  338. # bounds=[-1000, None], # useful for debugging, to distinguish between infeasible and unbounded problems
  339. )
  340. model.commitment_upwards_deviation = Var(
  341. model.c,
  342. domain=NonNegativeReals,
  343. initialize=0,
  344. # bounds=[None, 1000],
  345. )
  346. def _get_stock_change(m, d, j):
  347. """Determine final stock change of device d until time j.
  348. Apply conversion efficiencies to conversion from flow to stock change and vice versa,
  349. and apply storage efficiencies to stock levels from one datetime to the next.
  350. """
  351. if isinstance(initial_stock, list):
  352. # No initial stock defined for inflexible device
  353. initial_stock_d = initial_stock[d] if d < len(initial_stock) else 0
  354. else:
  355. initial_stock_d = initial_stock
  356. stock_changes = [
  357. (
  358. m.device_power_down[d, k] / m.device_derivative_down_efficiency[d, k]
  359. + m.device_power_up[d, k] * m.device_derivative_up_efficiency[d, k]
  360. + m.stock_delta[d, k]
  361. )
  362. for k in range(0, j + 1)
  363. ]
  364. efficiencies = [m.device_efficiency[d, k] for k in range(0, j + 1)]
  365. final_stock_change = [
  366. stock - initial_stock_d
  367. for stock in apply_stock_changes_and_losses(
  368. initial_stock_d, stock_changes, efficiencies
  369. )
  370. ][-1]
  371. return final_stock_change
  372. # Add constraints as a tuple of (lower bound, value, upper bound)
  373. def device_bounds(m, d, j):
  374. """Constraints on the device's stock."""
  375. return (
  376. m.device_min[d, j],
  377. _get_stock_change(m, d, j),
  378. m.device_max[d, j],
  379. )
  380. def device_derivative_bounds(m, d, j):
  381. return (
  382. m.device_derivative_min[d, j],
  383. m.device_power_down[d, j] + m.device_power_up[d, j],
  384. m.device_derivative_max[d, j],
  385. )
  386. def device_down_derivative_bounds(m, d, j):
  387. """Strictly non-positive."""
  388. return (
  389. min(m.device_derivative_min[d, j], 0),
  390. m.device_power_down[d, j],
  391. 0,
  392. )
  393. def device_up_derivative_bounds(m, d, j):
  394. """Strictly non-negative."""
  395. return (
  396. 0,
  397. m.device_power_up[d, j],
  398. max(0, m.device_derivative_max[d, j]),
  399. )
  400. def device_up_derivative_sign(m, d, j):
  401. """Derivative up if sign points up, derivative not up if sign points down."""
  402. return m.device_power_up[d, j] <= M * m.device_power_sign[d, j]
  403. def device_down_derivative_sign(m, d, j):
  404. """Derivative down if sign points down, derivative not down if sign points up."""
  405. return -m.device_power_down[d, j] <= M * (1 - m.device_power_sign[d, j])
  406. def ems_derivative_bounds(m, j):
  407. return m.ems_derivative_min[j], sum(m.ems_power[:, j]), m.ems_derivative_max[j]
  408. def device_stock_commitment_equalities(m, c, j, d):
  409. """Couple device stocks to each commitment."""
  410. if (
  411. "device" not in commitments[c].columns
  412. or (commitments[c]["device"] != d).all()
  413. or m.commitment_quantity[c, j] == -infinity
  414. ):
  415. # Commitment c does not concern device d
  416. return Constraint.Skip
  417. # Determine center part of the lhs <= center part <= rhs constraint
  418. center_part = (
  419. m.commitment_quantity[c, j]
  420. + m.commitment_downwards_deviation[c]
  421. + m.commitment_upwards_deviation[c]
  422. )
  423. if commitments[c]["class"].apply(lambda cl: cl == StockCommitment).all():
  424. center_part -= _get_stock_change(m, d, j)
  425. elif commitments[c]["class"].apply(lambda cl: cl == FlowCommitment).all():
  426. center_part -= m.ems_power[d, j]
  427. else:
  428. raise NotImplementedError("Unknown commitment class")
  429. return (
  430. 0 if "upwards deviation price" in commitments[c].columns else None,
  431. center_part,
  432. 0 if "downwards deviation price" in commitments[c].columns else None,
  433. )
  434. def ems_flow_commitment_equalities(m, c, j):
  435. """Couple EMS flows (sum over devices) to each commitment.
  436. - Creates an inequality for one-sided commitments.
  437. - Creates an equality for two-sided commitments and for groups of size 1.
  438. """
  439. if (
  440. "device" in commitments[c].columns
  441. and not pd.isnull(commitments[c]["device"]).all()
  442. ) or m.commitment_quantity[c, j] == -infinity:
  443. # Commitment c does not concern EMS
  444. return Constraint.Skip
  445. if (
  446. "class" in commitments[c].columns
  447. and not (
  448. commitments[c]["class"].apply(lambda cl: cl == FlowCommitment)
  449. ).all()
  450. ):
  451. raise NotImplementedError(
  452. "StockCommitment on an EMS level has not been implemented. Please file a GitHub ticket explaining your use case."
  453. )
  454. return (
  455. (
  456. 0
  457. if len(commitments[c]) == 1
  458. or "upwards deviation price" in commitments[c].columns
  459. else None
  460. ),
  461. # 0 if "upwards deviation price" in commitments[c].columns else None, # todo: possible simplification
  462. m.commitment_quantity[c, j]
  463. + m.commitment_downwards_deviation[c]
  464. + m.commitment_upwards_deviation[c]
  465. - sum(m.ems_power[:, j]),
  466. (
  467. 0
  468. if len(commitments[c]) == 1
  469. or "downwards deviation price" in commitments[c].columns
  470. else None
  471. ),
  472. # 0 if "downwards deviation price" in commitments[c].columns else None, # todo: possible simplification
  473. )
  474. def device_derivative_equalities(m, d, j):
  475. """Couple device flows to EMS flows per device."""
  476. return (
  477. 0,
  478. m.device_power_up[d, j] + m.device_power_down[d, j] - m.ems_power[d, j],
  479. 0,
  480. )
  481. model.device_energy_bounds = Constraint(model.d, model.j, rule=device_bounds)
  482. model.device_power_bounds = Constraint(
  483. model.d, model.j, rule=device_derivative_bounds
  484. )
  485. model.device_power_down_bounds = Constraint(
  486. model.d, model.j, rule=device_down_derivative_bounds
  487. )
  488. model.device_power_up_bounds = Constraint(
  489. model.d, model.j, rule=device_up_derivative_bounds
  490. )
  491. model.device_power_up_sign = Constraint(
  492. model.d, model.j, rule=device_up_derivative_sign
  493. )
  494. model.device_power_down_sign = Constraint(
  495. model.d, model.j, rule=device_down_derivative_sign
  496. )
  497. model.ems_power_bounds = Constraint(model.j, rule=ems_derivative_bounds)
  498. model.ems_power_commitment_equalities = Constraint(
  499. model.cj, rule=ems_flow_commitment_equalities
  500. )
  501. model.device_energy_commitment_equalities = Constraint(
  502. model.cj, model.d, rule=device_stock_commitment_equalities
  503. )
  504. model.device_power_equalities = Constraint(
  505. model.d, model.j, rule=device_derivative_equalities
  506. )
  507. # Add objective
  508. def cost_function(m):
  509. costs = 0
  510. m.commitment_costs = {
  511. c: m.commitment_downwards_deviation[c] * m.down_price[c]
  512. + m.commitment_upwards_deviation[c] * m.up_price[c]
  513. for c in m.c
  514. }
  515. for c in m.c:
  516. costs += m.commitment_costs[c]
  517. return costs
  518. model.costs = Objective(rule=cost_function, sense=minimize)
  519. # Solve
  520. solver_name = current_app.config.get("FLEXMEASURES_LP_SOLVER")
  521. solver = SolverFactory(solver_name)
  522. # disable logs for the HiGHS solver in case that LOGGING_LEVEL is INFO
  523. if current_app.config["LOGGING_LEVEL"] == "INFO" and (
  524. "highs" in solver_name.lower()
  525. ):
  526. solver.options["output_flag"] = "false"
  527. # load_solutions=False to avoid a RuntimeError exception in appsi solvers when solving an infeasible problem.
  528. results = solver.solve(model, load_solutions=False)
  529. # load the results only if a feasible solution has been found
  530. if len(results.solution) > 0:
  531. model.solutions.load_from(results)
  532. planned_costs = value(model.costs)
  533. subcommitment_costs = {g: value(cost) for g, cost in model.commitment_costs.items()}
  534. commitment_costs = {}
  535. # Map subcommitment costs to commitments
  536. for g, v in subcommitment_costs.items():
  537. c = commitment_mapping[g]
  538. commitment_costs[c] = commitment_costs.get(c, 0) + v
  539. planned_power_per_device = []
  540. for d in model.d:
  541. planned_device_power = [model.ems_power[d, j].value for j in model.j]
  542. planned_power_per_device.append(
  543. initialize_series(
  544. data=planned_device_power,
  545. start=start,
  546. end=end,
  547. resolution=to_offset(resolution),
  548. )
  549. )
  550. model.commitment_costs = commitment_costs
  551. # model.pprint()
  552. # model.display()
  553. # print(results.solver.termination_condition)
  554. # print(planned_costs)
  555. return planned_power_per_device, planned_costs, results, model