Passed
Pull Request — dev (#821)
by Uwe
02:17
created

solph._models.BaseModel._add_parent_block_sets()   A

Complexity

Conditions 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 5
rs 10
c 0
b 0
f 0
cc 1
nop 1
1
# -*- coding: utf-8 -*-
2
3
"""Solph Optimization Models.
4
5
SPDX-FileCopyrightText: Uwe Krien <[email protected]>
6
SPDX-FileCopyrightText: Simon Hilpert
7
SPDX-FileCopyrightText: Cord Kaldemeyer
8
SPDX-FileCopyrightText: gplssm
9
SPDX-FileCopyrightText: Patrik Schönfeldt
10
11
SPDX-License-Identifier: MIT
12
13
"""
14
import logging
15
import warnings
16
17
from pyomo import environ as po
18
from pyomo.core.plugins.transform.relax_integrality import RelaxIntegrality
19
from pyomo.opt import SolverFactory
20
21
from oemof.solph import processing
22
from oemof.solph._plumbing import sequence
23
from oemof.solph.buses._bus import BusBlock
24
from oemof.solph.components._transformer import TransformerBlock
25
from oemof.solph.flows._flow import FlowBlock
26
from oemof.solph.flows._investment_flow import InvestmentFlowBlock
27
from oemof.solph.flows._non_convex_flow import NonConvexFlowBlock
28
29
30
class LoggingError(BaseException):
31
    """Raised when the wrong logging level is used."""
32
33
    pass
34
35
36
class BaseModel(po.ConcreteModel):
37
    """The BaseModel for other solph-models (Model, MultiPeriodModel, etc.)
38
39
    Parameters
40
    ----------
41
    energysystem : EnergySystem object
42
        Object that holds the nodes of an oemof energy system graph
43
    constraint_groups : list (optional)
44
        Solph looks for these groups in the given energy system and uses them
45
        to create the constraints of the optimization problem.
46
        Defaults to `Model.CONSTRAINTS`
47
    objective_weighting : array like (optional)
48
        Weights used for temporal objective function
49
        expressions. If nothing is passed `timeincrement` will be used which
50
        is calculated from the freq length of the energy system timeindex .
51
    auto_construct : boolean
52
        If this value is true, the set, variables, constraints, etc. are added,
53
        automatically when instantiating the model. For sequential model
54
        building process set this value to False
55
        and use methods `_add_parent_block_sets`,
56
        `_add_parent_block_variables`, `_add_blocks`, `_add_objective`
57
58
    Attributes:
59
    -----------
60
    timeincrement : sequence
61
        Time increments.
62
    flows : dict
63
        Flows of the model.
64
    name : str
65
        Name of the model.
66
    es : solph.EnergySystem
67
        Energy system of the model.
68
    meta : `pyomo.opt.results.results_.SolverResults` or None
69
        Solver results.
70
    dual : ... or None
71
    rc : ... or None
72
73
    """
74
75
    CONSTRAINT_GROUPS = []
76
77
    def __init__(self, energysystem, **kwargs):
78
        super().__init__()
79
80
        # Check root logger. Due to a problem with pyomo the building of the
81
        # model will take up to a 100 times longer if the root logger is set
82
        # to DEBUG
83
        from logging import getLogger
84
85
        if getLogger().level <= 10 and kwargs.get("debug", False) is False:
86
            msg = (
87
                "The root logger level is 'DEBUG'.\nDue to a communication "
88
                "problem between solph and the pyomo package,\nusing the "
89
                "DEBUG level will slow down the modelling process by the "
90
                "factor ~100.\nIf you need the debug-logging you can "
91
                "initialise the Model with 'debug=True`\nYou should only do "
92
                "this for small models. To avoid the slow-down use the "
93
                "logger\nfunction of oemof.tools (read docstring) or "
94
                "change the level of the root logger:\n\nimport logging\n"
95
                "logging.getLogger().setLevel(logging.INFO)"
96
            )
97
            raise LoggingError(msg)
98
99
        # ########################  Arguments #################################
100
101
        self.name = kwargs.get("name", type(self).__name__)
102
        self.es = energysystem
103
        self.timeincrement = sequence(
104
            kwargs.get("timeincrement", self.es.timeincrement)
105
        )
106
        if self.timeincrement[0] is None:
107
            try:
108
                self.timeincrement = sequence(
109
                    self.es.timeindex.freq.nanos / 3.6e12
110
                )
111
            except AttributeError:
112
                msg = (
113
                    "No valid time increment found. Please pass a valid "
114
                    "timeincremet parameter or pass an EnergySystem with "
115
                    "a valid time index. Please note that a valid time"
116
                    "index need to have a 'freq' attribute."
117
                )
118
                raise AttributeError(msg)
119
120
        self.objective_weighting = kwargs.get(
121
            "objective_weighting", self.timeincrement
122
        )
123
124
        self._constraint_groups = type(self).CONSTRAINT_GROUPS + kwargs.get(
125
            "constraint_groups", []
126
        )
127
128
        self._constraint_groups += [
129
            i
130
            for i in self.es.groups
131
            if hasattr(i, "CONSTRAINT_GROUP")
132
            and i not in self._constraint_groups
133
        ]
134
135
        self.flows = self.es.flows()
136
137
        self.solver_results = None
138
        self.dual = None
139
        self.rc = None
140
141
        if kwargs.get("auto_construct", True):
142
            self._construct()
143
144
    def _construct(self):
145
        """ """
146
        self._add_parent_block_sets()
147
        self._add_parent_block_variables()
148
        self._add_child_blocks()
149
        self._add_objective()
150
151
    def _add_parent_block_sets(self):
152
        """ " Method to create all sets located at the parent block, i.e. the
153
        model itself as they are to be shared across all model components.
154
        """
155
        pass
156
157
    def _add_parent_block_variables(self):
158
        """ " Method to create all variables located at the parent block,
159
        i.e. the model itself as these variables  are to be shared across
160
        all model components.
161
        """
162
        pass
163
164
    def _add_child_blocks(self):
165
        """Method to add the defined child blocks for components that have
166
        been grouped in the defined constraint groups.
167
        """
168
169
        for group in self._constraint_groups:
170
            # create instance for block
171
            block = group()
172
            # Add block to model
173
            self.add_component(str(block), block)
174
            # create constraints etc. related with block for all nodes
175
            # in the group
176
            block._create(group=self.es.groups.get(group))
177
178
    def _add_objective(self, sense=po.minimize, update=False):
179
        """Method to sum up all objective expressions from the child blocks
180
        that have been created. This method looks for `_objective_expression`
181
        attribute in the block definition and will call this method to add
182
        their return value to the objective function.
183
        """
184
        if update:
185
            self.del_component("objective")
186
187
        expr = 0
188
189
        for block in self.component_data_objects():
190
            if hasattr(block, "_objective_expression"):
191
                expr += block._objective_expression()
192
193
        self.objective = po.Objective(sense=sense, expr=expr)
194
195
    def receive_duals(self):
196
        """Method sets solver suffix to extract information about dual
197
        variables from solver. Shadow prices (duals) and reduced costs (rc) are
198
        set as attributes of the model.
199
200
        """
201
        # shadow prices
202
        self.dual = po.Suffix(direction=po.Suffix.IMPORT)
203
        # reduced costs
204
        self.rc = po.Suffix(direction=po.Suffix.IMPORT)
205
206
    def results(self):
207
        """Returns a nested dictionary of the results of this optimization"""
208
        return processing.results(self)
209
210
    def solve(self, solver="cbc", solver_io="lp", **kwargs):
211
        r"""Takes care of communication with solver to solve the model.
212
213
        Parameters
214
        ----------
215
        solver : string
216
            solver to be used e.g. "glpk","gurobi","cplex"
217
        solver_io : string
218
            pyomo solver interface file format: "lp","python","nl", etc.
219
        \**kwargs : keyword arguments
220
            Possible keys can be set see below:
221
222
        Other Parameters
223
        ----------------
224
        solve_kwargs : dict
225
            Other arguments for the pyomo.opt.SolverFactory.solve() method
226
            Example : {"tee":True}
227
        cmdline_options : dict
228
            Dictionary with command line options for solver e.g.
229
            {"mipgap":"0.01"} results in "--mipgap 0.01"
230
            {"interior":" "} results in "--interior"
231
            Gurobi solver takes numeric parameter values such as
232
            {"method": 2}
233
234
        """
235
        solve_kwargs = kwargs.get("solve_kwargs", {})
236
        solver_cmdline_options = kwargs.get("cmdline_options", {})
237
238
        opt = SolverFactory(solver, solver_io=solver_io)
239
        # set command line options
240
        options = opt.options
241
        for k in solver_cmdline_options:
242
            options[k] = solver_cmdline_options[k]
243
244
        solver_results = opt.solve(self, **solve_kwargs)
245
246
        status = solver_results["Solver"][0]["Status"]
247
        termination_condition = solver_results["Solver"][0][
248
            "Termination condition"
249
        ]
250
251
        if status == "ok" and termination_condition == "optimal":
252
            logging.info("Optimization successful...")
253
        else:
254
            msg = (
255
                "Optimization ended with status {0} and termination "
256
                "condition {1}"
257
            )
258
            warnings.warn(
259
                msg.format(status, termination_condition), UserWarning
260
            )
261
        self.es.results = solver_results
262
        self.solver_results = solver_results
263
264
        return solver_results
265
266
    def relax_problem(self):
267
        """Relaxes integer variables to reals of optimization model self."""
268
        relaxer = RelaxIntegrality()
269
        relaxer._apply_to(self)
270
271
        return self
272
273
274
class Model(BaseModel):
275
    """An  energy system model for operational and investment
276
    optimization.
277
278
    Parameters
279
    ----------
280
    energysystem : EnergySystem object
281
        Object that holds the nodes of an oemof energy system graph
282
    constraint_groups : list
283
        Solph looks for these groups in the given energy system and uses them
284
        to create the constraints of the optimization problem.
285
        Defaults to `Model.CONSTRAINTS`
286
287
    **The following basic sets are created**:
288
289
    NODES :
290
        A set with all nodes of the given energy system.
291
292
    TIMESTEPS :
293
        A set with all timesteps of the given time horizon.
294
295
    FLOWS :
296
        A 2 dimensional set with all flows. Index: `(source, target)`
297
298
    **The following basic variables are created**:
299
300
    flow
301
        FlowBlock from source to target indexed by FLOWS, TIMESTEPS.
302
        Note: Bounds of this variable are set depending on attributes of
303
        the corresponding flow object.
304
305
    """
306
307
    CONSTRAINT_GROUPS = [
308
        BusBlock,
309
        TransformerBlock,
310
        InvestmentFlowBlock,
311
        FlowBlock,
312
        NonConvexFlowBlock,
313
    ]
314
315
    def __init__(self, energysystem, **kwargs):
316
        super().__init__(energysystem, **kwargs)
317
318
    def _add_parent_block_sets(self):
319
        """ """
320
        # set with all nodes
321
        self.NODES = po.Set(initialize=[n for n in self.es.nodes])
322
323
        # pyomo set for timesteps of optimization problem
324
        self.TIMESTEPS = po.Set(
325
            initialize=range(len(self.es.timeindex)), ordered=True
326
        )
327
328
        # previous timesteps
329
        previous_timesteps = [x - 1 for x in self.TIMESTEPS]
330
        previous_timesteps[0] = self.TIMESTEPS.last()
331
332
        self.previous_timesteps = dict(zip(self.TIMESTEPS, previous_timesteps))
333
334
        # pyomo set for all flows in the energy system graph
335
        self.FLOWS = po.Set(
336
            initialize=self.flows.keys(), ordered=True, dimen=2
337
        )
338
339
        self.BIDIRECTIONAL_FLOWS = po.Set(
340
            initialize=[
341
                k
342
                for (k, v) in self.flows.items()
343
                if hasattr(v, "bidirectional")
344
            ],
345
            ordered=True,
346
            dimen=2,
347
            within=self.FLOWS,
348
        )
349
350
        self.UNIDIRECTIONAL_FLOWS = po.Set(
351
            initialize=[
352
                k
353
                for (k, v) in self.flows.items()
354
                if not hasattr(v, "bidirectional")
355
            ],
356
            ordered=True,
357
            dimen=2,
358
            within=self.FLOWS,
359
        )
360
361
    def _add_parent_block_variables(self):
362
        """ """
363
        self.flow = po.Var(self.FLOWS, self.TIMESTEPS, within=po.Reals)
364
365
        for (o, i) in self.FLOWS:
366
            if self.flows[o, i].nominal_value is not None:
367
                if self.flows[o, i].fix[self.TIMESTEPS[1]] is not None:
368
                    for t in self.TIMESTEPS:
369
                        self.flow[o, i, t].value = (
370
                            self.flows[o, i].fix[t]
371
                            * self.flows[o, i].nominal_value
372
                        )
373
                        self.flow[o, i, t].fix()
374
                else:
375
                    for t in self.TIMESTEPS:
376
                        self.flow[o, i, t].setub(
377
                            self.flows[o, i].max[t]
378
                            * self.flows[o, i].nominal_value
379
                        )
380
381
                    if not self.flows[o, i].nonconvex:
382
                        for t in self.TIMESTEPS:
383
                            self.flow[o, i, t].setlb(
384
                                self.flows[o, i].min[t]
385
                                * self.flows[o, i].nominal_value
386
                            )
387
                    elif (o, i) in self.UNIDIRECTIONAL_FLOWS:
388
                        for t in self.TIMESTEPS:
389
                            self.flow[o, i, t].setlb(0)
390
            else:
391
                if (o, i) in self.UNIDIRECTIONAL_FLOWS:
392
                    for t in self.TIMESTEPS:
393
                        self.flow[o, i, t].setlb(0)
394