scoptimize.network
1import pulp 2import type_enforced 3from .utils import large_m, Error 4 5 6@type_enforced.Enforcer 7class NetworkStructure(Error): 8 def __init__( 9 self, 10 name: str, 11 cashflow_for_use: [int, float] = 0, 12 cashflow_per_unit: [int, float] = 0, 13 min_units: [int, float] = 0, 14 max_units: [int, float] = 0, 15 ): 16 """ 17 Initialize a generic network structure object. 18 19 Requires: 20 21 - `name`: 22 - Type: str 23 - What: The name of this network object 24 - `cashflow_for_use`: 25 - Type: int | float 26 - What: The fixed cashflow that occurs if a non zero number of units flow through this network structure 27 - `cashflow_per_unit`: 28 - Type: int | float 29 - What: The cashflow that occurs for each unit that flows through this network structure 30 - `min_units`: 31 - Type: int | float 32 - What: The minimum units that must flow through this network structure 33 - `max_units`: 34 - Type: int | float 35 - What: The maximum units that must flow through this network structure 36 - Note: `max_units` must be larger than `min_units` 37 """ 38 self.name = name 39 self.min_units = min_units 40 if max_units == 0 and min_units > 0: 41 max_units = min_units 42 self.max_units = max_units 43 if min_units > max_units: 44 self.warn( 45 f"`min_units` is larger than `max_units` for `{self.name}`. This creates an infeasible constraint." 46 ) 47 self.cashflow_per_unit = cashflow_per_unit 48 49 self.cashflow_for_use = cashflow_for_use 50 if self.cashflow_for_use != 0: 51 self.use = pulp.LpVariable(name=f"{self.name}_use", cat="Binary") 52 53 self.inflows = [] 54 self.outflows = [] 55 self.reflows_in = [] 56 self.reflows_out = [] 57 58 def sum_flows(self, flow_list: list): 59 """ 60 Returns the sum of flows in a provided `flow_list` of `Flow` object as a float value. 61 62 Requires: 63 64 - `flow_list`: 65 - Type: list 66 - What: A list of `flow` objects 67 """ 68 return float(sum([i.flow.value() for i in flow_list])) 69 70 def lp_sum_flows(self, flow_list: list): 71 """ 72 Returns a pulp pulp function to sum of flows in a provided `flow_list` of `Flow` object. 73 74 Requires: 75 76 - `flow_list`: 77 - Type: list 78 - What: A list of flow objects 79 """ 80 return pulp.lpSum([i.flow for i in flow_list]) 81 82 def add_constraints(self, model): 83 """ 84 Updates a provided model with all the constraints needed for this network structure 85 86 Requires: 87 88 - `model`: 89 - Type: Model object 90 - What: The relevant model object that will get the constraints from this network structure 91 """ 92 # Enforce max unit constraint 93 model += self.lp_sum_flows(self.outflows) <= self.max_units 94 model += self.lp_sum_flows(self.outflows) >= self.min_units 95 # Enforce binary open constraint if cashflow_for_use is not 0 96 if self.cashflow_for_use != 0: 97 model += self.lp_sum_flows(self.outflows) <= large_m * self.use 98 99 def get_objective_fn(self): 100 """ 101 Gets the objective function (in pulp variables) for this network structure 102 """ 103 variable_cashflow = self.lp_sum_flows(self.outflows) * self.cashflow_per_unit 104 if self.cashflow_for_use != 0: 105 fixed_cashflow = self.use * self.cashflow_for_use 106 else: 107 fixed_cashflow = 0 108 return variable_cashflow + fixed_cashflow 109 110 111@type_enforced.Enforcer 112class Flow(NetworkStructure): 113 def __init__( 114 self, start: str, end: str, *args, reflow: bool = False, cat: str = "Continuous", **kwargs 115 ): 116 """ 117 Extends the NetworkStructure initialization to initialize a new Flow object. 118 119 Requires: 120 121 - `start`: 122 - Type: str 123 - What: The start Node name for this flow 124 - `end`: 125 - Type: str 126 - What: The end Node name for this flow 127 128 Optional: 129 130 - `reflow`: 131 - Type: bool 132 - What: Indicate if this flow is treated as a reflow 133 - Default: False 134 - Note: Reflows do not impact max_units or min_units for attached nodes 135 - Note: Reflows would normally be used to capture the idea of inventory in a multi time period model 136 - `cat`: 137 - Type: str 138 - What: The type of flow variable to create 139 - Options: ['Continuous','Binary','Integer'] 140 - Default: Continuous 141 """ 142 super().__init__(*args, **kwargs) 143 self.start = start 144 self.end = end 145 self.flow = pulp.LpVariable(name=f"{self.name}", cat=cat) 146 self.outflows.append(self) 147 self.reflow = reflow 148 149 def add_flows(self, objects: dict): 150 """ 151 Adds this flow object to the `start` and `end` nodes for the purpose of calculating node throughput. 152 153 Requires: 154 155 - `objects`: 156 - Type: dict 157 - What: An object dictionary (key=Object.name, Value=Object) for all nodes in the current model 158 """ 159 start_entity = objects.get(self.start) 160 if start_entity == None: 161 self.exception( 162 f"`start` entity ({self.start}) not found in current objects list. Did you forget to add it to your nodes? This should be done prior to adding flows. See the `add_nodes` method." 163 ) 164 end_entity = objects.get(self.end) 165 if end_entity == None: 166 self.exception( 167 f"`end` entity ({self.end}) not found in current objects list. Did you forget to add it to your nodes? This should be done prior to adding flows. See the `add_nodes` method." 168 ) 169 start_entity.add_outflow(self) 170 end_entity.add_inflow(self) 171 172 def get_stats(self): 173 """ 174 Get the stats relevant to this flow object 175 """ 176 # Dont recalculate stats for this object if it has already been calculated 177 if hasattr(self, "stats"): 178 return self.stats 179 self.stats = { 180 "name": self.name, 181 "class": self.__class__.__name__, 182 "reflow": self.reflow, 183 "start": self.start, 184 "end": self.end, 185 "flow": self.sum_flows(self.outflows), 186 "use": 1.0, 187 } 188 if self.cashflow_for_use != 0: 189 self.stats["use"] = self.use.value() 190 self.stats = { 191 **self.stats, 192 "variable_cashflow": self.stats["flow"] * self.cashflow_per_unit, 193 "fixed_cashflow": self.stats["use"] * self.cashflow_for_use, 194 } 195 return self.stats 196 197 198@type_enforced.Enforcer 199class Node(NetworkStructure): 200 def __init__(self, *args, origin: bool = False, destination: bool = False, **kwargs): 201 """ 202 Extends the NetworkStructure initialization process to initialize a new Node object. 203 204 Optional: 205 206 - `origin`: 207 - Type: bool 208 - What: Indicate if this node is an origin 209 - Default: False 210 - Note: Origin nodes do not have a preservation of flow constraint added and new flows be started in them them (from nothingness) 211 - `origin`: 212 - Type: bool 213 - What: Indicate if this node is a destination 214 - Default: False 215 - Note: Destination nodes do not have a preservation of flow constraint added and flows can be terminated in them (into nothingness) 216 """ 217 super().__init__(*args, **kwargs) 218 self.origin = origin 219 self.destination = destination 220 221 if origin and destination: 222 self.exception( 223 f"`origin` and `destination` can not both be `true` for any node but are for node `{self.name}`" 224 ) 225 226 if self.destination: 227 # Create a link from inflows to outflows to allow general 228 # NetworkStructure class logic to propagate destination nodes 229 self.outflows = self.inflows 230 231 def add_constraints(self, model): 232 """ 233 Extends the add_constraints function in NetworkStructure to include preservation of flow to the passed model. 234 235 This only applies if the this node is not an `origin` or `destination` node. 236 237 Preservation of flow fn: `inflows + reflows_in = outflows + reflows_out` 238 239 Requires: 240 241 - `model`: 242 - Type: Model object 243 - What: The relevant model object that will get the constraints from this network structure 244 """ 245 super().add_constraints(model) 246 if not (self.origin or self.destination): 247 # Balance Inflows + Reflows_In with Outflows + Reflows_Out 248 model += (self.lp_sum_flows(self.inflows) + self.lp_sum_flows(self.reflows_in)) == ( 249 self.lp_sum_flows(self.outflows) + self.lp_sum_flows(self.reflows_out) 250 ) 251 252 def add_inflow(self, obj: Flow): 253 """ 254 Adds an inflow to this node 255 256 Requires: 257 258 - `obj`: 259 - Type: Flow object 260 - What: A flow that is entering this node 261 """ 262 if obj.reflow: 263 self.reflows_in.append(obj) 264 else: 265 self.inflows.append(obj) 266 267 def add_outflow(self, obj: Flow): 268 """ 269 Adds an outflow to this node 270 271 Requires: 272 273 - `obj`: 274 - Type: Flow object 275 - What: A flow that is leaving this node 276 """ 277 if obj.reflow: 278 self.reflows_out.append(obj) 279 else: 280 self.outflows.append(obj) 281 282 def get_stats(self): 283 """ 284 Get the stats relevant to this node object 285 """ 286 # Dont recalculate stats for this object if it has already been calculated 287 if hasattr(self, "stats"): 288 return self.stats 289 self.stats = { 290 "name": self.name, 291 "class": self.__class__.__name__, 292 "origin": self.origin, 293 "destination": self.destination, 294 "inflows": self.sum_flows(self.inflows), 295 "outflows": self.sum_flows(self.outflows), 296 "reflows_in": self.sum_flows(self.reflows_in), 297 "reflows_out": self.sum_flows(self.reflows_out), 298 "use": 1.0, 299 } 300 if self.cashflow_for_use != 0: 301 self.stats["use"] = self.use.value() 302 self.stats = { 303 **self.stats, 304 "variable_cashflow": self.stats["outflows"] * self.cashflow_per_unit, 305 "fixed_cashflow": self.stats["use"] * self.cashflow_for_use, 306 } 307 # Fix the special outflows logic post solve to undo the special 308 # logic in self.__init__() above 309 if self.destination: 310 self.stats["outflows"] = 0 311 return self.stats 312 313 314@type_enforced.Enforcer 315class Model(Error): 316 def __init__(self, name: str, objects: [dict, type(None)] = None): 317 """ 318 Initialize a new model object to be solved 319 320 Requires: 321 322 - `name`: 323 - Type: str 324 - What: The name of this model object 325 326 Optional: 327 328 - `objects`: 329 - Type: dict 330 - What: A dictionary that contains any pre aggregated `Node`s or `Flow`s. 331 - Note: This should normally only be used for internal testing. Unless you need to custom functionaility, use the Model.add_object function instead of this. 332 - Default: {} 333 """ 334 if objects == None: 335 objects = {} 336 self.name = name 337 self.objects = objects 338 339 def solve(self, pulp_log: bool = False, except_on_infeasible: bool = True): 340 """ 341 Solve this model given all of the Nodes and Flows 342 343 Optional: 344 345 - `pulp_log`: 346 - Type: bool 347 - What: Indicate if the pulp log should be shown in the terminal 348 - Default: False 349 - `except_on_infeasible`: 350 - Type: bool 351 - What: Indicate if the model should throw an exception if it is infeasible 352 - Note: If False, the model will relax constraints until a solution is found 353 - Default: True 354 """ 355 # Create PuLP Model 356 self.model = pulp.LpProblem(name=self.name, sense=pulp.LpMaximize) 357 # Set objective function 358 self.model += pulp.lpSum([i.get_objective_fn() for i in self.objects.values()]) 359 # Add constraints 360 [i.add_constraints(self.model) for i in self.objects.values()] 361 # Solve the model 362 self.model.solve(pulp.PULP_CBC_CMD(msg=(3 if pulp_log else 0))) 363 364 if self.model.status == -1: 365 if except_on_infeasible: 366 self.exception("The current model is infeasible and can not be solved.") 367 else: 368 self.warn( 369 "The current model is infeasible and can not be solved. Constraints have been relaxed to provide a solution anyway." 370 ) 371 372 # Parse the objective value 373 self.objective = self.model.objective.value() 374 375 def get_object_stats(self): 376 """ 377 Get the statistics for every `Node` and `Flow` in this `Model`. 378 """ 379 return {key: value.get_stats() for key, value in self.objects.items()} 380 381 def add_object(self, obj: [Node, Flow]): 382 """ 383 Adds a `Node` or `Flow` to this model 384 385 Requires: 386 387 - `obj`: 388 - Type: Flow object | Node object 389 - What: A `Node` or `Flow` to be added to this model 390 """ 391 if obj.name in self.objects.keys(): 392 self.exception(f"Duplicate name detected: `{obj.name}`") 393 if isinstance(obj, (Flow)): 394 obj.add_flows(self.objects) 395 self.objects[obj.name] = obj
7@type_enforced.Enforcer 8class NetworkStructure(Error): 9 def __init__( 10 self, 11 name: str, 12 cashflow_for_use: [int, float] = 0, 13 cashflow_per_unit: [int, float] = 0, 14 min_units: [int, float] = 0, 15 max_units: [int, float] = 0, 16 ): 17 """ 18 Initialize a generic network structure object. 19 20 Requires: 21 22 - `name`: 23 - Type: str 24 - What: The name of this network object 25 - `cashflow_for_use`: 26 - Type: int | float 27 - What: The fixed cashflow that occurs if a non zero number of units flow through this network structure 28 - `cashflow_per_unit`: 29 - Type: int | float 30 - What: The cashflow that occurs for each unit that flows through this network structure 31 - `min_units`: 32 - Type: int | float 33 - What: The minimum units that must flow through this network structure 34 - `max_units`: 35 - Type: int | float 36 - What: The maximum units that must flow through this network structure 37 - Note: `max_units` must be larger than `min_units` 38 """ 39 self.name = name 40 self.min_units = min_units 41 if max_units == 0 and min_units > 0: 42 max_units = min_units 43 self.max_units = max_units 44 if min_units > max_units: 45 self.warn( 46 f"`min_units` is larger than `max_units` for `{self.name}`. This creates an infeasible constraint." 47 ) 48 self.cashflow_per_unit = cashflow_per_unit 49 50 self.cashflow_for_use = cashflow_for_use 51 if self.cashflow_for_use != 0: 52 self.use = pulp.LpVariable(name=f"{self.name}_use", cat="Binary") 53 54 self.inflows = [] 55 self.outflows = [] 56 self.reflows_in = [] 57 self.reflows_out = [] 58 59 def sum_flows(self, flow_list: list): 60 """ 61 Returns the sum of flows in a provided `flow_list` of `Flow` object as a float value. 62 63 Requires: 64 65 - `flow_list`: 66 - Type: list 67 - What: A list of `flow` objects 68 """ 69 return float(sum([i.flow.value() for i in flow_list])) 70 71 def lp_sum_flows(self, flow_list: list): 72 """ 73 Returns a pulp pulp function to sum of flows in a provided `flow_list` of `Flow` object. 74 75 Requires: 76 77 - `flow_list`: 78 - Type: list 79 - What: A list of flow objects 80 """ 81 return pulp.lpSum([i.flow for i in flow_list]) 82 83 def add_constraints(self, model): 84 """ 85 Updates a provided model with all the constraints needed for this network structure 86 87 Requires: 88 89 - `model`: 90 - Type: Model object 91 - What: The relevant model object that will get the constraints from this network structure 92 """ 93 # Enforce max unit constraint 94 model += self.lp_sum_flows(self.outflows) <= self.max_units 95 model += self.lp_sum_flows(self.outflows) >= self.min_units 96 # Enforce binary open constraint if cashflow_for_use is not 0 97 if self.cashflow_for_use != 0: 98 model += self.lp_sum_flows(self.outflows) <= large_m * self.use 99 100 def get_objective_fn(self): 101 """ 102 Gets the objective function (in pulp variables) for this network structure 103 """ 104 variable_cashflow = self.lp_sum_flows(self.outflows) * self.cashflow_per_unit 105 if self.cashflow_for_use != 0: 106 fixed_cashflow = self.use * self.cashflow_for_use 107 else: 108 fixed_cashflow = 0 109 return variable_cashflow + fixed_cashflow
9 def __init__( 10 self, 11 name: str, 12 cashflow_for_use: [int, float] = 0, 13 cashflow_per_unit: [int, float] = 0, 14 min_units: [int, float] = 0, 15 max_units: [int, float] = 0, 16 ): 17 """ 18 Initialize a generic network structure object. 19 20 Requires: 21 22 - `name`: 23 - Type: str 24 - What: The name of this network object 25 - `cashflow_for_use`: 26 - Type: int | float 27 - What: The fixed cashflow that occurs if a non zero number of units flow through this network structure 28 - `cashflow_per_unit`: 29 - Type: int | float 30 - What: The cashflow that occurs for each unit that flows through this network structure 31 - `min_units`: 32 - Type: int | float 33 - What: The minimum units that must flow through this network structure 34 - `max_units`: 35 - Type: int | float 36 - What: The maximum units that must flow through this network structure 37 - Note: `max_units` must be larger than `min_units` 38 """ 39 self.name = name 40 self.min_units = min_units 41 if max_units == 0 and min_units > 0: 42 max_units = min_units 43 self.max_units = max_units 44 if min_units > max_units: 45 self.warn( 46 f"`min_units` is larger than `max_units` for `{self.name}`. This creates an infeasible constraint." 47 ) 48 self.cashflow_per_unit = cashflow_per_unit 49 50 self.cashflow_for_use = cashflow_for_use 51 if self.cashflow_for_use != 0: 52 self.use = pulp.LpVariable(name=f"{self.name}_use", cat="Binary") 53 54 self.inflows = [] 55 self.outflows = [] 56 self.reflows_in = [] 57 self.reflows_out = []
Initialize a generic network structure object.
Requires:
name
:- Type: str
- What: The name of this network object
cashflow_for_use
:- Type: int | float
- What: The fixed cashflow that occurs if a non zero number of units flow through this network structure
cashflow_per_unit
:- Type: int | float
- What: The cashflow that occurs for each unit that flows through this network structure
min_units
:- Type: int | float
- What: The minimum units that must flow through this network structure
max_units
:
59 def sum_flows(self, flow_list: list): 60 """ 61 Returns the sum of flows in a provided `flow_list` of `Flow` object as a float value. 62 63 Requires: 64 65 - `flow_list`: 66 - Type: list 67 - What: A list of `flow` objects 68 """ 69 return float(sum([i.flow.value() for i in flow_list]))
Returns the sum of flows in a provided flow_list
of Flow
object as a float value.
Requires:
flow_list
:- Type: list
- What: A list of
flow
objects
71 def lp_sum_flows(self, flow_list: list): 72 """ 73 Returns a pulp pulp function to sum of flows in a provided `flow_list` of `Flow` object. 74 75 Requires: 76 77 - `flow_list`: 78 - Type: list 79 - What: A list of flow objects 80 """ 81 return pulp.lpSum([i.flow for i in flow_list])
Returns a pulp pulp function to sum of flows in a provided flow_list
of Flow
object.
Requires:
flow_list
:- Type: list
- What: A list of flow objects
83 def add_constraints(self, model): 84 """ 85 Updates a provided model with all the constraints needed for this network structure 86 87 Requires: 88 89 - `model`: 90 - Type: Model object 91 - What: The relevant model object that will get the constraints from this network structure 92 """ 93 # Enforce max unit constraint 94 model += self.lp_sum_flows(self.outflows) <= self.max_units 95 model += self.lp_sum_flows(self.outflows) >= self.min_units 96 # Enforce binary open constraint if cashflow_for_use is not 0 97 if self.cashflow_for_use != 0: 98 model += self.lp_sum_flows(self.outflows) <= large_m * self.use
Updates a provided model with all the constraints needed for this network structure
Requires:
model
:- Type: Model object
- What: The relevant model object that will get the constraints from this network structure
100 def get_objective_fn(self): 101 """ 102 Gets the objective function (in pulp variables) for this network structure 103 """ 104 variable_cashflow = self.lp_sum_flows(self.outflows) * self.cashflow_per_unit 105 if self.cashflow_for_use != 0: 106 fixed_cashflow = self.use * self.cashflow_for_use 107 else: 108 fixed_cashflow = 0 109 return variable_cashflow + fixed_cashflow
Gets the objective function (in pulp variables) for this network structure
Inherited Members
112@type_enforced.Enforcer 113class Flow(NetworkStructure): 114 def __init__( 115 self, start: str, end: str, *args, reflow: bool = False, cat: str = "Continuous", **kwargs 116 ): 117 """ 118 Extends the NetworkStructure initialization to initialize a new Flow object. 119 120 Requires: 121 122 - `start`: 123 - Type: str 124 - What: The start Node name for this flow 125 - `end`: 126 - Type: str 127 - What: The end Node name for this flow 128 129 Optional: 130 131 - `reflow`: 132 - Type: bool 133 - What: Indicate if this flow is treated as a reflow 134 - Default: False 135 - Note: Reflows do not impact max_units or min_units for attached nodes 136 - Note: Reflows would normally be used to capture the idea of inventory in a multi time period model 137 - `cat`: 138 - Type: str 139 - What: The type of flow variable to create 140 - Options: ['Continuous','Binary','Integer'] 141 - Default: Continuous 142 """ 143 super().__init__(*args, **kwargs) 144 self.start = start 145 self.end = end 146 self.flow = pulp.LpVariable(name=f"{self.name}", cat=cat) 147 self.outflows.append(self) 148 self.reflow = reflow 149 150 def add_flows(self, objects: dict): 151 """ 152 Adds this flow object to the `start` and `end` nodes for the purpose of calculating node throughput. 153 154 Requires: 155 156 - `objects`: 157 - Type: dict 158 - What: An object dictionary (key=Object.name, Value=Object) for all nodes in the current model 159 """ 160 start_entity = objects.get(self.start) 161 if start_entity == None: 162 self.exception( 163 f"`start` entity ({self.start}) not found in current objects list. Did you forget to add it to your nodes? This should be done prior to adding flows. See the `add_nodes` method." 164 ) 165 end_entity = objects.get(self.end) 166 if end_entity == None: 167 self.exception( 168 f"`end` entity ({self.end}) not found in current objects list. Did you forget to add it to your nodes? This should be done prior to adding flows. See the `add_nodes` method." 169 ) 170 start_entity.add_outflow(self) 171 end_entity.add_inflow(self) 172 173 def get_stats(self): 174 """ 175 Get the stats relevant to this flow object 176 """ 177 # Dont recalculate stats for this object if it has already been calculated 178 if hasattr(self, "stats"): 179 return self.stats 180 self.stats = { 181 "name": self.name, 182 "class": self.__class__.__name__, 183 "reflow": self.reflow, 184 "start": self.start, 185 "end": self.end, 186 "flow": self.sum_flows(self.outflows), 187 "use": 1.0, 188 } 189 if self.cashflow_for_use != 0: 190 self.stats["use"] = self.use.value() 191 self.stats = { 192 **self.stats, 193 "variable_cashflow": self.stats["flow"] * self.cashflow_per_unit, 194 "fixed_cashflow": self.stats["use"] * self.cashflow_for_use, 195 } 196 return self.stats
114 def __init__( 115 self, start: str, end: str, *args, reflow: bool = False, cat: str = "Continuous", **kwargs 116 ): 117 """ 118 Extends the NetworkStructure initialization to initialize a new Flow object. 119 120 Requires: 121 122 - `start`: 123 - Type: str 124 - What: The start Node name for this flow 125 - `end`: 126 - Type: str 127 - What: The end Node name for this flow 128 129 Optional: 130 131 - `reflow`: 132 - Type: bool 133 - What: Indicate if this flow is treated as a reflow 134 - Default: False 135 - Note: Reflows do not impact max_units or min_units for attached nodes 136 - Note: Reflows would normally be used to capture the idea of inventory in a multi time period model 137 - `cat`: 138 - Type: str 139 - What: The type of flow variable to create 140 - Options: ['Continuous','Binary','Integer'] 141 - Default: Continuous 142 """ 143 super().__init__(*args, **kwargs) 144 self.start = start 145 self.end = end 146 self.flow = pulp.LpVariable(name=f"{self.name}", cat=cat) 147 self.outflows.append(self) 148 self.reflow = reflow
Extends the NetworkStructure initialization to initialize a new Flow object.
Requires:
start
:- Type: str
- What: The start Node name for this flow
end
:- Type: str
- What: The end Node name for this flow
Optional:
reflow
:- Type: bool
- What: Indicate if this flow is treated as a reflow
- Default: False
- Note: Reflows do not impact max_units or min_units for attached nodes
- Note: Reflows would normally be used to capture the idea of inventory in a multi time period model
cat
:- Type: str
- What: The type of flow variable to create
- Options: ['Continuous','Binary','Integer']
- Default: Continuous
150 def add_flows(self, objects: dict): 151 """ 152 Adds this flow object to the `start` and `end` nodes for the purpose of calculating node throughput. 153 154 Requires: 155 156 - `objects`: 157 - Type: dict 158 - What: An object dictionary (key=Object.name, Value=Object) for all nodes in the current model 159 """ 160 start_entity = objects.get(self.start) 161 if start_entity == None: 162 self.exception( 163 f"`start` entity ({self.start}) not found in current objects list. Did you forget to add it to your nodes? This should be done prior to adding flows. See the `add_nodes` method." 164 ) 165 end_entity = objects.get(self.end) 166 if end_entity == None: 167 self.exception( 168 f"`end` entity ({self.end}) not found in current objects list. Did you forget to add it to your nodes? This should be done prior to adding flows. See the `add_nodes` method." 169 ) 170 start_entity.add_outflow(self) 171 end_entity.add_inflow(self)
173 def get_stats(self): 174 """ 175 Get the stats relevant to this flow object 176 """ 177 # Dont recalculate stats for this object if it has already been calculated 178 if hasattr(self, "stats"): 179 return self.stats 180 self.stats = { 181 "name": self.name, 182 "class": self.__class__.__name__, 183 "reflow": self.reflow, 184 "start": self.start, 185 "end": self.end, 186 "flow": self.sum_flows(self.outflows), 187 "use": 1.0, 188 } 189 if self.cashflow_for_use != 0: 190 self.stats["use"] = self.use.value() 191 self.stats = { 192 **self.stats, 193 "variable_cashflow": self.stats["flow"] * self.cashflow_per_unit, 194 "fixed_cashflow": self.stats["use"] * self.cashflow_for_use, 195 } 196 return self.stats
Get the stats relevant to this flow object
199@type_enforced.Enforcer 200class Node(NetworkStructure): 201 def __init__(self, *args, origin: bool = False, destination: bool = False, **kwargs): 202 """ 203 Extends the NetworkStructure initialization process to initialize a new Node object. 204 205 Optional: 206 207 - `origin`: 208 - Type: bool 209 - What: Indicate if this node is an origin 210 - Default: False 211 - Note: Origin nodes do not have a preservation of flow constraint added and new flows be started in them them (from nothingness) 212 - `origin`: 213 - Type: bool 214 - What: Indicate if this node is a destination 215 - Default: False 216 - Note: Destination nodes do not have a preservation of flow constraint added and flows can be terminated in them (into nothingness) 217 """ 218 super().__init__(*args, **kwargs) 219 self.origin = origin 220 self.destination = destination 221 222 if origin and destination: 223 self.exception( 224 f"`origin` and `destination` can not both be `true` for any node but are for node `{self.name}`" 225 ) 226 227 if self.destination: 228 # Create a link from inflows to outflows to allow general 229 # NetworkStructure class logic to propagate destination nodes 230 self.outflows = self.inflows 231 232 def add_constraints(self, model): 233 """ 234 Extends the add_constraints function in NetworkStructure to include preservation of flow to the passed model. 235 236 This only applies if the this node is not an `origin` or `destination` node. 237 238 Preservation of flow fn: `inflows + reflows_in = outflows + reflows_out` 239 240 Requires: 241 242 - `model`: 243 - Type: Model object 244 - What: The relevant model object that will get the constraints from this network structure 245 """ 246 super().add_constraints(model) 247 if not (self.origin or self.destination): 248 # Balance Inflows + Reflows_In with Outflows + Reflows_Out 249 model += (self.lp_sum_flows(self.inflows) + self.lp_sum_flows(self.reflows_in)) == ( 250 self.lp_sum_flows(self.outflows) + self.lp_sum_flows(self.reflows_out) 251 ) 252 253 def add_inflow(self, obj: Flow): 254 """ 255 Adds an inflow to this node 256 257 Requires: 258 259 - `obj`: 260 - Type: Flow object 261 - What: A flow that is entering this node 262 """ 263 if obj.reflow: 264 self.reflows_in.append(obj) 265 else: 266 self.inflows.append(obj) 267 268 def add_outflow(self, obj: Flow): 269 """ 270 Adds an outflow to this node 271 272 Requires: 273 274 - `obj`: 275 - Type: Flow object 276 - What: A flow that is leaving this node 277 """ 278 if obj.reflow: 279 self.reflows_out.append(obj) 280 else: 281 self.outflows.append(obj) 282 283 def get_stats(self): 284 """ 285 Get the stats relevant to this node object 286 """ 287 # Dont recalculate stats for this object if it has already been calculated 288 if hasattr(self, "stats"): 289 return self.stats 290 self.stats = { 291 "name": self.name, 292 "class": self.__class__.__name__, 293 "origin": self.origin, 294 "destination": self.destination, 295 "inflows": self.sum_flows(self.inflows), 296 "outflows": self.sum_flows(self.outflows), 297 "reflows_in": self.sum_flows(self.reflows_in), 298 "reflows_out": self.sum_flows(self.reflows_out), 299 "use": 1.0, 300 } 301 if self.cashflow_for_use != 0: 302 self.stats["use"] = self.use.value() 303 self.stats = { 304 **self.stats, 305 "variable_cashflow": self.stats["outflows"] * self.cashflow_per_unit, 306 "fixed_cashflow": self.stats["use"] * self.cashflow_for_use, 307 } 308 # Fix the special outflows logic post solve to undo the special 309 # logic in self.__init__() above 310 if self.destination: 311 self.stats["outflows"] = 0 312 return self.stats
201 def __init__(self, *args, origin: bool = False, destination: bool = False, **kwargs): 202 """ 203 Extends the NetworkStructure initialization process to initialize a new Node object. 204 205 Optional: 206 207 - `origin`: 208 - Type: bool 209 - What: Indicate if this node is an origin 210 - Default: False 211 - Note: Origin nodes do not have a preservation of flow constraint added and new flows be started in them them (from nothingness) 212 - `origin`: 213 - Type: bool 214 - What: Indicate if this node is a destination 215 - Default: False 216 - Note: Destination nodes do not have a preservation of flow constraint added and flows can be terminated in them (into nothingness) 217 """ 218 super().__init__(*args, **kwargs) 219 self.origin = origin 220 self.destination = destination 221 222 if origin and destination: 223 self.exception( 224 f"`origin` and `destination` can not both be `true` for any node but are for node `{self.name}`" 225 ) 226 227 if self.destination: 228 # Create a link from inflows to outflows to allow general 229 # NetworkStructure class logic to propagate destination nodes 230 self.outflows = self.inflows
Extends the NetworkStructure initialization process to initialize a new Node object.
Optional:
origin
:- Type: bool
- What: Indicate if this node is an origin
- Default: False
- Note: Origin nodes do not have a preservation of flow constraint added and new flows be started in them them (from nothingness)
origin
:- Type: bool
- What: Indicate if this node is a destination
- Default: False
- Note: Destination nodes do not have a preservation of flow constraint added and flows can be terminated in them (into nothingness)
232 def add_constraints(self, model): 233 """ 234 Extends the add_constraints function in NetworkStructure to include preservation of flow to the passed model. 235 236 This only applies if the this node is not an `origin` or `destination` node. 237 238 Preservation of flow fn: `inflows + reflows_in = outflows + reflows_out` 239 240 Requires: 241 242 - `model`: 243 - Type: Model object 244 - What: The relevant model object that will get the constraints from this network structure 245 """ 246 super().add_constraints(model) 247 if not (self.origin or self.destination): 248 # Balance Inflows + Reflows_In with Outflows + Reflows_Out 249 model += (self.lp_sum_flows(self.inflows) + self.lp_sum_flows(self.reflows_in)) == ( 250 self.lp_sum_flows(self.outflows) + self.lp_sum_flows(self.reflows_out) 251 )
Extends the add_constraints function in NetworkStructure to include preservation of flow to the passed model.
This only applies if the this node is not an origin
or destination
node.
Preservation of flow fn: inflows + reflows_in = outflows + reflows_out
Requires:
model
:- Type: Model object
- What: The relevant model object that will get the constraints from this network structure
253 def add_inflow(self, obj: Flow): 254 """ 255 Adds an inflow to this node 256 257 Requires: 258 259 - `obj`: 260 - Type: Flow object 261 - What: A flow that is entering this node 262 """ 263 if obj.reflow: 264 self.reflows_in.append(obj) 265 else: 266 self.inflows.append(obj)
Adds an inflow to this node
Requires:
obj
:- Type: Flow object
- What: A flow that is entering this node
268 def add_outflow(self, obj: Flow): 269 """ 270 Adds an outflow to this node 271 272 Requires: 273 274 - `obj`: 275 - Type: Flow object 276 - What: A flow that is leaving this node 277 """ 278 if obj.reflow: 279 self.reflows_out.append(obj) 280 else: 281 self.outflows.append(obj)
Adds an outflow to this node
Requires:
obj
:- Type: Flow object
- What: A flow that is leaving this node
283 def get_stats(self): 284 """ 285 Get the stats relevant to this node object 286 """ 287 # Dont recalculate stats for this object if it has already been calculated 288 if hasattr(self, "stats"): 289 return self.stats 290 self.stats = { 291 "name": self.name, 292 "class": self.__class__.__name__, 293 "origin": self.origin, 294 "destination": self.destination, 295 "inflows": self.sum_flows(self.inflows), 296 "outflows": self.sum_flows(self.outflows), 297 "reflows_in": self.sum_flows(self.reflows_in), 298 "reflows_out": self.sum_flows(self.reflows_out), 299 "use": 1.0, 300 } 301 if self.cashflow_for_use != 0: 302 self.stats["use"] = self.use.value() 303 self.stats = { 304 **self.stats, 305 "variable_cashflow": self.stats["outflows"] * self.cashflow_per_unit, 306 "fixed_cashflow": self.stats["use"] * self.cashflow_for_use, 307 } 308 # Fix the special outflows logic post solve to undo the special 309 # logic in self.__init__() above 310 if self.destination: 311 self.stats["outflows"] = 0 312 return self.stats
Get the stats relevant to this node object
315@type_enforced.Enforcer 316class Model(Error): 317 def __init__(self, name: str, objects: [dict, type(None)] = None): 318 """ 319 Initialize a new model object to be solved 320 321 Requires: 322 323 - `name`: 324 - Type: str 325 - What: The name of this model object 326 327 Optional: 328 329 - `objects`: 330 - Type: dict 331 - What: A dictionary that contains any pre aggregated `Node`s or `Flow`s. 332 - Note: This should normally only be used for internal testing. Unless you need to custom functionaility, use the Model.add_object function instead of this. 333 - Default: {} 334 """ 335 if objects == None: 336 objects = {} 337 self.name = name 338 self.objects = objects 339 340 def solve(self, pulp_log: bool = False, except_on_infeasible: bool = True): 341 """ 342 Solve this model given all of the Nodes and Flows 343 344 Optional: 345 346 - `pulp_log`: 347 - Type: bool 348 - What: Indicate if the pulp log should be shown in the terminal 349 - Default: False 350 - `except_on_infeasible`: 351 - Type: bool 352 - What: Indicate if the model should throw an exception if it is infeasible 353 - Note: If False, the model will relax constraints until a solution is found 354 - Default: True 355 """ 356 # Create PuLP Model 357 self.model = pulp.LpProblem(name=self.name, sense=pulp.LpMaximize) 358 # Set objective function 359 self.model += pulp.lpSum([i.get_objective_fn() for i in self.objects.values()]) 360 # Add constraints 361 [i.add_constraints(self.model) for i in self.objects.values()] 362 # Solve the model 363 self.model.solve(pulp.PULP_CBC_CMD(msg=(3 if pulp_log else 0))) 364 365 if self.model.status == -1: 366 if except_on_infeasible: 367 self.exception("The current model is infeasible and can not be solved.") 368 else: 369 self.warn( 370 "The current model is infeasible and can not be solved. Constraints have been relaxed to provide a solution anyway." 371 ) 372 373 # Parse the objective value 374 self.objective = self.model.objective.value() 375 376 def get_object_stats(self): 377 """ 378 Get the statistics for every `Node` and `Flow` in this `Model`. 379 """ 380 return {key: value.get_stats() for key, value in self.objects.items()} 381 382 def add_object(self, obj: [Node, Flow]): 383 """ 384 Adds a `Node` or `Flow` to this model 385 386 Requires: 387 388 - `obj`: 389 - Type: Flow object | Node object 390 - What: A `Node` or `Flow` to be added to this model 391 """ 392 if obj.name in self.objects.keys(): 393 self.exception(f"Duplicate name detected: `{obj.name}`") 394 if isinstance(obj, (Flow)): 395 obj.add_flows(self.objects) 396 self.objects[obj.name] = obj
317 def __init__(self, name: str, objects: [dict, type(None)] = None): 318 """ 319 Initialize a new model object to be solved 320 321 Requires: 322 323 - `name`: 324 - Type: str 325 - What: The name of this model object 326 327 Optional: 328 329 - `objects`: 330 - Type: dict 331 - What: A dictionary that contains any pre aggregated `Node`s or `Flow`s. 332 - Note: This should normally only be used for internal testing. Unless you need to custom functionaility, use the Model.add_object function instead of this. 333 - Default: {} 334 """ 335 if objects == None: 336 objects = {} 337 self.name = name 338 self.objects = objects
Initialize a new model object to be solved
Requires:
name
:- Type: str
- What: The name of this model object
Optional:
objects
:- Type: dict
- What: A dictionary that contains any pre aggregated
Node
s orFlow
s. - Note: This should normally only be used for internal testing. Unless you need to custom functionaility, use the Model.add_object function instead of this.
- Default: {}
340 def solve(self, pulp_log: bool = False, except_on_infeasible: bool = True): 341 """ 342 Solve this model given all of the Nodes and Flows 343 344 Optional: 345 346 - `pulp_log`: 347 - Type: bool 348 - What: Indicate if the pulp log should be shown in the terminal 349 - Default: False 350 - `except_on_infeasible`: 351 - Type: bool 352 - What: Indicate if the model should throw an exception if it is infeasible 353 - Note: If False, the model will relax constraints until a solution is found 354 - Default: True 355 """ 356 # Create PuLP Model 357 self.model = pulp.LpProblem(name=self.name, sense=pulp.LpMaximize) 358 # Set objective function 359 self.model += pulp.lpSum([i.get_objective_fn() for i in self.objects.values()]) 360 # Add constraints 361 [i.add_constraints(self.model) for i in self.objects.values()] 362 # Solve the model 363 self.model.solve(pulp.PULP_CBC_CMD(msg=(3 if pulp_log else 0))) 364 365 if self.model.status == -1: 366 if except_on_infeasible: 367 self.exception("The current model is infeasible and can not be solved.") 368 else: 369 self.warn( 370 "The current model is infeasible and can not be solved. Constraints have been relaxed to provide a solution anyway." 371 ) 372 373 # Parse the objective value 374 self.objective = self.model.objective.value()
Solve this model given all of the Nodes and Flows
Optional:
pulp_log
:- Type: bool
- What: Indicate if the pulp log should be shown in the terminal
- Default: False
except_on_infeasible
:- Type: bool
- What: Indicate if the model should throw an exception if it is infeasible
- Note: If False, the model will relax constraints until a solution is found
- Default: True
382 def add_object(self, obj: [Node, Flow]): 383 """ 384 Adds a `Node` or `Flow` to this model 385 386 Requires: 387 388 - `obj`: 389 - Type: Flow object | Node object 390 - What: A `Node` or `Flow` to be added to this model 391 """ 392 if obj.name in self.objects.keys(): 393 self.exception(f"Duplicate name detected: `{obj.name}`") 394 if isinstance(obj, (Flow)): 395 obj.add_flows(self.objects) 396 self.objects[obj.name] = obj