• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

oemof / oemof-solph / 17377487432

01 Sep 2025 12:22PM UTC coverage: 81.765% (-5.8%) from 87.55%
17377487432

Pull #1174

github

web-flow
Merge 553ade8ee into 49e9272c9
Pull Request #1174: Feature/custom attributes for investments

925 of 1228 branches covered (75.33%)

Branch coverage included in aggregate %.

2 of 17 new or added lines in 2 files covered. (11.76%)

179 existing lines in 7 files now uncovered.

2707 of 3214 relevant lines covered (84.23%)

0.84 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

17.31
/src/oemof/solph/constraints/investment_limit.py
1
# -*- coding: utf-8 -*-
2

3
"""Limits for investments.
4

5
SPDX-FileCopyrightText: Uwe Krien <krien@uni-bremen.de>
6
SPDX-FileCopyrightText: Simon Hilpert
7
SPDX-FileCopyrightText: Patrik Schönfeldt
8
SPDX-FileCopyrightText: Johannes Röder
9
SPDX-FileCopyrightText: Johannes Kochems
10
SPDX-FileCopyrightText: Johannes Giehl
11

12
SPDX-License-Identifier: MIT
13

14
"""
15

16
from pyomo import environ as po
1✔
17

18
from oemof.solph._plumbing import sequence
1✔
19

20

21
def investment_limit(model, limit=None):
1✔
22
    r"""Set an absolute limit for the total investment costs of an investment
23
    optimization problem (over all periods in case of a multi-period model):
24

25
    .. math:: \sum_{investment\_costs} \leq limit
26

27
    Parameters
28
    ----------
29
    model : oemof.solph._models.Model
30
        Model to which the constraint is added
31
    limit : float
32
        Absolute limit of the investment (i.e. RHS of constraint)
33
    """
34

35
    def investment_rule(m):
×
36
        expr = 0
×
37

38
        if hasattr(m, "InvestmentFlowBlock"):
×
39
            expr += m.InvestmentFlowBlock.investment_costs
×
40

41
        if hasattr(m, "GenericInvestmentStorageBlock"):
×
42
            expr += m.GenericInvestmentStorageBlock.investment_costs
×
43

44
        if hasattr(m, "SinkDSMOemofInvestmentBlock"):
×
45
            expr += m.SinkDSMOemofInvestmentBlock.investment_costs
×
46

47
        if hasattr(m, "SinkDSMDIWInvestmentBlock"):
×
48
            expr += m.SinkDSMDIWInvestmentBlock.investment_costs
×
49

50
        if hasattr(m, "SinkDSMDLRInvestmentBlock"):
×
51
            expr += m.SinkDSMDLRInvestmentBlock.investment_costs
×
52

53
        return expr <= limit
×
54

55
    model.investment_limit = po.Constraint(rule=investment_rule)
×
56

57
    return model
×
58

59

60
def investment_limit_per_period(model, limit=None):
1✔
61
    r"""Set an absolute limit for the total investment costs of a
62
    investment optimization problem for each period
63
    of the multi-period problem.
64

65
    .. math::
66
        \sum_{investment\_costs(p)} \leq limit(p)
67
        \forall p \in \textrm{PERIODS}
68

69
    Parameters
70
    ----------
71
    model : oemof.solph.Model
72
        Model to which the constraint is added
73
    limit : sequence of float, :math:`limit(p)`
74
        Absolute limit of the investment for each period
75
        (i.e. RHS of constraint)
76
    """
77

78
    if model.es.periods is None:
×
79
        msg = (
×
80
            "investment_limit_per_period is only applicable "
81
            "for multi-period models.\nIn order to create such a model, "
82
            "explicitly set attribute `periods` of your energy system."
83
        )
84
        raise ValueError(msg)
×
85

86
    if limit is not None:
×
87
        limit = sequence(limit)
×
88
    else:
89
        msg = (
×
90
            "You have to provide an investment limit for each period!\n"
91
            "If you provide a scalar value, this will be applied as a "
92
            "limit for each period."
93
        )
94
        raise ValueError(msg)
×
95

96
    def investment_period_rule(m, p):
×
97
        expr = 0
×
98

99
        if hasattr(m, "InvestmentFlowBlock"):
×
100
            expr += m.InvestmentFlowBlock.period_investment_costs[p]
