|
| 1 | +############################################################################### |
| 2 | +# mpi-sppy: MPI-based Stochastic Programming in PYthon |
| 3 | +# |
| 4 | +# Copyright (c) 2024, Lawrence Livermore National Security, LLC, Alliance for |
| 5 | +# Sustainable Energy, LLC, The Regents of the University of California, et al. |
| 6 | +# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md for |
| 7 | +# full copyright and license information. |
| 8 | +############################################################################### |
| 9 | +# ___________________________________________________________________________ |
| 10 | +# |
| 11 | +# Pyomo: Python Optimization Modeling Objects |
| 12 | +# Copyright 2017 National Technology and Engineering Solutions of Sandia, LLC |
| 13 | +# Under the terms of Contract DE-NA0003525 with National Technology and |
| 14 | +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain |
| 15 | +# rights in this software. |
| 16 | +# This software is distributed under the 3-clause BSD License. |
| 17 | +# ___________________________________________________________________________ |
| 18 | +# |
| 19 | +# This is the two-period version of the SIZES optimization model |
| 20 | +# derived from the three stage model in: |
| 21 | +#A. L{\o}kketangen and D. L. Woodruff, |
| 22 | +#"Progressive Hedging and Tabu Search Applied to Mixed Integer (0,1) Multistage Stochastic Programming", |
| 23 | +#Journal of Heuristics, 1996, Vol 2, Pages 111-128. |
| 24 | +# This version uses expressions for stage costs. January 2025. |
| 25 | + |
| 26 | +from pyomo.core import * |
| 27 | + |
| 28 | +# |
| 29 | +# Model |
| 30 | +# |
| 31 | + |
| 32 | +model = AbstractModel() |
| 33 | + |
| 34 | +# |
| 35 | +# Parameters |
| 36 | +# |
| 37 | + |
| 38 | +# the number of product sizes. |
| 39 | +model.NumSizes = Param(within=NonNegativeIntegers) |
| 40 | + |
| 41 | +# the set of sizes, labeled 1 through NumSizes. |
| 42 | +def product_sizes_rule(model): |
| 43 | + return list(range(1, model.NumSizes()+1)) |
| 44 | +model.ProductSizes = Set(initialize=product_sizes_rule) |
| 45 | + |
| 46 | +# the deterministic demands for product at each size. |
| 47 | +model.DemandsFirstStage = Param(model.ProductSizes, within=NonNegativeIntegers) |
| 48 | +model.DemandsSecondStage = Param(model.ProductSizes, within=NonNegativeIntegers) |
| 49 | + |
| 50 | +# the unit production cost at each size. |
| 51 | +model.UnitProductionCosts = Param(model.ProductSizes, within=NonNegativeReals) |
| 52 | + |
| 53 | +# the setup cost for producing any units of size i. |
| 54 | +model.SetupCosts = Param(model.ProductSizes, within=NonNegativeReals) |
| 55 | + |
| 56 | +# the cost to reduce a unit i to a lower unit j. |
| 57 | +model.UnitReductionCost = Param(within=NonNegativeReals) |
| 58 | + |
| 59 | +# a cap on the overall production within any time stage. |
| 60 | +model.Capacity = Param(within=PositiveReals) |
| 61 | + |
| 62 | +# a derived set to constrain the NumUnitsCut variable domain. |
| 63 | +# TBD: the (i,j) with i >= j set should be a generic utility. |
| 64 | +def num_units_cut_domain_rule(model): |
| 65 | + ans = list() |
| 66 | + for i in range(1,model.NumSizes()+1): |
| 67 | + for j in range(1, i+1): |
| 68 | + ans.append((i,j)) |
| 69 | + return ans |
| 70 | + |
| 71 | +model.NumUnitsCutDomain = Set(initialize=num_units_cut_domain_rule, dimen=2) |
| 72 | + |
| 73 | +# |
| 74 | +# Variables |
| 75 | +# |
| 76 | + |
| 77 | +# are any products at size i produced? |
| 78 | +model.ProduceSizeFirstStage = Var(model.ProductSizes, domain=Boolean) |
| 79 | +model.ProduceSizeSecondStage = Var(model.ProductSizes, domain=Boolean) |
| 80 | + |
| 81 | +# NOTE: The following (num-produced and num-cut) variables are implicitly integer |
| 82 | +# under the normal cost objective, but with the PH cost objective, this isn't |
| 83 | +# the case. |
| 84 | + |
| 85 | +# the number of units at each size produced. |
| 86 | +model.NumProducedFirstStage = Var(model.ProductSizes, domain=NonNegativeIntegers, bounds=(0.0, model.Capacity)) |
| 87 | +model.NumProducedSecondStage = Var(model.ProductSizes, domain=NonNegativeIntegers, bounds=(0.0, model.Capacity)) |
| 88 | + |
| 89 | +# the number of units of size i cut (down) to meet demand for units of size j. |
| 90 | +model.NumUnitsCutFirstStage = Var(model.NumUnitsCutDomain, domain=NonNegativeIntegers, bounds=(0.0, model.Capacity)) |
| 91 | +model.NumUnitsCutSecondStage = Var(model.NumUnitsCutDomain, domain=NonNegativeIntegers, bounds=(0.0, model.Capacity)) |
| 92 | + |
| 93 | +# stage-specific cost variables for use in the pysp scenario tree / analysis. |
| 94 | +# changed to Expressions January 2025 |
| 95 | +#model.FirstStageCost = Var(domain=NonNegativeReals) |
| 96 | +#model.SecondStageCost = Var(domain=NonNegativeReals) |
| 97 | + |
| 98 | +# |
| 99 | +# Constraints |
| 100 | +# |
| 101 | + |
| 102 | +# ensure that demand is satisfied in each time stage, accounting for cut-downs. |
| 103 | +def demand_satisfied_first_stage_rule(model, i): |
| 104 | + return (0.0, sum([model.NumUnitsCutFirstStage[j,i] for j in model.ProductSizes if j >= i]) - model.DemandsFirstStage[i], None) |
| 105 | + |
| 106 | +def demand_satisfied_second_stage_rule(model, i): |
| 107 | + return (0.0, sum([model.NumUnitsCutSecondStage[j,i] for j in model.ProductSizes if j >= i]) - model.DemandsSecondStage[i], None) |
| 108 | + |
| 109 | +model.DemandSatisfiedFirstStage = Constraint(model.ProductSizes, rule=demand_satisfied_first_stage_rule) |
| 110 | +model.DemandSatisfiedSecondStage = Constraint(model.ProductSizes, rule=demand_satisfied_second_stage_rule) |
| 111 | + |
| 112 | +# ensure that you don't produce any units if the decision has been made to disable producion. |
| 113 | +def enforce_production_first_stage_rule(model, i): |
| 114 | + # The production capacity per time stage serves as a simple upper bound for "M". |
| 115 | + return (None, model.NumProducedFirstStage[i] - model.Capacity * model.ProduceSizeFirstStage[i], 0.0) |
| 116 | + |
| 117 | +def enforce_production_second_stage_rule(model, i): |
| 118 | + # The production capacity per time stage serves as a simple upper bound for "M". |
| 119 | + return (None, model.NumProducedSecondStage[i] - model.Capacity * model.ProduceSizeSecondStage[i], 0.0) |
| 120 | + |
| 121 | +model.EnforceProductionBinaryFirstStage = Constraint(model.ProductSizes, rule=enforce_production_first_stage_rule) |
| 122 | +model.EnforceProductionBinarySecondStage = Constraint(model.ProductSizes, rule=enforce_production_second_stage_rule) |
| 123 | + |
| 124 | +# ensure that the production capacity is not exceeded for each time stage. |
| 125 | +def enforce_capacity_first_stage_rule(model): |
| 126 | + return (None, sum([model.NumProducedFirstStage[i] for i in model.ProductSizes]) - model.Capacity, 0.0) |
| 127 | + |
| 128 | +def enforce_capacity_second_stage_rule(model): |
| 129 | + return (None, sum([model.NumProducedSecondStage[i] for i in model.ProductSizes]) - model.Capacity, 0.0) |
| 130 | + |
| 131 | +model.EnforceCapacityLimitFirstStage = Constraint(rule=enforce_capacity_first_stage_rule) |
| 132 | +model.EnforceCapacityLimitSecondStage = Constraint(rule=enforce_capacity_second_stage_rule) |
| 133 | + |
| 134 | +# ensure that you can't generate inventory out of thin air. |
| 135 | +def enforce_inventory_first_stage_rule(model, i): |
| 136 | + return (None, \ |
| 137 | + sum([model.NumUnitsCutFirstStage[i,j] for j in model.ProductSizes if j <= i]) - \ |
| 138 | + model.NumProducedFirstStage[i], \ |
| 139 | + 0.0) |
| 140 | + |
| 141 | +def enforce_inventory_second_stage_rule(model, i): |
| 142 | + return (None, \ |
| 143 | + sum([model.NumUnitsCutFirstStage[i,j] for j in model.ProductSizes if j <= i]) + \ |
| 144 | + sum([model.NumUnitsCutSecondStage[i,j] for j in model.ProductSizes if j <= i]) \ |
| 145 | + - model.NumProducedFirstStage[i] - model.NumProducedSecondStage[i], \ |
| 146 | + 0.0) |
| 147 | + |
| 148 | +model.EnforceInventoryFirstStage = Constraint(model.ProductSizes, rule=enforce_inventory_first_stage_rule) |
| 149 | +model.EnforceInventorySecondStage = Constraint(model.ProductSizes, rule=enforce_inventory_second_stage_rule) |
| 150 | + |
| 151 | +# stage-specific cost computations. |
| 152 | +def first_stage_cost_rule(model): |
| 153 | + production_costs = sum([model.SetupCosts[i] * model.ProduceSizeFirstStage[i] + \ |
| 154 | + model.UnitProductionCosts[i] * model.NumProducedFirstStage[i] \ |
| 155 | + for i in model.ProductSizes]) |
| 156 | + cut_costs = sum([model.UnitReductionCost * model.NumUnitsCutFirstStage[i,j] \ |
| 157 | + for (i,j) in model.NumUnitsCutDomain if i != j]) |
| 158 | + return (production_costs - cut_costs) |
| 159 | + |
| 160 | +model.FirstStageCost = Expression(rule=first_stage_cost_rule) |
| 161 | + |
| 162 | +def second_stage_cost_rule(model): |
| 163 | + production_costs = sum([model.SetupCosts[i] * model.ProduceSizeSecondStage[i] + \ |
| 164 | + model.UnitProductionCosts[i] * model.NumProducedSecondStage[i] \ |
| 165 | + for i in model.ProductSizes]) |
| 166 | + cut_costs = sum([model.UnitReductionCost * model.NumUnitsCutSecondStage[i,j] \ |
| 167 | + for (i,j) in model.NumUnitsCutDomain if i != j]) |
| 168 | + return (production_costs - cut_costs) |
| 169 | + |
| 170 | +model.SecondStageCost = Expression(rule=second_stage_cost_rule) |
| 171 | + |
| 172 | +# |
| 173 | +# PySP Auto-generated Objective |
| 174 | +# |
| 175 | +# minimize: sum of StageCosts |
| 176 | +# |
| 177 | +# A active scenario objective equivalent to that generated by PySP is |
| 178 | +# included here for informational purposes. |
| 179 | +def total_cost_rule(model): |
| 180 | + return model.FirstStageCost + model.SecondStageCost |
| 181 | +model.Total_Cost_Objective = Objective(rule=total_cost_rule, sense=minimize) |
| 182 | + |
0 commit comments