×
101

102
        if hasattr(m, "GenericInvestmentStorageBlock"):
×
103
            expr += m.GenericInvestmentStorageBlock.period_investment_costs[p]
×
104

105
        if hasattr(m, "SinkDSMOemofInvestmentBlock"):
×
106
            expr += m.SinkDSMOemofInvestmentBlock.period_investment_costs[p]
×
107

108
        if hasattr(m, "SinkDSMDIWInvestmentBlock"):
×
109
            expr += m.SinkDSMDIWInvestmentBlock.period_investment_costs[p]
×
110

111
        if hasattr(m, "SinkDSMDLRInvestmentBlock"):
×
112
            expr += m.SinkDSMDLRInvestmentBlock.period_investment_costs[p]
×
113

114
        return expr <= limit[p]
×
115

116
    model.investment_limit_per_period = po.Constraint(
×
117
        model.PERIODS, rule=investment_period_rule
118
    )
119

120
    return model
×
121

122

123
def additional_investment_flow_limit(model, keyword, limit=None):
1✔
124
    r"""
125
    Global limit for investment flows weighted by an attribute keyword.
126

127
    This constraint is only valid for Flows not for components such as an
128
    investment storage.
129

130
    The attribute named by keyword has to be added to every Investment
131
    attribute of the flow you want to take into account.
132
    Total value of keyword attributes after optimization can be retrieved
133
    calling the `oemof.solph._models.Model.invest_limit_${keyword}()`.
134

135
    .. math::
136
        \sum_{p \in \textrm{PERIODS}}
137
        \sum_{i \in IF}  P_{i}(p) \cdot w_i \leq limit
138

139
    With `IF` being the set of InvestmentFlows considered for the integral
140
    limit.
141

142
    The symbols used are defined as follows
143
    (with Variables (V) and Parameters (P)):
144

145
    +------------------+---------------------------------------+------+--------------------------------------------------------------+
146
    | symbol           | attribute                             | type | explanation                                                  |
147
    +==================+=======================================+======+==============================================================+
148
    | :math:`P_{i}(p)` | `InvestmentFlowBlock.invest[i, o, p]` | V    | invested capacity of investment flow in period p             |
149
    +------------------+---------------------------------------+------+--------------------------------------------------------------+
150
    | :math:`w_i`      | `keyword`                             | P    | weight given to investment flow named according to `keyword` |
151
    +------------------+---------------------------------------+------+--------------------------------------------------------------+
152
    | :math:`limit`    | `limit`                               | P    | global limit given by keyword `limit`                        |
153
    +------------------+---------------------------------------+------+--------------------------------------------------------------+
154

155
    Parameters
156
    ----------
157
    model : oemof.solph.Model
158
        Model to which constraints are added.
159
    keyword : attribute to consider
160
        All flows with Investment attribute containing the keyword will be
161
        used.
162
    limit : numeric
163
        Global limit of keyword attribute for the energy system.
164

165
    Note
166
    ----
167
    The Investment attribute of the considered (Investment-)flows requires an
168
    attribute named like keyword!
169

170
    Examples
171
    --------
172
    >>> import pandas as pd
173
    >>> from oemof import solph
174
    >>> date_time_index = pd.date_range('1/1/2020', periods=6, freq='h')
175
    >>> es = solph.EnergySystem(
176
    ...     timeindex=date_time_index,
177
    ...     infer_last_interval=False,
178
    ... )
179
    >>> bus = solph.buses.Bus(label='bus_1')
180
    >>> sink = solph.components.Sink(label="sink", inputs={bus:
181
    ...     solph.flows.Flow(nominal_capacity=10, fix=[10, 20, 30, 40, 50])})
182
    >>> src1 = solph.components.Source(
183
    ...     label='source_0', outputs={bus: solph.flows.Flow(
184
    ...         nominal_capacity=solph.Investment(
185
    ...             ep_costs=50, custom_attributes={"space": 4},
186
    ...         ))
187
    ...     })
188
    >>> src2 = solph.components.Source(
189
    ...     label='source_1', outputs={bus: solph.flows.Flow(
190
    ...         nominal_capacity=solph.Investment(
191
    ...              ep_costs=100, custom_attributes={"space": 1},
192
    ...         ))
193
    ...     })
194
    >>> es.add(bus, sink, src1, src2)
195
    >>> model = solph.Model(es)
196
    >>> model = solph.constraints.additional_investment_flow_limit(
197
    ...     model, "space", limit=1500)
198
    >>> a = model.solve(solver="cbc")
199
    >>> int(round(model.invest_limit_space()))
200
    1500
201
    """  # noqa: E501
202
    invest_flows = {}
1✔
203

204
    for i, o in model.flows:
1✔
205
        if hasattr(model.flows[i, o].investment, keyword):
1✔
206
            invest_flows[(i, o)] = model.flows[i, o].investment
1✔
207

208
    limit_name = "invest_limit_" + keyword
1✔
209

210
    setattr(
1✔
211
        model,
212
        limit_name,
213
        po.Expression(
214
            expr=sum(
215
                model.InvestmentFlowBlock.invest[inflow, outflow, p]
216
                * getattr(invest_flows[inflow, outflow], keyword)
217
                for (inflow, outflow) in invest_flows
218
                for p in model.PERIODS
219
            )
220
        ),
221
    )
222

223
    setattr(
1✔
224
        model,
225
        limit_name + "_constraint",
226
        po.Constraint(expr=(getattr(model, limit_name) <= limit)),
227
    )
228

229
    return model
1✔
230

231
def additional_total_limit(model, keyword, limit=None):
1✔
232
    r"""
233
    Global limit for investment flows and operation flows
234
    weighted by an attribute keyword.
235

236
    This constraint is  valid for Flows and for an
237
    investment storage.
238

239
    The attribute named by keyword has to be added to every Investment
240
    attribute of the flow you want to take into account.
241
    Total value of keyword attributes after optimization can be retrieved
242
    calling the `oemof.solph._models.Model.total_limit_${keyword}()`.
243

244
    .. math::
245
        \sum_{p \in \textrm{PERIODS}}
246
        \sum_{i \in IF}  P_{i}(p) \cdot w_i
247
        \sum_{i \in F_E} \sum_{t \in T} P_i(p, t) \cdot w_i(t)
248
               \cdot \tau(t) \leq limit
249

250
    With `IF` being the set of InvestmentFlows considered for the integral
251
    limit,  `F_I` being the set of flows considered for the integral limit and
252
    `T` being the set of time steps.
253

254
    The symbols used are defined as follows
255
    (with Variables (V) and Parameters (P)):
256

257
    +------------------+---------------------------------------+------+--------------------------------------------------------------+
258
    | symbol           | attribute                             | type | explanation                                                  |
259
    +==================+=======================================+======+==============================================================+
260
    | :math:`P_{i}(p)` | `InvestmentFlowBlock.invest[i, o, p]` | V    | invested capacity of investment flow in period p             |
261
    +------------------+---------------------------------------+------+--------------------------------------------------------------+
262
    | :math:`w_i`      | `keyword`                             | P    | weight given to investment flow named according to `keyword` |
263
    +------------------+---------------------------------------+------+--------------------------------------------------------------+
264
    | :math:`limit`    | `limit`                               | P    | global limit given by keyword `limit`                        |
265
    +------------------+---------------------------------------+------+--------------------------------------------------------------+
266
    | :math:`P_n(p, t)`| `limit`                               | P    | power flow :math:`n` at time index :math:`p, t`              |
267
    +------------------+---------------------------------------+------+--------------------------------------------------------------+
268
    | :math:`w_N(t)`   | `limit`                               | P    | weight given to Flow named according to `keyword`            |
269
    +------------------+---------------------------------------+------+--------------------------------------------------------------+
270
    | :math:`\tau(t)`  | `limit`                               | P    | width of time step :math:`t`                                 |
271
    +------------------+---------------------------------------+------+--------------------------------------------------------------+
272

273
    Parameters
274
    ----------
275
    model : oemof.solph.Model
276
        Model to which constraints are added.
277
    keyword : attribute to consider
278
        All flows with Investment attribute containing the keyword will be
279
        used.
280
    limit : numeric
281
        Global limit of keyword attribute for the energy system.
282

283
    Note
284
    ----
285
    The Investment attribute of the considered (Investment-)flows requires an
286
    attribute named like keyword!
287

288
    Examples
289
    --------
290
    >>> import pandas as pd
291
    >>> from oemof import solph
292
    >>> date_time_index = pd.date_range('1/1/2020', periods=6, freq='h')
293
    >>> es = solph.EnergySystem(
294
    ...     timeindex=date_time_index,
295
    ...     infer_last_interval=False,
296
    ... )
297
    >>> bus = solph.buses.Bus(label='bus_1')
298
    >>> sink = solph.components.Sink(label="sink", inputs={bus:
299
    ...     solph.flows.Flow(nominal_capacity=10, fix=[10, 20, 30, 40, 50])})
300
    >>> src1 = solph.components.Source(
301
    ...     label='source_0', outputs={bus: solph.flows.Flow(
302
    ...         nominal_capacity=solph.Investment(
303
    ...             ep_costs=50, custom_attributes={"space": 4},
304
    ...         ))
305
    ...     })
306
    >>> src2 = solph.components.Source(
307
    ...     label='source_1', outputs={bus: solph.flows.Flow(
308
    ...         nominal_capacity=solph.Investment(
309
    ...              ep_costs=100, custom_attributes={"space": 1},
310
    ...         ))
311
    ...     })
312
    >>> es.add(bus, sink, src1, src2)
313
    >>> model = solph.Model(es)
314
    >>> model = solph.constraints.additional_investment_flow_limit(
315
    ...     model, "space", limit=1500)
316
    >>> a = model.solve(solver="cbc")
317
    >>> int(round(model.invest_limit_space()))
318
    1500
319
    """  # noqa: E501
NEW
320
    invest_flows = {}
×
NEW
321
    operational_flows = {}
×
NEW
322
    storages = {}
×
NEW
323
    for i, o in model.flows:
×
NEW
324
        if hasattr(model.flows[i, o].investment, keyword):
×
NEW
325
            invest_flows[(i, o)] = model.flows[i, o].investment
×
NEW
326
        if hasattr(model.flows[i, o], keyword):
×
NEW
327
            operational_flows[(i, o)] = model.flows[i, o]
×
NEW
328
    limit_name = "total_limit_" + keyword
×
329

NEW
330
    if hasattr(model, "GenericInvestmentStorageBlock"):
×
NEW
331
        for st, _ in model.GenericInvestmentStorageBlock.invest:
×
NEW
332
            storages[st] = [st]
×
333

NEW
334
    setattr(
×
335
        model,
336
        limit_name,
337
        po.Expression(
338
            rule=lambda m:
339
            sum(
340
                model.InvestmentFlowBlock.invest[inflow, outflow, p]
341
                * getattr(invest_flows[inflow, outflow], keyword).get("cost", 0)
342
                + (
343
                    model.InvestmentFlowBlock.invest_status[inflow, outflow, p]
344
                    * getattr(invest_flows[inflow, outflow], keyword).get("offset", 0)
345
                    if (inflow, outflow, p) in model.InvestmentFlowBlock.invest_status
346
                    else 0
347
                )
348
                for (inflow, outflow) in invest_flows
349
                for p in model.PERIODS
350
            ) +
351
            sum(
352
                model.flow[inflow, outflow, t]
353
                * model.tsam_weighting[t]
354
                * model.timeincrement[t]
355
                * sequence(getattr(operational_flows[inflow, outflow], keyword))[t]
356
                for (inflow, outflow) in operational_flows
357
                for p in model.PERIODS
358
                for t in model.TIMESTEPS_IN_PERIOD[p]
359
            ) +
360
            sum(
361
                model.GenericInvestmentStorageBlock.invest[st, p]
362
                * getattr(storages[st][0].investment, keyword).get("cost", 0) / 2 
363
                + model.GenericInvestmentStorageBlock.invest_status[st, p]
364
                * getattr(storages[st][0].investment, keyword).get("offset", 0)
365
                if (st, p) in model.GenericInvestmentStorageBlock.invest_status else 0
366
                for st in storages
367
                for p in model.PERIODS
368
                if hasattr(model, "GenericInvestmentStorageBlock")
369
            )
370
        ),
371
    )
372

NEW
373
    setattr(
×
374
        model,
375
        limit_name + "_constraint",
376
        po.Constraint(expr=(getattr(model, limit_name) <= limit)),
377
    )
378

NEW
379
    return model
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc