diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..7c7b737 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,18 @@ +name: Lint + +on: [push, pull_request] + +jobs: + lint: + name: lint/style-and-typos + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Black Formatting Check + uses: psf/black@stable + with: + options: "-S -C --check --diff" + - name: Spell Check + uses: crate-ci/typos@master + with: + config: ./.github/workflows/typos.toml diff --git a/.github/workflows/typos.toml b/.github/workflows/typos.toml new file mode 100644 index 0000000..a7e28f0 --- /dev/null +++ b/.github/workflows/typos.toml @@ -0,0 +1,5 @@ +[default.extend-words] +# Ignore HDA +hda = "hda" +HDA = "HDA" +equil = "equil" diff --git a/.gitignore b/.gitignore index 9b8da19..b9c4f32 100644 --- a/.gitignore +++ b/.gitignore @@ -130,3 +130,5 @@ dmypy.json # Pycharm .idea/ + +gdplib/*/benchmark_result/ diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 0000000..54026d5 --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,43 @@ +cff-version: 1.0.0 +message: "If you use this software, please cite it as below." +authors: + - family-names: Bernal Neira + given-names: David E. + orcid: https://orcid.org/0000-0002-8308-5016 + - family-names: Peng + given-names: Zedong + orcid: https://orcid.org/0000-0001-6001-1738 + - family-names: Chen + given-names: Qi + orcid: https://orcid.org/0000-0002-2389-2238 + - family-names: Liu + given-names: Yunshan + - family-names: Johnson + given-names: Emma + orcid: https://orcid.org/0000-0002-4285-5184 +title: "GDPLib: an open library of Generalized Disjunctive Programming (GDP) models" +version: 1.0.0 +url: https://github.com/SECQUOIA/gdplib +license-url: https://github.com/SECQUOIA/gdplib/blob/master/LICENSE +preferred-citation: + type: incollection + booktitle: "Computer Aided Chemical Engineering" + volume: 49 + start: 1285 + end: 1290 + title: "Advances in Generalized Disjunctive and Mixed-Integer Nonlinear Programming Algorithms and Software for Superstructure Optimization" + year:2022 + publisher: "Elsevier" + authors: + - family-names: "Bernal Neira" + given-names: "David E." + - family-names: "Liu" + given-names: "Yunshan" + - family-names: "Bynum" + given-names: "Michael L" + - family-names: "Laird" + given-names: "Carl D" + - family-names: "Siirola" + given-names: "John D" + - family-names: "Grossmann" + given-names: "Ignacio E" diff --git a/benchmark.py b/benchmark.py new file mode 100644 index 0000000..1bb9df8 --- /dev/null +++ b/benchmark.py @@ -0,0 +1,109 @@ +import os +import json +import time +import sys +from datetime import datetime +from importlib import import_module +from pyomo.environ import * + + +def benchmark(model, strategy, timelimit, result_dir, subsolver="scip"): + """Benchmark the model using the given strategy and subsolver. + + The result files include the solver output and the JSON representation of the results. + + Parameters + ---------- + model : PyomoModel + the model to be solved + strategy : string + the strategy used to solve the model + timelimit : int + the time limit for the solver + result_dir : string + the directory to store the benchmark results + + Returns + ------- + None + """ + model = model.clone() + stdout = sys.stdout + if strategy in ["gdp.bigm", "gdp.hull"]: + transformation_start_time = time.time() + TransformationFactory(strategy).apply_to(model) + transformation_end_time = time.time() + with open( + result_dir + "/" + strategy + "_" + subsolver + ".log", "w" + ) as sys.stdout: + results = SolverFactory(subsolver).solve( + model, tee=True, timelimit=timelimit + ) + results.solver.transformation_time = ( + transformation_end_time - transformation_start_time + ) + print(results) + elif strategy in [ + "gdpopt.enumerate", + "gdpopt.loa", + "gdpopt.gloa", + "gdpopt.lbb", + "gdpopt.ric", + ]: + with open( + result_dir + "/" + strategy + "_" + subsolver + ".log", "w" + ) as sys.stdout: + results = SolverFactory(strategy).solve( + model, + tee=True, + nlp_solver=subsolver, + mip_solver=subsolver, + minlp_solver=subsolver, + local_minlp_solver=subsolver, + time_limit=timelimit, + ) + print(results) + + sys.stdout = stdout + with open(result_dir + "/" + strategy + "_" + subsolver + ".json", "w") as f: + json.dump(results.json_repn(), f) + return None + + +if __name__ == "__main__": + instance_list = [ + # "batch_processing", + # "biofuel", + # "disease_model", + # "gdp_col", + # "hda", + "jobshop", + # "kaibel", + # "logical", + # "med_term_purchasing", + # "methanol", + # "mod_hens", + # "modprodnet", + # "stranded_gas", + # "syngas", + ] + strategy_list = [ + "gdp.bigm", + "gdp.hull", + "gdpopt.enumerate", + "gdpopt.loa", + "gdpopt.gloa", + "gdpopt.ric", + ] + current_time = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + timelimit = 600 + + for instance in instance_list: + print("Benchmarking instance: " + instance) + result_dir = "gdplib/" + instance + "/benchmark_result/" + os.makedirs(result_dir, exist_ok=True) + + model = import_module("gdplib." + instance).build_model() + + for strategy in strategy_list: + benchmark(model, strategy, timelimit, result_dir) diff --git a/gdplib/__init__.py b/gdplib/__init__.py index 1812f52..a3cb501 100644 --- a/gdplib/__init__.py +++ b/gdplib/__init__.py @@ -2,9 +2,13 @@ import gdplib.modprodnet import gdplib.biofuel import gdplib.logical # Requires logical expression system -import gdplib.pyomo_examples import gdplib.stranded_gas # Requires logical expression system import gdplib.gdp_col import gdplib.hda import gdplib.kaibel import gdplib.methanol +import gdplib.batch_processing +import gdplib.jobshop +import gdplib.disease_model +import gdplib.med_term_purchasing +import gdplib.syngas diff --git a/gdplib/batch_processing/__init__.py b/gdplib/batch_processing/__init__.py new file mode 100644 index 0000000..58f319a --- /dev/null +++ b/gdplib/batch_processing/__init__.py @@ -0,0 +1,3 @@ +from .batch_processing import build_model + +__all__ = ['build_model'] diff --git a/gdplib/pyomo_examples/batch_processing.dat b/gdplib/batch_processing/batch_processing.dat similarity index 100% rename from gdplib/pyomo_examples/batch_processing.dat rename to gdplib/batch_processing/batch_processing.dat diff --git a/gdplib/batch_processing/batch_processing.py b/gdplib/batch_processing/batch_processing.py new file mode 100644 index 0000000..f11610c --- /dev/null +++ b/gdplib/batch_processing/batch_processing.py @@ -0,0 +1,704 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright 2017 National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from os.path import join + +from pyomo.common.fileutils import this_file_dir +from pyomo.environ import * +from pyomo.gdp import * + +# Problem from http://www.minlp.org/library/problem/index.php?i=172&lib=GDP +# We are minimizing the cost of a design of a plant with parallel processing units and storage tanks +# in between. We decide the number and volume of units, and the volume and location of the storage +# tanks. The problem is convexified and has a nonlinear objective and global constraints + +# NOTE: When I refer to 'gams' in the comments, that is Batch101006_BM.gms for now. It's confusing +# because the _opt file is different (It has hard-coded bigM parameters so that each constraint +# has the "optimal" bigM). + + +def build_model(): + """ + Constructs and initializes a Pyomo model for the batch processing problem. + + The model is designed to minimize the total cost associated with the design and operation of a plant consisting of multiple + parallel processing units with intermediate storage tanks. + It involves determining the optimal number and sizes of processing units, batch sizes for different products at various stages, and sizes and + placements of storage tanks to ensure operational efficiency while meeting production requirements within a specified time horizon. + + Parameters + ---------- + None + + Returns + ------- + Pyomo.ConcreteModel + An instance of the Pyomo ConcreteModel class representing the batch processing optimization model, + ready to be solved with an appropriate solver. + + References + ---------- + [1] Ravemark, E. Optimization models for design and operation of chemical batch processes. Ph.D. Thesis, ETH Zurich, 1995. https://doi.org/10.3929/ethz-a-001591449 + [2] Vecchietti, A., & Grossmann, I. E. (1999). LOGMIP: a disjunctive 0–1 non-linear optimizer for process system models. Computers & chemical engineering, 23(4-5), 555-565. https://doi.org/10.1016/S0098-1354(97)87539-4 + """ + model = AbstractModel("Batch Processing Optimization Problem") + + # TODO: it looks like they set a bigM for each j. Which I need to look up how to do... + model.BigM = Suffix(direction=Suffix.LOCAL) + model.BigM[None] = 1000 + + # Constants from GAMS + StorageTankSizeFactor = 10 + StorageTankSizeFactorByProd = 3 + MinFlow = -log(10000) + VolumeLB = log(300) + VolumeUB = log(3500) + StorageTankSizeLB = log(100) + StorageTankSizeUB = log(15000) + UnitsInPhaseUB = log(6) + UnitsOutOfPhaseUB = log(6) + # TODO: YOU ARE HERE. YOU HAVEN'T ACTUALLY MADE THESE THE BOUNDS YET, NOR HAVE YOU FIGURED OUT WHOSE + # BOUNDS THEY ARE. AND THERE ARE MORE IN GAMS. + + # Sets + + model.PRODUCTS = Set(doc='Set of Products') + model.STAGES = Set(doc='Set of Stages', ordered=True) + model.PARALLELUNITS = Set(doc='Set of Parallel Units', ordered=True) + + # TODO: this seems like an over-complicated way to accomplish this task... + def filter_out_last(model, j): + """ + Filters out the last stage from the set of stages to avoid considering it in certain constraints + or disjunctions where the next stage would be required but doesn't exist. + + Parameters + ---------- + model : Pyomo.ConcreteModel + The Pyomo model for the batch processing optimization problem. + j : int + The index representing the stage in the processing sequence. Stages are ordered and include various + processing steps required for product completion. + + Returns + ------- + bool + Returns True if the stage is not the last one in the set, False otherwise. + """ + return j != model.STAGES.last() + + model.STAGESExceptLast = Set(initialize=model.STAGES, filter=filter_out_last) + + # TODO: these aren't in the formulation?? + # model.STORAGE_TANKS = Set() + + # Parameters + + model.HorizonTime = Param(doc='Horizon Time') + model.Alpha1 = Param(doc='Cost Parameter of the units') + model.Alpha2 = Param(doc='Cost Parameter of the intermediate storage tanks') + model.Beta1 = Param(doc='Exponent Parameter of the units') + model.Beta2 = Param(doc='Exponent Parameter of the intermediate storage tanks') + + model.ProductionAmount = Param(model.PRODUCTS, doc='Production Amount') + model.ProductSizeFactor = Param( + model.PRODUCTS, model.STAGES, doc='Product Size Factor' + ) + model.ProcessingTime = Param(model.PRODUCTS, model.STAGES, doc='Processing Time') + + # These are hard-coded in the GAMS file, hence the defaults + model.StorageTankSizeFactor = Param( + model.STAGES, default=StorageTankSizeFactor, doc='Storage Tank Size Factor' + ) + model.StorageTankSizeFactorByProd = Param( + model.PRODUCTS, + model.STAGES, + default=StorageTankSizeFactorByProd, + doc='Storage Tank Size Factor by Product', + ) + + # TODO: bonmin wasn't happy and I think it might have something to do with this? + # or maybe issues with convexity or a lack thereof... I don't know yet. + # I made PRODUCTS ordered so I could do this... Is that bad? And it does index + # from 1, right? + def get_log_coeffs(model, k): + """ + Calculates the logarithmic coefficients used in the model, typically for transforming linear + relationships into logarithmic form for optimization purposes. + + Parameters + ---------- + model : Pyomo.ConcreteModel + The Pyomo model for the batch processing optimization problem. + k : int + The index representing a parallel unit. + + Returns + ------- + float + The logarithm of the position of the parallel unit within its set, used as a coefficient in the model. + """ + return log(model.PARALLELUNITS.ord(k)) + + model.LogCoeffs = Param( + model.PARALLELUNITS, initialize=get_log_coeffs, doc='Logarithmic Coefficients' + ) + + # bounds + model.volumeLB = Param( + model.STAGES, default=VolumeLB, doc='Lower Bound of Volume of the Units' + ) + model.volumeUB = Param( + model.STAGES, default=VolumeUB, doc='Upper Bound of Volume of the Units' + ) + model.storageTankSizeLB = Param( + model.STAGES, default=StorageTankSizeLB, doc='Lower Bound of Storage Tank Size' + ) + model.storageTankSizeUB = Param( + model.STAGES, default=StorageTankSizeUB, doc='Upper Bound of Storage Tank Size' + ) + model.unitsInPhaseUB = Param( + model.STAGES, default=UnitsInPhaseUB, doc='Upper Bound of Units in Phase' + ) + model.unitsOutOfPhaseUB = Param( + model.STAGES, default=UnitsOutOfPhaseUB, doc='Upper Bound of Units Out of Phase' + ) + + # Variables + + # TODO: right now these match the formulation. There are more in GAMS... + + # unit size of stage j + # model.volume = Var(model.STAGES) + # # TODO: GAMS has a batch size indexed just by products that isn't in the formulation... I'm going + # # to try to avoid it for the moment... + # # batch size of product i at stage j + # model.batchSize = Var(model.PRODUCTS, model.STAGES) + # # TODO: this is different in GAMS... They index by stages too? + # # cycle time of product i divided by batch size of product i + # model.cycleTime = Var(model.PRODUCTS) + # # number of units in parallel out-of-phase (or in phase) at stage j + # model.unitsOutOfPhase = Var(model.STAGES) + # model.unitsInPhase = Var(model.STAGES) + # # TODO: what are we going to do as a boundary condition here? For that last stage? + # # size of intermediate storage tank between stage j and j+1 + # model.storageTankSize = Var(model.STAGES) + + # variables for convexified problem + # TODO: I am beginning to think these are my only variables actually. + # GAMS never un-logs them, I don't think. And I think the GAMs ones + # must be the log ones. + def get_volume_bounds(model, j): + """ + Defines the bounds for the volume of processing units at each stage. + + Parameters + ---------- + model : Pyomo.ConcreteModel + The Pyomo model for the batch processing optimization problem. + j : int + Index for the processing stages in the plant. Stages are ordered and include various processing steps. + + Returns + ------- + tuple + A tuple containing the lower and upper bounds for the volume of processing units at stage j.. + """ + return (model.volumeLB[j], model.volumeUB[j]) + + model.volume_log = Var( + model.STAGES, bounds=get_volume_bounds, doc='Logarithmic Volume of the Units' + ) + model.batchSize_log = Var( + model.PRODUCTS, model.STAGES, doc='Logarithmic Batch Size of the Products' + ) + model.cycleTime_log = Var( + model.PRODUCTS, doc='Logarithmic Cycle Time of the Products' + ) + + def get_unitsOutOfPhase_bounds(model, j): + """ + Defines the bounds for the logarithmic representation of the number of units out of phase at each stage. + + Parameters + ---------- + model : Pyomo.ConcreteModel + The Pyomo model for the batch processing optimization problem. + j : int + Index for the processing stages in the plant. Stages are ordered and include various processing steps. + + Returns + ------- + tuple + A tuple containing the lower and upper bounds for the logarithmic representation of the number of units out of phase at stage j. + """ + return (0, model.unitsOutOfPhaseUB[j]) + + model.unitsOutOfPhase_log = Var( + model.STAGES, + bounds=get_unitsOutOfPhase_bounds, + doc='Logarithmic Units Out of Phase', + ) + + def get_unitsInPhase_bounds(model, j): + """ + Defines the allowable bounds for the logarithmic number of processing units operating in phase at a given stage in the manufacturing process. + + Parameters + ---------- + model : Pyomo.ConcreteModel + The Pyomo model for the batch processing optimization problem. + j : int + Index for the processing stages in the plant. Stages are ordered and include various processing steps. + + Returns + ------- + tuple + A tuple containing the minimum and maximum bounds for the logarithmic number of units in phase at stage j, ensuring model constraints are met. + """ + return (0, model.unitsInPhaseUB[j]) + + model.unitsInPhase_log = Var( + model.STAGES, bounds=get_unitsInPhase_bounds, doc='Logarithmic Units In Phase' + ) + + def get_storageTankSize_bounds(model, j): + """ + Determines the lower and upper bounds for the logarithmic representation of the storage tank size between stages j and j+1. + + Parameters + ---------- + model : Pyomo.ConcreteModel + The Pyomo model for the batch processing optimization problem. + j : int + Index for the processing stages in the plant. Stages are ordered and include various processing steps. + + Returns + ------- + tuple + A tuple containing the lower and upper bounds for the storage tank size at the specified stage. + """ + return (model.storageTankSizeLB[j], model.storageTankSizeUB[j]) + + # TODO: these bounds make it infeasible... + model.storageTankSize_log = Var( + model.STAGES, + bounds=get_storageTankSize_bounds, + doc='Logarithmic Storage Tank Size', + ) + + # binary variables for deciding number of parallel units in and out of phase + model.outOfPhase = Var( + model.STAGES, model.PARALLELUNITS, within=Binary, doc='Out of Phase Units' + ) + model.inPhase = Var( + model.STAGES, model.PARALLELUNITS, within=Binary, doc='In Phase Units' + ) + + # Objective + + def get_cost_rule(model): + """ + Defines the objective function for the model, representing the total cost of the plant design. + + Parameters + ---------- + model : Pyomo.ConcreteModel + The Pyomo model for the batch processing optimization problem. + + Returns + ------- + Pyomo.Expression + A Pyomo expression representing the total cost of the plant design. + + Notes + ----- + The cost is a function of the volume of processing units and the size of storage tanks, each scaled by respective cost + parameters and exponentiated to reflect non-linear cost relationships. + """ + return model.Alpha1 * sum( + exp( + model.unitsInPhase_log[j] + + model.unitsOutOfPhase_log[j] + + model.Beta1 * model.volume_log[j] + ) + for j in model.STAGES + ) + model.Alpha2 * sum( + exp(model.Beta2 * model.storageTankSize_log[j]) + for j in model.STAGESExceptLast + ) + + model.min_cost = Objective( + rule=get_cost_rule, doc='Minimize the Total Cost of the Plant Design' + ) + + # Constraints + def processing_capacity_rule(model, j, i): + """ + Ensures that the volume of each processing unit at stage j is sufficient to accommodate the batch size of product i, + taking into account the size factor of the product and the number of units in phase at that stage. + + Parameters + ---------- + model : Pyomo.ConcreteModel + The Pyomo model for the batch processing optimization problem. + j : int + Index for the processing stages in the plant. Stages are ordered and include various processing steps. + i : int + The index representing a specific product. Products have unique processing requirements, including + batch sizes and processing times, that vary by stage. + + Returns + ------- + Pyomo.Expression + A Pyomo expression that defines the processing capacity constraint for product `i` at stage `j`. + """ + return ( + model.volume_log[j] + >= log(model.ProductSizeFactor[i, j]) + + model.batchSize_log[i, j] + - model.unitsInPhase_log[j] + ) + + model.processing_capacity = Constraint( + model.STAGES, + model.PRODUCTS, + rule=processing_capacity_rule, + doc='Processing Capacity', + ) + + def processing_time_rule(model, j, i): + """ + Ensures that the cycle time for product i at stage j, adjusted for the number of out-of-phase units, meets the required processing time. + + Parameters + ---------- + model : Pyomo.ConcreteModel + The Pyomo model for the batch processing optimization problem. + j : int + Index for the processing stages in the plant. Stages are ordered and include various processing steps. + i : int + Product index, representing different products being processed in the plant, each with its own set of + processing times across various stages. + + Returns + ------- + Pyomo.Expression + A Pyomo expression defining the constraint that the cycle time for processing product `i` at stage `j` + must not exceed the maximum allowed, considering the batch size and the units out of phase at this stage. + """ + return ( + model.cycleTime_log[i] + >= log(model.ProcessingTime[i, j]) + - model.batchSize_log[i, j] + - model.unitsOutOfPhase_log[j] + ) + + model.processing_time = Constraint( + model.STAGES, model.PRODUCTS, rule=processing_time_rule, doc='Processing Time' + ) + + def finish_in_time_rule(model): + """ + Ensures that the total production time across all products does not exceed the defined time horizon for the process. + + Parameters + ---------- + model : Pyomo.ConcreteModel + The Pyomo model for the batch processing optimization problem. + + Returns + ------- + Pyomo.Expression + A Pyomo constraint expression ensuring the total production time does not exceed the time horizon for the plant. + """ + return model.HorizonTime >= sum( + model.ProductionAmount[i] * exp(model.cycleTime_log[i]) + for i in model.PRODUCTS + ) + + model.finish_in_time = Constraint(rule=finish_in_time_rule, doc='Finish in Time') + + # Disjunctions + + def storage_tank_selection_disjunct_rule(disjunct, selectStorageTank, j): + """ + Defines the conditions under which a storage tank will be included or excluded between stages j and j+1. + This rule is applied to a disjunct, which is part of a disjunction representing this binary decision. + + Parameters + ---------- + disjunct : Pyomo.Disjunct + A Pyomo Disjunct object representing a specific case within the disjunction. + selectStorageTank : int + A binary indicator (0 or 1) where 1 means a storage tank is included and 0 means it is not. + j : int + Index for the processing stages in the plant. Stages are ordered and include various processing steps. + + Returns + ------- + None + This function defines constraints within the disjunct based on the decision to include (selectStorageTank=1) or exclude (selectStorageTank=0) a storage tank. + Constraints ensure the storage tank's volume can accommodate the batch sizes at stage j and j+1 if included, or ensure batch size continuity if excluded. + """ + model = disjunct.model() + + def volume_stage_j_rule(disjunct, i): + """ + Ensures the storage tank size between stages j and j+1 is sufficient to accommodate the batch size of product i at stage j, considering the storage tank size factor. + + Parameters + ---------- + disjunct : Pyomo.Disjunct + The disjunct within which this constraint is defined. + i : int + Product index, representing different products being processed in the plant, each with its own set of + processing times across various stages. + + Returns + ------- + Pyomo.Constraint + A constraint ensuring the storage tank size is sufficient for the batch size at stage j. + """ + return ( + model.storageTankSize_log[j] + >= log(model.StorageTankSizeFactor[j]) + model.batchSize_log[i, j] + ) + + def volume_stage_jPlus1_rule(disjunct, i): + """ + Ensures the storage tank size between stages j and j+1 is sufficient to accommodate the batch size of product i at stage j+1, considering the storage tank size factor. + + Parameters + ---------- + disjunct : Pyomo.Disjunct + The disjunct within which this constraint is defined. + i : int + Product index, representing different products being processed in the plant, each with its own set of + processing times across various stages. + + Returns + ------- + Pyomo.Constraint + A constraint ensuring the storage tank size is sufficient for the batch size at stage j+1. + """ + return ( + model.storageTankSize_log[j] + >= log(model.StorageTankSizeFactor[j]) + model.batchSize_log[i, j + 1] + ) + + def batch_size_rule(disjunct, i): + """ + Ensures the difference in batch sizes between stages j and j+1 for product i is within the acceptable limits defined by the storage tank size factor by product. + + Parameters + ---------- + disjunct : Pyomo.Disjunct + The disjunct within which this constraint is defined. + i : int + Product index, representing different products being processed in the plant, each with its own set of + processing times across various stages. + + Returns + ------- + Pyomo.Constraint + A constraint enforcing acceptable batch size differences between stages j and j+1. + """ + return inequality( + -log(model.StorageTankSizeFactorByProd[i, j]), + model.batchSize_log[i, j] - model.batchSize_log[i, j + 1], + log(model.StorageTankSizeFactorByProd[i, j]), + ) + + def no_batch_rule(disjunct, i): + """ + Enforces batch size continuity between stages j and j+1 for product i, applicable when no storage tank is selected. + + Parameters + ---------- + disjunct : Pyomo.Disjunct + The disjunct within which this constraint is defined. + i : int + Product index, representing different products being processed in the plant, each with its own set of + processing times across various stages. + + Returns + ------- + Pyomo.Constraint + A constraint ensuring batch size continuity between stages j and j+1 + """ + return model.batchSize_log[i, j] - model.batchSize_log[i, j + 1] == 0 + + if selectStorageTank: + disjunct.volume_stage_j = Constraint( + model.PRODUCTS, rule=volume_stage_j_rule + ) + disjunct.volume_stage_jPlus1 = Constraint( + model.PRODUCTS, rule=volume_stage_jPlus1_rule + ) + disjunct.batch_size = Constraint(model.PRODUCTS, rule=batch_size_rule) + else: + # The formulation says 0, but GAMS has this constant. + # 04/04: Francisco says volume should be free: + # disjunct.no_volume = Constraint(expr=model.storageTankSize_log[j] == MinFlow) + disjunct.no_batch = Constraint(model.PRODUCTS, rule=no_batch_rule) + + model.storage_tank_selection_disjunct = Disjunct( + [0, 1], + model.STAGESExceptLast, + rule=storage_tank_selection_disjunct_rule, + doc='Storage Tank Selection Disjunct', + ) + + def select_storage_tanks_rule(model, j): + """ + Defines a disjunction for the model to choose between including or not including a storage tank between stages j and j+1. + + Parameters + ---------- + model : Pyomo.ConcreteModel + The Pyomo model for the batch processing optimization problem. + j : int + Index for the processing stages in the plant. Stages are ordered and include various processing steps. + + Returns + ------- + list + A list of disjuncts representing the choices for including or not including a storage tank between stages j and j+1. + """ + return [ + model.storage_tank_selection_disjunct[selectTank, j] + for selectTank in [0, 1] + ] + + model.select_storage_tanks = Disjunction( + model.STAGESExceptLast, + rule=select_storage_tanks_rule, + doc='Select Storage Tanks', + ) + + # though this is a disjunction in the GAMs model, it is more efficiently formulated this way: + # TODO: what on earth is k? Number of Parallel units. + def units_out_of_phase_rule(model, j): + """ + Defines the constraints for the logarithmic representation of the number of units k out of phase in stage j. + + Parameters + ---------- + model : Pyomo.ConcreteModel + The Pyomo model for the batch processing optimization problem. + j : int + Index for the processing stages in the plant. Stages are ordered and include various processing steps. + + Returns + ------- + None + Adds a constraint to the Pyomo model representing the logarithmic sum of out-of-phase units at stage j. + This constraint is not returned but directly added to the model. + + Notes + ----- + These are not directly related to disjunctions but more to the logical modeling of unit operations. + """ + return model.unitsOutOfPhase_log[j] == sum( + model.LogCoeffs[k] * model.outOfPhase[j, k] for k in model.PARALLELUNITS + ) + + model.units_out_of_phase = Constraint( + model.STAGES, rule=units_out_of_phase_rule, doc='Units Out of Phase' + ) + + def units_in_phase_rule(model, j): + """_summary_ + Defines the constraints for the logarithmic representation of the number of units k in-phase in stage j. + + Parameters + ---------- + model : Pyomo.ConcreteModel + The Pyomo model for the batch processing optimization problem. + j : int + Index for the processing stages in the plant. Stages are ordered and include various processing steps. + + Returns + ------- + None + Incorporates a constraint into the Pyomo model that corresponds to the logarithmic sum of in-phase units at stage j. + The constraint is directly applied to the model without an explicit return value. + + Notes + ----- + These are not directly related to disjunctions but more to the logical modeling of unit operations. + """ + return model.unitsInPhase_log[j] == sum( + model.LogCoeffs[k] * model.inPhase[j, k] for k in model.PARALLELUNITS + ) + + model.units_in_phase = Constraint( + model.STAGES, rule=units_in_phase_rule, doc='Units In Phase' + ) + + def units_out_of_phase_xor_rule(model, j): + """ + Enforces an exclusive OR (XOR) constraint ensuring that exactly one configuration for the number of units out of phase is selected at stage j. + + Parameters + ---------- + model : Pyomo.ConcreteModel + The Pyomo model for the batch processing optimization problem. + j : int + Index for the processing stages in the plant. Stages are ordered and include various processing steps. + + Returns + ------- + Pyomo.Constraint + A Pyomo constraint expression calculating the logarithmic representation of the number of units out of phase at stage j + """ + return sum(model.outOfPhase[j, k] for k in model.PARALLELUNITS) == 1 + + model.units_out_of_phase_xor = Constraint( + model.STAGES, + rule=units_out_of_phase_xor_rule, + doc='Exclusive OR for Units Out of Phase', + ) + + def units_in_phase_xor_rule(model, j): + """ + Enforces an exclusive OR (XOR) constraint ensuring that exactly one configuration for the number of units in phase is selected at stage j. + + Parameters + ---------- + model : Pyomo.ConcreteModel + The Pyomo model for the batch processing optimization problem. + j : int + Index for the processing stages in the plant. Stages are ordered and include various processing steps. + + Returns + ------- + Pyomo.Constraint + A Pyomo constraint expression enforcing the XOR condition for units out of phase at stage j. + """ + return sum(model.inPhase[j, k] for k in model.PARALLELUNITS) == 1 + + model.units_in_phase_xor = Constraint( + model.STAGES, + rule=units_in_phase_xor_rule, + doc='Exclusive OR for Units In Phase', + ) + + return model.create_instance(join(this_file_dir(), 'batch_processing.dat')) + + +if __name__ == "__main__": + m = build_model() + TransformationFactory('gdp.bigm').apply_to(m) + SolverFactory('gams').solve( + m, solver='baron', tee=True, add_options=['option optcr=1e-6;'] + ) + m.min_cost.display() diff --git a/gdplib/biofuel/model.py b/gdplib/biofuel/model.py index 9ed8908..84820f4 100644 --- a/gdplib/biofuel/model.py +++ b/gdplib/biofuel/model.py @@ -1,40 +1,113 @@ -from __future__ import division +""" +model.py +This model describes a cost minimization for a multi-period biofuel processing network. + +The model enforces constraints to ensure that raw material supplies do not exceed available amounts, product shipments meet market demands exactly, and production at each site matches outgoing shipments and available resources. +It also optimizes transportation costs by managing both variable and fixed costs associated with active transportation routes. +The disjunctions in the model define the operational modes for facility sites (modular, conventional, or inactive) and the activity status of supply and product routes (active or inactive). +These elements allow the model to simulate different operational scenarios and strategic decisions, optimizing the network's layout and logistics based on economic and market conditions. +The objective of the model is to optimize the network layout and production allocation to minimize total costs, which include setup and teardown of facilities, production costs, and transportation costs. + +References: + [1] Lara, C. L., Trespalacios, F., & Grossmann, I. E. (2018). Global optimization algorithm for capacitated multi-facility continuous location-allocation problems. Journal of Global Optimization, 71(4), 871-889. https://doi.org/10.1007/s10898-018-0621-6 + [2] Chen, Q., & Grossmann, I. E. (2019). Effective generalized disjunctive programming models for modular process synthesis. Industrial & Engineering Chemistry Research, 58(15), 5873-5886. https://doi.org/10.1021/acs.iecr.8b04600 +""" import os from math import fabs import pandas as pd from pyomo.environ import ( - ConcreteModel, Constraint, Integers, minimize, NonNegativeReals, Objective, Param, RangeSet, SolverFactory, sqrt, - Suffix, summation, TransformationFactory, value, Var, ) + ConcreteModel, + Constraint, + Integers, + minimize, + NonNegativeReals, + Objective, + Param, + RangeSet, + SolverFactory, + sqrt, + Suffix, + summation, + TransformationFactory, + value, + Var, +) from pyomo.gdp import Disjunct def build_model(): - m = ConcreteModel() - m.bigM = Suffix(direction=Suffix.LOCAL) + """ + Build a concrete model that describes a cost minimization for a multi-period biofuel processing network. + + Returns + ------- + Pyomo.ConcreteModel + The Pyomo concrete model which describes the multiperiod location-allocation optimization model designed to determine the most cost-effective network layout and production allocation to meet market demands. + + References + ---------- + [1] Lara, C. L., Trespalacios, F., & Grossmann, I. E. (2018). Global optimization algorithm for capacitated multi-facility continuous location-allocation problems. Journal of Global Optimization, 71(4), 871-889. https://doi.org/10.1007/s10898-018-0621-6 + [2] Chen, Q., & Grossmann, I. E. (2019). Effective generalized disjunctive programming models for modular process synthesis. Industrial & Engineering Chemistry Research, 58(15), 5873-5886. https://doi.org/10.1021/acs.iecr.8b04600 + """ + m = ConcreteModel('Biofuel processing network') + m.bigM = Suffix(direction=Suffix.LOCAL, initialize=7000) m.time = RangeSet(0, 120, doc="months in 10 years") - m.suppliers = RangeSet(10) - m.markets = RangeSet(10) - m.potential_sites = RangeSet(12) - m.discount_rate = Param(initialize=0.08, doc="8%") + m.suppliers = RangeSet(10) # 10 suppliers + m.markets = RangeSet(10) # 10 markets + m.potential_sites = RangeSet(12) # 12 facility sites + m.discount_rate = Param(initialize=0.08, doc="discount rate [8%]") m.conv_setup_time = Param(initialize=12) m.modular_setup_time = Param(initialize=3) m.modular_teardown_time = Param(initialize=3) - m.teardown_value = Param(initialize=0.30, doc="30%") - m.conventional_salvage_value = Param(initialize=0.05, doc="5%") + m.teardown_value = Param(initialize=0.30, doc="tear down value [30%]") + m.conventional_salvage_value = Param(initialize=0.05, doc="salvage value [5%]") @m.Param(m.time) def discount_factor(m, t): + """ + Calculate the discount factor for a given time period 't', based on a monthly compounding interest rate. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo concrete model which describes the multiperiod location-allocation optimization model + t : int + Index of time in months from 0 to 120 (10 years) + + Returns + ------- + Pyomo.Parameter + The discount factor for month 't', calculated using the formula (1 + r/12)**(-t/12) where 'r' is the annual discount rate. + """ return (1 + m.discount_rate / 12) ** (-t / 12) xls_data = pd.read_excel( os.path.join(os.path.dirname(__file__), "problem_data.xlsx"), sheet_name=["sources", "markets", "sites", "growth", "decay"], - index_col=0) + index_col=0, + ) @m.Param(m.markets, m.time, doc="Market demand [thousand ton/month]") def market_demand(m, mkt, t): + """ + Calculate the market demand for a given market 'mkt' at time 't', based on the demand data provided in the Excel file. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo concrete model which describes the multiperiod location-allocation optimization model + mkt : int + Index of the market from 1 to 10 + t : int + Index of time in months from 0 to 120 (10 years) + + Returns + ------- + Pyomo.Parameter + If the conversion setup time is less than or equal to 't' and 't' is less than the maximum time period minus 3 months, return the market demand in thousand tons per month, otherwise return 0. + """ if m.conv_setup_time <= t <= max(m.time) - 3: return float(xls_data["markets"]["demand"][mkt]) / 1000 / 12 else: @@ -42,6 +115,23 @@ def market_demand(m, mkt, t): @m.Param(m.suppliers, m.time, doc="Raw material supply [thousand ton/month]") def available_supply(m, sup, t): + """ + Calculate the available supply of raw materials for a given supplier 'sup' at time 't', based on the supply data provided in the Excel file. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo concrete model which describes the multiperiod location-allocation optimization model + sup : int + Index of the supplier from 1 to 10 + t : int + Index of time in months from 0 to 120 (10 years) + + Returns + ------- + Pyomo.Parameter + If 't' is before the growth period or after the decay period, return 0, otherwise return the available supply in thousand tons per month. + """ # If t is before supply available or after supply decayed, then no # supply if t < float(xls_data["sources"]["growth"][sup]): @@ -53,173 +143,715 @@ def available_supply(m, sup, t): @m.Param(m.suppliers) def supplier_x(m, sup): + """ + Get the x-coordinate of the supplier location in miles from the Excel data. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo concrete model which describes the multiperiod location-allocation optimization model + sup : int + Index of the supplier from 1 to 10 + + Returns + ------- + Pyomo.Parameter + x-coordinate of the supplier location in miles + """ return float(xls_data["sources"]["x"][sup]) @m.Param(m.suppliers) def supplier_y(m, sup): + """ + Get the y-coordinate of the supplier location in miles from the Excel data. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo concrete model which describes the multiperiod location-allocation optimization model + sup : int + Index of the supplier from 1 to 10 + + Returns + ------- + Pyomo.Parameter + y-coordinate of the supplier location in miles + """ return float(xls_data["sources"]["y"][sup]) @m.Param(m.markets) def market_x(m, mkt): + """ + Get the x-coordinate of the market location in miles from the Excel data. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo concrete model which describes the multiperiod location-allocation optimization model + mkt : int + Index of the market from 1 to 10 + + Returns + ------- + Pyomo.Parameter + x-coordinate of the market location in miles + """ return float(xls_data["markets"]["x"][mkt]) @m.Param(m.markets) def market_y(m, mkt): + """ + Get the y-coordinate of the market location in miles from the Excel data. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo concrete model which describes the multiperiod location-allocation optimization model + mkt : int + Index of the market from 1 to 10 + + Returns + ------- + Pyomo.Parameter + y-coordinate of the market location in miles + """ return float(xls_data["markets"]["y"][mkt]) @m.Param(m.potential_sites) def site_x(m, site): + """ + Get the x-coordinate of the facility site location in miles from the Excel data. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo concrete model which describes the multiperiod location-allocation optimization model + site : int + Index of the facility site from 1 to 12 + + Returns + ------- + Pyomo.Parameter + x-coordinate of the facility site location in miles + """ return float(xls_data["sites"]["x"][site]) @m.Param(m.potential_sites) def site_y(m, site): + """ + Get the y-coordinate of the facility site location in miles from the Excel data. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo concrete model which describes the multiperiod location-allocation optimization model + site : int + Index of the facility site from 1 to 12 + + Returns + ------- + Pyomo.Parameter + y-coordinate of the facility site location in miles + """ return float(xls_data["sites"]["y"][site]) @m.Param(m.suppliers, m.potential_sites, doc="Miles") def dist_supplier_to_site(m, sup, site): - return sqrt((m.supplier_x[sup] - m.site_x[site]) ** 2 + - (m.supplier_y[sup] - m.site_y[site]) ** 2) + """ + Calculate the distance in miles between a supplier 'sup' and a facility site 'site' using the Euclidean distance formula. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo concrete model which describes the multiperiod location-allocation optimization model + sup : int + Index of the supplier from 1 to 10 + site : int + Index of the facility site from 1 to 12 + + Returns + ------- + Pyomo.Parameter + The distance in miles between the supplier and the facility site + """ + return sqrt( + (m.supplier_x[sup] - m.site_x[site]) ** 2 + + (m.supplier_y[sup] - m.site_y[site]) ** 2 + ) @m.Param(m.potential_sites, m.markets, doc="Miles") def dist_site_to_market(m, site, mkt): - return sqrt((m.site_x[site] - m.market_x[mkt]) ** 2 + - (m.site_y[site] - m.market_y[mkt]) ** 2) + """ + Calculate the distance in miles between a facility site 'site' and a market 'mkt' using the Euclidean distance formula. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo concrete model which describes the multiperiod location-allocation optimization model + site : int + Index of the facility site from 1 to 12 + mkt : int + Index of the market from 1 to 10 + + Returns + ------- + Pyomo.Parameter + The distance in miles between the facility site and the market + """ + return sqrt( + (m.site_x[site] - m.market_x[mkt]) ** 2 + + (m.site_y[site] - m.market_y[mkt]) ** 2 + ) m.conversion = Param(initialize=0.26, doc="overall conversion to product") m.conv_site_size = Var( m.potential_sites, - bounds=(120 / 12 / 10, 120 / 12), initialize=1, - doc="Product capacity of site [thousand ton/mo]") + bounds=(120 / 12 / 10, 120 / 12), + initialize=1, + doc="Product capacity of site [thousand ton/mo]", + ) - m.conv_base_cost = Param(initialize=268.4, doc="Cost for size 120k per year [million $]") - m.module_base_cost = Param(initialize=268.4, doc="Cost for size 120k per year [million $]") + m.conv_base_cost = Param( + initialize=268.4, doc="Cost for size 120k per year [million $]" + ) + m.module_base_cost = Param( + initialize=268.4, doc="Cost for size 120k per year [million $]" + ) m.conv_exponent = Param(initialize=0.7) - m.supply = Var(m.potential_sites, m.time, bounds=(0, 120 / 12 / 0.26 * 10), doc="thousand ton/mo") - m.production = Var(m.potential_sites, m.time, bounds=(0, 120 / 12 * 10), doc="thousand ton/mo") - m.num_modules = Var(m.potential_sites, m.time, domain=Integers, bounds=(0, 10)) - m.modules_purchased = Var(m.potential_sites, m.time, domain=Integers, bounds=(0, 10)) - m.modules_sold = Var(m.potential_sites, m.time, domain=Integers, bounds=(0, 10)) + m.supply = Var( + m.potential_sites, + m.time, + bounds=(0, 120 / 12 / 0.26 * 10), + doc="thousand ton/mo", + ) + m.production = Var( + m.potential_sites, m.time, bounds=(0, 120 / 12 * 10), doc="thousand ton/mo" + ) + m.num_modules = Var( + m.potential_sites, + m.time, + domain=Integers, + bounds=(0, 10), + doc="Number of modules", + ) + m.modules_purchased = Var( + m.potential_sites, + m.time, + domain=Integers, + bounds=(0, 10), + doc="Modules purchased", + ) + m.modules_sold = Var( + m.potential_sites, m.time, domain=Integers, bounds=(0, 10), doc="Modules sold" + ) m.conv_build_cost = Var( m.potential_sites, doc="Cost of building conventional facility [milllion $]", - bounds=(0, 1350 * 10), initialize=0) + bounds=(0, 1350 * 10), + initialize=0, + ) @m.Param(m.suppliers, m.time) def raw_material_unit_cost(m, sup, t): + """ + Calculate the unit cost of raw materials for a given supplier 'sup' at time 't', based on the cost data provided in the Excel file. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo concrete model which describes the multiperiod location-allocation optimization model + sup : int + Index of the supplier from 1 to 10 + t : int + Index of time in months from 0 to 120 (10 years) + + Returns + ------- + Pyomo.Parameter + The unit cost of raw materials for the supplier at time 't', calculated as the cost from the Excel data multiplied by the discount factor for time 't'. + """ return float(xls_data["sources"]["cost"][sup]) * m.discount_factor[t] @m.Param(m.time) def module_unit_cost(m, t): + """ + Calculate the unit cost of modules at time 't', based on the cost data provided in the Excel file. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo concrete model which describes the multiperiod location-allocation optimization model + t : int + Index of time in months from 0 to 120 (10 years) + + Returns + ------- + Pyomo.Parameter + The unit cost of modules at time 't', calculated as the cost from the Excel data multiplied by the discount factor for time 't'. + """ return m.module_base_cost * m.discount_factor[t] @m.Param(m.time, doc="$/ton") def unit_production_cost(m, t): + """ + Calculate the unit production cost at time 't', the production cost is 300 $/ton multiplied by the discount factor for time 't'. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo concrete model which describes the multiperiod location-allocation optimization model + t : int + Index of time in months from 0 to 120 (10 years) + + Returns + ------- + Pyomo.Parameter + The unit production cost at time 't', calculated as 300 $/ton multiplied by the discount factor for time 't'. + """ return 300 * m.discount_factor[t] @m.Param(doc="thousand $") def transport_fixed_cost(m): + """ + Fixed cost of transportation in thousand dollars. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo concrete model which describes the multiperiod location-allocation optimization model + + Returns + ------- + Pyomo.Parameter + The fixed cost of transportation in thousand dollars, the cost is 125 thousand dollars. + """ return 125 @m.Param(m.time, doc="$/ton-mile") def unit_product_transport_cost(m, t): + """ + Calculate the unit product transport cost at time 't', the cost is 0.13 $/ton-mile multiplied by the discount factor for time 't'. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo concrete model which describes the multiperiod location-allocation optimization model + t : int + Index of time in months from 0 to 120 (10 years) + + Returns + ------- + Pyomo.Parameter + The unit product transport cost at time 't', calculated as 0.13 $/ton-mile multiplied by the discount factor for time 't'. + """ return 0.13 * m.discount_factor[t] @m.Param(m.time, doc="$/ton-mile") def unit_raw_material_transport_cost(m, t): + """ + Calculate the unit raw material transport cost at time 't', the cost is 2 $/ton-mile multiplied by the discount factor for time 't'. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo concrete model which describes the multiperiod location-allocation optimization model + t : int + Index of time in months from 0 to 120 (10 years) + + Returns + ------- + Pyomo.Parameter + The unit raw material transport cost at time 't', calculated as 2 $/ton-mile multiplied by the discount factor for time 't'. + """ return 2 * m.discount_factor[t] m.supply_shipments = Var( - m.suppliers, m.potential_sites, m.time, domain=NonNegativeReals, - bounds=(0, 120 / 12 / 0.26), doc="thousand ton/mo") + m.suppliers, + m.potential_sites, + m.time, + domain=NonNegativeReals, + bounds=(0, 120 / 12 / 0.26), + doc="thousand ton/mo", + ) m.product_shipments = Var( - m.potential_sites, m.markets, m.time, domain=NonNegativeReals, - bounds=(0, 120 / 12), doc="thousand ton/mo") + m.potential_sites, + m.markets, + m.time, + domain=NonNegativeReals, + bounds=(0, 120 / 12), + doc="thousand ton/mo", + ) @m.Constraint(m.suppliers, m.time) def supply_limits(m, sup, t): - return sum(m.supply_shipments[sup, site, t] - for site in m.potential_sites) <= m.available_supply[sup, t] + """ + Ensure that the total supply from a supplier 'sup' at time 't' does not exceed the available supply from the supplier. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo concrete model which describes the multiperiod location-allocation optimization model + sup : int + Index of the supplier from 1 to 10 + t : int + Index of time in months from 0 to 120 (10 years) + + Returns + ------- + Pyomo.Constraint + The total supply from the supplier 'sup' at time 't' should not exceed the available supply from the supplier. + """ + return ( + sum(m.supply_shipments[sup, site, t] for site in m.potential_sites) + <= m.available_supply[sup, t] + ) @m.Constraint(m.markets, m.time) def demand_satisfaction(m, mkt, t): - return sum(m.product_shipments[site, mkt, t] - for site in m.potential_sites) == m.market_demand[mkt, t] + """ + Ensure that the total product shipments to a market 'mkt' at time 't' meets the market demand for the product. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo concrete model which describes the multiperiod location-allocation optimization model + mkt : int + Index of the market from 1 to 10 + t : int + Index of time in months from 0 to 120 (10 years) + + Returns + ------- + Pyomo.Constraint + The total product shipments to the market 'mkt' at time 't' should meet the market demand for the product. + """ + return ( + sum(m.product_shipments[site, mkt, t] for site in m.potential_sites) + == m.market_demand[mkt, t] + ) @m.Constraint(m.potential_sites, m.time) def product_balance(m, site, t): - return m.production[site, t] == sum(m.product_shipments[site, mkt, t] - for mkt in m.markets) + """ + Ensure that the total product shipments from a facility site 'site' at time 't' meets the production from the site. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo concrete model which describes the multiperiod location-allocation optimization model + site : int + Index of the facility site from 1 to 12 + t : int + Index of time in months from 0 to 120 (10 years) + + Returns + ------- + Pyomo.Constraint + The total product shipments from the facility site 'site' at time 't' should meet the production from the site. + """ + return m.production[site, t] == sum( + m.product_shipments[site, mkt, t] for mkt in m.markets + ) @m.Constraint(m.potential_sites, m.time) def require_raw_materials(m, site, t): + """ + Ensure that the raw materials required for production at a facility site 'site' at time 't' are available from the suppliers. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo concrete model which describes the multiperiod location-allocation optimization model + site : int + Index of the facility site from 1 to 12 + t : int + Index of time in months from 0 to 120 (10 years) + + Returns + ------- + Pyomo.Constraint + The production at the facility site 'site' at time 't' should not exceed the raw materials available from the suppliers which is the supply multiplied by the conversion factor. + """ return m.production[site, t] <= m.conversion * m.supply[site, t] - m.modular = Disjunct(m.potential_sites, rule=_build_modular_disjunct) - m.conventional = Disjunct(m.potential_sites, rule=_build_conventional_disjunct) - m.site_inactive = Disjunct(m.potential_sites, rule=_build_site_inactive_disjunct) + m.modular = Disjunct( + m.potential_sites, rule=_build_modular_disjunct, doc="Disjunct for modular site" + ) + m.conventional = Disjunct( + m.potential_sites, + rule=_build_conventional_disjunct, + doc="Disjunct for conventional site", + ) + m.site_inactive = Disjunct( + m.potential_sites, + rule=_build_site_inactive_disjunct, + doc="Disjunct for inactive site", + ) - m.supply_route_active = Disjunct(m.suppliers, m.potential_sites, rule=_build_supply_route_active) - m.supply_route_inactive = Disjunct(m.suppliers, m.potential_sites, rule=_build_supply_route_inactive) + m.supply_route_active = Disjunct( + m.suppliers, + m.potential_sites, + rule=_build_supply_route_active, + doc="Disjunct for active supply route", + ) + m.supply_route_inactive = Disjunct( + m.suppliers, + m.potential_sites, + rule=_build_supply_route_inactive, + doc="Disjunct for inactive supply route", + ) - m.product_route_active = Disjunct(m.potential_sites, m.markets, rule=_build_product_route_active) - m.product_route_inactive = Disjunct(m.potential_sites, m.markets, rule=_build_product_route_inactive) + m.product_route_active = Disjunct( + m.potential_sites, + m.markets, + rule=_build_product_route_active, + doc="Disjunct for active product route", + ) + m.product_route_inactive = Disjunct( + m.potential_sites, + m.markets, + rule=_build_product_route_inactive, + doc="Disjunct for inactive product route", + ) @m.Disjunction(m.potential_sites) def site_type(m, site): + """ + Define the disjunction for the facility site type, which can be modular, conventional, or inactive. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo concrete model which describes the multiperiod location-allocation optimization model + site : int + Index of the facility site from 1 to 12 + + Returns + ------- + Pyomo.Disjunction + The disjunction for the facility site type, which can be modular, conventional, or inactive. + """ return [m.modular[site], m.conventional[site], m.site_inactive[site]] @m.Disjunction(m.suppliers, m.potential_sites) def supply_route_active_or_not(m, sup, site): + """ + Define the disjunction for the supply route between a supplier and a facility site, which can be active or inactive. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo concrete model which describes the multiperiod location-allocation optimization model + sup : int + Index of the supplier from 1 to 10 + site : int + Index of the facility site from 1 to 12 + + Returns + ------- + Pyomo.Disjunction + The disjunction for the supply route between a supplier and a facility site, which can be active or inactive. + """ return [m.supply_route_active[sup, site], m.supply_route_inactive[sup, site]] @m.Disjunction(m.potential_sites, m.markets) def product_route_active_or_not(m, site, mkt): + """ + Define the disjunction for the product route between a facility site and a market, which can be active or inactive. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo concrete model which describes the multiperiod location-allocation optimization model + site : int + Index of the facility site from 1 to 12 + mkt : int + Index of the market from 1 to 10 + + Returns + ------- + Pyomo.Disjunction + The disjunction for the product route between a facility site and a market, which can be active or inactive. + """ return [m.product_route_active[site, mkt], m.product_route_inactive[site, mkt]] @m.Expression(m.suppliers, m.potential_sites, doc="million $") def raw_material_transport_cost(m, sup, site): + """ + Calculate the cost of transporting raw materials from a supplier 'sup' to a facility site 'site' at each time period using the unit raw material transport cost, the supply shipments, and the distance between the supplier and the site. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo concrete model which describes the multiperiod location-allocation optimization model + sup : int + _description_ + site : int + Index of the facility site from 1 to 12 + + Returns + ------- + Pyomo.Expression + Total transportation cost considering the quantity of shipments, unit cost per time period, and distance between suppliers and sites. + """ return sum( - m.supply_shipments[sup, site, t] - * m.unit_raw_material_transport_cost[t] - * m.dist_supplier_to_site[sup, site] / 1000 - for t in m.time) + m.supply_shipments[sup, site, t] # [1000 ton/month] + * m.unit_raw_material_transport_cost[t] # [$/ton-mile] + * m.dist_supplier_to_site[sup, site] + / 1000 # [mile], [million/1000] + for t in m.time + ) @m.Expression(doc="million $") def raw_material_fixed_transport_cost(m): + """ + Calculate the fixed cost of transporting raw materials to the facility sites based on the total number of active supply routes and the fixed transportation cost. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo concrete model which describes the multiperiod location-allocation optimization model + + Returns + ------- + Pyomo.Expression + Sum of fixed transport costs, accounting for the activation of each route. + """ return ( - sum(m.supply_route_active[sup, site].binary_indicator_var - for sup in m.suppliers for site in m.potential_sites) - * m.transport_fixed_cost / 1000) + sum( + m.supply_route_active[sup, site].binary_indicator_var + for sup in m.suppliers + for site in m.potential_sites + ) + * m.transport_fixed_cost + / 1000 + ) # [thousand $] [million/1000] @m.Expression(m.potential_sites, m.markets, doc="million $") def product_transport_cost(m, site, mkt): + """ + Calculate the cost of transporting products from a facility site 'site' to a market 'mkt' at each time period using the unit product transport cost, the product shipments, and the distance between the site and the market. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo concrete model which describes the multiperiod location-allocation optimization model + site : int + Index of the facility site from 1 to 12 + mkt : int + _description_ + + Returns + ------- + Pyomo.Expression + Total transportation cost considering the quantity of shipments, unit cost per time period, and distance between sites and markets. + """ return sum( - m.product_shipments[site, mkt, t] - * m.unit_product_transport_cost[t] - * m.dist_site_to_market[site, mkt] / 1000 - for t in m.time) + m.product_shipments[site, mkt, t] # [1000 ton/month] + * m.unit_product_transport_cost[t] # [$/ton-mile] + * m.dist_site_to_market[site, mkt] + / 1000 # [mile], [million/1000] + for t in m.time + ) @m.Expression(doc="million $") def product_fixed_transport_cost(m): + """ + Calculate the fixed cost of transporting products to the markets based on the total number of active product routes and the fixed transportation cost. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo concrete model which describes the multiperiod location-allocation optimization model + + Returns + ------- + Pyomo.Expression + Sum of fixed transport costs, accounting for the activation of each route. + """ return ( - sum(m.product_route_active[site, mkt].binary_indicator_var - for site in m.potential_sites for mkt in m.markets) - * m.transport_fixed_cost / 1000) - - @m.Expression(m.potential_sites, m.time, doc="Cost of module setups in each month [million $]") + sum( + m.product_route_active[site, mkt].binary_indicator_var + for site in m.potential_sites + for mkt in m.markets + ) + * m.transport_fixed_cost + / 1000 + ) # [thousand $] [million/1000] + + @m.Expression( + m.potential_sites, m.time, doc="Cost of module setups in each month [million $]" + ) def module_setup_cost(m, site, t): + """ + Calculate the cost of setting up modules at a facility site 'site' at each time period using the unit module cost and the number of modules purchased. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo concrete model which describes the multiperiod location-allocation optimization model + site : int + Index of the facility site from 1 to 12 + t : int + Index of time in months from 0 to 120 (10 years) + + Returns + ------- + Pyomo.Expression + Total setup cost considering the quantity of modules purchased and the unit cost per time period. + """ return m.modules_purchased[site, t] * m.module_unit_cost[t] - @m.Expression(m.potential_sites, m.time, doc="Value of module teardowns in each month [million $]") + @m.Expression( + m.potential_sites, + m.time, + doc="Value of module teardowns in each month [million $]", + ) def module_teardown_credit(m, site, t): + """ + Calculate the value of tearing down modules at a facility site 'site' at each time period using the unit module cost and the number of modules sold. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo concrete model which describes the multiperiod location-allocation optimization model + site : int + Index of the facility site from 1 to 12 + t : int + Index of time in months from 0 to 120 (10 years) + + Returns + ------- + Pyomo.Expression + Total teardown value considering the quantity of modules sold and the unit cost per time period. + """ return m.modules_sold[site, t] * m.module_unit_cost[t] * m.teardown_value @m.Expression(m.potential_sites, doc="Conventional site salvage value") def conv_salvage_value(m, site): - return m.conv_build_cost[site] * m.discount_factor[m.time.last()] * m.conventional_salvage_value + """ + Calculate the salvage value of a conventional facility site 'site' using the build cost, the discount factor for the last time period, and the conventional salvage value. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo concrete model which describes the multiperiod location-allocation optimization model + site : int + Index of the facility site from 1 to 12 + + Returns + ------- + Pyomo.Expression + Salvage value of the conventional facility site 'site' considering the build cost, discount factor, and salvage value. + """ + return ( + m.conv_build_cost[site] + * m.discount_factor[m.time.last()] + * m.conventional_salvage_value + ) m.total_cost = Objective( expr=0 @@ -232,42 +864,151 @@ def conv_salvage_value(m, site): + summation(m.product_transport_cost) + summation(m.product_fixed_transport_cost) + 0, - sense=minimize) + sense=minimize, + doc="Total cost [million $]", + ) return m def _build_site_inactive_disjunct(disj, site): + """ + Configure the disjunct for a facility site marked as inactive. + + Parameters + ---------- + disj : Pyomo.Disjunct + Pyomo disjunct for inactive site + site : int + Index of the facility site from 1 to 12 + + Returns + ------- + None + None, but adds constraints to the disjunct + """ m = disj.model() @disj.Constraint() def no_modules(disj): - return sum(m.num_modules[...]) + sum(m.modules_purchased[...]) + sum(m.modules_sold[...]) == 0 + """ + Ensure that there are no modules at the inactive site. + + Parameters + ---------- + disj : Pyomo.Disjunct + The disjunct object defining constraints for the inactive site + + Returns + ------- + Pyomo.Constraint + The constraint that there are no modules at the inactive site + """ + return ( + sum(m.num_modules[...]) + + sum(m.modules_purchased[...]) + + sum(m.modules_sold[...]) + == 0 + ) @disj.Constraint() def no_production(disj): + """ + Ensure that there is no production at the inactive site. + + Parameters + ---------- + disj : Pyomo.Disjunct + The disjunct object defining constraints for the inactive site + + Returns + ------- + Pyomo.Constraint + The constraint that there is no production at the inactive site. + """ return sum(m.production[site, t] for t in m.time) == 0 @disj.Constraint() def no_supply(disj): + """ + Ensure that there is no supply at the inactive site. + + Parameters + ---------- + disj : Pyomo.Disjunct + The disjunct object defining constraints for the inactive site + + Returns + ------- + Pyomo.Constraint + The constraint that there is no supply at the inactive site. + """ return sum(m.supply[site, t] for t in m.time) == 0 def _build_conventional_disjunct(disj, site): + """ + Configure the disjunct for a conventional facility site. + + Parameters + ---------- + disj : Pyomo.Disjunct + The disjunct object associated with a conventional site. + site : int + Index of the facility site from 1 to 12 + + Returns + ------- + None + None, but adds constraints to the disjunct + """ m = disj.model() disj.cost_calc = Constraint( - expr=m.conv_build_cost[site] == ( - m.conv_base_cost * (m.conv_site_size[site] / 10) ** m.conv_exponent)) + expr=m.conv_build_cost[site] + == (m.conv_base_cost * (m.conv_site_size[site] / 10) ** m.conv_exponent), + doc="the build cost for the conventional facility", + ) # m.bigM[disj.cost_calc] = 7000 @disj.Constraint(m.time) def supply_balance(disj, t): + """ + Ensure that the supply at the conventional site meets the supply shipments from the suppliers. + + Parameters + ---------- + disj : Pyomo.Disjunct + The disjunct object for a conventional site. + t : int + Index of time in months from 0 to 120 (10 years) + + Returns + ------- + Pyomo.Constraint + A constraint that the total supply at the site during each time period equals the total shipments received. + """ return m.supply[site, t] == sum( - m.supply_shipments[sup, site, t] for sup in m.suppliers) + m.supply_shipments[sup, site, t] for sup in m.suppliers + ) @disj.Constraint(m.time) def conv_production_limit(conv_disj, t): + """ + Limit the production at the site based on its capacity. No production is allowed before the setup time. + + Parameters + ---------- + conv_disj : Pyomo.Disjunct + The disjunct object for a conventional site. + t : int + Index of time in months from 0 to 120 (10 years) + + Returns + ------- + Pyomo.Constraint + A constraint that limits production to the site's capacity after setup and prohibits production before setup. + """ if t < m.conv_setup_time: return m.production[site, t] == 0 else: @@ -275,57 +1016,233 @@ def conv_production_limit(conv_disj, t): @disj.Constraint() def no_modules(disj): - return sum(m.num_modules[...]) + sum(m.modules_purchased[...]) + sum(m.modules_sold[...]) == 0 + """ + Ensure no modular units are present, purchased, or sold at the conventional site. + + Parameters + ---------- + disj : Pyomo.Disjunct + The disjunct object for a conventional site. + + Returns + ------- + Pyomo.Constraint + A constraint that the number of modules (present, purchased, sold) at the site is zero. + """ + return ( + sum(m.num_modules[...]) + + sum(m.modules_purchased[...]) + + sum(m.modules_sold[...]) + == 0 + ) def _build_modular_disjunct(disj, site): + """ + Configure the disjunct for a modular facility site. + + Parameters + ---------- + disj : Pyomo.Disjunct + The disjunct object associated with a modular site. + site : int + Index of the facility site from 1 to 12 + + Returns + ------- + None + None, but adds constraints to the disjunct + """ m = disj.model() @disj.Constraint(m.time) def supply_balance(disj, t): + """ + Ensure that the supply at the modular site meets the supply shipments from the suppliers. + + Parameters + ---------- + disj : Pyomo.Disjunct + The disjunct object for a modular site. + t : int + Index of time in months from 0 to 120 (10 years) + + Returns + ------- + Pyomo.Constraint + A constraint that the total supply at the site during each time period equals the total shipments received. + """ return m.supply[site, t] == sum( - m.supply_shipments[sup, site, t] for sup in m.suppliers) + m.supply_shipments[sup, site, t] for sup in m.suppliers + ) @disj.Constraint(m.time) def module_balance(disj, t): + """ + Ensure that the number of modules at the site is consistent with the number of modules purchased and sold. + + Parameters + ---------- + disj : Pyomo.Disjunct + The disjunct object for a modular site. + t : int + Index of time in months from 0 to 120 (10 years) + + Returns + ------- + Pyomo.Constraint + A constraint that maintains the number of modules based on previous balances, new purchases, and modules sold. + """ existing_modules = 0 if t == m.time.first() else m.num_modules[site, t - 1] - new_modules = 0 if t < m.modular_setup_time else m.modules_purchased[site, t - m.modular_setup_time] + new_modules = ( + 0 + if t < m.modular_setup_time + else m.modules_purchased[site, t - m.modular_setup_time] + ) sold_modules = m.modules_sold[site, t] return m.num_modules[site, t] == existing_modules + new_modules - sold_modules + # Fix the number of modules to zero during the setup time for t in range(value(m.modular_setup_time)): m.num_modules[site, t].fix(0) @disj.Constraint(m.time) def modular_production_limit(mod_disj, t): + """ + Limit the production at the site based on the number of modules present. No production is allowed before the setup time. + + Parameters + ---------- + mod_disj : Pyomo.Disjunct + The disjunct object for a modular site. + t : int + Index of time in months from 0 to 120 (10 years) + + Returns + ------- + Pyomo.Constraint + A constraint that limits production to the site's capacity after setup and prohibits production before setup. + """ return m.production[site, t] <= 10 * m.num_modules[site, t] def _build_supply_route_active(disj, sup, site): + """ + Build the disjunct for an active supply route from a supplier to a facility site. + + Parameters + ---------- + disj : Pyomo.Disjunct + The disjunct object for an active supply route + sup : int + Index of the supplier from 1 to 10 + site : int + Index of the facility site from 1 to 12 + """ m = disj.model() def _build_supply_route_inactive(disj, sup, site): + """ + Build the disjunct for an inactive supply route from a supplier to a facility site. + + Parameters + ---------- + disj : Pyomo.Disjunct + The disjunct object for an inactive supply route + sup : int + Index of the supplier from 1 to 10 + site : int + Index of the facility site from 1 to 12 + + Returns + ------- + None + None, but adds constraints to the disjunct + """ m = disj.model() @disj.Constraint() def no_supply(disj): + """ + Ensure that there are no supply shipments from the supplier to the site. + + Parameters + ---------- + disj : Pyomo.Disjunct + The disjunct object for an inactive supply route + + Returns + ------- + Pyomo.Constraint + A constraint that there are no supply shipments from the supplier to the site. + """ return sum(m.supply_shipments[sup, site, t] for t in m.time) == 0 def _build_product_route_active(disj, site, mkt): + """ + Build the disjunct for an active product route from a facility site to a market. + + Parameters + ---------- + disj : Pyomo.Disjunct + The disjunct object for an active product route + site : int + Index of the facility site from 1 to 12 + mkt : int + Index of the market from 1 to 10 + """ m = disj.model() def _build_product_route_inactive(disj, site, mkt): + """ + Build the disjunct for an inactive product route from a facility site to a market. + + Parameters + ---------- + disj : Pyomo.Disjunct + The disjunct object for an inactive product route + site : int + Index of the facility site from 1 to 12 + mkt : int + Index of the market from 1 to 10 + + Returns + ------- + None + None, but adds constraints to the disjunct + """ m = disj.model() @disj.Constraint() def no_product(disj): + """ + Ensure that there are no product shipments from the site to the market. + + Parameters + ---------- + disj : Pyomo.Disjunct + _description_ + + Returns + ------- + Pyomo.Constraint + A constraint that there are no product shipments from the site to the market. + """ return sum(m.product_shipments[site, mkt, t] for t in m.time) == 0 def print_nonzeros(var): + """ + Print the nonzero values of a Pyomo variable + + Parameters + ---------- + var : pyomo.Var + Pyomo variable of the model + """ for i in var: if var[i].value != 0: print("%7s : %10f : %10f : %10f" % (i, var[i].lb, var[i].value, var[i].ub)) @@ -341,45 +1258,72 @@ def print_nonzeros(var): TransformationFactory('gdp.bigm').apply_to(m, bigM=7000) # res = SolverFactory('gurobi').solve(m, tee=True) res = SolverFactory('gams').solve( - m, tee=True, + m, + tee=True, solver='scip', # solver='gurobi', # add_options=['option reslim = 1200;', 'option optcr=0.0001;'], - add_options=[ - 'option reslim = 1200;', - 'OPTION threads=4;', - 'option optcr=0.01', - ], - ) + add_options=['option reslim = 1200;', 'OPTION threads=4;', 'option optcr=0.01'], + ) # res = SolverFactory('gdpopt').solve( # m, tee=True, # iterlim=2, # mip_solver='gams', # mip_solver_args=dict(add_options=['option reslim = 30;'])) - results = pd.DataFrame([ - ['Total Cost', value(m.total_cost)], - ['Conv Build Cost', value(summation(m.conv_build_cost))], - ['Conv Salvage Value', value(summation(m.conv_salvage_value))], - ['Module Build Cost', value(summation(m.module_setup_cost))], - ['Module Salvage Value', value(summation(m.module_teardown_credit))], - ['Raw Material Transport', value(summation(m.raw_material_transport_cost) + summation(m.raw_material_fixed_transport_cost))], - ['Product Transport', value(summation(m.product_transport_cost) + summation(m.product_fixed_transport_cost))] - ], columns=['Quantity', 'Value [million $]']).set_index('Quantity').round(0) + results = ( + pd.DataFrame( + [ + ['Total Cost', value(m.total_cost)], + ['Conv Build Cost', value(summation(m.conv_build_cost))], + ['Conv Salvage Value', value(summation(m.conv_salvage_value))], + ['Module Build Cost', value(summation(m.module_setup_cost))], + ['Module Salvage Value', value(summation(m.module_teardown_credit))], + [ + 'Raw Material Transport', + value( + summation(m.raw_material_transport_cost) + + summation(m.raw_material_fixed_transport_cost) + ), + ], + [ + 'Product Transport', + value( + summation(m.product_transport_cost) + + summation(m.product_fixed_transport_cost) + ), + ], + ], + columns=['Quantity', 'Value [million $]'], + ) + .set_index('Quantity') + .round(0) + ) print(results) - df = pd.DataFrame([ + df = pd.DataFrame( [ - site, t, - value(m.num_modules[site, t]), - value(m.modules_purchased[site, t]), - value(m.modules_sold[site, t]), - value(m.module_setup_cost[site, t]), - value(m.module_teardown_credit[site, t]), - value(m.production[site, t])] for site, t in m.potential_sites * m.time + [ + site, + t, + value(m.num_modules[site, t]), + value(m.modules_purchased[site, t]), + value(m.modules_sold[site, t]), + value(m.module_setup_cost[site, t]), + value(m.module_teardown_credit[site, t]), + value(m.production[site, t]), + ] + for site, t in m.potential_sites * m.time ], - columns=("Site", "Month", "Num Modules", "Buy Modules", - "Sell Modules", - "Setup Cost", "Teardown Credit", "Production") + columns=( + "Site", + "Month", + "Num Modules", + "Buy Modules", + "Sell Modules", + "Setup Cost", + "Teardown Credit", + "Production", + ), ) df.to_excel("facility_config.xlsx") @@ -387,42 +1331,79 @@ def print_nonzeros(var): # exit() import matplotlib.pyplot as plt - plt.plot([x for x in m.site_x.values()], - [y for y in m.site_y.values()], 'k.', markersize=12) - plt.plot([x for x in m.market_x.values()], - [y for y in m.market_y.values()], 'b.', markersize=12) - plt.plot([x for x in m.supplier_x.values()], - [y for y in m.supplier_y.values()], 'r.', markersize=12) + plt.plot( + [x for x in m.site_x.values()], + [y for y in m.site_y.values()], + 'k.', + markersize=12, + ) + plt.plot( + [x for x in m.market_x.values()], + [y for y in m.market_y.values()], + 'b.', + markersize=12, + ) + plt.plot( + [x for x in m.supplier_x.values()], + [y for y in m.supplier_y.values()], + 'r.', + markersize=12, + ) for mkt in m.markets: - plt.annotate('m%s' % mkt, (m.market_x[mkt], m.market_y[mkt]), - (m.market_x[mkt] + 2, m.market_y[mkt] + 2), - fontsize='x-small') + plt.annotate( + 'm%s' % mkt, + (m.market_x[mkt], m.market_y[mkt]), + (m.market_x[mkt] + 2, m.market_y[mkt] + 2), + fontsize='x-small', + ) for site in m.potential_sites: if m.site_inactive[site].binary_indicator_var.value == 0: plt.annotate( - 'p%s' % site, (m.site_x[site], m.site_y[site]), + 'p%s' % site, + (m.site_x[site], m.site_y[site]), (m.site_x[site] + 2, m.site_y[site] + 2), - fontsize='x-small') + fontsize='x-small', + ) else: plt.annotate( - 'x%s' % site, (m.site_x[site], m.site_y[site]), + 'x%s' % site, + (m.site_x[site], m.site_y[site]), (m.site_x[site] + 2, m.site_y[site] + 2), - fontsize='x-small') + fontsize='x-small', + ) for sup in m.suppliers: plt.annotate( - 's%s' % sup, (m.supplier_x[sup], m.supplier_y[sup]), + 's%s' % sup, + (m.supplier_x[sup], m.supplier_y[sup]), (m.supplier_x[sup] + 2, m.supplier_y[sup] + 2), - fontsize='x-small') + fontsize='x-small', + ) for sup, site in m.suppliers * m.potential_sites: - if fabs(m.supply_route_active[sup, site].binary_indicator_var.value - 1) <= 1E-3: - plt.arrow(m.supplier_x[sup], m.supplier_y[sup], - m.site_x[site] - m.supplier_x[sup], - m.site_y[site] - m.supplier_y[sup], - width=0.8, length_includes_head=True, color='b') + if ( + fabs(m.supply_route_active[sup, site].binary_indicator_var.value - 1) + <= 1e-3 + ): + plt.arrow( + m.supplier_x[sup], + m.supplier_y[sup], + m.site_x[site] - m.supplier_x[sup], + m.site_y[site] - m.supplier_y[sup], + width=0.8, + length_includes_head=True, + color='b', + ) for site, mkt in m.potential_sites * m.markets: - if fabs(m.product_route_active[site, mkt].binary_indicator_var.value - 1) <= 1E-3: - plt.arrow(m.site_x[site], m.site_y[site], - m.market_x[mkt] - m.site_x[site], - m.market_y[mkt] - m.site_y[site], - width=0.8, length_includes_head=True, color='r') + if ( + fabs(m.product_route_active[site, mkt].binary_indicator_var.value - 1) + <= 1e-3 + ): + plt.arrow( + m.site_x[site], + m.site_y[site], + m.market_x[mkt] - m.site_x[site], + m.market_y[mkt] - m.site_y[site], + width=0.8, + length_includes_head=True, + color='r', + ) plt.show() diff --git a/gdplib/cstr/README.md b/gdplib/cstr/README.md new file mode 100644 index 0000000..18882a4 --- /dev/null +++ b/gdplib/cstr/README.md @@ -0,0 +1,30 @@ +# GDP Reactor Series Design +Function that builds CSTR superstructure model of size NT (default = 5). +NT is the number of reactors in series. +The CSTRs have a single 1st order auto catalytic reaction A -> B and minimizes total reactors series volume. +The optimal solution should yield NT reactors with a recycle before reactor NT. + +Reference: +> Linan, D. A., Bernal, D. E., Gomez, J. M., & Ricardez-Sandoval, L. A. (2021). Optimal synthesis and design of catalytic distillation columns: A rate-based modeling approach. Chemical Engineering Science, 231, 116294. https://doi.org/10.1016/j.ces.2020.116294 + +### Solution + +Best known objective value: 3.06181298849707 + +### Size + +Number of reactors in series is 5. + +| Problem | vars | Bool | bin | int | cont | cons | nl | disj | disjtn | +|-----------|------|------|-----|-----|------|------|----|------|--------| +| gdp_reactors | 71 | 15 | 0 | 0 | 56 | 25 | 2 | 20 | 10 | + +- ``vars``: variables +- ``Bool``: Boolean variables +- ``bin``: binary variables +- ``int``: integer variables +- ``cont``: continuous variables +- ``cons``: constraints +- ``nl``: nonlinear constraints +- ``disj``: disjuncts +- ``disjtn``: disjunctions \ No newline at end of file diff --git a/gdplib/cstr/gdp_reactor.py b/gdplib/cstr/gdp_reactor.py new file mode 100644 index 0000000..b357384 --- /dev/null +++ b/gdplib/cstr/gdp_reactor.py @@ -0,0 +1,924 @@ +""" +gdp_reactor.py + +This script builds a Pyomo GDP model for a CSTR superstructure with a single 1st order auto catalytic reaction A -> B. +The model minimizes the total reactors series volume. +The optimal solution should yield NT reactors with a recycle before reactor NT. + +Reference: + > Linan, D. A., Bernal, D. E., Gomez, J. M., & Ricardez-Sandoval, L. A. (2021). Optimal synthesis and design of catalytic distillation columns: A rate-based modeling approach. Chemical Engineering Science, 231, 116294. https://doi.org/10.1016/j.ces.2020.116294 +""" + +import os +import sys +import pyomo.environ as pyo +from pyomo.core.base.misc import display +from pyomo.gdp import Disjunct, Disjunction +from pyomo.opt.base.solvers import SolverFactory + + +def build_cstrs(NT: int = 5) -> pyo.ConcreteModel(): + """ + Build the CSTR superstructure model of size NT. + NT is the number of reactors in series. + The CSTRs have a single 1st order auto catalytic reaction A -> B and minimizes total reactors series volume. + The optimal solution should yield NT reactors with a recycle before reactor NT. + + Parameters + ---------- + NT : int + Number of possible reactors in the reactor series superstructure + + Returns + ------- + m : Pyomo.ConcreteModel + Pyomo GDP model which represents the superstructure model of size of NT reactors. + """ + + # PYOMO MODEL + m = pyo.ConcreteModel(name="gdp_reactors") + + # SETS + m.I = pyo.Set(initialize=["A", "B"], doc="Set of components") + m.N = pyo.RangeSet(1, NT, doc="Set of units in the superstructure") + + # PARAMETERS + m.k = pyo.Param( + initialize=2, doc="Kinetic constant [L/(mol*s)]" + ) # Kinetic constant [L/(mol*s)] + m.order1 = pyo.Param( + initialize=1, doc="Partial order of reaction 1" + ) # Partial order of reacton 1 + m.order2 = pyo.Param( + initialize=1, doc="Partial order of reaction 2" + ) # Partial order of reaction 2 + m.QF0 = pyo.Param( + initialize=1, doc="Inlet volumetric flow [L/s]" + ) # Inlet volumetric flow [L/s] + C0_Def = {"A": 0.99, "B": 0.01} + + # Initial concentration of reagents [mol/L] + m.C0 = pyo.Param( + m.I, initialize=C0_Def, doc="Initial concentration of reagents [mol/L]" + ) + + # Inlet molar flow [mol/s] + + def F0_Def(m, i): + """ + Inlet molar flow [mol/s] for component i. + The function multiplies the initial concentration of component i by the inlet volumetric flow. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo GDP model of the CSTR superstructure. + i : float + Index of the component in the reactor series. + + Returns + ------- + Pyomo.Param + Inlet molar flow [mol/s] for component i + """ + return m.C0[i] * m.QF0 + + m.F0 = pyo.Param(m.I, initialize=F0_Def) + + # BOOLEAN VARIABLES + + # Unreacted feed in reactor n + m.YF = pyo.BooleanVar(m.N, doc="Unreacted feed in reactor n") + + # Existence of recycle flow in unit n + m.YR = pyo.BooleanVar(m.N, doc="Existence of recycle flow in unit n") + + # Unit operation in n (True if unit n is a CSTR, False if unit n is a bypass) + m.YP = pyo.BooleanVar(m.N, doc="Unit operation in n") + + # REAL VARIABLES + + # Network Variables + # Outlet flow rate of the superstructure unit [L/s] + m.Q = pyo.Var( + m.N, + initialize=0, + within=pyo.NonNegativeReals, + bounds=(0, 10), + doc="Outlet flow rate of the superstructure unit [L/s]", + ) + + # Outlet flow rate recycle activation of the superstructure unit [L/s] + m.QFR = pyo.Var( + m.N, + initialize=0, + within=pyo.NonNegativeReals, + bounds=(0, 10), + doc="Outlet flow rate recycle activation of the superstructure unit [L/s]", + ) + + # Molar flow [mol/s] + m.F = pyo.Var( + m.I, + m.N, + initialize=0, + within=pyo.NonNegativeReals, + bounds=(0, 10), + doc="Molar flow [mol/s]", + ) + + # Molar flow recycle activation [mol/s] + m.FR = pyo.Var( + m.I, + m.N, + initialize=0, + within=pyo.NonNegativeReals, + bounds=(0, 10), + doc="Molar flow recycle activation [mol/s]", + ) + + # Reaction rate [mol/(L*s)] + m.rate = pyo.Var( + m.I, + m.N, + initialize=0, + within=pyo.Reals, + bounds=(-10, 10), + doc="Reaction rate [mol/(L*s)]", + ) + + # Reactor volume [L] + m.V = pyo.Var( + m.N, + initialize=0, + within=pyo.NonNegativeReals, + bounds=(0, 10), + doc="Reactor volume [L]", + ) + + # Volume activation [L] + m.c = pyo.Var( + m.N, + initialize=0, + within=pyo.NonNegativeReals, + bounds=(0, 10), + doc="Volume activation [L]", + ) + + # Splitter Variables + # Recycle flow rate [L/s] + m.QR = pyo.Var( + initialize=0, + within=pyo.NonNegativeReals, + bounds=(0, 10), + doc="Recycle flow rate [L/s]", + ) + + # Product flow rate [L/s] + m.QP = pyo.Var( + initialize=0, + within=pyo.NonNegativeReals, + bounds=(0, 10), + doc="Product flow rate [L/s]", + ) + + # Recycle molar flow [mol/s] + m.R = pyo.Var( + m.I, + initialize=0, + within=pyo.NonNegativeReals, + bounds=(0, 10), + doc="Recycle molar flow [mol/s]", + ) + + # Product molar flow [mol/s] + m.P = pyo.Var( + m.I, + initialize=0, + within=pyo.NonNegativeReals, + bounds=(0, 10), + doc="Product molar flow [mol/s]", + ) + + # CONSTRAINTS + + # Unreacted Feed Balances + # Unreacted feed unit mole balance + + def unreact_mole_rule(m, i, n): + """ + Unreacted feed unit mole balance. + The mole balance is calculated using the inlet molar flow, the recycle molar flow, the outlet molar flow, and the reaction rate for each component. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo GDP model of the CSTR superstructure. + i : float + Index of the component in the reactor series. + n : int + Index of the reactor in the reactor series. The reactor index starts at 1. The number increases on the left direction of the reactor series. The last reactor is indexed as NT which is the feed for reagent. + + Returns + ------- + Pyomo.Constraint or Pyomo.Constraint.Skip + The constraint for the unreacted feed unit mole balance if n is equal to NT. Otherwise, it returns Pyomo.Constraint.Skip. + """ + if n == NT: + return m.F0[i] + m.FR[i, n] - m.F[i, n] + m.rate[i, n] * m.V[n] == 0 + else: + return pyo.Constraint.Skip + + m.unreact_mole = pyo.Constraint( + m.I, m.N, rule=unreact_mole_rule, doc="Unreacted feed unit mole balance" + ) + + # Unreacted feed unit continuity + + def unreact_cont_rule(m, n): + """ + Unreacted feed unit continuity. + The continuity is calculated using the inlet volumetric flow, the recycle flow rate, and the outlet flow rate for the reactor NT. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo GDP model of the CSTR superstructure. + n : int + Index of the reactor in the reactor series. + + Returns + ------- + Pyomo.Constraint or Pyomo.Constraint.Skip + The constraint for the unreacted feed unit continuity if n is equal to NT. Otherwise, it returns Pyomo.Constraint.Skip. + """ + if n == NT: + return m.QF0 + m.QFR[n] - m.Q[n] == 0 + else: + return pyo.Constraint.Skip + + m.unreact_cont = pyo.Constraint( + m.N, rule=unreact_cont_rule, doc="Unreacted feed unit continuity" + ) + + # Reactor Balances + # Reactor mole balance + + def react_mole_rule(m, i, n): + """ + Reactor mole balance. + The mole balance is calculated using the inlet molar flow, the recycle molar flow, the outlet molar flow, and the reaction rate for each component. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo GDP model of the CSTR superstructure. + i : float + Index of the component in the reactor series. + n : int + Index of the reactor in the reactor series. + + Returns + ------- + Pyomo.Constraint or Pyomo.Constraint.Skip + The constraint for the reactor mole balance if n is different from NT. Otherwise, it returns Pyomo.Constraint.Skip. + """ + if n != NT: + return m.F[i, n + 1] + m.FR[i, n] - m.F[i, n] + m.rate[i, n] * m.V[n] == 0 + else: + return pyo.Constraint.Skip + + m.react_mole = pyo.Constraint( + m.I, m.N, rule=react_mole_rule, doc="Reactor mole balance" + ) + + # Reactor continuity + + def react_cont_rule(m, n): + """ + Reactor continuity. + The continuity is calculated using the inlet volumetric flow, the recycle flow rate, and the outlet flow rate for the reactor n. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo GDP model of the CSTR superstructure. + n : int + Index of the reactor in the reactor series. + + Returns + ------- + Pyomo.Constraint or Pyomo.Constraint.Skip + The constraint for the reactor continuity if n is different from NT. Otherwise, it returns Pyomo.Constraint.Skip. + """ + if n != NT: + return m.Q[n + 1] + m.QFR[n] - m.Q[n] == 0 + else: + return pyo.Constraint.Skip + + m.react_cont = pyo.Constraint(m.N, rule=react_cont_rule, doc="Reactor continuity") + + # Splitting Point Balances + # Splitting point mole balance + + def split_mole_rule(m, i): + """ + Splitting point mole balance. + The mole balance is calculated using the product molar flow, the recycle molar flow, and the outlet molar flow for each component. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo GDP model of the CSTR superstructure. + i : float + Index of the component in the reactor series. + + Returns + ------- + Pyomo.Constraint + The constraint for the splitting point mole balance. + """ + return m.F[i, 1] - m.P[i] - m.R[i] == 0 + + m.split_mole = pyo.Constraint( + m.I, rule=split_mole_rule, doc="Splitting point mole balance" + ) + + # Splitting point continuity + + def split_cont_rule(m): + """ + Splitting point continuity. + The continuity is calculated using the product flow rate, the recycle flow rate, and the outlet flow rate for the reactor 1. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo GDP model of the CSTR superstructure. + + Returns + ------- + Pyomo.Constraint + The constraint for the splitting point continuity. + """ + return m.Q[1] - m.QP - m.QR == 0 + + m.split_cont = pyo.Constraint( + rule=split_cont_rule, doc="Splitting point continuity" + ) + + # Splitting point additional constraints + + def split_add_rule(m, i): + """ + Splitting point additional constraints. + Molarity constraints over initial and final flows, read as an multiplication avoid the numerical complication. + m.P[i]/m.QP = m.F[i,1]/m.Q[1] (molarity balance) + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo GDP model of the CSTR superstructure. + i : float + Index of the component in the reactor series. + + Returns + ------- + Pyomo.Constraint + The constraint for the splitting point additional constraints. + """ + return m.P[i] * m.Q[1] - m.F[i, 1] * m.QP == 0 + + m.split_add = pyo.Constraint( + m.I, rule=split_add_rule, doc="Splitting point additional constraints" + ) + + # Product Specification + + def prod_spec_rule(m): + """ + Product B Specification. + The product B specification is calculated using the product flow rate and the product molar flow. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo GDP model of the CSTR superstructure. + + Returns + ------- + Pyomo.Constraint + The constraint for the product B specification. + """ + return m.QP * 0.95 - m.P["B"] == 0 + + m.prod_spec = pyo.Constraint(rule=prod_spec_rule, doc="Product B Specification") + + # Volume Constraint + + def vol_cons_rule(m, n): + """ + Volume Constraint. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo GDP model of the CSTR superstructure. + n : int + Index of the reactor in the reactor series. + + Returns + ------- + Pyomo.Constraint or Pyomo.Constraint.Skip + The constraint for the volume constraint if n is different from 1. Otherwise, it returns Pyomo.Constraint.Skip. + """ + if n != 1: + return m.V[n] - m.V[n - 1] == 0 + else: + return pyo.Constraint.Skip + + m.vol_cons = pyo.Constraint(m.N, rule=vol_cons_rule, doc="Volume Constraint") + + # YD Disjunction block equation definition + + def build_cstr_equations(disjunct, n): + """ + Build the constraints for the activation of the CSTR reactor. + + Parameters + ---------- + disjunct : Pyomo.Disjunct + Pyomo Disjunct block to include the constraints for the activation of the CSTR reactor. + n : int + Index of the reactor in the reactor series. + + Returns + ------- + None + None, the function builds the constraints for the activation of the CSTR reactor. + """ + m = disjunct.model() + + # Reaction rates calculation + @disjunct.Constraint() + def YPD_rate_calc(disjunct): + """ + Calculate the reaction rate of A for each reactor. + The reaction rate is calculated using the kinetic constant, the outlet flow rate, the molar flow of A, and the molar flow of B. + The outlet flow is multiplied on the reaction rate to avoid numerical complications. + + Parameters + ---------- + disjunct : Pyomo.Disjunct + Pyomo Disjunct block to include the constraints for the activation of the CSTR reactor. + + Returns + ------- + Pyomo.Constraint + The constraint for the calculation of the reaction rate of A for each reactor. + """ + return ( + m.rate["A", n] * ((m.Q[n]) ** m.order1) * ((m.Q[n]) ** m.order2) + + m.k * ((m.F["A", n]) ** m.order1) * ((m.F["B", n]) ** m.order2) + == 0 + ) + + # Reaction rate relation + @disjunct.Constraint() + def YPD_rate_rel(disjunct): + """ + Reaction rate relation for defining pyomo model. + Since the chemical reaction goes from A to B, the rate of A is equal to the negative rate of B. + + A -> B + + Parameters + ---------- + disjunct : Pyomo.Disjunct + Pyomo Disjunct block to include the constraints for the activation of the CSTR reactor. + + Returns + ------- + Pyomo.Constraint + Reaction rate relation for defining pyomo model. + """ + return m.rate["B", n] + m.rate["A", n] == 0 + + # Volume activation + @disjunct.Constraint() + def YPD_vol_act(disjunct): + """ + Volume Activation function for defining pyomo model. + + Parameters + ---------- + disjunct : Pyomo.Disjunct + Pyomo Disjunct block to include the constraints for the activation of the CSTR reactor. + + Returns + ------- + Pyomo.Constraint + Activation function for the volume of the reactor. + """ + return m.c[n] - m.V[n] == 0 + + def build_bypass_equations(disjunct, n): + """ + Build the constraints for the deactivation of the reactor (bypass the reactor). + + Parameters + ---------- + disjunct : Pyomo.Disjunct + Pyomo Disjunct block to include the constraints for the deactivation of the reactor (bypass the reactor). + n : int + Index of the reactor in the reactor series. + + Returns + ------- + None + None, the function builds the constraints for the deactivation of the reactor (bypass the reactor). + """ + m = disjunct.model() + + # FR deactivation + @disjunct.Constraint(m.I) + def neg_YPD_FR_deactivation(disjunct, i): + """ + Deactivation of the recycle flow for each component in the reactor series. + There are no recycle flows when the reactor is deactivated (bypassed). + + Parameters + ---------- + disjunct : Pyomo.Disjunct + Pyomo Disjunct block to include the constraints for the deactivation of the reactor (bypass the reactor). + i : float + Index of the component in the reactor series. + + Returns + ------- + Pyomo.Constraint + Deactivation of the recycle flow for each component in the reactor series. + """ + return m.FR[i, n] == 0 + + # Rate deactivation + @disjunct.Constraint(m.I) + def neg_YPD_rate_deactivation(disjunct, i): + """ + Deactivate the reaction rate for each component in the reactor series. + There are no reaction rates when the reactor is deactivated (bypassed). + + Parameters + ---------- + disjunct : Pyomo.Disjunct + Pyomo Disjunct block to include the constraints for the deactivation of the reactor (bypass the reactor). + i : float + Index of the component in the reactor series. + + Returns + ------- + Pyomo.Constraint + Deactivation of the reaction rate for each component in the reactor series. + """ + return m.rate[i, n] == 0 + + # QFR deactivation + @disjunct.Constraint() + def neg_YPD_QFR_deactivation(disjunct): + """ + Deactivate the outlet flow rate recycle activation of the reactor. + There is no outlet flow rate recycle activation when the reactor is deactivated (bypassed). + + Parameters + ---------- + disjunct : Pyomo.Disjunct + Pyomo Disjunct block to include the constraints for the deactivation of the reactor (bypass the reactor). + + Returns + ------- + Pyomo.Constraint + Deactivation of the outlet flow rate recycle activation of the reactor. + """ + return m.QFR[n] == 0 + + @disjunct.Constraint() + def neg_YPD_vol_deactivation(disjunct): + """ + Volume deactivation function for defining pyomo model. + There is no volume when the reactor is deactivated (bypassed). + + Parameters + ---------- + disjunct : Pyomo.Disjunct + Pyomo Disjunct block to include the constraints for the deactivation of the reactor (bypass the reactor). + + Returns + ------- + Pyomo.Constraint + Volume deactivation function for defining pyomo model. + """ + return m.c[n] == 0 + + # YR Disjuction block equation definition + + def build_recycle_equations(disjunct, n): + """ + Build the constraints for the activation of the recycle flow. + + Parameters + ---------- + disjunct : Pyomo.Disjunct + Pyomo Disjunct block to include the constraints for the activation of the reactor (recycle flow existence). + n : int + Index of the reactor in the reactor series. + + Returns + ------- + None + None, the function builds the constraints for the activation of the recycle flow. + """ + m = disjunct.model() + + # FR activation + @disjunct.Constraint(m.I) + def YRD_FR_act(disjunct, i): + """ + Activation of the recycle flow for each component in the reactor series. + + Parameters + ---------- + disjunct : Pyomo.Disjunct + Pyomo Disjunct block to include the constraints for the activation of the reactor (recycle flow existence). + i : float + Index of the component in the reactor series. + + Returns + ------- + Pyomo.Constraint + Activation of the recycle flow for each component in the reactor series. + """ + return m.FR[i, n] - m.R[i] == 0 + + # QFR activation + @disjunct.Constraint() + def YRD_QFR_act(disjunct): + """ + Activation of the outlet flow rate recycle activation of the reactor. + + Parameters + ---------- + disjunct : Pyomo.Disjunct + Pyomo Disjunct block to include the constraints for the activation of the reactor (recycle flow existence). + + Returns + ------- + Pyomo.Constraint + Activation of the outlet flow rate recycle activation of the reactor. + """ + return m.QFR[n] - m.QR == 0 + + def build_no_recycle_equations(disjunct, n): + """ + Build the constraints for the deactivation of the recycle flow. + + Parameters + ---------- + disjunct : Pyomo.Disjunct + Pyomo Disjunct block to include the constraints for the deactivation of the reactor (recycle flow absence). + n : int + Index of the reactor in the reactor series. + + Returns + ------- + None + None, the function builds the constraints for the deactivation of the recycle flow. + """ + m = disjunct.model() + + # FR deactivation + @disjunct.Constraint(m.I) + def neg_YRD_FR_deactivation(disjunct, i): + """ + Deactivation of the recycle flow for each component in the reactor series. + + Parameters + ---------- + disjunct : Pyomo.Disjunct + Pyomo Disjunct block to include the constraints for the deactivation of the reactor (recycle flow absence). + i : float + Index of the component in the reactor series. + + Returns + ------- + Pyomo.Constraint + Deactivation of the recycle flow for each component in the reactor series. + """ + return m.FR[i, n] == 0 + + # QFR deactivation + @disjunct.Constraint() + def neg_YRD_QFR_deactivation(disjunct): + """ + Deactivation of the outlet flow rate recycle activation of the reactor. + + Parameters + ---------- + disjunct : Pyomo.Disjunct + Pyomo Disjunct block to include the constraints for the deactivation of the reactor (recycle flow absence). + + Returns + ------- + Pyomo.Constraint + Deactivation of the outlet flow rate recycle activation of the reactor. + """ + return m.QFR[n] == 0 + + # Create disjunction blocks + m.YR_is_recycle = Disjunct( + m.N, rule=build_recycle_equations, doc="Recycle flow in reactor n" + ) + m.YR_is_not_recycle = Disjunct( + m.N, rule=build_no_recycle_equations, doc="No recycle flow in reactor n" + ) + + m.YP_is_cstr = Disjunct(m.N, rule=build_cstr_equations, doc="CSTR reactor n") + m.YP_is_bypass = Disjunct(m.N, rule=build_bypass_equations, doc="Bypass reactor n") + + # Create disjunctions + + @m.Disjunction(m.N) + def YP_is_cstr_or_bypass(m, n): + """ + Build the disjunction for the activation of the CSTR reactor or bypass the reactor. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo GDP model of the CSTR superstructure. + n : int + Index of the reactor in the reactor series. + + Returns + ------- + list + list of the disjunctions for the activation of the CSTR reactor or bypass the reactor + """ + return [m.YP_is_cstr[n], m.YP_is_bypass[n]] + + @m.Disjunction(m.N) + def YR_is_recycle_or_not(m, n): + """ + Build the disjunction for the existence of a recycle flow in the reactor. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo GDP model of the CSTR superstructure. + n : int + Index of the reactor in the reactor series. + + Returns + ------- + list + list of the disjunctions for the existence of a recycle flow in the reactor + """ + return [m.YR_is_recycle[n], m.YR_is_not_recycle[n]] + + # Associate Boolean variables with with disjunctions + for n in m.N: + m.YP[n].associate_binary_var(m.YP_is_cstr[n].indicator_var) + m.YR[n].associate_binary_var(m.YR_is_recycle[n].indicator_var) + + # Logic Constraints + # Unit must be a CSTR to include a recycle + + def cstr_if_recycle_rule(m, n): + """ + Build the logical constraint for the unit to be a CSTR to include a recycle. + The existence of a recycle flow implies the existence of a CSTR reactor. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo GDP model of the CSTR superstructure. + n : int + Index of the reactor in the reactor series. + + Returns + ------- + Pyomo.LogicalConstraint + Logical constraint for the unit to be a CSTR to include a recycle. + """ + return m.YR[n].implies(m.YP[n]) + + m.cstr_if_recycle = pyo.LogicalConstraint( + m.N, rule=cstr_if_recycle_rule, doc="Unit must be a CSTR to include a recycle" + ) + + # There is only one unreacted feed + + def one_unreacted_feed_rule(m): + """ + Build the logical constraint for the existence of only one unreacted feed. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo GDP model of the CSTR superstructure. + + Returns + ------- + Pyomo.LogicalConstraint + Logical constraint for the existence of only one unreacted feed. + """ + return pyo.exactly(1, m.YF) + + m.one_unreacted_feed = pyo.LogicalConstraint( + rule=one_unreacted_feed_rule, doc="There is only one unreacted feed" + ) + + # There is only one recycle stream + + def one_recycle_rule(m): + """ + Build the logical constraint for the existence of only one recycle stream. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo GDP model of the CSTR superstructure. + + Returns + ------- + Pyomo.LogicalConstraint + Logical constraint for the existence of only one recycle stream. + """ + return pyo.exactly(1, m.YR) + + m.one_recycle = pyo.LogicalConstraint( + rule=one_recycle_rule, doc="There is only one recycle stream" + ) + + # Unit operation in n constraint + + def unit_in_n_rule(m, n): + """ + Build the logical constraint for the unit operation in n. + If n is equal to 1, the unit operation is a CSTR. + Otherwise, the unit operation for reactor n except reactor 1 is equivalent to the logical OR of the negation of the unreacted feed of the previous reactors and the unreacted feed of reactor n. + Reactor n is active if either the previous reactors (1 through n-1) have no unreacted feed or reactor n has unreacted feed. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo GDP model of the CSTR superstructure. + n : int + Index of the reactor in the reactor series. + + Returns + ------- + Pyomo.LogicalConstraint + Logical constraint for the unit operation in n. + """ + if n == 1: + return m.YP[n].equivalent_to(True) + else: + return m.YP[n].equivalent_to( + pyo.lor(pyo.land(~m.YF[n2] for n2 in range(1, n)), m.YF[n]) + ) + + m.unit_in_n = pyo.LogicalConstraint(m.N, rule=unit_in_n_rule) + + # OBJECTIVE + + def obj_rule(m): + """ + Objective function to minimize the total reactor volume. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo GDP model of the CSTR superstructure. + + Returns + ------- + Pyomo Objective + Objective function to minimize the total reactor volume. + """ + return sum(m.c[n] for n in m.N) + + m.obj = pyo.Objective( + rule=obj_rule, sense=pyo.minimize, doc="minimum total reactor volume" + ) + + return m + + +if __name__ == "__main__": + m = build_cstrs() + pyo.TransformationFactory("core.logical_to_linear").apply_to(m) + pyo.TransformationFactory("gdp.bigm").apply_to(m) + pyo.SolverFactory("gams").solve( + m, solver="baron", tee=True, add_options=["option optcr=1e-6;"] + ) + display(m) diff --git a/gdplib/disease_model/__init__.py b/gdplib/disease_model/__init__.py new file mode 100644 index 0000000..7058d12 --- /dev/null +++ b/gdplib/disease_model/__init__.py @@ -0,0 +1,3 @@ +from .disease_model import build_model + +__all__ = ['build_model'] diff --git a/gdplib/disease_model/disease_model.py b/gdplib/disease_model/disease_model.py new file mode 100644 index 0000000..1f60644 --- /dev/null +++ b/gdplib/disease_model/disease_model.py @@ -0,0 +1,1976 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright 2017 National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +# ============================================ +# SIR disease model using a low/high transmission parameter +# This is formulated as a disjunctive program +# +# Daniel Word, November 1, 2010 +# ============================================ + +# import packages +from pyomo.environ import * +from pyomo.gdp import * +import math + + +def build_model(): + """ + Builds the model for the SIR disease model using a low/high transmission parameter. + + The model simulates the spread of an infectious disease over a series of bi-weekly periods, using a disjunctive programming approach to account for variations in disease transmission rates. + + Parameters + ---------- + None + + Returns + ------- + model : Pyomo.ConcreteModel + Pyomo model object that represent the SIR disease model using a low/high transmission parameter. + """ + # import data + # Population Data + + pop = [ + 15.881351, + 15.881339, + 15.881320, + 15.881294, + 15.881261, + 15.881223, + 15.881180, + 15.881132, + 15.881079, + 15.881022, + 15.880961, + 15.880898, + 15.880832, + 15.880764, + 15.880695, + 15.880624, + 15.880553, + 15.880480, + 15.880409, + 15.880340, + 15.880270, + 15.880203, + 15.880138, + 15.880076, + 15.880016, + 15.879960, + 15.879907, + 15.879852, + 15.879799, + 15.879746, + 15.879693, + 15.879638, + 15.879585, + 15.879531, + 15.879477, + 15.879423, + 15.879370, + 15.879315, + 15.879262, + 15.879209, + 15.879155, + 15.879101, + 15.879048, + 15.878994, + 15.878940, + 15.878886, + 15.878833, + 15.878778, + 15.878725, + 15.878672, + 15.878618, + 15.878564, + 15.878510, + 15.878457, + 15.878402, + 15.878349, + 15.878295, + 15.878242, + 15.878187, + 15.878134, + 15.878081, + 15.878026, + 15.877973, + 15.877919, + 15.877864, + 15.877811, + 15.877758, + 15.877704, + 15.877650, + 15.877596, + 15.877543, + 15.877488, + 15.877435, + 15.877381, + 15.877326, + 15.877273, + 15.877220, + 15.877166, + 15.877111, + 15.877058, + 15.877005, + 15.876950, + 15.876896, + 15.876843, + 15.876789, + 15.876735, + 15.876681, + 15.876628, + 15.876573, + 15.876520, + 15.876466, + 15.876411, + 15.876358, + 15.876304, + 15.876251, + 15.876196, + 15.876143, + 15.876089, + 15.876034, + 15.875981, + 15.875927, + 15.875872, + 15.875819, + 15.875765, + 15.875712, + 15.875657, + 15.875604, + 15.875550, + 15.875495, + 15.875442, + 15.875388, + 15.875335, + 15.875280, + 15.875226, + 15.875173, + 15.875118, + 15.875064, + 15.875011, + 15.874956, + 15.874902, + 15.874849, + 15.874795, + 15.874740, + 15.874687, + 15.874633, + 15.874578, + 15.874525, + 15.874471, + 15.874416, + 15.874363, + 15.874309, + 15.874256, + 15.874201, + 15.874147, + 15.874094, + 15.874039, + 15.873985, + 15.873931, + 15.873878, + 15.873823, + 15.873769, + 15.873716, + 15.873661, + 15.873607, + 15.873554, + 15.873499, + 15.873445, + 15.873391, + 15.873338, + 15.873283, + 15.873229, + 15.873175, + 15.873121, + 15.873067, + 15.873013, + 15.872960, + 15.872905, + 15.872851, + 15.872797, + 15.872742, + 15.872689, + 15.872635, + 15.872580, + 15.872526, + 15.872473, + 15.872419, + 15.872364, + 15.872310, + 15.872256, + 15.872202, + 15.872148, + 15.872094, + 15.872039, + 15.871985, + 15.871932, + 15.871878, + 15.871823, + 15.871769, + 15.871715, + 15.871660, + 15.871607, + 15.871553, + 15.871499, + 15.871444, + 15.871390, + 15.871337, + 15.871282, + 15.871228, + 15.871174, + 15.871119, + 15.871065, + 15.871012, + 15.870958, + 15.870903, + 15.870849, + 15.870795, + 15.870740, + 15.870686, + 15.870633, + 15.870577, + 15.870524, + 15.870470, + 15.870416, + 15.870361, + 15.870307, + 15.870253, + 15.870198, + 15.870144, + 15.870091, + 15.870037, + 15.869982, + 15.869928, + 15.869874, + 15.869819, + 15.869765, + 15.869711, + 15.869656, + 15.869602, + 15.869548, + 15.869495, + 15.869439, + 15.869386, + 15.869332, + 15.869277, + 15.869223, + 15.869169, + 15.869114, + 15.869060, + 15.869006, + 15.868952, + 15.868897, + 15.868843, + 15.868789, + 15.868734, + 15.868679, + 15.868618, + 15.868556, + 15.868489, + 15.868421, + 15.868351, + 15.868280, + 15.868208, + 15.868134, + 15.868063, + 15.867991, + 15.867921, + 15.867852, + 15.867785, + 15.867721, + 15.867659, + 15.867601, + 15.867549, + 15.867499, + 15.867455, + 15.867416, + 15.867383, + 15.867357, + 15.867338, + 15.867327, + 15.867321, + 15.867327, + 15.867338, + 15.867359, + 15.867386, + 15.867419, + 15.867459, + 15.867505, + 15.867555, + 15.867610, + 15.867671, + 15.867734, + 15.867801, + 15.867869, + 15.867941, + 15.868012, + 15.868087, + 15.868161, + 15.868236, + 15.868310, + 15.868384, + 15.868457, + 15.868527, + 15.868595, + 15.868661, + 15.868722, + 15.868780, + 15.868837, + 15.868892, + 15.868948, + 15.869005, + 15.869061, + 15.869116, + 15.869173, + 15.869229, + 15.869284, + 15.869341, + 15.869397, + 15.869452, + 15.869509, + 15.869565, + 15.869620, + 15.869677, + 15.869733, + 15.869788, + 15.869845, + 15.869901, + 15.869956, + 15.870012, + 15.870069, + 15.870124, + 15.870180, + 15.870237, + 15.870292, + 15.870348, + 15.870405, + 15.870461, + 15.870516, + 15.870572, + 15.870629, + 15.870684, + 15.870740, + 15.870796, + 15.870851, + 15.870908, + 15.870964, + 15.871019, + 15.871076, + 15.871132, + 15.871187, + 15.871243, + 15.871300, + 15.871355, + 15.871411, + 15.871467, + 15.871522, + 15.871579, + 15.871635, + 15.871691, + 15.871746, + 15.871802, + 15.871859, + 15.871914, + 15.871970, + 15.872026, + 15.872081, + 15.872138, + 15.872194, + 15.872249, + 15.872305, + 15.872361, + 15.872416, + 15.872473, + 15.872529, + 15.872584, + 15.872640, + 15.872696, + 15.872751, + 15.872807, + 15.872864, + 15.872919, + 15.872975, + 15.873031, + 15.873087, + 15.873142, + 15.873198, + 15.873255, + 15.873310, + 15.873366, + 15.873422, + 15.873477, + 15.873533, + 15.873589, + 15.873644, + 15.873700, + 15.873757, + 15.873811, + 15.873868, + 15.873924, + 15.873979, + 15.874035, + 15.874091, + 15.874146, + 15.874202, + 15.874258, + 15.874313, + 15.874369, + 15.874425, + 15.874481, + 15.874536, + 15.874592, + ] + + logIstar = [ + 7.943245, + 8.269994, + 8.517212, + 8.814208, + 9.151740, + 9.478472, + 9.559847, + 9.664087, + 9.735378, + 9.852583, + 9.692265, + 9.498807, + 9.097634, + 8.388878, + 7.870516, + 7.012956, + 6.484941, + 5.825368, + 5.346815, + 5.548361, + 5.706732, + 5.712617, + 5.709714, + 5.696888, + 5.530087, + 5.826563, + 6.643563, + 7.004292, + 7.044663, + 7.190259, + 7.335926, + 7.516861, + 7.831779, + 8.188895, + 8.450204, + 8.801436, + 8.818379, + 8.787658, + 8.601685, + 8.258338, + 7.943364, + 7.425585, + 7.062834, + 6.658307, + 6.339600, + 6.526984, + 6.679178, + 6.988758, + 7.367331, + 7.746694, + 8.260558, + 8.676522, + 9.235582, + 9.607778, + 9.841917, + 10.081571, + 10.216090, + 10.350366, + 10.289668, + 10.248842, + 10.039504, + 9.846343, + 9.510392, + 9.190923, + 8.662465, + 7.743221, + 7.128458, + 5.967898, + 5.373883, + 5.097497, + 4.836570, + 5.203345, + 5.544798, + 5.443047, + 5.181152, + 5.508669, + 6.144130, + 6.413744, + 6.610423, + 6.748885, + 6.729511, + 6.789841, + 6.941034, + 7.093516, + 7.307039, + 7.541077, + 7.644803, + 7.769145, + 7.760187, + 7.708017, + 7.656795, + 7.664983, + 7.483828, + 6.887324, + 6.551093, + 6.457449, + 6.346064, + 6.486300, + 6.612378, + 6.778753, + 6.909477, + 7.360570, + 8.150303, + 8.549044, + 8.897572, + 9.239323, + 9.538751, + 9.876531, + 10.260911, + 10.613536, + 10.621510, + 10.661115, + 10.392899, + 10.065536, + 9.920090, + 9.933097, + 9.561691, + 8.807713, + 8.263463, + 7.252184, + 6.669083, + 5.877763, + 5.331878, + 5.356563, + 5.328469, + 5.631146, + 6.027497, + 6.250717, + 6.453919, + 6.718444, + 7.071636, + 7.348905, + 7.531528, + 7.798226, + 8.197941, + 8.578809, + 8.722964, + 8.901152, + 8.904370, + 8.889865, + 8.881902, + 8.958903, + 8.721281, + 8.211509, + 7.810624, + 7.164607, + 6.733688, + 6.268503, + 5.905983, + 5.900432, + 5.846547, + 6.245427, + 6.786271, + 7.088480, + 7.474295, + 7.650063, + 7.636703, + 7.830990, + 8.231516, + 8.584816, + 8.886908, + 9.225216, + 9.472778, + 9.765505, + 9.928623, + 10.153033, + 10.048574, + 9.892620, + 9.538818, + 8.896100, + 8.437584, + 7.819738, + 7.362598, + 6.505880, + 5.914972, + 6.264584, + 6.555019, + 6.589319, + 6.552029, + 6.809771, + 7.187616, + 7.513918, + 8.017712, + 8.224957, + 8.084474, + 8.079148, + 8.180991, + 8.274269, + 8.413748, + 8.559599, + 8.756090, + 9.017927, + 9.032720, + 9.047983, + 8.826873, + 8.366489, + 8.011876, + 7.500830, + 7.140406, + 6.812626, + 6.538719, + 6.552218, + 6.540129, + 6.659927, + 6.728530, + 7.179692, + 7.989210, + 8.399173, + 8.781128, + 9.122303, + 9.396378, + 9.698512, + 9.990104, + 10.276543, + 10.357284, + 10.465869, + 10.253833, + 10.018503, + 9.738407, + 9.484367, + 9.087025, + 8.526409, + 8.041126, + 7.147168, + 6.626706, + 6.209446, + 5.867231, + 5.697439, + 5.536769, + 5.421413, + 5.238297, + 5.470136, + 5.863007, + 6.183083, + 6.603569, + 6.906278, + 7.092324, + 7.326612, + 7.576052, + 7.823430, + 7.922775, + 8.041677, + 8.063403, + 8.073229, + 8.099726, + 8.168522, + 8.099041, + 8.011404, + 7.753147, + 6.945211, + 6.524244, + 6.557723, + 6.497742, + 6.256247, + 5.988794, + 6.268093, + 6.583316, + 7.106842, + 8.053929, + 8.508237, + 8.938915, + 9.311863, + 9.619753, + 9.931745, + 10.182361, + 10.420978, + 10.390829, + 10.389230, + 10.079342, + 9.741479, + 9.444561, + 9.237448, + 8.777687, + 7.976436, + 7.451502, + 6.742856, + 6.271545, + 5.782289, + 5.403089, + 5.341954, + 5.243509, + 5.522993, + 5.897001, + 6.047042, + 6.100738, + 6.361727, + 6.849562, + 7.112544, + 7.185346, + 7.309412, + 7.423746, + 7.532142, + 7.510318, + 7.480175, + 7.726362, + 8.061117, + 8.127072, + 8.206166, + 8.029634, + 7.592953, + 7.304869, + 7.005394, + 6.750019, + 6.461377, + 6.226432, + 6.287047, + 6.306452, + 6.783694, + 7.450957, + 7.861692, + 8.441530, + 8.739626, + 8.921994, + 9.168961, + 9.428077, + 9.711664, + 10.032714, + 10.349937, + 10.483985, + 10.647475, + 10.574038, + 10.522431, + 10.192246, + 9.756246, + 9.342511, + 8.872072, + 8.414189, + 7.606582, + 7.084701, + 6.149903, + 5.517257, + 5.839429, + 6.098090, + 6.268935, + 6.475965, + 6.560543, + 6.598942, + 6.693938, + 6.802531, + 6.934345, + 7.078370, + 7.267736, + 7.569640, + 7.872204, + 8.083603, + 8.331226, + 8.527144, + 8.773523, + 8.836599, + 8.894303, + 8.808326, + 8.641717, + 8.397901, + 7.849034, + 7.482899, + 7.050252, + 6.714103, + 6.900603, + 7.050765, + 7.322905, + 7.637986, + 8.024340, + 8.614505, + 8.933591, + 9.244008, + 9.427410, + 9.401385, + 9.457744, + 9.585068, + 9.699673, + 9.785478, + 9.884559, + 9.769732, + 9.655075, + 9.423071, + 9.210198, + 8.786654, + 8.061787, + 7.560976, + 6.855829, + 6.390707, + 5.904006, + 5.526631, + 5.712303, + 5.867027, + 5.768367, + 5.523352, + 5.909118, + 6.745543, + 6.859218, + ] + + deltaS = [ + 9916.490263, + 12014.263380, + 13019.275755, + 12296.373612, + 8870.995603, + 1797.354574, + -6392.880771, + -16150.825387, + -27083.245106, + -40130.421462, + -50377.169958, + -57787.717468, + -60797.223427, + -59274.041897, + -55970.213230, + -51154.650927, + -45877.841034, + -40278.553775, + -34543.967175, + -28849.633641, + -23192.776605, + -17531.130740, + -11862.021829, + -6182.456792, + -450.481090, + 5201.184400, + 10450.773882, + 15373.018272, + 20255.699431, + 24964.431669, + 29470.745887, + 33678.079947, + 37209.808930, + 39664.432393, + 41046.735479, + 40462.982011, + 39765.070209, + 39270.815830, + 39888.077002, + 42087.276604, + 45332.012929, + 49719.128772, + 54622.190928, + 59919.718626, + 65436.341097, + 70842.911460, + 76143.747430, + 81162.358574, + 85688.102884, + 89488.917734, + 91740.108470, + 91998.787916, + 87875.986012, + 79123.877908, + 66435.611045, + 48639.250610, + 27380.282817, + 2166.538464, + -21236.428084, + -43490.803535, + -60436.624080, + -73378.401966, + -80946.278268, + -84831.969493, + -84696.627286, + -81085.365407, + -76410.847049, + -70874.415387, + -65156.276464, + -59379.086883, + -53557.267619, + -47784.164830, + -42078.001172, + -36340.061427, + -30541.788202, + -24805.281435, + -19280.817165, + -13893.690606, + -8444.172221, + -3098.160839, + 2270.908649, + 7594.679295, + 12780.079247, + 17801.722109, + 22543.091206, + 26897.369814, + 31051.285734, + 34933.809557, + 38842.402859, + 42875.230152, + 47024.395356, + 51161.516122, + 55657.298307, + 60958.155424, + 66545.635029, + 72202.930397, + 77934.761905, + 83588.207792, + 89160.874522, + 94606.115027, + 99935.754968, + 104701.404975, + 107581.670606, + 108768.440311, + 107905.700480, + 104062.148863, + 96620.281684, + 83588.443029, + 61415.088182, + 27124.031692, + -7537.285321, + -43900.451653, + -70274.062783, + -87573.481475, + -101712.148408, + -116135.719087, + -124187.225446, + -124725.278371, + -122458.145590, + -117719.918256, + -112352.138605, + -106546.806030, + -100583.803012, + -94618.253238, + -88639.090897, + -82725.009842, + -76938.910669, + -71248.957807, + -65668.352795, + -60272.761991, + -55179.538428, + -50456.021161, + -46037.728058, + -42183.912670, + -39522.184006, + -38541.255303, + -38383.665728, + -39423.998130, + -40489.466130, + -41450.406768, + -42355.156592, + -43837.562085, + -43677.262972, + -41067.896944, + -37238.628465, + -32230.392026, + -26762.766062, + -20975.163308, + -15019.218554, + -9053.105545, + -3059.663132, + 2772.399618, + 8242.538397, + 13407.752291, + 18016.047539, + 22292.125752, + 26616.583347, + 30502.564253, + 33153.890890, + 34216.684448, + 33394.220786, + 29657.417791, + 23064.375405, + 12040.831532, + -2084.921068, + -21390.235970, + -38176.615985, + -51647.714482, + -59242.564959, + -60263.150854, + -58599.245165, + -54804.972560, + -50092.112608, + -44465.812552, + -38533.096297, + -32747.104307, + -27130.082610, + -21529.632955, + -15894.611939, + -10457.566933, + -5429.042583, + -903.757828, + 2481.947589, + 5173.789976, + 8358.768202, + 11565.584635, + 14431.147931, + 16951.619820, + 18888.807708, + 20120.884465, + 20222.141242, + 18423.168124, + 16498.668271, + 14442.624242, + 14070.038273, + 16211.370808, + 19639.815904, + 24280.360465, + 29475.380079, + 35030.793540, + 40812.325095, + 46593.082382, + 52390.906885, + 58109.310860, + 63780.896094, + 68984.456561, + 72559.442320, + 74645.487900, + 74695.219755, + 72098.143876, + 66609.929889, + 56864.971296, + 41589.295266, + 19057.032104, + -5951.329863, + -34608.796853, + -56603.801584, + -72678.838057, + -83297.070856, + -90127.593511, + -92656.040614, + -91394.995510, + -88192.056842, + -83148.833075, + -77582.587173, + -71750.440823, + -65765.369857, + -59716.101820, + -53613.430067, + -47473.832358, + -41287.031890, + -35139.919259, + -29097.671507, + -23178.836760, + -17486.807388, + -12046.775779, + -6802.483422, + -1867.556171, + 2644.380534, + 6615.829501, + 10332.557518, + 13706.737038, + 17017.991307, + 20303.136670, + 23507.386461, + 26482.194102, + 29698.585356, + 33196.305757, + 37385.914179, + 42872.996212, + 48725.617879, + 54564.488527, + 60453.841604, + 66495.146265, + 72668.620416, + 78723.644870, + 84593.136677, + 89974.936239, + 93439.798630, + 95101.207834, + 94028.126381, + 89507.925620, + 80989.846001, + 66944.274744, + 47016.422041, + 19932.783790, + -6198.433172, + -32320.379400, + -49822.852084, + -60517.553414, + -66860.548269, + -70849.714105, + -71058.721556, + -67691.947812, + -63130.703822, + -57687.607311, + -51916.952488, + -45932.054982, + -39834.909941, + -33714.535713, + -27564.443333, + -21465.186188, + -15469.326408, + -9522.358787, + -3588.742161, + 2221.802073, + 7758.244339, + 13020.269708, + 18198.562827, + 23211.338588, + 28051.699645, + 32708.577247, + 37413.795242, + 42181.401920, + 46462.499633, + 49849.582315, + 53026.578940, + 55930.600705, + 59432.642178, + 64027.356857, + 69126.843653, + 74620.328837, + 80372.056070, + 86348.152766, + 92468.907239, + 98568.998246, + 104669.511588, + 110445.790143, + 115394.348973, + 119477.553152, + 121528.574511, + 121973.674087, + 121048.017786, + 118021.473181, + 112151.993711, + 102195.999157, + 85972.731130, + 61224.719621, + 31949.279603, + -3726.022971, + -36485.298619, + -67336.469799, + -87799.366129, + -98865.713558, + -104103.651120, + -105068.402300, + -103415.820781, + -99261.356633, + -94281.850081, + -88568.701325, + -82625.711921, + -76766.776770, + -70998.803524, + -65303.404499, + -59719.198305, + -54182.230439, + -48662.904657, + -43206.731668, + -37732.701095, + -32375.478519, + -27167.508567, + -22197.211891, + -17722.869502, + -13925.135219, + -10737.893027, + -8455.327914, + -7067.008358, + -7086.991191, + -7527.693561, + -8378.025732, + -8629.383998, + -7854.586079, + -5853.040657, + -1973.225485, + 2699.850783, + 8006.098287, + 13651.734934, + 19139.318072, + 24476.645420, + 29463.480336, + 33899.078820, + 37364.528796, + 38380.214949, + 37326.585649, + 33428.470616, + 27441.000494, + 21761.126583, + 15368.408081, + 7224.234078, + -2702.217396, + -14109.682505, + -27390.915614, + -38569.562393, + -47875.155339, + -53969.121872, + -57703.473001, + -57993.198171, + -54908.391840, + -50568.410328, + -45247.622563, + -39563.224328, + -33637.786521, + -27585.345413, + -21572.074797, + -15597.363909, + -9577.429076, + -3475.770622, + 2520.378408, + 8046.881775, + 13482.345595, + ] + + beta_set = [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + ] + + # from new_data_set import * # Uncomment this line to use new data set + + # declare model name + model = ConcreteModel('SIR Disease Model') + + # declare constants + bpy = 26 # biweeks per year + years = 15 # years of data + bigM = 50.0 # big M for disjunction constraints + + # declare sets + model.S_meas = RangeSet(1, bpy * years) + model.S_meas_small = RangeSet(1, bpy * years - 1) + model.S_beta = RangeSet(1, bpy) + + # define variable bounds + def _gt_zero(m, i): + """ + Defines boundary constraints ensuring variables remain greater than zero. + + Parameters + ---------- + m : Pyomo.ConcreteModel + SIR disease model using a low/high transmission parameter + i : int + index of biweekly periods in the data set + + Returns + ------- + tuple + A tuple representing the lower and upper bounds for the variable, ensuring it remains positive. + """ + return (0.0, 1e7) + + def _beta_bounds(m): + """ + Sets the bounds for the transmission parameter beta within the model. + + Parameters + ---------- + m : Pyomo.ConcreteModel + SIR disease model using a low/high transmission parameter + + Returns + ------- + tuple + A tuple representing the lower and upper bounds for the beta variable. + """ + return (None, 5.0) + + # Define variables + # Log of estimated cases; All the variables are represented as common logarithm which the log base is 10. + # The original code inside build_model employs a disjunctive approach with integrated constraints. + # On the other hand, the commented code uses separate constraints for each scenario, applying the Big-M Reformulation. + # Binary variables (model.y) are defined inside on the build_model() function. The disjuncts for the Big-M Reformulation is written outside of the code. + + # model.logI = Var(model.S_meas, bounds=_gt_zero, doc='log of estimated cases') + model.logI = Var(model.S_meas, bounds=(0.001, 1e7), doc='log of estimated cases') + # log of transmission parameter beta + # model.logbeta = Var(model.S_beta, bounds=_gt_zero, doc='log of transmission parameter beta') + model.logbeta = Var( + model.S_beta, bounds=(0.0001, 5), doc='log of transmission parameter beta' + ) + # binary variable y over all betas + # model.y = Var(model.S_beta, within=Binary, doc='binary variable y over all betas') + # low value of beta + # model.logbeta_low = Var(bounds=_beta_bounds, doc='low value of beta') + model.logbeta_low = Var(bounds=(0.0001, 5)) + # high value of beta + # model.logbeta_high = Var(bounds=_beta_bounds, doc='high value of beta') + model.logbeta_high = Var(bounds=(0.0001, 5), doc='high value of beta') + # dummy variables + model.p = Var(model.S_meas, bounds=_gt_zero, doc='dummy variable p') + model.n = Var(model.S_meas, bounds=_gt_zero, doc='dummy variable n') + + # define indexed constants + + # log of measured cases after adjusting for under-reporting + logIstar = logIstar + # changes in susceptible population profile from susceptible reconstruction + deltaS = deltaS + # mean susceptibles (Number of Population) + # meanS = 1.04e6 + meanS = 8.65e5 # Number of Population(people) + logN = pop # log of measured population (Number of Population(log scale with log base 10)) + # define index for beta over all measurements () + beta_set = beta_set + + # define objective + def _obj_rule(m): + """ + Objective function for the SIR disease model, aiming to minimize the total discrepancy between estimated and observed infectious cases. + + Parameters + ---------- + m : Pyomo.ConcreteModel + SIR disease model using a low/high transmission parameter + + Returns + ------- + Pyomo.Expression + The expression for the objective function, which is the sum of overestimation and underestimation errors across all time periods. + + Notes + ----- + These variables ('p' and 'n') are likely to represent the overestimation and underestimation errors, respectively, + in the model's estimation of infectious cases compared to the observed data. + By minimizing their sum, the model seeks to closely align its estimations with the actual observed data. + """ + expr = sum(m.p[i] + m.n[i] for i in m.S_meas) + return expr + + model.obj = Objective(rule=_obj_rule, sense=minimize, doc='objective function') + + # define constraints + def _logSIR(m, i): + """ + SIR model constraint capturing the dynamics of infectious disease spread using a logarithmic formulation. + + Parameters + ---------- + m : Pyomo.ConcreteModel + SIR disease model using a low/high transmission parameter + i : int + index of biweekly periods in the data set + + Returns + ------- + tuple + A tuple containing the constraint expression for the SIR dynamics at the i-th bi-weekly period. + + Notes + ----- + This constraint is based on the differential equations of the SIR model, discretized and transformed into a logarithmic scale. + The 0.0 in (0.0, expr) enforces expr to be zero, defining an equality constraint essential for the SIR model to accurately capture the exact dynamics of disease transmission between time steps. + """ + expr = m.logI[i + 1] - ( + m.logbeta[beta_set[i - 1]] + + m.logI[i] + + math.log(deltaS[i - 1] + meanS) + - logN[i - 1] + ) + return (0.0, expr) + + model.logSIR = Constraint( + model.S_meas_small, rule=_logSIR, doc='log of SIR disease model' + ) + + # objective function constraint + def _p_n_const(m, i): + """ + Defines a constraint relating the model's estimated infectious cases to observed data, adjusted for overestimation and underestimation. + + It includes the variables 'p' and 'n', which represent the overestimation and underestimation errors, respectively. + + Parameters + ---------- + m : Pyomo.ConcreteModel + SIR disease model using a low/high transmission parameter + i : int + index of biweekly periods in the data set + + Returns + ------- + tuple + A tuple containing the constraint expression for adjusting the model's estimated cases at the i-th bi-weekly period. + + Notes + ----- + The constraint is formulated to account for the difference between the logarithm of observed infectious cases (logIstar) and the model's logarithm of estimated infectious cases (m.logI). + The 'p' and 'n' variables in the model represent overestimation and underestimation errors, respectively, and this constraint helps in aligning the model's estimates with actual observed data. + """ + expr = logIstar[i - 1] - m.logI[i] - m.p[i] + m.n[i] + return (0.0, expr) + + model.p_n_const = Constraint( + model.S_meas, rule=_p_n_const, doc='constraint for p and n' + ) + + # disjuncts + + model.BigM = Suffix() + model.y = RangeSet(0, 1) + + def _high_low(disjunct, i, y): + """ + Disjunct function for setting high and low beta values based on the binary variable. + + Parameters + ---------- + disjunct : Pyomo.Disjunction + The disjunct block being defined. + i : int + Index of biweekly periods in the data set. + y : int + Binary variable indicating whether the high or low beta value is used. + + Returns + ------- + None + Modifies the disjunct block to include the appropriate constraint based on the value of y. + + Notes + ----- + This function contributes to the disjunctive formulation of the model, allowing for the selection between high and low transmission rates for the disease. + """ + model = disjunct.model() + if y: + disjunct.c = Constraint(expr=model.logbeta_high - model.logbeta[i] == 0.0) + else: + disjunct.c = Constraint(expr=model.logbeta[i] - model.logbeta_low == 0.0) + model.BigM[disjunct.c] = bigM + + model.high_low = Disjunct( + model.S_beta, + model.y, + rule=_high_low, + doc='disjunct for high and low beta values', + ) + + # disjunctions + def _disj(model, i): + """ + Defines a disjunction for each beta value to choose between high and low transmission rates. + + Parameters + ---------- + model : Pyomo.Disjunction + disjunction for the high and low beta values + i : int + Index of biweekly periods in the data set + + Returns + ------- + list + A list of disjuncts for the i-th biweekly period, enabling the model to choose between + high and low beta values. + Each disjunct represents a set of constraints that are activated + based on the binary decision variables. + + Notes + ----- + This list is used by Pyomo to create a disjunction for each biweekly period, allowing the model to choose between the high or low beta value constraints based on the optimization process. + The defined disjunctions are integral to the model, enabling it to adaptively select the appropriate beta value for each time period, reflecting changes in disease transmission dynamics. + """ + return [model.high_low[i, j] for j in model.y] + + model.disj = Disjunction( + model.S_beta, rule=_disj, doc='disjunction for high and low beta values' + ) + + return model + + +# disjuncts +# The commented code sets up explicit high and low beta constraints for the SIR model using a big-M reformulation, bypassing Pyomo's built-in disjunctive programming tools. +# high beta disjuncts +# def highbeta_L(m,i): +# """ +# Defines the lower bound constraint for the high transmission parameter beta in the SIR model. + +# Parameters +# ---------- +# m : Pyomo.ConcreteModel +# SIR disease model using a low/high transmission parameter +# i : int +# index of biweekly periods in the data set + +# Returns +# ------- +# tuple +# A tuple (0.0, expr, None) where expr is the Pyomo expression for the lower bound of the high beta disjunct at the i-th biweekly period. +# This represents the lower bound of the high beta disjunct at the i-th biweekly period. + +# Notes +# ----- +# The given function is given as the expression that the disjunctions are converted by big-M reformulation. +# The binary variable m.y[i] is commented inside the model function. +# """ +# expr = m.logbeta[i] - m.logbeta_high + bigM*(1-m.y[i]) +# return (0.0, expr, None) +# model.highbeta_L = Constraint(model.S_beta, rule=highbeta_L) + +# def highbeta_U(m,i): +# """ +# Defines the upper bound constraint for the high transmission parameter beta in the SIR model. + +# Parameters +# ---------- +# m : Pyomo.ConcreteModel +# SIR disease model using a low/high transmission parameter +# i : int +# Index of biweekly periods in the data set. + +# Returns +# ------- +# tuple +# A tuple (None, expr, 0.0) where expr is the Pyomo expression for the upper bound of the high beta disjunct at the i-th biweekly period. +# This represents the upper bound of the high beta disjunct at the i-th biweekly period. + +# Notes +# The given function is given as the expression that the disjunctions are converted by big-M reformulation. +# The binary variable m.y[i] is commented inside the model function. +# """ +# expr = m.logbeta[i] - m.logbeta_high +# return (None, expr, 0.0) +# model.highbeta_U = Constraint(model.S_beta, rule=highbeta_U) + +# # low beta disjuncts +# def lowbeta_U(m,i): +# """ +# Defines the upper bound constraint for the low transmission parameter beta in the SIR model. + +# Parameters +# ---------- +# m (Pyomo.ConcreteModel): SIR disease model using a low/high transmission parameter +# i (int): index of biweekly periods in the data set + +# Returns: +# A tuple (None, expr, 0.0) where expr is the Pyomo expression for the upper bound of the low beta disjunct at the i-th biweekly period. +# This represents the upper bound of the low beta disjunct at the i-th biweekly period. + +# Notes +# ----- +# The given function is given as the expression that the disjunctions are converted by big-M reformulation. +# The binary variable m.y[i] is commented inside the model function. +# """ +# expr = m.logbeta[i] - m.logbeta_low - bigM*(m.y[i]) +# return (None, expr, 0.0) +# model.lowbeta_U = Constraint(model.S_beta, rule=lowbeta_U) + +# def lowbeta_L(m,i): +# """ +# Defines the lower bound constraint for the low transmission parameter beta in the SIR model. + +# Parameters +# ---------- +# m (Pyomo.ConcreteModel): SIR disease model using a low/high transmission parameter +# i (int): index of biweekly periods in the data set + +# Returns +# ------- +# tuple +# A tuple (0.0, expr, None) where expr is the Pyomo expression for the lower bound of the low beta disjunct at the i-th biweekly period. +# This represents the lower bound of the low beta disjunct at the i-th biweekly period. + +# Notes +# ----- +# This lower bound constraint is part of the model's disjunctive framework, allowing for the differentiation between low and high transmission rates. +# The big-M method integrates this constraint into the model based on the state of the binary decision variable `m.y[i]`. +# """ +# expr = m.logbeta[i] - m.logbeta_low +# return (0.0, expr, None) +# model.lowbeta_L = Constraint(model.S_beta, rule=lowbeta_L) + + +if __name__ == "__main__": + m = build_model() + TransformationFactory('gdp.bigm').apply_to(m) + SolverFactory('gams').solve( + m, solver='baron', tee=True, add_options=['option optcr=1e-6;'] + ) + m.obj.display() diff --git a/gdplib/gdp_col/column.py b/gdplib/gdp_col/column.py index 8b1bc4a..16ee9ef 100644 --- a/gdplib/gdp_col/column.py +++ b/gdplib/gdp_col/column.py @@ -3,24 +3,52 @@ from __future__ import division from pyomo.environ import ( - Block, ConcreteModel, Constraint, log, minimize, NonNegativeReals, Objective, RangeSet, Set, Var, ) + Block, + ConcreteModel, + Constraint, + log, + minimize, + NonNegativeReals, + Objective, + RangeSet, + Set, + Var, +) from pyomo.gdp import Disjunct, Disjunction def build_column(min_trays, max_trays, xD, xB): - """Builds the column model.""" + """ + Build a Pyomo model of a distillation column for separation of benzene and toluene. + + Parameters + ---------- + min_trays : int + Minimum number of trays in the column + max_trays : int + Maximum number of trays in the column + xD : float + Distillate(benzene) purity + xB : float + Bottoms(toluene) purity + + Returns + ------- + Pyomo.ConcreteModel + A Pyomo model of the distillation column for separation of benzene and toluene. + """ m = ConcreteModel('benzene-toluene column') - m.comps = Set(initialize=['benzene', 'toluene']) - min_T, max_T = 300, 400 - max_flow = 500 + m.comps = Set(initialize=['benzene', 'toluene'], doc='Set of components') + min_T, max_T = 300, 400 # Define temperature bounds [K] + max_flow = 500 # maximum flow rate [mol/s] m.T_feed = Var( - doc='Feed temperature [K]', domain=NonNegativeReals, - bounds=(min_T, max_T), initialize=368) - m.feed_vap_frac = Var( - doc='Vapor fraction of feed', - initialize=0, bounds=(0, 1)) - m.feed = Var( - m.comps, doc='Total component feed flow [mol/s]', initialize=50) + doc='Feed temperature [K]', + domain=NonNegativeReals, + bounds=(min_T, max_T), + initialize=368, + ) + m.feed_vap_frac = Var(doc='Vapor fraction of feed', initialize=0, bounds=(0, 1)) + m.feed = Var(m.comps, doc='Total component feed flow [mol/s]', initialize=50) m.condens_tray = max_trays m.feed_tray = int(round(max_trays / 2)) @@ -31,54 +59,121 @@ def build_column(min_trays, max_trays, xD, xB): m.trays = RangeSet(max_trays, doc='Set of potential trays') m.conditional_trays = Set( initialize=m.trays - [m.condens_tray, m.feed_tray, m.reboil_tray], - doc="Trays that may be turned on and off.") + doc="Trays that may be turned on and off.", + ) m.tray = Disjunct(m.conditional_trays, doc='Disjunct for tray existence') m.no_tray = Disjunct(m.conditional_trays, doc='Disjunct for tray absence') @m.Disjunction(m.conditional_trays, doc='Tray exists or does not') def tray_no_tray(b, t): + """ + Disjunction for tray existence or absence. + + Parameters + ---------- + b : Pyomo.Disjunct + Pyomo disjunct representing the existence or absence of a tray in the distillation column model. + t : int + Index of tray in the distillation column model. Tray numbering ascends from the reboiler at the bottom (tray 1) to the condenser at the top (tray max_trays) + + Returns + ------- + List of Disjuncts + List of disjuncts representing the existence or absence of a tray in the distillation column model. + """ return [b.tray[t], b.no_tray[t]] + m.minimum_num_trays = Constraint( - expr=sum(m.tray[t].binary_indicator_var - for t in m.conditional_trays) + 1 # for feed tray - >= min_trays) - - m.x = Var(m.comps, m.trays, doc='Liquid mole fraction', - bounds=(0, 1), domain=NonNegativeReals, initialize=0.5) - m.y = Var(m.comps, m.trays, doc='Vapor mole fraction', - bounds=(0, 1), domain=NonNegativeReals, initialize=0.5) - m.L = Var(m.comps, m.trays, - doc='component liquid flows from tray in kmol', - domain=NonNegativeReals, bounds=(0, max_flow), - initialize=50) - m.V = Var(m.comps, m.trays, - doc='component vapor flows from tray in kmol', - domain=NonNegativeReals, bounds=(0, max_flow), - initialize=50) - m.liq = Var(m.trays, domain=NonNegativeReals, - doc='liquid flows from tray in kmol', initialize=100, - bounds=(0, max_flow)) - m.vap = Var(m.trays, domain=NonNegativeReals, - doc='vapor flows from tray in kmol', initialize=100, - bounds=(0, max_flow)) - m.B = Var(m.comps, domain=NonNegativeReals, - doc='bottoms component flows in kmol', - bounds=(0, max_flow), initialize=50) - m.D = Var(m.comps, domain=NonNegativeReals, - doc='distillate component flows in kmol', - bounds=(0, max_flow), initialize=50) - m.bot = Var(domain=NonNegativeReals, initialize=50, bounds=(0, 100), - doc='bottoms flow in kmol') - m.dis = Var(domain=NonNegativeReals, initialize=50, - doc='distillate flow in kmol', bounds=(0, 100)) - m.reflux_ratio = Var(domain=NonNegativeReals, bounds=(0.5, 4), - doc='reflux ratio', initialize=0.8329) - m.reboil_ratio = Var(domain=NonNegativeReals, bounds=(0.5, 4), - doc='reboil ratio', initialize=0.9527) - m.reflux_frac = Var(domain=NonNegativeReals, bounds=(0, 1 - 1E-6), - doc='reflux fractions') - m.boilup_frac = Var(domain=NonNegativeReals, bounds=(0, 1 - 1E-6), - doc='boilup fraction') + expr=sum(m.tray[t].binary_indicator_var for t in m.conditional_trays) + + 1 # for feed tray + >= min_trays, + doc='Minimum number of trays', + ) + + m.x = Var( + m.comps, + m.trays, + doc='Liquid mole fraction', + bounds=(0, 1), + domain=NonNegativeReals, + initialize=0.5, + ) + m.y = Var( + m.comps, + m.trays, + doc='Vapor mole fraction', + bounds=(0, 1), + domain=NonNegativeReals, + initialize=0.5, + ) + m.L = Var( + m.comps, + m.trays, + doc='component liquid flows from tray in mol/s', + domain=NonNegativeReals, + bounds=(0, max_flow), + initialize=50, + ) + m.V = Var( + m.comps, + m.trays, + doc='component vapor flows from tray in mol/s', + domain=NonNegativeReals, + bounds=(0, max_flow), + initialize=50, + ) + m.liq = Var( + m.trays, + domain=NonNegativeReals, + doc='liquid flows from tray in mol/s', + initialize=100, + bounds=(0, max_flow), + ) + m.vap = Var( + m.trays, + domain=NonNegativeReals, + doc='vapor flows from tray in mol/s', + initialize=100, + bounds=(0, max_flow), + ) + m.B = Var( + m.comps, + domain=NonNegativeReals, + doc='bottoms component flows in mol/s', + bounds=(0, max_flow), + initialize=50, + ) + m.D = Var( + m.comps, + domain=NonNegativeReals, + doc='distillate component flows in mol/s', + bounds=(0, max_flow), + initialize=50, + ) + m.bot = Var( + domain=NonNegativeReals, + initialize=50, + bounds=(0, 100), + doc='bottoms flow in mol/s', + ) + m.dis = Var( + domain=NonNegativeReals, + initialize=50, + doc='distillate flow in mol/s', + bounds=(0, 100), + ) + m.reflux_ratio = Var( + domain=NonNegativeReals, bounds=(0.5, 4), doc='reflux ratio', initialize=0.8329 + ) + m.reboil_ratio = Var( + domain=NonNegativeReals, bounds=(0.5, 4), doc='reboil ratio', initialize=0.9527 + ) + m.reflux_frac = Var( + domain=NonNegativeReals, bounds=(0, 1 - 1e-6), doc='reflux fractions' + ) + m.boilup_frac = Var( + domain=NonNegativeReals, bounds=(0, 1 - 1e-6), doc='boilup fraction' + ) m.partial_cond = Disjunct() m.total_cond = Disjunct() @@ -90,67 +185,185 @@ def tray_no_tray(b, t): _build_condenser_mass_balance(m) _build_reboiler_mass_balance(m) - @m.Constraint(m.comps, - doc="Bottoms flow is equal to liquid leaving reboiler.") + @m.Constraint(m.comps, doc="Bottoms flow is equal to liquid leaving reboiler.") def bottoms_mass_balance(m, c): + """ + Constraint that the bottoms flow is equal to the liquid leaving the reboiler. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo model of the distillation column. + c : str + Index of component in the distillation column model. 'benzene' or 'toluene'. + + Returns + ------- + Pyomo.Constraint + Constraint that the bottoms flow is equal to the liquid leaving the reboiler. + """ return m.B[c] == m.L[c, m.reboil_tray] @m.Constraint() def boilup_frac_defn(m): + """ + Boilup fraction is the ratio between the bottoms flow and the liquid leaving the reboiler. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo model of the distillation column. + + Returns + ------- + Pyomo.Constraint + Constraint that the boilup fraction is the ratio between the bottoms flow and the liquid leaving the reboiler. + """ return m.bot == (1 - m.boilup_frac) * m.liq[m.reboil_tray + 1] @m.Constraint() def reflux_frac_defn(m): + """ + Reflux fraction is the ratio between the distillate flow and the difference in vapor flow in the condenser tray. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo model of the distillation column. + + Returns + ------- + Pyomo.Constraint + Constraint that the reflux fraction is the ratio between the distillate flow and the difference in vapor flow in the condenser tray. + """ return m.dis == (1 - m.reflux_frac) * ( - m.vap[m.condens_tray - 1] - m.vap[m.condens_tray]) + m.vap[m.condens_tray - 1] - m.vap[m.condens_tray] + ) @m.Constraint(m.trays) def liquid_sum(m, t): + """ + Total liquid flow on each tray is the sum of all component liquid flows on the tray. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo model of the distillation column. + t : int + Index of tray in the distillation column model. + + Returns + ------- + Pyomo.Constraint + Constraint that the total liquid flow on each tray is the sum of all component liquid flows on the tray. + """ return sum(m.L[c, t] for c in m.comps) == m.liq[t] @m.Constraint(m.trays) def vapor_sum(m, t): + """ + Total vapor flow on each tray is the sum of all component vapor flows on the tray. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo model of the distillation column. + t : int + Index of tray in the distillation column model. + + Returns + ------- + Pyomo.Constraint + Constraint that the total vapor flow on each tray is the sum of all component vapor flows on the tray. + """ return sum(m.V[c, t] for c in m.comps) == m.vap[t] m.bottoms_sum = Constraint( - expr=sum(m.B[c] for c in m.comps) == m.bot) + expr=sum(m.B[c] for c in m.comps) == m.bot, + doc="Total bottoms flow is the sum of all component flows at the bottom.", + ) m.distil_sum = Constraint( - expr=sum(m.D[c] for c in m.comps) == m.dis) + expr=sum(m.D[c] for c in m.comps) == m.dis, + doc="Total distillate flow is the sum of all component flows at the top.", + ) """Phase Equilibrium relations""" m.Kc = Var( - m.comps, m.trays, doc='Phase equilibrium constant', - domain=NonNegativeReals, initialize=1, bounds=(0, 1000)) - m.T = Var(m.trays, doc='Temperature [K]', - domain=NonNegativeReals, - bounds=(min_T, max_T)) + m.comps, + m.trays, + doc='Phase equilibrium constant', + domain=NonNegativeReals, + initialize=1, + bounds=(0, 1000), + ) + m.T = Var( + m.trays, doc='Temperature [K]', domain=NonNegativeReals, bounds=(min_T, max_T) + ) @m.Constraint(m.trays) def monotonoic_temperature(_, t): + """ + Temperature of tray t is greater than or equal to temperature of tray t+1. The temperature decreases as the trays ascend. + + Parameters + ---------- + _ : Pyomo.ConcreteModel + An unused placeholder parameter required by Pyomo's constraint interface, representing each potential tray in the distillation column where the temperature constraint is applied. + t : int + Index of tray in the distillation column model. + + Returns + ------- + Pyomo.Constraint + Constraint that the temperature of tray t is greater than or equal to temperature of tray t+1. If t is the condenser tray, the constraint is skipped. + """ return m.T[t] >= m.T[t + 1] if t < max_trays else Constraint.Skip - m.P = Var(doc='Pressure [bar]', - bounds=(0, 5)) + m.P = Var(doc='Pressure [bar]', bounds=(0, 5)) m.P.fix(1.01) m.T_ref = 298.15 m.gamma = Var( - m.comps, m.trays, - doc='liquid activity coefficent of component on tray', - domain=NonNegativeReals, bounds=(0, 10), initialize=1) + m.comps, + m.trays, + doc='liquid activity coefficient of component on tray', + domain=NonNegativeReals, + bounds=(0, 10), + initialize=1, + ) m.Pvap = Var( - m.comps, m.trays, + m.comps, + m.trays, doc='pure component vapor pressure of component on tray in bar', - domain=NonNegativeReals, bounds=(1E-3, 5), initialize=0.4) + domain=NonNegativeReals, + bounds=(1e-3, 5), + initialize=0.4, + ) m.Pvap_X = Var( - m.comps, m.trays, + m.comps, + m.trays, doc='Related to fraction of critical temperature (1 - T/Tc)', - bounds=(0.25, 0.5), initialize=0.4) + bounds=(0.25, 0.5), + initialize=0.4, + ) m.pvap_const = { - 'benzene': {'A': -6.98273, 'B': 1.33213, 'C': -2.62863, - 'D': -3.33399, 'Tc': 562.2, 'Pc': 48.9}, - 'toluene': {'A': -7.28607, 'B': 1.38091, 'C': -2.83433, - 'D': -2.79168, 'Tc': 591.8, 'Pc': 41.0}} + 'benzene': { + 'A': -6.98273, + 'B': 1.33213, + 'C': -2.62863, + 'D': -3.33399, + 'Tc': 562.2, + 'Pc': 48.9, + }, + 'toluene': { + 'A': -7.28607, + 'B': 1.38091, + 'C': -2.83433, + 'D': -2.79168, + 'Tc': 591.8, + 'Pc': 41.0, + }, + } for t in m.conditional_trays: _build_tray_phase_equilibrium(m, t, m.tray[t]) @@ -162,68 +375,164 @@ def monotonoic_temperature(_, t): _build_tray_phase_equilibrium(m, m.condens_tray, m.condenser_phase_eq) m.H_L = Var( - m.comps, m.trays, bounds=(0.1, 16), - doc='Liquid molar enthalpy of component in tray (kJ/mol)') + m.comps, + m.trays, + bounds=(0.1, 16), + doc='Liquid molar enthalpy of component in tray (kJ/mol)', + ) m.H_V = Var( - m.comps, m.trays, bounds=(30, 16 + 40), - doc='Vapor molar enthalpy of component in tray (kJ/mol)') + m.comps, + m.trays, + bounds=(30, 16 + 40), + doc='Vapor molar enthalpy of component in tray (kJ/mol)', + ) m.H_L_spec_feed = Var( - m.comps, doc='Component liquid molar enthalpy in feed [kJ/mol]', - initialize=0, bounds=(0.1, 16)) + m.comps, + doc='Component liquid molar enthalpy in feed [kJ/mol]', + initialize=0, + bounds=(0.1, 16), + ) m.H_V_spec_feed = Var( - m.comps, doc='Component vapor molar enthalpy in feed [kJ/mol]', - initialize=0, bounds=(30, 16 + 40)) - m.Qb = Var(domain=NonNegativeReals, doc='reboiler duty (MJ/s)', - initialize=1, bounds=(0, 8)) - m.Qc = Var(domain=NonNegativeReals, doc='condenser duty (MJ/s)', - initialize=1, bounds=(0, 8)) + m.comps, + doc='Component vapor molar enthalpy in feed [kJ/mol]', + initialize=0, + bounds=(30, 16 + 40), + ) + m.Qb = Var( + domain=NonNegativeReals, doc='reboiler duty (MJ/s)', initialize=1, bounds=(0, 8) + ) + m.Qc = Var( + domain=NonNegativeReals, + doc='condenser duty (MJ/s)', + initialize=1, + bounds=(0, 8), + ) m.vap_Cp_const = { - 'benzene': {'A': -3.392E1, 'B': 4.739E-1, 'C': -3.017E-4, - 'D': 7.130E-8, 'E': 0}, - 'toluene': {'A': -2.435E1, 'B': 5.125E-1, 'C': -2.765E-4, - 'D': 4.911E-8, 'E': 0}} + 'benzene': { + 'A': -3.392e1, + 'B': 4.739e-1, + 'C': -3.017e-4, + 'D': 7.130e-8, + 'E': 0, + }, + 'toluene': { + 'A': -2.435e1, + 'B': 5.125e-1, + 'C': -2.765e-4, + 'D': 4.911e-8, + 'E': 0, + }, + } m.liq_Cp_const = { - 'benzene': {'A': 1.29E5, 'B': -1.7E2, 'C': 6.48E-1, - 'D': 0, 'E': 0}, - 'toluene': {'A': 1.40E5, 'B': -1.52E2, 'C': 6.95E-1, - 'D': 0, 'E': 0}} - m.dH_vap = {'benzene': 33.770E3, 'toluene': 38.262E3} # J/mol + 'benzene': {'A': 1.29e5, 'B': -1.7e2, 'C': 6.48e-1, 'D': 0, 'E': 0}, + 'toluene': {'A': 1.40e5, 'B': -1.52e2, 'C': 6.95e-1, 'D': 0, 'E': 0}, + } + m.dH_vap = {'benzene': 33.770e3, 'toluene': 38.262e3} # J/mol _build_column_heat_relations(m) @m.Constraint() def distillate_req(m): + """ + Flow of benzene in the distillate meets the specified purity requirement. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo model of the distillation column. + + Returns + ------- + Pyomo.Constraint + Constraint that the flow of benzene in the distillate meets the specified purity requirement. The flow of benzene in the distillate is greater than or equal to the distillate purity times the total distillate flow. + """ return m.D['benzene'] >= m.distillate_purity * m.dis @m.Constraint() def bottoms_req(m): + """ + Flow of toluene in the bottoms meets the specified purity requirement. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo model of the distillation column. + + Returns + ------- + Pyomo.Constraint + Constraint that the flow of toluene in the bottoms meets the specified purity requirement. The flow of toluene in the bottoms is greater than or equal to the bottoms purity times the total bottoms flow. + """ return m.B['toluene'] >= m.bottoms_purity * m.bot + # Define the objective function as the sum of reboiler and condenser duty plus an indicator for tray activation + # The objective is to minimize the sum of condenser and reboiler duties, Qc and Qb, multiplied by 1E3 to convert units, + # and also the number of activated trays, which is obtained by summing up the indicator variables for the trays by 1E3 [$/No. of Trays]. # m.obj = Objective(expr=(m.Qc + m.Qb) * 1E-3, sense=minimize) - m.obj = Objective( expr=(m.Qc + m.Qb) * 1E3 + 1E3 * ( - sum(m.tray[t].binary_indicator_var for t in m.conditional_trays) + 1), - sense=minimize) + m.obj = Objective( + expr=(m.Qc + m.Qb) * 1e3 + + 1e3 * (sum(m.tray[t].binary_indicator_var for t in m.conditional_trays) + 1), + sense=minimize, + ) # m.obj = Objective( # expr=sum(m.tray[t].indicator_var for t in m.conditional_trays) + 1) @m.Constraint() def reflux_ratio_calc(m): + """ + Reflux ratio is the ratio between the distillate flow and the difference in vapor flow in the condenser tray. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo model of the distillation column.. + + Returns + ------- + Pyomo.Constraint + Constraint that the reflux ratio is the ratio between the distillate flow and the difference in vapor flow in the condenser tray. + """ return m.reflux_frac * (m.reflux_ratio + 1) == m.reflux_ratio @m.Constraint() def reboil_ratio_calc(m): + """ + Reboil ratio is the ratio between the bottoms flow and the liquid leaving the reboiler. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo model of the distillation column. + + Returns + ------- + Pyomo.Constraint + Constraint that the reboil ratio is the ratio between the bottoms flow and the liquid leaving the reboiler. + """ return m.boilup_frac * (m.reboil_ratio + 1) == m.reboil_ratio @m.Constraint(m.conditional_trays) def tray_ordering(m, t): - """Trays close to the feed should be activated first.""" + """ + Trays close to the feed should be activated first. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo model of the distillation column. + t : int + Index of tray in the distillation column model. + + Returns + ------- + Pyomo.Constraint + Constraint that trays close to the feed should be activated first. + """ if t + 1 < m.condens_tray and t > m.feed_tray: - return m.tray[t].binary_indicator_var >= \ - m.tray[t + 1].binary_indicator_var + return m.tray[t].binary_indicator_var >= m.tray[t + 1].binary_indicator_var elif t > m.reboil_tray and t + 1 < m.feed_tray: - return m.tray[t + 1].binary_indicator_var >= \ - m.tray[t].binary_indicator_var + return m.tray[t + 1].binary_indicator_var >= m.tray[t].binary_indicator_var else: return Constraint.NoConstraint @@ -232,12 +541,42 @@ def tray_ordering(m, t): def _build_conditional_tray_mass_balance(m, t, tray, no_tray): """ - t = tray number - tray = tray exists disjunct - no_tray = tray absent disjunct + Builds the constraints for mass balance, liquid and vapor composition for a given tray (t) in the distillation column. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo model of the distillation column. + t : int + Index of tray in the distillation column model. + tray : Pyomo.Disjunct + Disjunct representing the existence of the tray. + no_tray : Pyomo.Disjunct + Disjunct representing the absence of the tray. + + Returns + ------- + None + None, but the mass balance constraints for the conditional tray are added to the Pyomo model. """ + @tray.Constraint(m.comps) def mass_balance(_, c): + """ + Mass balance on each component on a tray. + + Parameters + ---------- + _ : Pyomo.ConcreteModel + An unused placeholder, typically used to represent the model instance when defining constraints within a method, but not utilized in this specific constraint function. This placeholder denotes the conditional trays in the distillation column model. + c : str + Index of component in the distillation column model. 'benzene' or 'toluene'. + + Returns + ------- + Pyomo.Constraint + Constraint that the mass balance on each component on a tray is equal to the sum of the feed in, vapor from the tray, liquid from the tray above, liquid to the tray below, and vapor from the tray below. + """ return ( # Feed in if feed tray (m.feed[c] if t == m.feed_tray else 0) @@ -252,164 +591,630 @@ def mass_balance(_, c): # Liquid to tray below if not reboiler - (m.L[c, t] if t > m.reboil_tray else 0) # Vapor from tray below if not reboiler - + (m.V[c, t - 1] if t > m.reboil_tray else 0) == 0) + + (m.V[c, t - 1] if t > m.reboil_tray else 0) + == 0 + ) @tray.Constraint(m.comps) def tray_liquid_composition(_, c): + """ + Liquid composition constraint for the tray. + + Parameters + ---------- + _ : Pyomo.ConcreteModel + An unused placeholder denoting the conditional trays in the distillation column model. + c : str + Index of component in the distillation column model. 'benzene' or 'toluene'. + + Returns + ------- + Pyomo.Constraint + Constraint that the liquid flow rate for each component is equal to the liquid flow rate on the tray times the liquid composition on the tray. + """ return m.L[c, t] == m.liq[t] * m.x[c, t] @tray.Constraint(m.comps) def tray_vapor_compositions(_, c): + """ + Vapor composition constraint for the tray. + + Parameters + ---------- + _ : Pyomo.ConcreteModel + An unused placeholder denoting the conditional trays in the distillation column model. + c : str + Index of component in the distillation column model. 'benzene' or 'toluene'. + + Returns + ------- + Pyomo.Constraint + Constraint that the vapor flow rate for each component is equal to the vapor flow rate on the tray times the vapor composition on the tray. + """ return m.V[c, t] == m.vap[t] * m.y[c, t] @no_tray.Constraint(m.comps) def liq_comp_pass_through(_, c): + """ + Liquid composition constraint for the case when the tray does not exist. + + Parameters + ---------- + _ : Pyomo.ConcreteModel + An unused placeholder denoting the conditional trays in the distillation column model. + c : str + Index of component in the distillation column model. 'benzene' or 'toluene'. + + Returns + ------- + Pyomo.Constraint + Constraint that the liquid composition is equal to the liquid composition on the tray above, when the tray is not present. + """ return m.x[c, t] == m.x[c, t + 1] @no_tray.Constraint(m.comps) def liq_flow_pass_through(_, c): + """ + Liquid flow rate constraint for the case when the tray does not exist. + + Parameters + ---------- + _ : Pyomo.ConcreteModel + An unused placeholder denoting the conditional trays in the distillation column model. + c : str + Index of component in the distillation column model. 'benzene' or 'toluene'. + + Returns + ------- + Pyomo.Constraint + Constraint that the liquid flow rate is equal to the liquid flow rate on the tray above, when the tray is not present. + """ return m.L[c, t] == m.L[c, t + 1] @no_tray.Constraint(m.comps) def vap_comp_pass_through(_, c): + """ + Vapor composition constraint for the case when the tray does not exist. + + Parameters + ---------- + _ : Pyomo.ConcreteModel + An unused placeholder denoting the conditional trays in the distillation column model. + c : str + Index of component in the distillation column model. 'benzene' or 'toluene'. + + Returns + ------- + Pyomo.Constraint + Constraint that the vapor composition is equal to the vapor composition on the tray below, when the tray is not present. + """ return m.y[c, t] == m.y[c, t - 1] @no_tray.Constraint(m.comps) def vap_flow_pass_through(_, c): + """ + Vapor flow rate constraint for the case when the tray does not exist. + + Parameters + ---------- + _ : Pyomo.ConcreteModel + An unused placeholder denoting the conditional trays in the distillation column model. + c : str + Index of component in the distillation column model. 'benzene' or 'toluene'. + + Returns + ------- + Pyomo.Constraint + Constraint that the vapor flow rate is equal to the vapor flow rate on the tray below, when the tray is not present. + """ return m.V[c, t] == m.V[c, t - 1] def _build_feed_tray_mass_balance(m): + """ + Constructs the mass balance and composition constraints for the feed tray in the distillation column. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo model of the distillation column. + + Returns + ------- + None + None, but the mass balance constraints for the feed tray are added to the Pyomo model. + """ t = m.feed_tray @m.Constraint(m.comps) def feed_mass_balance(_, c): + """ + Mass balance on each component on a tray. + + Parameters + ---------- + _ : Pyomo.ConcreteModel + A placeholder for the model instance, required for defining constraints within the Pyomo framework. It specifies the context in which the feed tray conditions are applied. + c : str + Index of component in the distillation column model. 'benzene' or 'toluene'. + + Returns + ------- + Pyomo.Constraint + Constraint that the mass balance on each component on a tray is equal to the sum of the feed in, vapor from the tray, liquid from the tray above, liquid to the tray below, and vapor from the tray below. + """ return ( - m.feed[c] # Feed in - - m.V[c, t] # Vapor from tray t + m.feed[c] # Feed in + - m.V[c, t] # Vapor from tray t + m.L[c, t + 1] # Liquid from tray above - - m.L[c, t] # Liquid to tray below + - m.L[c, t] # Liquid to tray below + m.V[c, t - 1] # Vapor from tray below - == 0) + == 0 + ) @m.Constraint(m.comps) def feed_tray_liquid_composition(_, c): + """ + Liquid composition constraint for the feed tray. + + Parameters + ---------- + _ : Pyomo.ConcreteModel + A placeholder for the model instance, required for defining constraints within the Pyomo framework. It specifies the context in which the feed tray conditions are applied. + c : str + Index of component in the distillation column model. 'benzene' or 'toluene'. + + Returns + ------- + Pyomo.Constraint + Constraint that the liquid flow rate for each component is equal to the liquid flow rate on the tray times the liquid composition on the tray. + """ return m.L[c, t] == m.liq[t] * m.x[c, t] @m.Constraint(m.comps) def feed_tray_vapor_composition(_, c): + """ + Vapor composition on each component on a tray. + + Parameters + ---------- + _ : Pyomo.ConcreteModel + A placeholder for the model instance, required for defining constraints within the Pyomo framework. It specifies the context in which the feed tray conditions are applied. + c : str + Index of component in the distillation column model. 'benzene' or 'toluene'. + + Returns + ------- + Pyomo.Constraint + Constraint that the vapor flow rate for each component is equal to the vapor flow rate on the tray times the vapor composition on the tray. + """ return m.V[c, t] == m.vap[t] * m.y[c, t] def _build_condenser_mass_balance(m): + """ + Constructs the mass balance equations for the condenser tray in a distillation column. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo model of the distillation column.. + + Returns + ------- + None + None, but the mass balance constraints for the condenser are added to the Pyomo model. + """ t = m.condens_tray @m.Constraint(m.comps) def condenser_mass_balance(_, c): + """ + Mass balance for each component in the condenser tray. + + Parameters + ---------- + _ : Pyomo.ConcreteModel + A placeholder representing the Pyomo model instance. It specifies the context for applying the mass balance and composition constraints to the condenser tray in the distillation column. + c : str + Index of component in the distillation column model. 'benzene' or 'toluene'. + + Returns + ------- + Pyomo.Constraint + Constraint that the mass balance for each component in the condenser tray is equal to the sum of the vapor from the tray, loss to distillate, liquid to the tray below, and vapor from the tray below. + """ return ( - - m.V[c, t] # Vapor from tray t - - m.D[c] # Loss to distillate - - m.L[c, t] # Liquid to tray below + -m.V[c, t] # Vapor from tray t + - m.D[c] # Loss to distillate + - m.L[c, t] # Liquid to tray below + m.V[c, t - 1] # Vapor from tray below - == 0) + == 0 + ) @m.partial_cond.Constraint(m.comps) def condenser_liquid_composition(_, c): + """ + Liquid composition constraint for the condenser tray. + + Parameters + ---------- + _ : Pyomo.ConcreteModel + A placeholder representing the Pyomo model instance. It specifies the context for applying the mass balance and composition constraints to the condenser tray in the distillation column. + c : str + Index of component in the distillation column model. 'benzene' or 'toluene'. + + Returns + ------- + Pyomo.Constraint + Constraint that the liquid flow rate for each component is equal to the liquid flow rate on the tray times the liquid composition on the tray. + """ return m.L[c, t] == m.liq[t] * m.x[c, t] @m.partial_cond.Constraint(m.comps) def condenser_vapor_composition(_, c): + """ + Vapor composition constraint for the condenser tray. + + Parameters + ---------- + _ : Pyomo.ConcreteModel + A placeholder representing the Pyomo model instance. It specifies the context for applying the mass balance and composition constraints to the condenser tray in the distillation column. + c : str + Index of component in the distillation column model. 'benzene' or 'toluene'. + + Returns + ------- + Pyomo.Constraint + Constraint that the vapor flow rate for each component is equal to the vapor flow rate on the tray times the vapor composition on the tray. + """ return m.V[c, t] == m.vap[t] * m.y[c, t] @m.total_cond.Constraint(m.comps) def no_vapor_flow(_, c): + """ + No vapor flow for each component in the case of total condensation. + + Parameters + ---------- + _ : Pyomo.ConcreteModel + A placeholder representing the Pyomo model instance. It specifies the context for applying the mass balance and composition constraints to the condenser tray in the distillation column. + c : str + Index of component in the distillation column model. 'benzene' or 'toluene'. + + Returns + ------- + Pyomo.Constraint + Constraint that there is no vapor flow for each component in the case of total condensation. + """ return m.V[c, t] == 0 @m.total_cond.Constraint() def no_total_vapor_flow(_): + """ + No total vapor flow for the condenser tray. + + Parameters + ---------- + _ : Pyomo.ConcreteModel + A placeholder representing the Pyomo model instance. It specifies the context for applying the mass balance and composition constraints to the condenser tray in the distillation column. + + Returns + ------- + Pyomo.Constraint + Constraint that there is no total vapor flow for the condenser tray. + """ return m.vap[t] == 0 @m.total_cond.Constraint(m.comps) def liquid_fraction_pass_through(_, c): + """ + Liquid fraction pass-through for each component in the case of total condensation. + + Parameters + ---------- + _ : Pyomo.ConcreteModel + A placeholder representing the Pyomo model instance. It specifies the context for applying the mass balance and composition constraints to the condenser tray in the distillation column. + c : str + Index of component in the distillation column model. 'benzene' or 'toluene'. + + Returns + ------- + Pyomo.Constraint + Constraint that the liquid fraction is equal to the vapor fraction on the tray below in the case of total condensation. + """ return m.x[c, t] == m.y[c, t - 1] @m.Constraint(m.comps) def condenser_distillate_composition(_, c): + """ + Distillate composition constraint for the condenser tray. + + Parameters + ---------- + _ : Pyomo.ConcreteModel + A placeholder representing the Pyomo model instance. It specifies the context for applying the mass balance and composition constraints to the condenser tray in the distillation column. + c : str + Index of component in the distillation column model. 'benzene' or 'toluene'. + + Returns + ------- + Pyomo.Constraint + Constraint that the distillate flow rate for each component is equal to the distillate flow rate times the distillate composition. + """ return m.D[c] == m.dis * m.x[c, t] def _build_reboiler_mass_balance(m): + """ + Constructs the mass balance equations for the reboiler tray in a distillation column. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo model of the distillation column.. + + Returns + ------- + None + None, but the mass balance constraints for the reboiler are added to the Pyomo model. + """ t = m.reboil_tray @m.Constraint(m.comps) def reboiler_mass_balance(_, c): + """ + Mass balance for each component in the reboiler tray. + + Parameters + ---------- + _ : Pyomo.ConcreteModel + A placeholder representing the Pyomo model instance. It specifies the context for applying the mass balance and composition constraints to the reboiler tray in the distillation column. + c : str + Index of component in the distillation column model. 'benzene' or 'toluene'. + + Returns + ------- + Pyomo.Constraint + Constraint that the mass balance for each component in the reboiler tray is equal to the sum of the vapor from the tray, liquid from the tray above, and loss to bottoms. + """ t = m.reboil_tray return ( - - m.V[c, t] # Vapor from tray t + -m.V[c, t] # Vapor from tray t + m.L[c, t + 1] # Liquid from tray above - - m.B[c] # Loss to bottoms - == 0) + - m.B[c] # Loss to bottoms + == 0 + ) @m.Constraint(m.comps) def reboiler_liquid_composition(_, c): + """ + Liquid composition constraint for the reboiler tray. + + Parameters + ---------- + _ : Pyomo.ConcreteModel + A placeholder representing the Pyomo model instance. It specifies the context for applying the mass balance and composition constraints to the reboiler tray in the distillation column. + c : str + Index of component in the distillation column model. 'benzene' or 'toluene'. + + Returns + ------- + Pyomo.Constraint + Constraint that the liquid flow rate for each component is equal to the liquid flow rate on the tray times the liquid composition on the tray. + """ return m.L[c, t] == m.liq[t] * m.x[c, t] @m.Constraint(m.comps) def reboiler_vapor_composition(_, c): + """ + Vapor composition constraint for the reboiler tray. + + Parameters + ---------- + _ : Pyomo.ConcreteModel + A placeholder representing the Pyomo model instance. It specifies the context for applying the mass balance and composition constraints to the reboiler tray in the distillation column. + c : str + Index of component in the distillation column model. 'benzene' or 'toluene'. + + Returns + ------- + Pyomo.Constraint + Constraint that the vapor flow rate for each component is equal to the vapor flow rate on the tray times the vapor composition on the tray. + """ return m.V[c, t] == m.vap[t] * m.y[c, t] def _build_tray_phase_equilibrium(m, t, tray): + """_summary_ + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo model of the distillation column. + t : int + Index of tray in the distillation column model. + tray : Pyomo.Disjunct + Disjunct representing the existence of the tray. + + Returns + ------- + None + None, but the phase equilibrium constraints for the tray are added to the Pyomo model. + """ + @tray.Constraint(m.comps) def raoults_law(_, c): + """ + Raoult's law for each component in a tray. + + Parameters + ---------- + _ : Pyomo.ConcreteModel + A placeholder representing the Pyomo model instance. It specifies the context for applying the phase equilibrium constraints to the trays in the distillation column. + c : str + Index of component in the distillation column model. 'benzene' or 'toluene'. + + Returns + ------- + Pyomo.Constraint + The Raoult's law for each component in trays is calculated as the product of the liquid mole fraction and the phase equilibrium constant. The product is equal to the vapor mole fraction. + """ return m.y[c, t] == m.x[c, t] * m.Kc[c, t] @tray.Constraint(m.comps) def phase_equil_const(_, c): - return m.Kc[c, t] * m.P == ( - m.gamma[c, t] * m.Pvap[c, t]) + """ + Phase equilibrium constraint for each component in a tray. + + Parameters + ---------- + _ : Pyomo.ConcreteModel + A placeholder representing the Pyomo model instance. It specifies the context for applying the phase equilibrium constraints to the trays in the distillation column. + c : str + Index of component in the distillation column model. 'benzene' or 'toluene'. + + Returns + ------- + Pyomo.Constraint + The phase equilibrium constant for each component in a tray multiplied with the pressure is equal to the product of the activity coefficient and the pure component vapor pressure. + """ + return m.Kc[c, t] * m.P == (m.gamma[c, t] * m.Pvap[c, t]) @tray.Constraint(m.comps) def Pvap_relation(_, c): + """ + Antoine's equation for the vapor pressure of each component in a tray. + + Parameters + ---------- + _ : Pyomo.ConcreteModel + A placeholder representing the Pyomo model instance. It specifies the context for applying the phase equilibrium constraints to the trays in the distillation column. + c : str + Index of component in the distillation column model. 'benzene' or 'toluene'. + + Returns + ------- + Pyomo.Constraint + Antoine's equation for the vapor pressure of each component in a tray is calculated as the logarithm of the vapor pressure minus the logarithm of the critical pressure times one minus the fraction of critical temperature. The equation is equal to the sum of the Antoine coefficients times the fraction of critical temperature raised to different powers. + """ k = m.pvap_const[c] x = m.Pvap_X[c, t] return (log(m.Pvap[c, t]) - log(k['Pc'])) * (1 - x) == ( - k['A'] * x + - k['B'] * x ** 1.5 + - k['C'] * x ** 3 + - k['D'] * x ** 6) + k['A'] * x + k['B'] * x**1.5 + k['C'] * x**3 + k['D'] * x**6 + ) @tray.Constraint(m.comps) def Pvap_X_defn(_, c): + """ + Defines the relationship between the one minus the reduced temperature variable (Pvap_X) for each component in a tray, and the actual temperature of the tray, normalized by the critical temperature of the component (Tc). + + Parameters + ---------- + _ : Pyomo.ConcreteModel + A placeholder representing the Pyomo model instance. It specifies the context for applying the phase equilibrium constraints to the trays in the distillation column. + c : str + Index of component in the distillation column model. 'benzene' or 'toluene'. + + Returns + ------- + Pyomo.Constraint + The relationship between the one minus the reduced temperature variable (Pvap_X) for each component in a tray, and the actual temperature of the tray, normalized by the critical temperature of the component (Tc). + """ k = m.pvap_const[c] return m.Pvap_X[c, t] == 1 - m.T[t] / k['Tc'] @tray.Constraint(m.comps) def gamma_calc(_, c): + """ + Calculates the activity coefficient for each component in a tray. + + Parameters + ---------- + _ : Pyomo.ConcreteModel + A placeholder representing the Pyomo model instance. It specifies the context for applying the phase equilibrium constraints to the trays in the distillation column. + c : str + Index of component in the distillation column model. 'benzene' or 'toluene'. + + Returns + ------- + Pyomo.Constraint + The activity coefficient for each component in a tray is calculated as 1. + """ return m.gamma[c, t] == 1 def _build_column_heat_relations(m): + """ + Constructs the enthalpy relations for both liquid and vapor phases in each tray of a distillation column. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo model of the distillation column.. + + Returns + ------- + None + None, but the energy balance constraints for the distillation column are added to the Pyomo model. + """ + @m.Expression(m.trays, m.comps) def liq_enthalpy_expr(_, t, c): + """ + Liquid phase enthalpy based on the heat capacity coefficients and the temperature difference from a reference temperature [kJ/mol]. + + Parameters + ---------- + _ : Pyomo.ConcreteModel + A placeholder representing the Pyomo model instance. It specifies the context for applying the energy balance constraints to the distillation column. + t : int + Index of tray in the distillation column model. + c : str + Index of component in the distillation column model. 'benzene' or 'toluene'. + + Returns + ------- + Pyomo.Expression + Enthalpy based on the heat capacity coefficients and the temperature difference from a reference temperature [kJ/mol]. + """ k = m.liq_Cp_const[c] return ( - k['A'] * (m.T[t] - m.T_ref) + - k['B'] * (m.T[t] ** 2 - m.T_ref ** 2) / 2 + - k['C'] * (m.T[t] ** 3 - m.T_ref ** 3) / 3 + - k['D'] * (m.T[t] ** 4 - m.T_ref ** 4) / 4 + - k['E'] * (m.T[t] ** 5 - m.T_ref ** 5) / 5) * 1E-6 + k['A'] * (m.T[t] - m.T_ref) + + k['B'] * (m.T[t] ** 2 - m.T_ref**2) / 2 + + k['C'] * (m.T[t] ** 3 - m.T_ref**3) / 3 + + k['D'] * (m.T[t] ** 4 - m.T_ref**4) / 4 + + k['E'] * (m.T[t] ** 5 - m.T_ref**5) / 5 + ) * 1e-6 # Convert [J/mol] to [MJ/mol] @m.Expression(m.trays, m.comps) def vap_enthalpy_expr(_, t, c): + """ + Vapor phase enthalpy based on the heat capacity coefficients and the temperature difference from a reference temperature [kJ/mol]. + + Parameters + ---------- + _ : Pyomo.ConcreteModel + A placeholder representing the Pyomo model instance. It specifies the context for applying the energy balance constraints to the distillation column. + t : int + Index of tray in the distillation column model. + c : str + Index of component in the distillation column model. 'benzene' or 'toluene'. + + Returns + ------- + Pyomo.Expression + Enthalpy based on the heat capacity coefficients and the temperature difference from a reference temperature [kJ/mol]. + """ k = m.vap_Cp_const[c] return ( - m.dH_vap[c] + - k['A'] * (m.T[t] - m.T_ref) + - k['B'] * (m.T[t] ** 2 - m.T_ref ** 2) / 2 + - k['C'] * (m.T[t] ** 3 - m.T_ref ** 3) / 3 + - k['D'] * (m.T[t] ** 4 - m.T_ref ** 4) / 4 + - k['E'] * (m.T[t] ** 5 - m.T_ref ** 5) / 5) * 1E-3 + m.dH_vap[c] + + k['A'] * (m.T[t] - m.T_ref) + + k['B'] * (m.T[t] ** 2 - m.T_ref**2) / 2 + + k['C'] * (m.T[t] ** 3 - m.T_ref**3) / 3 + + k['D'] * (m.T[t] ** 4 - m.T_ref**4) / 4 + + k['E'] * (m.T[t] ** 5 - m.T_ref**5) / 5 + ) * 1e-3 # Convert [J/mol] to [kJ/mol] for t in m.conditional_trays: _build_conditional_tray_energy_balance(m, t, m.tray[t], m.no_tray[t]) @@ -419,43 +1224,170 @@ def vap_enthalpy_expr(_, t, c): def _build_conditional_tray_energy_balance(m, t, tray, no_tray): + """ + Constructs the energy balance constraints for a specific tray in a distillation column, considering both active and inactive (pass-through) scenarios. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo model of the distillation column. + t : int + Index of tray in the distillation column model. + tray : Pyomo.Disjunct + Disjunct representing the existence of the tray. + no_tray : Pyomo.Disjunct + Disjunct representing the absence of the tray. + + Returns + ------- + None + None, but the energy balance constraints for the conditional tray are added to the Pyomo model. + """ + @tray.Constraint() def energy_balance(_): - return sum( - m.L[c, t + 1] * m.H_L[c, t + 1] # heat of liquid from tray above - - m.L[c, t] * m.H_L[c, t] # heat of liquid to tray below - + m.V[c, t - 1] * m.H_V[c, t - 1] # heat of vapor from tray below - - m.V[c, t] * m.H_V[c, t] # heat of vapor to tray above - for c in m.comps) * 1E-3 == 0 + """_summary_ + + Parameters + ---------- + _ : Pyomo.ConcreteModel + A placeholder representing the Pyomo model instance. It specifies the context for applying the energy balance constraints to the trays in the distillation column. + + Returns + ------- + Pyomo.Constraint + _description_ + """ + return ( + sum( + m.L[c, t + 1] * m.H_L[c, t + 1] # heat of liquid from tray above + - m.L[c, t] * m.H_L[c, t] # heat of liquid to tray below + + m.V[c, t - 1] * m.H_V[c, t - 1] # heat of vapor from tray below + - m.V[c, t] * m.H_V[c, t] # heat of vapor to tray above + for c in m.comps + ) + * 1e-3 + == 0 + ) @tray.Constraint(m.comps) def liq_enthalpy_calc(_, c): + """ + Liquid enthalpy calculation for each component in a tray. + + Parameters + ---------- + _ : Pyomo.ConcreteModel + A placeholder representing the Pyomo model instance. It specifies the context for applying the energy balance constraints to the trays in the distillation column. + c : str + Index of component in the distillation column model. 'benzene' or 'toluene'. + + Returns + ------- + Pyomo.Constraint + Constraint that the liquid enthalpy for each component is equal to the liquid enthalpy expression. + """ return m.H_L[c, t] == m.liq_enthalpy_expr[t, c] @tray.Constraint(m.comps) def vap_enthalpy_calc(_, c): + """ + Vapor enthalpy calculation for each component in a tray. + + Parameters + ---------- + _ : Pyomo.ConcreteModel + A placeholder representing the Pyomo model instance. It specifies the context for applying the energy balance constraints to the trays in the distillation column. + c : str + Index of component in the distillation column model. 'benzene' or 'toluene'. + + Returns + ------- + Pyomo.Constraint + Constraint that the vapor enthalpy for each component is equal to the vapor enthalpy expression. + """ return m.H_V[c, t] == m.vap_enthalpy_expr[t, c] @no_tray.Constraint(m.comps) def liq_enthalpy_pass_through(_, c): + """ + Liquid enthalpy pass-through for each component in the case of no tray. + + Parameters + ---------- + _ : Pyomo.ConcreteModel + A placeholder representing the Pyomo model instance. It specifies the context for applying the energy balance constraints to the trays in the distillation column. + c : str + Index of component in the distillation column model. 'benzene' or 'toluene'. + + Returns + ------- + Pyomo.Constraint + Constraint that the liquid enthalpy is equal to the liquid enthalpy on the tray below, when the tray is not present. + """ return m.H_L[c, t] == m.H_L[c, t + 1] @no_tray.Constraint(m.comps) def vap_enthalpy_pass_through(_, c): + """ + Vapor enthalpy pass-through for each component in the case of no tray. + + Parameters + ---------- + _ : Pyomo.ConcreteModel + A placeholder representing the Pyomo model instance. It specifies the context for applying the energy balance constraints to the trays in the distillation column. + c : str + Index of component in the distillation column model. 'benzene' or 'toluene'. + + Returns + ------- + Pyomo.Constraint + Constraint that the vapor enthalpy is equal to the vapor enthalpy on the tray above, when the tray is not present. + """ return m.H_V[c, t] == m.H_V[c, t - 1] def _build_feed_tray_energy_balance(m): + """ + Energy balance for the feed tray. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo model of the distillation column.. + + Returns + ------- + None + None, but adds constraints, which are energy balances for the feed tray, to the Pyomo model of the distillation column + """ t = m.feed_tray @m.Constraint() def feed_tray_energy_balance(_): + """ + Energy balance for the feed tray. + + Parameters + ---------- + _ : Pyomo.ConcreteModel + A placeholder representing the Pyomo model instance. It specifies the context for applying the energy balance constraints to the feed tray in the distillation column. + + Returns + ------- + Pyomo.Constraint + Constraint that the sum of the heat of the feed and the heat of the liquid and vapor streams is equal to zero. + """ return ( - sum(m.feed[c] * ( - m.H_L_spec_feed[c] * (1 - m.feed_vap_frac) + - m.H_V_spec_feed[c] * m.feed_vap_frac) - for c in m.comps) + sum( + m.feed[c] + * ( + m.H_L_spec_feed[c] * (1 - m.feed_vap_frac) + + m.H_V_spec_feed[c] * m.feed_vap_frac + ) + for c in m.comps + ) + + sum( # Heat of liquid from tray above m.L[c, t + 1] * m.H_L[c, t + 1] # heat of liquid to tray below @@ -464,90 +1396,326 @@ def feed_tray_energy_balance(_): + m.V[c, t - 1] * m.H_V[c, t - 1] # heat of vapor to tray above - m.V[c, t] * m.H_V[c, t] - for c in m.comps)) * 1E-3 == 0 + for c in m.comps + ) + ) * 1e-3 == 0 @m.Constraint(m.comps) def feed_tray_liq_enthalpy_calc(_, c): + """ + Liquid enthalpy calculation for the feed tray. + + Parameters + ---------- + _ : Pyomo.ConcreteModel + A placeholder representing the Pyomo model instance. It specifies the context for applying the energy balance constraints to the feed tray in the distillation column. + c : str + Index of component in the distillation column model. 'benzene' or 'toluene'. + + Returns + ------- + Pyomo.Constraint + Constraint that the liquid enthalpy is equal to the liquid enthalpy expression. + """ return m.H_L[c, t] == m.liq_enthalpy_expr[t, c] @m.Constraint(m.comps) def feed_tray_vap_enthalpy_calc(_, c): + """ + Vapor enthalpy calculation for the feed tray. + + Parameters + ---------- + _ : Pyomo.ConcreteModel + A placeholder representing the Pyomo model instance. It specifies the context for applying the energy balance constraints to the feed tray in the distillation column. + c : str + Index of component in the distillation column model. 'benzene' or 'toluene'. + + Returns + ------- + Pyomo.Constraint + Constraint that the vapor enthalpy is equal to the vapor enthalpy expression. + """ return m.H_V[c, t] == m.vap_enthalpy_expr[t, c] @m.Expression(m.comps) def feed_liq_enthalpy_expr(_, c): + """ + Liquid enthalpy expression for the feed tray. + + Parameters + ---------- + _ : Pyomo.ConcreteModel + A placeholder representing the Pyomo model instance. It specifies the context for applying the energy balance constraints to the feed tray in the distillation column. + c : str + Index of component in the distillation column model. 'benzene' or 'toluene'. + + Returns + ------- + Pyomo.Expression + Liquid enthalpy expression for the feed tray. + """ k = m.liq_Cp_const[c] return ( - k['A'] * (m.T_feed - m.T_ref) + - k['B'] * (m.T_feed ** 2 - m.T_ref ** 2) / 2 + - k['C'] * (m.T_feed ** 3 - m.T_ref ** 3) / 3 + - k['D'] * (m.T_feed ** 4 - m.T_ref ** 4) / 4 + - k['E'] * (m.T_feed ** 5 - m.T_ref ** 5) / 5) * 1E-6 + k['A'] * (m.T_feed - m.T_ref) + + k['B'] * (m.T_feed**2 - m.T_ref**2) / 2 + + k['C'] * (m.T_feed**3 - m.T_ref**3) / 3 + + k['D'] * (m.T_feed**4 - m.T_ref**4) / 4 + + k['E'] * (m.T_feed**5 - m.T_ref**5) / 5 + ) * 1e-6 # Convert the result from [J/mol] to [MJ/mol] @m.Constraint(m.comps) def feed_liq_enthalpy_calc(_, c): + """_summary_ + + Parameters + ---------- + _ : Pyomo.ConcreteModel + A placeholder representing the Pyomo model instance. It specifies the context for applying the energy balance constraints to the feed tray in the distillation column. + c : str + Index of component in the distillation column model. 'benzene' or 'toluene'. + + Returns + ------- + Pyomo.Constraint + _description_ + """ return m.H_L_spec_feed[c] == m.feed_liq_enthalpy_expr[c] @m.Expression(m.comps) def feed_vap_enthalpy_expr(_, c): + """ + Vapor enthalpy expression for the feed tray. + + Parameters + ---------- + _ : Pyomo.ConcreteModel + A placeholder representing the Pyomo model instance. It specifies the context for applying the energy balance constraints to the feed tray in the distillation column. + c : str + Index of component in the distillation column model. 'benzene' or 'toluene'. + + Returns + ------- + Pyomo.Expression + Vapor enthalpy expression for the feed tray. + """ k = m.vap_Cp_const[c] return ( - m.dH_vap[c] + - k['A'] * (m.T_feed - m.T_ref) + - k['B'] * (m.T_feed ** 2 - m.T_ref ** 2) / 2 + - k['C'] * (m.T_feed ** 3 - m.T_ref ** 3) / 3 + - k['D'] * (m.T_feed ** 4 - m.T_ref ** 4) / 4 + - k['E'] * (m.T_feed ** 5 - m.T_ref ** 5) / 5) * 1E-3 + m.dH_vap[c] + + k['A'] * (m.T_feed - m.T_ref) + + k['B'] * (m.T_feed**2 - m.T_ref**2) / 2 + + k['C'] * (m.T_feed**3 - m.T_ref**3) / 3 + + k['D'] * (m.T_feed**4 - m.T_ref**4) / 4 + + k['E'] * (m.T_feed**5 - m.T_ref**5) / 5 + ) * 1e-3 # Convert the result from [J/mol] to [kJ/mol] @m.Constraint(m.comps) def feed_vap_enthalpy_calc(_, c): + """ + Vapor enthalpy calculation for the feed tray. + + Parameters + ---------- + _ : Pyomo.ConcreteModel + A placeholder representing the Pyomo model instance. It specifies the context for applying the energy balance constraints to the feed tray in the distillation column. + c : str + Index of component in the distillation column model. 'benzene' or 'toluene'. + + Returns + ------- + Pyomo.Constraint + Constraint that the vapor enthalpy is equal to the vapor enthalpy expression. + """ return m.H_V_spec_feed[c] == m.feed_vap_enthalpy_expr[c] def _build_condenser_energy_balance(m): + """ + Energy balance for the condenser. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo model of the distillation column. + + Returns + ------- + None + None, but adds constraints, which are energy balances for the condenser, to the Pyomo model of the distillation column + """ t = m.condens_tray @m.partial_cond.Constraint() def partial_condenser_energy_balance(_): - return -m.Qc + sum( - - m.D[c] * m.H_L[c, t] # heat of liquid distillate - - m.L[c, t] * m.H_L[c, t] # heat of liquid to tray below - + m.V[c, t - 1] * m.H_V[c, t - 1] # heat of vapor from tray below - - m.V[c, t] * m.H_V[c, t] # heat of vapor from partial condenser - for c in m.comps) * 1E-3 == 0 + """ + Energy balance for the partial condenser. + + Parameters + ---------- + _ : Pyomo.ConcreteModel + A placeholder representing the Pyomo model instance. It specifies the context for applying the energy balance constraints to the condenser in the distillation column. + + Returns + ------- + Pyomo.Constraint + Constraint that the sum of the heat of the liquid distillate, the heat of the liquid to the tray below, the heat of the vapor from the tray below, and the heat of the vapor from the partial condenser is equal to zero. + """ + return ( + -m.Qc + + sum( + -m.D[c] * m.H_L[c, t] # heat of liquid distillate + - m.L[c, t] * m.H_L[c, t] # heat of liquid to tray below + + m.V[c, t - 1] * m.H_V[c, t - 1] # heat of vapor from tray below + - m.V[c, t] * m.H_V[c, t] # heat of vapor from partial condenser + for c in m.comps + ) + * 1e-3 + == 0 + ) # Convert the result from [kJ/mol] to [MJ/mol] @m.total_cond.Constraint() def total_condenser_energy_balance(_): - return -m.Qc + sum( - - m.D[c] * m.H_L[c, t] # heat of liquid distillate - - m.L[c, t] * m.H_L[c, t] # heat of liquid to tray below - + m.V[c, t - 1] * m.H_V[c, t - 1] # heat of vapor from tray below - for c in m.comps) * 1E-3 == 0 + """ + Energy balance for the total condenser. + + Parameters + ---------- + _ : Pyomo.ConcreteModel + A placeholder representing the Pyomo model instance. It specifies the context for applying the energy balance constraints to the condenser in the distillation column. + + Returns + ------- + Pyomo.Constraint + Constraint that the sum of the heat of the liquid distillate, the heat of the liquid to the tray below, and the heat of the vapor from the tray below is equal to zero. + """ + return ( + -m.Qc + + sum( + -m.D[c] * m.H_L[c, t] # heat of liquid distillate + - m.L[c, t] * m.H_L[c, t] # heat of liquid to tray below + + m.V[c, t - 1] * m.H_V[c, t - 1] # heat of vapor from tray below + for c in m.comps + ) + * 1e-3 + == 0 + ) @m.Constraint(m.comps) def condenser_liq_enthalpy_calc(_, c): + """ + Liquid enthalpy calculation for each component in the condenser. + + Parameters + ---------- + _ : Pyomo.ConcreteModel + A placeholder representing the Pyomo model instance. It specifies the context for applying the energy balance constraints to the condenser in the distillation column. + c : str + Index of component in the distillation column model. 'benzene' or 'toluene'. + + Returns + ------- + Pyomo.Constraint + Constraint that the liquid enthalpy for each component is equal to the liquid enthalpy expression. + """ return m.H_L[c, t] == m.liq_enthalpy_expr[t, c] @m.partial_cond.Constraint(m.comps) def vap_enthalpy_calc(_, c): + """ + Vapor enthalpy calculation for each component in the condenser. + + Parameters + ---------- + _ : Pyomo.ConcreteModel + A placeholder representing the Pyomo model instance. It specifies the context for applying the energy balance constraints to the condenser in the distillation column. + c : str + Index of component in the distillation column model. 'benzene' or 'toluene'. + + Returns + ------- + Pyomo.Constraint + Constraint that the vapor enthalpy for each component is equal to the vapor enthalpy expression. + """ return m.H_V[c, t] == m.vap_enthalpy_expr[t, c] def _build_reboiler_energy_balance(m): + """ + Energy balance for the reboiler. + + Parameters + ---------- + m : Pyomo.ConcreteModel + Pyomo model of the distillation column. + + Returns + ------- + None + None, but adds constraints, which are energy balances for the reboiler, to the Pyomo model of the distillation column + """ t = m.reboil_tray @m.Constraint() def reboiler_energy_balance(_): - return m.Qb + sum( - m.L[c, t + 1] * m.H_L[c, t + 1] # Heat of liquid from tray above - - m.B[c] * m.H_L[c, t] # heat of liquid bottoms if reboiler - - m.V[c, t] * m.H_V[c, t] # heat of vapor to tray above - for c in m.comps) * 1E-3 == 0 + """ + Energy balance for the reboiler. + + Parameters + ---------- + _ : Pyomo.ConcreteModel + A placeholder representing the Pyomo model instance. It specifies the context for applying the energy balance constraints to the reboiler in the distillation column. + + Returns + ------- + Pyomo.Constraint + Constraint that the sum of the heat of the liquid bottoms, the heat of the liquid from the tray above, the heat of the vapor to the tray above, and the heat of the vapor from the reboiler is equal to zero. + """ + return ( + m.Qb + + sum( + m.L[c, t + 1] * m.H_L[c, t + 1] # Heat of liquid from tray above + - m.B[c] * m.H_L[c, t] # heat of liquid bottoms if reboiler + - m.V[c, t] * m.H_V[c, t] # heat of vapor to tray above + for c in m.comps + ) + * 1e-3 + == 0 + ) @m.Constraint(m.comps) def reboiler_liq_enthalpy_calc(_, c): + """ + Liquid enthalpy calculation for each component in the reboiler. + + Parameters + ---------- + _ : Pyomo.ConcreteModel + A placeholder representing the Pyomo model instance. It specifies the context for applying the energy balance constraints to the reboiler in the distillation column. + c : str + Index of component in the distillation column model. 'benzene' or 'toluene'. + + Returns + ------- + Pyomo.Constraint + Constraint that the liquid enthalpy for each component is equal to the liquid enthalpy expression. + """ return m.H_L[c, t] == m.liq_enthalpy_expr[t, c] @m.Constraint(m.comps) def reboiler_vap_enthalpy_calc(_, c): + """ + Vapor enthalpy calculation for each component in the reboiler. + + Parameters + ---------- + _ : Pyomo.ConcreteModel + A placeholder representing the Pyomo model instance. It specifies the context for applying the energy balance constraints to the reboiler in the distillation column. + c : str + Index of component in the distillation column model. 'benzene' or 'toluene'. + + Returns + ------- + Pyomo.Constraint + Constraint that the vapor enthalpy for each component is equal to the vapor enthalpy expression. + """ return m.H_V[c, t] == m.vap_enthalpy_expr[t, c] diff --git a/gdplib/gdp_col/fenske.py b/gdplib/gdp_col/fenske.py index 08f3864..d35aa8a 100644 --- a/gdplib/gdp_col/fenske.py +++ b/gdplib/gdp_col/fenske.py @@ -1,85 +1,233 @@ from __future__ import division -from pyomo.environ import (ConcreteModel, NonNegativeReals, Set, SolverFactory, - Var, log, sqrt) +from pyomo.environ import ( + ConcreteModel, + NonNegativeReals, + Set, + SolverFactory, + Var, + log, + sqrt, +) def calculate_Fenske(xD, xB): + """ + Calculate the minimum number of plates required for a given separation using the Fenske equation. + + Parameters + ---------- + xD : float + Distillate(benzene) purity + xB : float + Bottoms(toluene) purity + + Returns + ------- + None + None, but prints the Fenske equation calculating the minimum number of plates required for a given separation. + """ m = ConcreteModel() min_T, max_T = 300, 400 m.comps = Set(initialize=['benzene', 'toluene']) m.trays = Set(initialize=['condenser', 'reboiler']) m.Kc = Var( - m.comps, m.trays, doc='Phase equilibrium constant', - domain=NonNegativeReals, initialize=1, bounds=(0, 1000)) - m.T = Var(m.trays, doc='Temperature [K]', - domain=NonNegativeReals, - bounds=(min_T, max_T)) + m.comps, + m.trays, + doc='Phase equilibrium constant', + domain=NonNegativeReals, + initialize=1, + bounds=(0, 1000), + ) + m.T = Var( + m.trays, doc='Temperature [K]', domain=NonNegativeReals, bounds=(min_T, max_T) + ) m.T['condenser'].fix(82 + 273.15) m.T['reboiler'].fix(108 + 273.15) - m.P = Var(doc='Pressure [bar]', - bounds=(0, 5)) + m.P = Var(doc='Pressure [bar]', bounds=(0, 5)) m.P.fix(1.01) m.T_ref = 298.15 m.gamma = Var( - m.comps, m.trays, - doc='liquid activity coefficent of component on tray', - domain=NonNegativeReals, bounds=(0, 10), initialize=1) + m.comps, + m.trays, + doc='liquid activity coefficient of component on tray', + domain=NonNegativeReals, + bounds=(0, 10), + initialize=1, + ) m.Pvap = Var( - m.comps, m.trays, + m.comps, + m.trays, doc='pure component vapor pressure of component on tray in bar', - domain=NonNegativeReals, bounds=(1E-3, 5), initialize=0.4) + domain=NonNegativeReals, + bounds=(1e-3, 5), + initialize=0.4, + ) m.Pvap_X = Var( - m.comps, m.trays, + m.comps, + m.trays, doc='Related to fraction of critical temperature (1 - T/Tc)', - bounds=(0.25, 0.5), initialize=0.4) + bounds=(0.25, 0.5), + initialize=0.4, + ) m.pvap_const = { - 'benzene': {'A': -6.98273, 'B': 1.33213, 'C': -2.62863, - 'D': -3.33399, 'Tc': 562.2, 'Pc': 48.9}, - 'toluene': {'A': -7.28607, 'B': 1.38091, 'C': -2.83433, - 'D': -2.79168, 'Tc': 591.8, 'Pc': 41.0}} + 'benzene': { + 'A': -6.98273, + 'B': 1.33213, + 'C': -2.62863, + 'D': -3.33399, + 'Tc': 562.2, + 'Pc': 48.9, + }, + 'toluene': { + 'A': -7.28607, + 'B': 1.38091, + 'C': -2.83433, + 'D': -2.79168, + 'Tc': 591.8, + 'Pc': 41.0, + }, + } @m.Constraint(m.comps, m.trays) def phase_equil_const(_, c, t): - return m.Kc[c, t] * m.P == ( - m.gamma[c, t] * m.Pvap[c, t]) + """ + Phase equilibrium constraint for each component in a tray. + + Parameters + ---------- + _ : Pyomo.ConcreteModel + A placeholder representing the Pyomo model instance. It specifies the context for applying the phase equilibrium constraints to the trays in the distillation column. + c : str + Index of component in the distillation column model. 'benzene' or 'toluene'. + t : int + Index of tray in the distillation column model. + + Returns + ------- + Pyomo.Constraint + The phase equilibrium constant for each component in a tray multiplied with the pressure is equal to the product of the activity coefficient and the pure component vapor pressure. + """ + return m.Kc[c, t] * m.P == (m.gamma[c, t] * m.Pvap[c, t]) @m.Constraint(m.comps, m.trays) def Pvap_relation(_, c, t): + """ + Antoine's equation for the vapor pressure of each component in a tray. + + Parameters + ---------- + _ : Pyomo.ConcreteModel + A placeholder representing the Pyomo model instance. It specifies the context for applying the phase equilibrium constraints to the trays in the distillation column. + c : str + Index of component in the distillation column model. 'benzene' or 'toluene'. + t : int + Index of tray in the distillation column model. + + Returns + ------- + Pyomo.Constraint + Antoine's equation for the vapor pressure of each component in a tray is calculated as the logarithm of the vapor pressure minus the logarithm of the critical pressure times one minus the fraction of critical temperature. The equation is equal to the sum of the Antoine coefficients times the fraction of critical temperature raised to different powers. + """ k = m.pvap_const[c] x = m.Pvap_X[c, t] return (log(m.Pvap[c, t]) - log(k['Pc'])) * (1 - x) == ( - k['A'] * x + - k['B'] * x ** 1.5 + - k['C'] * x ** 3 + - k['D'] * x ** 6) + k['A'] * x + k['B'] * x**1.5 + k['C'] * x**3 + k['D'] * x**6 + ) @m.Constraint(m.comps, m.trays) def Pvap_X_defn(_, c, t): + """ + Defines the relationship between the one minus the reduced temperature variable (Pvap_X) for each component in a tray, and the actual temperature of the tray, normalized by the critical temperature of the component (Tc). + + Parameters + ---------- + _ : Pyomo.ConcreteModel + A placeholder representing the Pyomo model instance. It specifies the context for applying the phase equilibrium constraints to the trays in the distillation column. + c : str + Index of component in the distillation column model. 'benzene' or 'toluene'. + t : int + Index of tray in the distillation column model. + + Returns + ------- + Pyomo.Constraint + The relationship between the one minus the reduced temperature variable (Pvap_X) for each component in a tray, and the actual temperature of the tray, normalized by the critical temperature of the component (Tc). + """ k = m.pvap_const[c] return m.Pvap_X[c, t] == 1 - m.T[t] / k['Tc'] @m.Constraint(m.comps, m.trays) def gamma_calc(_, c, t): + """ + Activity coefficient calculation. + + Parameters + ---------- + _ : Pyomo.ConcreteModel + A placeholder representing the Pyomo model instance. It specifies the context for applying the phase equilibrium constraints to the trays in the distillation column. + c : str + Index of component in the distillation column model. 'benzene' or 'toluene'. + t : int + Index of tray in the distillation column model. + + Returns + ------- + Pyomo.Constraint + Set the activity coefficient of the component on the tray as 1. + """ return m.gamma[c, t] == 1 m.relative_volatility = Var(m.trays, domain=NonNegativeReals) @m.Constraint(m.trays) def relative_volatility_calc(_, t): - return m.Kc['benzene', t] == ( - m.Kc['toluene', t] * m.relative_volatility[t]) + """ + Relative volatility calculation. + + Parameters + ---------- + _ : Pyomo.ConcreteModel + A placeholder representing the Pyomo model instance. It specifies the context for applying the phase equilibrium constraints to the trays in the distillation column. + t : int + Index of tray in the distillation column model. + + Returns + ------- + Pyomo.Constraint + The relative volatility of benzene to toluene is the ratio of the phase equilibrium constants of benzene to toluene on the tray. + """ + return m.Kc['benzene', t] == (m.Kc['toluene', t] * m.relative_volatility[t]) @m.Expression() def fenske(_): + """ + Fenske equation for minimum number of plates. + + Parameters + ---------- + _ : Pyomo.ConcreteModel + A placeholder representing the Pyomo model instance. It specifies the context for applying the phase equilibrium constraints to the trays in the distillation column. + + Returns + ------- + Pyomo.Expression + The Fenske equation calculating the minimum number of plates required for a given separation. + """ return log((xD / (1 - xD)) * (xB / (1 - xB))) / ( - log(sqrt(m.relative_volatility['condenser'] * - m.relative_volatility['reboiler']))) + log( + sqrt( + m.relative_volatility['condenser'] + * m.relative_volatility['reboiler'] + ) + ) + ) SolverFactory('ipopt').solve(m, tee=True) from pyomo.util.infeasible import log_infeasible_constraints - log_infeasible_constraints(m, tol=1E-3) + + log_infeasible_constraints(m, tol=1e-3) m.fenske.display() diff --git a/gdplib/gdp_col/initialize.py b/gdplib/gdp_col/initialize.py index b48ddcc..a5b0140 100644 --- a/gdplib/gdp_col/initialize.py +++ b/gdplib/gdp_col/initialize.py @@ -1,4 +1,5 @@ """Initialization routine for distillation column""" + from __future__ import division import pandas @@ -10,27 +11,45 @@ def initialize(m, excel_file=None): - m.reflux_frac.set_value(value( - m.reflux_ratio / (1 + m.reflux_ratio))) - m.boilup_frac.set_value(value( - m.reboil_ratio / (1 + m.reboil_ratio))) - + """ + Initializes the distillation column model using data from an Excel file or default settings. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo model of the distillation column for separation of benzene and toluene. + excel_file : str, optional + The file path to an Excel file containing the initialization data. Defaults to 'init.xlsx' if not provided. + """ + m.reflux_frac.set_value(value(m.reflux_ratio / (1 + m.reflux_ratio))) + m.boilup_frac.set_value(value(m.reboil_ratio / (1 + m.reboil_ratio))) + if excel_file is None: excel_file = 'init.xlsx' print(gdp_col_dir) - _excel_sheets = pandas.read_excel("%s/init.xlsx" % gdp_col_dir, - sheet_name=None) + _excel_sheets = pandas.read_excel("%s/init.xlsx" % gdp_col_dir, sheet_name=None) def set_value_if_not_fixed(var, val): - """Set variable to the value if it is not fixed.""" + """ + Set variable to the value if it is not fixed. + + Parameters + ---------- + var : Pyomo.Var + The Pyomo variable to potentially modify. + val : float + The value to assign to the variable if it is not fixed. + """ if not var.fixed: var.set_value(val) active_trays = [ - t for t in m.trays - if t not in m.conditional_trays or - fabs(value(m.tray[t].binary_indicator_var - 1)) <= 1E-3] + t + for t in m.trays + if t not in m.conditional_trays + or fabs(value(m.tray[t].binary_indicator_var - 1)) <= 1e-3 + ] num_active_trays = len(active_trays) feed_tray = m.feed_tray @@ -40,11 +59,9 @@ def set_value_if_not_fixed(var, val): tray_indexed_data.set_index('tray', inplace=True) comp_and_tray_indexed_data = _excel_sheets['comps_and_trays'] - comp_and_tray_indexed_data.sort_values(by=['comp', 'tray'], - inplace=True) + comp_and_tray_indexed_data.sort_values(by=['comp', 'tray'], inplace=True) comp_and_tray_indexed_data.set_index(['comp', 'tray'], inplace=True) - comp_slices = {c: comp_and_tray_indexed_data.loc[c, :] - for c in m.comps} + comp_slices = {c: comp_and_tray_indexed_data.loc[c, :] for c in m.comps} num_data_trays = tray_indexed_data.index.size if num_active_trays < num_data_trays: @@ -52,43 +69,56 @@ def set_value_if_not_fixed(var, val): # do averaging new_indices = [1] + [ 1 + (num_data_trays - 1) / (num_active_trays - 1) * i - for i in range(1, num_active_trays)] + for i in range(1, num_active_trays) + ] for tray in range(2, num_active_trays): indx = new_indices[tray - 1] lower = floor(indx) frac_above = indx - lower # Take linear combination of values tray_indexed_data.loc[tray] = ( - tray_indexed_data.loc[lower] * (1 - frac_above) + - tray_indexed_data.loc[lower + 1] * frac_above) + tray_indexed_data.loc[lower] * (1 - frac_above) + + tray_indexed_data.loc[lower + 1] * frac_above + ) for c in m.comps: comp_slices[c].loc[tray] = ( - comp_slices[c].loc[lower] * (1 - frac_above) + - comp_slices[c].loc[lower + 1] * frac_above) - tray_indexed_data.loc[num_active_trays] = \ - tray_indexed_data.loc[num_data_trays] + comp_slices[c].loc[lower] * (1 - frac_above) + + comp_slices[c].loc[lower + 1] * frac_above + ) + tray_indexed_data.loc[num_active_trays] = tray_indexed_data.loc[num_data_trays] tray_indexed_data = tray_indexed_data.head(num_active_trays) for c in m.comps: - comp_slices[c].loc[num_active_trays] = \ - comp_slices[c].loc[num_data_trays] + comp_slices[c].loc[num_active_trays] = comp_slices[c].loc[num_data_trays] comp_slices[c] = comp_slices[c].head(num_active_trays) else: # Stretch the data out and do interpolation tray_indexed_data.index = pandas.Index( - [1] + [int(round(num_active_trays / num_data_trays * i)) - for i in range(2, num_data_trays + 1)], name='tray') + [1] + + [ + int(round(num_active_trays / num_data_trays * i)) + for i in range(2, num_data_trays + 1) + ], + name='tray', + ) tray_indexed_data = tray_indexed_data.reindex( - [i for i in range(1, num_active_trays + 1)]).interpolate() + [i for i in range(1, num_active_trays + 1)] + ).interpolate() for c in m.comps: comp_slices[c].index = pandas.Index( - [1] + [int(round(num_active_trays / num_data_trays * i)) - for i in range(2, num_data_trays + 1)], name='tray') + [1] + + [ + int(round(num_active_trays / num_data_trays * i)) + for i in range(2, num_data_trays + 1) + ], + name='tray', + ) # special handling necessary for V near top of column and L # near column bottom. Do not want to interpolate with one end # being potentially 0. (ie. V from total condenser). Instead, # use back fill and forward fill. comp_slices[c] = comp_slices[c].reindex( - [i for i in range(1, num_active_trays + 1)]) + [i for i in range(1, num_active_trays + 1)] + ) tray_below_condenser = sorted(active_trays, reverse=True)[1] if pandas.isna(comp_slices[c]['V'][tray_below_condenser]): # V of the tray below the condenser is N/A. Find a valid @@ -96,8 +126,8 @@ def set_value_if_not_fixed(var, val): val = next( comp_slices[c]['V'][t] for t in reversed(list(m.trays)) - if pandas.notna(comp_slices[c]['V'][t]) - and not t == m.condens_tray) + if pandas.notna(comp_slices[c]['V'][t]) and not t == m.condens_tray + ) comp_slices[c]['V'][tray_below_condenser] = val if pandas.isna(comp_slices[c]['L'][m.reboil_tray + 1]): # L of the tray above the reboiler is N/A. Find a valid @@ -105,22 +135,19 @@ def set_value_if_not_fixed(var, val): val = next( comp_slices[c]['L'][t] for t in m.trays - if pandas.notna(comp_slices[c]['L'][t]) - and not t == m.reboil_tray) + if pandas.notna(comp_slices[c]['L'][t]) and not t == m.reboil_tray + ) comp_slices[c]['L'][m.reboil_tray + 1] = val comp_slices[c] = comp_slices[c].interpolate() - tray_indexed_data.index = pandas.Index(sorted(active_trays), - name='tray') - tray_indexed_data = tray_indexed_data.reindex(sorted(m.trays), - method='bfill') + tray_indexed_data.index = pandas.Index(sorted(active_trays), name='tray') + tray_indexed_data = tray_indexed_data.reindex(sorted(m.trays), method='bfill') for t in m.trays: set_value_if_not_fixed(m.T[t], tray_indexed_data['T [K]'][t]) for c in m.comps: - comp_slices[c].index = pandas.Index(sorted(active_trays), - name='tray') + comp_slices[c].index = pandas.Index(sorted(active_trays), name='tray') comp_slices[c] = comp_slices[c].reindex(sorted(m.trays)) comp_slices[c][['L', 'x']] = comp_slices[c][['L', 'x']].bfill() comp_slices[c][['V', 'y']] = comp_slices[c][['V', 'y']].ffill() @@ -128,14 +155,10 @@ def set_value_if_not_fixed(var, val): comp_and_tray_indexed_data = pandas.concat(comp_slices) for c, t in m.comps * m.trays: - set_value_if_not_fixed(m.L[c, t], - comp_and_tray_indexed_data['L'][c, t]) - set_value_if_not_fixed(m.V[c, t], - comp_and_tray_indexed_data['V'][c, t]) - set_value_if_not_fixed(m.x[c, t], - comp_and_tray_indexed_data['x'][c, t]) - set_value_if_not_fixed(m.y[c, t], - comp_and_tray_indexed_data['y'][c, t]) + set_value_if_not_fixed(m.L[c, t], comp_and_tray_indexed_data['L'][c, t]) + set_value_if_not_fixed(m.V[c, t], comp_and_tray_indexed_data['V'][c, t]) + set_value_if_not_fixed(m.x[c, t], comp_and_tray_indexed_data['x'][c, t]) + set_value_if_not_fixed(m.y[c, t], comp_and_tray_indexed_data['y'][c, t]) for c in m.comps: m.H_L_spec_feed[c].set_value(value(m.feed_liq_enthalpy_expr[c])) @@ -148,14 +171,17 @@ def set_value_if_not_fixed(var, val): x.set_value(value(1 - m.T[t] / k['Tc'])) - m.Pvap[c, t].set_value(value(exp(( - k['A'] * x + - k['B'] * x ** 1.5 + - k['C'] * x ** 3 + - k['D'] * x ** 6) / (1 - x)) * k['Pc'])) + m.Pvap[c, t].set_value( + value( + exp( + (k['A'] * x + k['B'] * x**1.5 + k['C'] * x**3 + k['D'] * x**6) + / (1 - x) + ) + * k['Pc'] + ) + ) - m.Kc[c, t].set_value(value( - m.gamma[c, t] * m.Pvap[c, t] / m.P)) + m.Kc[c, t].set_value(value(m.gamma[c, t] * m.Pvap[c, t] / m.P)) m.H_L[c, t].set_value(value(m.liq_enthalpy_expr[t, c])) m.H_V[c, t].set_value(value(m.vap_enthalpy_expr[t, c])) @@ -166,18 +192,18 @@ def set_value_if_not_fixed(var, val): m.B['toluene'].set_value(44.56072) m.L['benzene', m.reboil_tray].set_value(7.67928) m.L['toluene', m.reboil_tray].set_value(44.56072) - m.V['benzene', m.reboil_tray].set_value(value( - m.L['benzene', m.reboil_tray + 1] - - m.L['benzene', m.reboil_tray])) - m.V['toluene', m.reboil_tray].set_value(value( - m.L['toluene', m.reboil_tray + 1] - - m.L['toluene', m.reboil_tray])) - m.L['benzene', m.condens_tray].set_value(value( - m.V['benzene', m.condens_tray - 1] - - m.D['benzene'])) - m.L['toluene', m.condens_tray].set_value(value( - m.V['toluene', m.condens_tray - 1] - - m.D['toluene'])) + m.V['benzene', m.reboil_tray].set_value( + value(m.L['benzene', m.reboil_tray + 1] - m.L['benzene', m.reboil_tray]) + ) + m.V['toluene', m.reboil_tray].set_value( + value(m.L['toluene', m.reboil_tray + 1] - m.L['toluene', m.reboil_tray]) + ) + m.L['benzene', m.condens_tray].set_value( + value(m.V['benzene', m.condens_tray - 1] - m.D['benzene']) + ) + m.L['toluene', m.condens_tray].set_value( + value(m.V['toluene', m.condens_tray - 1] - m.D['toluene']) + ) for t in m.trays: m.liq[t].set_value(value(sum(m.L[c, t] for c in m.comps))) @@ -185,13 +211,17 @@ def set_value_if_not_fixed(var, val): m.bot.set_value(52.24) m.dis.set_value(47.7599) for c in m.comps: - m.x[c, m.reboil_tray].set_value(value( - m.L[c, m.reboil_tray] / m.liq[m.reboil_tray])) - m.y[c, m.reboil_tray].set_value(value( - m.V[c, m.reboil_tray] / m.vap[m.reboil_tray])) - m.x[c, m.condens_tray].set_value(value( - m.L[c, m.condens_tray] / m.liq[m.condens_tray])) - m.y[c, m.condens_tray].set_value(value( - m.x[c, m.condens_tray] * m.Kc[c, m.condens_tray])) + m.x[c, m.reboil_tray].set_value( + value(m.L[c, m.reboil_tray] / m.liq[m.reboil_tray]) + ) + m.y[c, m.reboil_tray].set_value( + value(m.V[c, m.reboil_tray] / m.vap[m.reboil_tray]) + ) + m.x[c, m.condens_tray].set_value( + value(m.L[c, m.condens_tray] / m.liq[m.condens_tray]) + ) + m.y[c, m.condens_tray].set_value( + value(m.x[c, m.condens_tray] * m.Kc[c, m.condens_tray]) + ) m.Qb.set_value(2.307873115) m.Qc.set_value(3.62641882) diff --git a/gdplib/gdp_col/main.py b/gdplib/gdp_col/main.py index 08de52d..e9dce0f 100644 --- a/gdplib/gdp_col/main.py +++ b/gdplib/gdp_col/main.py @@ -12,6 +12,14 @@ def main(): + """ + Solve the distillation column model. + + Returns + ------- + m : Pyomo.ConcreteModel + The distillation column model solution obtained by using GDPopt, Logic-based Outer Approximation. + """ m = build_column(min_trays=8, max_trays=17, xD=0.95, xB=0.95) # Fix feed conditions m.feed['benzene'].fix(50) @@ -35,60 +43,78 @@ def main(): m.BigM[None] = 100 SolverFactory('gdpopt').solve( - m, tee=True, strategy='LOA', init_strategy='fix_disjuncts', - mip_solver='glpk') - log_infeasible_constraints(m, tol=1E-3) + m, tee=True, strategy='LOA', init_strategy='fix_disjuncts', mip_solver='glpk' + ) + log_infeasible_constraints(m, tol=1e-3) display_column(m) return m def display_column(m): + """ + Display the distillation column model solution. + + Parameters + ---------- + m : Pyomo.ConcreteModel + The distillation column model solution obtained by using GDPopt, Logic-based Outer Approximation. + """ print('Objective: %s' % value(m.obj)) - print('Qc: {: >3.0f}kW DB: {: >3.0f} DT: {: >3.0f} dis: {: >3.0f}' - .format(value(m.Qc * 1E3), - value(m.D['benzene']), - value(m.D['toluene']), - value(m.dis))) + print( + 'Qc: {: >3.0f}kW DB: {: >3.0f} DT: {: >3.0f} dis: {: >3.0f}'.format( + value(m.Qc * 1e3), + value(m.D['benzene']), + value(m.D['toluene']), + value(m.dis), + ) + ) for t in reversed(list(m.trays)): - print('T{: >2.0f}-{:1.0g} T: {: >3.0f} ' - 'F: {: >4.0f} ' - 'L: {: >4.0f} V: {: >4.0f} ' - 'xB: {: >3.0f} xT: {: >3.0f} yB: {: >3.0f} yT: {: >3.0f}' - .format(t, - fabs(value(m.tray[t].indicator_var)) - if t in m.conditional_trays else 1, - value(m.T[t]) - 273.15, - value(sum(m.feed[c] for c in m.comps)) - if t == m.feed_tray else 0, - value(m.liq[t]), - value(m.vap[t]), - value(m.x['benzene', t]) * 100, - value(m.x['toluene', t]) * 100, - value(m.y['benzene', t]) * 100, - value(m.y['toluene', t]) * 100 - )) - print('Qb: {: >3.0f}kW BB: {: > 3.0f} BT: {: >3.0f} bot: {: >3.0f}' - .format(value(m.Qb * 1E3), - value(m.B['benzene']), - value(m.B['toluene']), - value(m.bot))) + print( + 'T{: >2.0f}-{:1.0g} T: {: >3.0f} ' + 'F: {: >4.0f} ' + 'L: {: >4.0f} V: {: >4.0f} ' + 'xB: {: >3.0f} xT: {: >3.0f} yB: {: >3.0f} yT: {: >3.0f}'.format( + t, + fabs(value(m.tray[t].indicator_var)) if t in m.conditional_trays else 1, + value(m.T[t]) - 273.15, + value(sum(m.feed[c] for c in m.comps)) if t == m.feed_tray else 0, + value(m.liq[t]), + value(m.vap[t]), + value(m.x['benzene', t]) * 100, + value(m.x['toluene', t]) * 100, + value(m.y['benzene', t]) * 100, + value(m.y['toluene', t]) * 100, + ) + ) + print( + 'Qb: {: >3.0f}kW BB: {: > 3.0f} BT: {: >3.0f} bot: {: >3.0f}'.format( + value(m.Qb * 1e3), + value(m.B['benzene']), + value(m.B['toluene']), + value(m.bot), + ) + ) for t in reversed(list(m.trays)): - print('T{: >2.0f}-{:1.0g} ' - 'FB: {: >3.0f} FT: {: >3.0f} ' - 'LB: {: >4.0f} LT: {: >4.0f} VB: {: >4.0f} VT: {: >4.0f}' - .format(t, - fabs(value(m.tray[t].indicator_var)) - if t in m.conditional_trays else 1, - value(m.feed['benzene']) if t == m.feed_tray else 0, - value(m.feed['toluene']) if t == m.feed_tray else 0, - value(m.L['benzene', t]), - value(m.L['toluene', t]), - value(m.V['benzene', t]), - value(m.V['toluene', t]) - )) - print('RF: {: >3.2f} RB: {: >3.2f}' - .format(value(m.reflux_frac / (1 - m.reflux_frac)), - value(m.boilup_frac / (1 - m.boilup_frac)))) + print( + 'T{: >2.0f}-{:1.0g} ' + 'FB: {: >3.0f} FT: {: >3.0f} ' + 'LB: {: >4.0f} LT: {: >4.0f} VB: {: >4.0f} VT: {: >4.0f}'.format( + t, + fabs(value(m.tray[t].indicator_var)) if t in m.conditional_trays else 1, + value(m.feed['benzene']) if t == m.feed_tray else 0, + value(m.feed['toluene']) if t == m.feed_tray else 0, + value(m.L['benzene', t]), + value(m.L['toluene', t]), + value(m.V['benzene', t]), + value(m.V['toluene', t]), + ) + ) + print( + 'RF: {: >3.2f} RB: {: >3.2f}'.format( + value(m.reflux_frac / (1 - m.reflux_frac)), + value(m.boilup_frac / (1 - m.boilup_frac)), + ) + ) if __name__ == "__main__": diff --git a/gdplib/hda/HDA_GDP_gdpopt.py b/gdplib/hda/HDA_GDP_gdpopt.py index ef9565b..cf817b1 100644 --- a/gdplib/hda/HDA_GDP_gdpopt.py +++ b/gdplib/hda/HDA_GDP_gdpopt.py @@ -18,12 +18,11 @@ def HDA_model(): # ## scalars - m.alpha = Param(initialize=0.3665, doc="compressor coefficient") - m.compeff = Param(initialize=0.750, doc="compressor effiency") + m.compeff = Param(initialize=0.750, doc="compressor efficiency") m.gam = Param(initialize=1.300, doc="ratio of cp to cv") m.abseff = Param(initialize=0.333, doc="absorber tray efficiency") - m.disteff = Param(initialize=0.5000, doc="column tray effiency") + m.disteff = Param(initialize=0.5000, doc="column tray efficiency") m.uflow = Param(initialize=50, doc="upper bound - flow logicals") m.upress = Param(initialize=4.0, doc="upper bound - pressure logicals") m.utemp = Param(initialize=7.0, doc="upper bound - temperature logicals") @@ -35,7 +34,6 @@ def HDA_model(): # ## sets - def strset(i): s = [] i = 1 @@ -47,9 +45,11 @@ def strset(i): s.append(i) i += i return s + m.str = Set(initialize=strset, doc="process streams") - m.compon = Set(initialize=['h2', 'ch4', 'ben', - 'tol', 'dip'], doc="chemical components") + m.compon = Set( + initialize=['h2', 'ch4', 'ben', 'tol', 'dip'], doc="chemical components" + ) m.abs = RangeSet(1) m.comp = RangeSet(4) m.dist = RangeSet(3) @@ -67,22 +67,28 @@ def strset(i): m.spl = RangeSet(3) m.valve = RangeSet(6) m.str2 = Set(initialize=strset, doc="process streams") - m.compon2 = Set(initialize=['h2', 'ch4', 'ben', - 'tol', 'dip'], doc="chemical components") + m.compon2 = Set( + initialize=['h2', 'ch4', 'ben', 'tol', 'dip'], doc="chemical components" + ) # parameters Heatvap = {} Heatvap['tol'] = 30890.00 - m.heatvap = Param(m.compon, initialize=Heatvap, default=0, - doc='heat of vaporization (kj per kg-mol)') + m.heatvap = Param( + m.compon, + initialize=Heatvap, + default=0, + doc='heat of vaporization (kj per kg-mol)', + ) Cppure = {} Cppure['h2'] = 30 Cppure['ch4'] = 40 Cppure['ben'] = 225 Cppure['tol'] = 225 Cppure['dip'] = 450 - m.cppure = Param(m.compon, initialize=Cppure, default=0, - doc='pure component heat capacities') + m.cppure = Param( + m.compon, initialize=Cppure, default=0, doc='pure component heat capacities' + ) Gcomp = {} Gcomp[7, 'h2'] = 0.95 Gcomp[7, 'ch4'] = 0.05 @@ -183,13 +189,16 @@ def strset(i): Gcomp[71, 'tol'] = 0.10 Gcomp[72, 'h2'] = 0.50 Gcomp[72, 'ch4'] = 0.50 - m.gcomp = Param(m.str, m.compon, initialize=Gcomp, - default=0, doc='guess composition values') + m.gcomp = Param( + m.str, m.compon, initialize=Gcomp, default=0, doc='guess composition values' + ) def cppara(compon, stream): return sum(m.cppure[compon] * m.gcomp[stream, compon] for compon in m.compon) - m.cp = Param(m.str, initialize=cppara, default=0, - doc='heat capacities ( kj per kgmole-k)') + + m.cp = Param( + m.str, initialize=cppara, default=0, doc='heat capacities ( kj per kgmole-k)' + ) Anta = {} Anta['h2'] = 13.6333 @@ -197,24 +206,21 @@ def cppara(compon, stream): Anta['ben'] = 15.9008 Anta['tol'] = 16.0137 Anta['dip'] = 16.6832 - m.anta = Param(m.compon, initialize=Anta, - default=0, doc='antoine coefficient') + m.anta = Param(m.compon, initialize=Anta, default=0, doc='antoine coefficient') Antb = {} Antb['h2'] = 164.9 Antb['ch4'] = 897.84 Antb['ben'] = 2788.51 Antb['tol'] = 3096.52 Antb['dip'] = 4602.23 - m.antb = Param(m.compon, initialize=Antb, - default=0, doc='antoine coefficient') + m.antb = Param(m.compon, initialize=Antb, default=0, doc='antoine coefficient') Antc = {} Antc['h2'] = 3.19 Antc['ch4'] = -7.16 Antc['ben'] = -52.36 Antc['tol'] = -53.67 Antc['dip'] = -70.42 - m.antc = Param(m.compon, initialize=Antc, - default=0, doc='antoine coefficient') + m.antc = Param(m.compon, initialize=Antc, default=0, doc='antoine coefficient') Perm = {} for i in m.compon: Perm[i] = 0 @@ -222,282 +228,540 @@ def cppara(compon, stream): Perm['ch4'] = 2.3e-06 def Permset(m, compon): - return Perm[compon] * (1. / 22400.) * 1.0e4 * 750.062 * 60. / 1000. - m.perm = Param(m.compon, initialize=Permset, - default=0, doc='permeability ') + return Perm[compon] * (1.0 / 22400.0) * 1.0e4 * 750.062 * 60.0 / 1000.0 + + m.perm = Param(m.compon, initialize=Permset, default=0, doc='permeability ') Cbeta = {} Cbeta['h2'] = 1.0003 Cbeta['ch4'] = 1.0008 - Cbeta['dip'] = 1.0e+04 - m.cbeta = Param(m.compon, initialize=Cbeta, default=0, - doc='constant values (exp(beta)) in absorber') + Cbeta['dip'] = 1.0e04 + m.cbeta = Param( + m.compon, + initialize=Cbeta, + default=0, + doc='constant values (exp(beta)) in absorber', + ) Aabs = {} Aabs['ben'] = 1.4 Aabs['tol'] = 4.0 - m.aabs = Param(m.compon, initialize=Aabs, - default=0, doc=' absorption factors') + m.aabs = Param(m.compon, initialize=Aabs, default=0, doc=' absorption factors') m.eps1 = Param(initialize=1e-4, doc='small number to avoid div. by zero') Heatrxn = {} - Heatrxn[1] = 50100. - Heatrxn[2] = 50100. - m.heatrxn = Param(m.rct, initialize=Heatrxn, default=0, - doc='heat of reaction (kj per kg-mol)') + Heatrxn[1] = 50100.0 + Heatrxn[2] = 50100.0 + m.heatrxn = Param( + m.rct, initialize=Heatrxn, default=0, doc='heat of reaction (kj per kg-mol)' + ) F1comp = {} F1comp['h2'] = 0.95 F1comp['ch4'] = 0.05 F1comp['dip'] = 0.00 F1comp['ben'] = 0.00 F1comp['tol'] = 0.00 - m.f1comp = Param(m.compon, initialize=F1comp, default=0, - doc='feedstock compositions (h2 feed)') + m.f1comp = Param( + m.compon, initialize=F1comp, default=0, doc='feedstock compositions (h2 feed)' + ) F66comp = {} F66comp['tol'] = 1.0 F66comp['h2'] = 0.00 F66comp['ch4'] = 0.00 F66comp['dip'] = 0.00 F66comp['ben'] = 0.00 - m.f66comp = Param(m.compon, initialize=F66comp, default=0, - doc='feedstock compositions (tol feed)') + m.f66comp = Param( + m.compon, + initialize=F66comp, + default=0, + doc='feedstock compositions (tol feed)', + ) F67comp = {} F67comp['tol'] = 1.0 F67comp['h2'] = 0.00 F67comp['ch4'] = 0.00 F67comp['dip'] = 0.00 F67comp['ben'] = 0.00 - m.f67comp = Param(m.compon, initialize=F67comp, default=0, - doc='feedstock compositions (tol feed)') + m.f67comp = Param( + m.compon, + initialize=F67comp, + default=0, + doc='feedstock compositions (tol feed)', + ) # # matching streams - m.ilabs = Set(initialize=[(1, 67)], - doc="abs-stream (inlet liquid) matches") - m.olabs = Set(initialize=[(1, 68)], - doc="abs-stream (outlet liquid) matches") - m.ivabs = Set(initialize=[(1, 63)], - doc=" abs-stream (inlet vapor) matches ") - m.ovabs = Set(initialize=[(1, 64)], - doc="abs-stream (outlet vapor) matches") + m.ilabs = Set(initialize=[(1, 67)], doc="abs-stream (inlet liquid) matches") + m.olabs = Set(initialize=[(1, 68)], doc="abs-stream (outlet liquid) matches") + m.ivabs = Set(initialize=[(1, 63)], doc=" abs-stream (inlet vapor) matches ") + m.ovabs = Set(initialize=[(1, 64)], doc="abs-stream (outlet vapor) matches") m.asolv = Set(initialize=[(1, 'tol')], doc="abs-solvent component matches") - m.anorm = Set(initialize=[(1, 'ben')], - doc="abs-comp matches (normal model)") - m.asimp = Set(initialize=[(1, 'h2'), (1, 'ch4'), - (1, 'dip')], doc="abs-heavy component matches") - - m.icomp = Set(initialize=[(1, 5), (2, 59), (3, 64), - (4, 56)], doc="compressor-stream (inlet) matches") - m.ocomp = Set(initialize=[(1, 6), (2, 60), (3, 65), - (4, 57)], doc=" compressor-stream (outlet) matches") - - m.idist = Set(initialize=[(1, 25), (2, 30), (3, 33)], - doc="dist-stream (inlet) matches") - m.vdist = Set(initialize=[(1, 26), (2, 31), (3, 34)], - doc="dist-stream (vapor) matches") - m.ldist = Set(initialize=[(1, 27), (2, 32), (3, 35)], - doc="dist-stream (liquid) matches") - m.dl = Set(initialize=[(1, 'h2'), (2, 'ch4'), - (3, 'ben')], doc="dist-light components matches") - m.dlkey = Set(initialize=[(1, 'ch4'), (2, 'ben'), - (3, 'tol')], doc="dist-heavy key component matches") - m.dhkey = Set(initialize=[(1, 'ben'), (2, 'tol'), - (3, 'dip')], doc="dist-heavy components matches ") - m.dh = Set(initialize=[(1, 'tol'), (1, 'dip'), - (2, 'dip')], doc="dist-key component matches") + m.anorm = Set(initialize=[(1, 'ben')], doc="abs-comp matches (normal model)") + m.asimp = Set( + initialize=[(1, 'h2'), (1, 'ch4'), (1, 'dip')], + doc="abs-heavy component matches", + ) + + m.icomp = Set( + initialize=[(1, 5), (2, 59), (3, 64), (4, 56)], + doc="compressor-stream (inlet) matches", + ) + m.ocomp = Set( + initialize=[(1, 6), (2, 60), (3, 65), (4, 57)], + doc=" compressor-stream (outlet) matches", + ) + + m.idist = Set( + initialize=[(1, 25), (2, 30), (3, 33)], doc="dist-stream (inlet) matches" + ) + m.vdist = Set( + initialize=[(1, 26), (2, 31), (3, 34)], doc="dist-stream (vapor) matches" + ) + m.ldist = Set( + initialize=[(1, 27), (2, 32), (3, 35)], doc="dist-stream (liquid) matches" + ) + m.dl = Set( + initialize=[(1, 'h2'), (2, 'ch4'), (3, 'ben')], + doc="dist-light components matches", + ) + m.dlkey = Set( + initialize=[(1, 'ch4'), (2, 'ben'), (3, 'tol')], + doc="dist-heavy key component matches", + ) + m.dhkey = Set( + initialize=[(1, 'ben'), (2, 'tol'), (3, 'dip')], + doc="dist-heavy components matches ", + ) + m.dh = Set( + initialize=[(1, 'tol'), (1, 'dip'), (2, 'dip')], + doc="dist-key component matches", + ) i = list(m.dlkey) q = list(m.dhkey) dkeyset = i + q m.dkey = Set(initialize=dkeyset, doc='dist-key component matches') - m.iflsh = Set(initialize=[(1, 17), (2, 46), (3, 39)], - doc="flsh-stream (inlet) matches") - m.vflsh = Set(initialize=[(1, 18), (2, 47), (3, 40)], - doc="flsh-stream (vapor) matches") - m.lflsh = Set(initialize=[(1, 19), (2, 48), (3, 41)], - doc="flsh-stream (liquid) matches") - m.fkey = Set(initialize=[(1, 'ch4'), (2, 'ch4'), - (3, 'tol')], doc="flash-key component matches") + m.iflsh = Set( + initialize=[(1, 17), (2, 46), (3, 39)], doc="flsh-stream (inlet) matches" + ) + m.vflsh = Set( + initialize=[(1, 18), (2, 47), (3, 40)], doc="flsh-stream (vapor) matches" + ) + m.lflsh = Set( + initialize=[(1, 19), (2, 48), (3, 41)], doc="flsh-stream (liquid) matches" + ) + m.fkey = Set( + initialize=[(1, 'ch4'), (2, 'ch4'), (3, 'tol')], + doc="flash-key component matches", + ) m.ifurn = Set(initialize=[(1, 70)], doc="furn-stream (inlet) matches") m.ofurn = Set(initialize=[(1, 9)], doc="furn-stream (outlet) matches") - m.ihec = Set(initialize=[(1, 71), (2, 45)], - doc="hec-stream (inlet) matches") - m.ohec = Set(initialize=[(1, 17), (2, 46)], - doc="hec-stream (outlet) matches") - - m.iheh = Set(initialize=[(1, 24), (2, 23), (3, 37), - (4, 61)], doc="heh-stream (inlet) matches") - m.oheh = Set(initialize=[(1, 25), (2, 44), (3, 38), - (4, 73)], doc="heh-stream (outlet) matches") - - m.icexch = Set(initialize=[(1, 8)], - doc="exch-cold stream (inlet) matches") - m.ocexch = Set(initialize=[(1, 70)], - doc="exch-cold stream (outlet) matches") - m.ihexch = Set(initialize=[(1, 16)], - doc="exch-hot stream (inlet) matches") - m.ohexch = Set(initialize=[(1, 71)], - doc="exch-hot stream (outlet) matches") - - m.imemb = Set(initialize=[(1, 3), (2, 54)], - doc="memb-stream (inlet) matches") - m.nmemb = Set(initialize=[(1, 4), (2, 55)], - doc=" memb-stream (non-permeate) matches") - m.pmemb = Set(initialize=[(1, 5), (2, 56)], - doc="memb-stream (permeate) matches") - m.mnorm = Set(initialize=[(1, 'h2'), (1, 'ch4'), - (2, 'h2'), (2, 'ch4')], doc="normal components ") - m.msimp = Set(initialize=[(1, 'ben'), (1, 'tol'), (1, 'dip'), (2, 'ben'), - (2, 'tol'), (2, 'dip')], doc="simplified flux components ") - - m.imxr1 = Set(initialize=[(1, 2), (1, 6), (2, 11), (2, 13), (3, 27), (3, 48), ( - 4, 34), (4, 40), (5, 49), (5, 50)], doc="mixer-stream (inlet) matches") - m.omxr1 = Set(initialize=[(1, 7), (2, 14), (3, 30), (4, 42), - (5, 51)], doc=" mixer-stream (outlet) matches") - m.mxr1spl1 = Set(initialize=[(1, 2, 2), (1, 6, 3), (2, 11, 10), (2, 13, 12), (3, 27, 24), (3, 48, 23), ( - 4, 34, 33), (4, 40, 37), (5, 49, 23), (5, 50, 24)], doc="1-mxr-inlet 1-spl-outlet matches") - - m.imxr = Set(initialize=[(1, 7), (1, 43), (1, 66), (1, 72), (2, 15), (2, 20), (3, 21), ( - 3, 69), (4, 51), (4, 62), (5, 57), (5, 60), (5, 65)], doc="mixer-stream (inlet) matches") - m.omxr = Set(initialize=[(1, 8), (2, 16), (3, 22), (4, 63), - (5, 72)], doc=" mixer-stream (outlet) matches ") - - m.ipump = Set(initialize=[(1, 42), (2, 68)], - doc="pump-stream (inlet) matches") - m.opump = Set(initialize=[(1, 43), (2, 69)], - doc="pump-stream (outlet) matches") - - m.irct = Set(initialize=[(1, 10), (2, 12)], - doc="reactor-stream (inlet) matches") - m.orct = Set(initialize=[(1, 11), (2, 13)], - doc="reactor-stream (outlet) matches") - m.rkey = Set(initialize=[(1, 'tol'), (2, 'tol')], - doc="reactor-key component matches") - - m.ispl1 = Set(initialize=[(1, 1), (2, 9), (3, 22), (4, 32), - (5, 52), (6, 58)], doc="splitter-stream (inlet) matches ") - m.ospl1 = Set(initialize=[(1, 2), (1, 3), (2, 10), (2, 12), (3, 23), (3, 24), (4, 33), ( - 4, 37), (5, 53), (5, 54), (6, 59), (6, 61)], doc="splitter-stream (outlet) matches") - - m.ispl = Set(initialize=[(1, 19), (2, 18), (3, 26)], - doc="splitter-stream (inlet) matches") - m.ospl = Set(initialize=[(1, 20), (1, 21), (2, 52), (2, 58), - (3, 28), (3, 29)], doc="splitter-stream (outlet) matches") - - m.ival = Set(initialize=[(1, 44), (2, 38), (3, 14), (4, 47), - (5, 29), (6, 73)], doc="exp.valve-stream (inlet) matches") - m.oval = Set(initialize=[(1, 45), (2, 39), (3, 15), (4, 49), - (5, 50), (6, 62)], doc="exp.valve-stream (outlet) matches") + m.ihec = Set(initialize=[(1, 71), (2, 45)], doc="hec-stream (inlet) matches") + m.ohec = Set(initialize=[(1, 17), (2, 46)], doc="hec-stream (outlet) matches") + + m.iheh = Set( + initialize=[(1, 24), (2, 23), (3, 37), (4, 61)], + doc="heh-stream (inlet) matches", + ) + m.oheh = Set( + initialize=[(1, 25), (2, 44), (3, 38), (4, 73)], + doc="heh-stream (outlet) matches", + ) + + m.icexch = Set(initialize=[(1, 8)], doc="exch-cold stream (inlet) matches") + m.ocexch = Set(initialize=[(1, 70)], doc="exch-cold stream (outlet) matches") + m.ihexch = Set(initialize=[(1, 16)], doc="exch-hot stream (inlet) matches") + m.ohexch = Set(initialize=[(1, 71)], doc="exch-hot stream (outlet) matches") + + m.imemb = Set(initialize=[(1, 3), (2, 54)], doc="memb-stream (inlet) matches") + m.nmemb = Set( + initialize=[(1, 4), (2, 55)], doc=" memb-stream (non-permeate) matches" + ) + m.pmemb = Set(initialize=[(1, 5), (2, 56)], doc="memb-stream (permeate) matches") + m.mnorm = Set( + initialize=[(1, 'h2'), (1, 'ch4'), (2, 'h2'), (2, 'ch4')], + doc="normal components ", + ) + m.msimp = Set( + initialize=[ + (1, 'ben'), + (1, 'tol'), + (1, 'dip'), + (2, 'ben'), + (2, 'tol'), + (2, 'dip'), + ], + doc="simplified flux components ", + ) + + m.imxr1 = Set( + initialize=[ + (1, 2), + (1, 6), + (2, 11), + (2, 13), + (3, 27), + (3, 48), + (4, 34), + (4, 40), + (5, 49), + (5, 50), + ], + doc="mixer-stream (inlet) matches", + ) + m.omxr1 = Set( + initialize=[(1, 7), (2, 14), (3, 30), (4, 42), (5, 51)], + doc=" mixer-stream (outlet) matches", + ) + m.mxr1spl1 = Set( + initialize=[ + (1, 2, 2), + (1, 6, 3), + (2, 11, 10), + (2, 13, 12), + (3, 27, 24), + (3, 48, 23), + (4, 34, 33), + (4, 40, 37), + (5, 49, 23), + (5, 50, 24), + ], + doc="1-mxr-inlet 1-spl-outlet matches", + ) + + m.imxr = Set( + initialize=[ + (1, 7), + (1, 43), + (1, 66), + (1, 72), + (2, 15), + (2, 20), + (3, 21), + (3, 69), + (4, 51), + (4, 62), + (5, 57), + (5, 60), + (5, 65), + ], + doc="mixer-stream (inlet) matches", + ) + m.omxr = Set( + initialize=[(1, 8), (2, 16), (3, 22), (4, 63), (5, 72)], + doc=" mixer-stream (outlet) matches ", + ) + + m.ipump = Set(initialize=[(1, 42), (2, 68)], doc="pump-stream (inlet) matches") + m.opump = Set(initialize=[(1, 43), (2, 69)], doc="pump-stream (outlet) matches") + + m.irct = Set(initialize=[(1, 10), (2, 12)], doc="reactor-stream (inlet) matches") + m.orct = Set(initialize=[(1, 11), (2, 13)], doc="reactor-stream (outlet) matches") + m.rkey = Set( + initialize=[(1, 'tol'), (2, 'tol')], doc="reactor-key component matches" + ) + + m.ispl1 = Set( + initialize=[(1, 1), (2, 9), (3, 22), (4, 32), (5, 52), (6, 58)], + doc="splitter-stream (inlet) matches ", + ) + m.ospl1 = Set( + initialize=[ + (1, 2), + (1, 3), + (2, 10), + (2, 12), + (3, 23), + (3, 24), + (4, 33), + (4, 37), + (5, 53), + (5, 54), + (6, 59), + (6, 61), + ], + doc="splitter-stream (outlet) matches", + ) + + m.ispl = Set( + initialize=[(1, 19), (2, 18), (3, 26)], doc="splitter-stream (inlet) matches" + ) + m.ospl = Set( + initialize=[(1, 20), (1, 21), (2, 52), (2, 58), (3, 28), (3, 29)], + doc="splitter-stream (outlet) matches", + ) + + m.ival = Set( + initialize=[(1, 44), (2, 38), (3, 14), (4, 47), (5, 29), (6, 73)], + doc="exp.valve-stream (inlet) matches", + ) + m.oval = Set( + initialize=[(1, 45), (2, 39), (3, 15), (4, 49), (5, 50), (6, 62)], + doc="exp.valve-stream (outlet) matches", + ) # variables # absorber - m.nabs = Var(m.abs, within=NonNegativeReals, bounds=(0, 40), - initialize=1, doc='number of absorber trays') + m.nabs = Var( + m.abs, + within=NonNegativeReals, + bounds=(0, 40), + initialize=1, + doc='number of absorber trays', + ) m.gamma = Var(m.abs, m.compon, within=Reals, initialize=1) m.beta = Var(m.abs, m.compon, within=Reals, initialize=1) # compressor - m.elec = Var(m.comp, within=NonNegativeReals, bounds=(0, 100), - initialize=1, doc='electricity requirement (kw)') - m.presrat = Var(m.comp, within=NonNegativeReals, bounds=( - 1, 8/3), initialize=1, doc='ratio of outlet to inlet pressure') + m.elec = Var( + m.comp, + within=NonNegativeReals, + bounds=(0, 100), + initialize=1, + doc='electricity requirement (kw)', + ) + m.presrat = Var( + m.comp, + within=NonNegativeReals, + bounds=(1, 8 / 3), + initialize=1, + doc='ratio of outlet to inlet pressure', + ) # distillation m.nmin = Var(m.dist, within=NonNegativeReals, initialize=1) - m.ndist = Var(m.dist, within=NonNegativeReals, - initialize=1, doc='number of trays in column') - m.rmin = Var(m.dist, within=NonNegativeReals, - initialize=1, doc='minimum reflux ratio') - m.reflux = Var(m.dist, within=NonNegativeReals, - initialize=1, doc='reflux ratio') - m.distp = Var(m.dist, within=NonNegativeReals, initialize=1, - bounds=(0.1, 4.0), doc='column pressure') - m.avevlt = Var(m.dist, within=NonNegativeReals, - initialize=1, doc='average volatility') + m.ndist = Var( + m.dist, within=NonNegativeReals, initialize=1, doc='number of trays in column' + ) + m.rmin = Var( + m.dist, within=NonNegativeReals, initialize=1, doc='minimum reflux ratio' + ) + m.reflux = Var(m.dist, within=NonNegativeReals, initialize=1, doc='reflux ratio') + m.distp = Var( + m.dist, + within=NonNegativeReals, + initialize=1, + bounds=(0.1, 4.0), + doc='column pressure', + ) + m.avevlt = Var( + m.dist, within=NonNegativeReals, initialize=1, doc='average volatility' + ) # flash - m.flsht = Var(m.flsh, within=NonNegativeReals, initialize=1, - doc='flash temperature (100 k)') - m.flshp = Var(m.flsh, within=NonNegativeReals, initialize=1, - doc='flash pressure (mega-pascal)') - m.eflsh = Var(m.flsh, m.compon, within=NonNegativeReals, bounds=( - 0, 1), initialize=0.5, doc='vapor phase recovery in flash') + m.flsht = Var( + m.flsh, within=NonNegativeReals, initialize=1, doc='flash temperature (100 k)' + ) + m.flshp = Var( + m.flsh, + within=NonNegativeReals, + initialize=1, + doc='flash pressure (mega-pascal)', + ) + m.eflsh = Var( + m.flsh, + m.compon, + within=NonNegativeReals, + bounds=(0, 1), + initialize=0.5, + doc='vapor phase recovery in flash', + ) # furnace - m.qfuel = Var(m.furn, within=NonNegativeReals, bounds=( - None, 10), initialize=1, doc='heating requied (1.e+12 kj per yr)') + m.qfuel = Var( + m.furn, + within=NonNegativeReals, + bounds=(None, 10), + initialize=1, + doc='heating required (1.e+12 kj per yr)', + ) # cooler - m.qc = Var(m.hec, within=NonNegativeReals, bounds=(None, 10), - initialize=1, doc='utility requirement (1.e+12 kj per yr)') + m.qc = Var( + m.hec, + within=NonNegativeReals, + bounds=(None, 10), + initialize=1, + doc='utility requirement (1.e+12 kj per yr)', + ) # heater - m.qh = Var(m.heh, within=NonNegativeReals, bounds=(None, 10), - initialize=1, doc='utility requirement (1.e+12 kj per yr)') + m.qh = Var( + m.heh, + within=NonNegativeReals, + bounds=(None, 10), + initialize=1, + doc='utility requirement (1.e+12 kj per yr)', + ) # exchanger - m.qexch = Var(m.exch, within=NonNegativeReals, bounds=( - None, 10), initialize=1, doc='heat exchanged (1.e+12 kj per yr)') + m.qexch = Var( + m.exch, + within=NonNegativeReals, + bounds=(None, 10), + initialize=1, + doc='heat exchanged (1.e+12 kj per yr)', + ) # membrane - m.a = Var(m.memb, within=NonNegativeReals, bounds=(100, 10000), - initialize=1, doc='surface area for mass transfer ( m**2 )') + m.a = Var( + m.memb, + within=NonNegativeReals, + bounds=(100, 10000), + initialize=1, + doc='surface area for mass transfer ( m**2 )', + ) # mixer(1 input) - m.mxr1p = Var(m.mxr1, within=NonNegativeReals, bounds=( - 0.1, 4), initialize=0, doc='mixer temperature (100 k)') - m.mxr1t = Var(m.mxr1, within=NonNegativeReals, bounds=(3, 10), - initialize=0, doc='mixer pressure (m-pa)') + m.mxr1p = Var( + m.mxr1, + within=NonNegativeReals, + bounds=(0.1, 4), + initialize=0, + doc='mixer temperature (100 k)', + ) + m.mxr1t = Var( + m.mxr1, + within=NonNegativeReals, + bounds=(3, 10), + initialize=0, + doc='mixer pressure (m-pa)', + ) # mixer - m.mxrt = Var(m.mxr, within=NonNegativeReals, bounds=(3.0, 10), - initialize=3, doc='mixer temperature (100 k)') # ? - m.mxrp = Var(m.mxr, within=NonNegativeReals, bounds=(0.1, 4.0), - initialize=3, doc='mixer pressure (m-pa)') + m.mxrt = Var( + m.mxr, + within=NonNegativeReals, + bounds=(3.0, 10), + initialize=3, + doc='mixer temperature (100 k)', + ) # ? + m.mxrp = Var( + m.mxr, + within=NonNegativeReals, + bounds=(0.1, 4.0), + initialize=3, + doc='mixer pressure (m-pa)', + ) # reactor - m.rctt = Var(m.rct, within=NonNegativeReals, bounds=( - 8.9427, 9.7760), doc='reactor temperature (100 k)') - m.rctp = Var(m.rct, within=NonNegativeReals, bounds=( - 3.4474, 3.4474), doc=' reactor pressure (m-pa)') - m.rctvol = Var(m.rct, within=NonNegativeReals, bounds=( - None, 200), doc='reactor volume (cubic meter)') - m.krct = Var(m.rct, within=NonNegativeReals, initialize=1, - bounds=(0.0123471, 0.149543), doc='rate constant') - m.conv = Var(m.rct, m.compon, within=NonNegativeReals, bounds=( - None, 0.973), doc='conversion of key component') - m.sel = Var(m.rct, within=NonNegativeReals, bounds=( - None, 0.9964), doc='selectivity to benzene') - m.consum = Var(m.rct, m.compon, within=NonNegativeReals, bounds=( - 0, 10000000000), initialize=0, doc='consumption rate of key') - m.q = Var(m.rct, within=NonNegativeReals, bounds=( - 0, 10000000000), doc='heat removed (1.e+9 kj per yr)') + m.rctt = Var( + m.rct, + within=NonNegativeReals, + bounds=(8.9427, 9.7760), + doc='reactor temperature (100 k)', + ) + m.rctp = Var( + m.rct, + within=NonNegativeReals, + bounds=(3.4474, 3.4474), + doc=' reactor pressure (m-pa)', + ) + m.rctvol = Var( + m.rct, + within=NonNegativeReals, + bounds=(None, 200), + doc='reactor volume (cubic meter)', + ) + m.krct = Var( + m.rct, + within=NonNegativeReals, + initialize=1, + bounds=(0.0123471, 0.149543), + doc='rate constant', + ) + m.conv = Var( + m.rct, + m.compon, + within=NonNegativeReals, + bounds=(None, 0.973), + doc='conversion of key component', + ) + m.sel = Var( + m.rct, + within=NonNegativeReals, + bounds=(None, 0.9964), + doc='selectivity to benzene', + ) + m.consum = Var( + m.rct, + m.compon, + within=NonNegativeReals, + bounds=(0, 10000000000), + initialize=0, + doc='consumption rate of key', + ) + m.q = Var( + m.rct, + within=NonNegativeReals, + bounds=(0, 10000000000), + doc='heat removed (1.e+9 kj per yr)', + ) # splitter (1 output) - m.spl1t = Var(m.spl1, within=PositiveReals, bounds=( - 3.00, 10.00), doc='splitter temperature (100 k)') - m.spl1p = Var(m.spl1, within=PositiveReals, bounds=( - 0.1, 4.0), doc='splitter pressure (m-pa)') + m.spl1t = Var( + m.spl1, + within=PositiveReals, + bounds=(3.00, 10.00), + doc='splitter temperature (100 k)', + ) + m.spl1p = Var( + m.spl1, + within=PositiveReals, + bounds=(0.1, 4.0), + doc='splitter pressure (m-pa)', + ) # splitter - m.splp = Var(m.spl, within=Reals, bounds=(0.1, 4.0), - doc='splitter pressure (m-pa)') - m.splt = Var(m.spl, within=Reals, bounds=(3.0, 10.0), - doc='splitter temperature (100 k)') + m.splp = Var( + m.spl, within=Reals, bounds=(0.1, 4.0), doc='splitter pressure (m-pa)' + ) + m.splt = Var( + m.spl, within=Reals, bounds=(3.0, 10.0), doc='splitter temperature (100 k)' + ) # stream def bound_f(m, stream): if stream in range(8, 19): return (0, 50) elif stream in [52, 54, 56, 57, 58, 59, 60, 70, 71, 72]: - return(0, 50) + return (0, 50) else: return (0, 10) - m.f = Var(m.str, within=NonNegativeReals, bounds=bound_f, - initialize=1, doc='stream flowrates (kg-mole per min)') + + m.f = Var( + m.str, + within=NonNegativeReals, + bounds=bound_f, + initialize=1, + doc='stream flowrates (kg-mole per min)', + ) def bound_fc(m, stream, compon): if stream in range(8, 19) or stream in [52, 54, 56, 57, 58, 59, 60, 70, 71, 72]: return (0, 30) else: return (0, 10) - m.fc = Var(m.str, m.compon, within=Reals, bounds=bound_fc, - initialize=1, doc='component flowrates (kg-mole per min)') - m.p = Var(m.str, within=NonNegativeReals, bounds=(0.1, 4.0), - initialize=3.0, doc='stream pressure (mega_pascal)') - m.t = Var(m.str, within=NonNegativeReals, bounds=(3.0, 10.0), - initialize=3.0, doc='stream temperature (100 k)') - m.vp = Var(m.str, m.compon, within=NonNegativeReals, initialize=1, - bounds=(0, 10), doc='vapor pressure (mega-pascal)') + + m.fc = Var( + m.str, + m.compon, + within=Reals, + bounds=bound_fc, + initialize=1, + doc='component flowrates (kg-mole per min)', + ) + m.p = Var( + m.str, + within=NonNegativeReals, + bounds=(0.1, 4.0), + initialize=3.0, + doc='stream pressure (mega_pascal)', + ) + m.t = Var( + m.str, + within=NonNegativeReals, + bounds=(3.0, 10.0), + initialize=3.0, + doc='stream temperature (100 k)', + ) + m.vp = Var( + m.str, + m.compon, + within=NonNegativeReals, + initialize=1, + bounds=(0, 10), + doc='vapor pressure (mega-pascal)', + ) def boundsofe(m): if i == 20: @@ -506,8 +770,8 @@ def boundsofe(m): return (0.5, 1.0) else: return (None, 1.0) - m.e = Var(m.str, within=NonNegativeReals, - bounds=boundsofe, doc='split fraction') + + m.e = Var(m.str, within=NonNegativeReals, bounds=boundsofe, doc='split fraction') # obj function m.const = Param(initialize=22.5, doc='constant term in obj fcn') @@ -517,12 +781,12 @@ def boundsofe(m): for rct in m.rct: m.conv[rct, 'tol'].setub(0.973) m.sel.setub(1.0 - 0.0036) - m.reflux[1].setlb(0.02*1.2) - m.reflux[1].setub(0.10*1.2) - m.reflux[2].setlb(0.50*1.2) - m.reflux[2].setub(2.00*1.2) - m.reflux[3].setlb(0.02*1.2) - m.reflux[3].setub(0.1*1.2) + m.reflux[1].setlb(0.02 * 1.2) + m.reflux[1].setub(0.10 * 1.2) + m.reflux[2].setlb(0.50 * 1.2) + m.reflux[2].setub(2.00 * 1.2) + m.reflux[3].setlb(0.02 * 1.2) + m.reflux[3].setub(0.1 * 1.2) m.nmin[1].setlb(0) m.nmin[1].setub(4) m.nmin[2].setlb(8) @@ -530,11 +794,11 @@ def boundsofe(m): m.nmin[3].setlb(0) m.nmin[3].setub(4) m.ndist[1].setlb(0) - m.ndist[1].setub(4*2/m.disteff) + m.ndist[1].setub(4 * 2 / m.disteff) m.ndist[3].setlb(0) - m.ndist[3].setub(4*2/m.disteff) - m.ndist[2].setlb(8*2/m.disteff) - m.ndist[2].setub(14*2/m.disteff) + m.ndist[3].setub(4 * 2 / m.disteff) + m.ndist[2].setlb(8 * 2 / m.disteff) + m.ndist[2].setub(14 * 2 / m.disteff) m.rmin[1].setlb(0.02) m.rmin[1].setub(0.10) m.rmin[2].setlb(0.50) @@ -549,61 +813,144 @@ def boundsofe(m): m.t[26].setub(3.2) for i in range(49, 52): m.t[i].setlb(2.0) - m.t[27].setlb((m.antb['ben'] / (m.anta['ben'] - - log(m.distp[1].lb * 7500.6168)) - m.antc['ben']) / 100.) - m.t[27].setub((m.antb['ben'] / (m.anta['ben'] - - log(m.distp[1].ub * 7500.6168)) - m.antc['ben']) / 100.) - m.t[31].setlb((m.antb['ben'] / (m.anta['ben'] - - log(m.distp[2].lb * 7500.6168)) - m.antc['ben']) / 100.) - m.t[31].setub((m.antb['ben'] / (m.anta['ben'] - - log(m.distp[2].ub * 7500.6168)) - m.antc['ben']) / 100.) - m.t[32].setlb((m.antb['tol'] / (m.anta['tol'] - - log(m.distp[2].lb * 7500.6168)) - m.antc['tol']) / 100.) - m.t[32].setub((m.antb['tol'] / (m.anta['tol'] - - log(m.distp[2].ub * 7500.6168)) - m.antc['tol']) / 100.) - m.t[34].setlb((m.antb['tol'] / (m.anta['tol'] - - log(m.distp[3].lb * 7500.6168)) - m.antc['tol']) / 100.) - m.t[34].setub((m.antb['tol'] / (m.anta['tol'] - - log(m.distp[3].ub * 7500.6168)) - m.antc['tol']) / 100.) - m.t[35].setlb((m.antb['dip'] / (m.anta['dip'] - - log(m.distp[3].lb * 7500.6168)) - m.antc['dip']) / 100.) - m.t[35].setub((m.antb['dip'] / (m.anta['dip'] - - log(m.distp[3].ub * 7500.6168)) - m.antc['dip']) / 100.) + m.t[27].setlb( + ( + m.antb['ben'] / (m.anta['ben'] - log(m.distp[1].lb * 7500.6168)) + - m.antc['ben'] + ) + / 100.0 + ) + m.t[27].setub( + ( + m.antb['ben'] / (m.anta['ben'] - log(m.distp[1].ub * 7500.6168)) + - m.antc['ben'] + ) + / 100.0 + ) + m.t[31].setlb( + ( + m.antb['ben'] / (m.anta['ben'] - log(m.distp[2].lb * 7500.6168)) + - m.antc['ben'] + ) + / 100.0 + ) + m.t[31].setub( + ( + m.antb['ben'] / (m.anta['ben'] - log(m.distp[2].ub * 7500.6168)) + - m.antc['ben'] + ) + / 100.0 + ) + m.t[32].setlb( + ( + m.antb['tol'] / (m.anta['tol'] - log(m.distp[2].lb * 7500.6168)) + - m.antc['tol'] + ) + / 100.0 + ) + m.t[32].setub( + ( + m.antb['tol'] / (m.anta['tol'] - log(m.distp[2].ub * 7500.6168)) + - m.antc['tol'] + ) + / 100.0 + ) + m.t[34].setlb( + ( + m.antb['tol'] / (m.anta['tol'] - log(m.distp[3].lb * 7500.6168)) + - m.antc['tol'] + ) + / 100.0 + ) + m.t[34].setub( + ( + m.antb['tol'] / (m.anta['tol'] - log(m.distp[3].ub * 7500.6168)) + - m.antc['tol'] + ) + / 100.0 + ) + m.t[35].setlb( + ( + m.antb['dip'] / (m.anta['dip'] - log(m.distp[3].lb * 7500.6168)) + - m.antc['dip'] + ) + / 100.0 + ) + m.t[35].setub( + ( + m.antb['dip'] / (m.anta['dip'] - log(m.distp[3].ub * 7500.6168)) + - m.antc['dip'] + ) + / 100.0 + ) # absorber m.beta[1, 'ben'].setlb(0.00011776) m.beta[1, 'ben'].setub(5.72649) m.beta[1, 'tol'].setlb(0.00018483515) m.beta[1, 'tol'].setub(15) - m.gamma[1, 'tol'].setlb(log( - (1 - m.aabs['tol'] ** (m.nabs[1].lb * m.abseff + m.eps1)) / (1 - m.aabs['tol']))) - m.gamma[1, 'tol'].setub(min(15, log( - (1 - m.aabs['tol'] ** (m.nabs[1].ub * m.abseff + m.eps1)) / (1 - m.aabs['tol'])))) + m.gamma[1, 'tol'].setlb( + log( + (1 - m.aabs['tol'] ** (m.nabs[1].lb * m.abseff + m.eps1)) + / (1 - m.aabs['tol']) + ) + ) + m.gamma[1, 'tol'].setub( + min( + 15, + log( + (1 - m.aabs['tol'] ** (m.nabs[1].ub * m.abseff + m.eps1)) + / (1 - m.aabs['tol']) + ), + ) + ) for abso in m.abs: for compon in m.compon: - m.beta[abso, compon].setlb(log( - (1 - m.aabs[compon] ** (m.nabs[1].lb * m.abseff + m.eps1 + 1)) / (1 - m.aabs[compon]))) - m.beta[abso, compon].setub(min(15, log( - (1 - m.aabs[compon] ** (m.nabs[1].ub * m.abseff + m.eps1 + 1)) / (1 - m.aabs[compon])))) + m.beta[abso, compon].setlb( + log( + (1 - m.aabs[compon] ** (m.nabs[1].lb * m.abseff + m.eps1 + 1)) + / (1 - m.aabs[compon]) + ) + ) + m.beta[abso, compon].setub( + min( + 15, + log( + (1 - m.aabs[compon] ** (m.nabs[1].ub * m.abseff + m.eps1 + 1)) + / (1 - m.aabs[compon]) + ), + ) + ) m.t[67].setlb(3.0) m.t[67].setub(3.0) for compon in m.compon: - m.vp[67, compon].setlb((1. / 7500.6168) * exp(m.anta[compon] - - m.antb[compon] / (value(m.t[67]) * 100. + m.antc[compon]))) - m.vp[67, compon].setub((1. / 7500.6168) * exp(m.anta[compon] - - m.antb[compon] / (value(m.t[67]) * 100. + m.antc[compon]))) - - - flashdata_file = os.path.join(dir_path,'flashdata.csv') + m.vp[67, compon].setlb( + (1.0 / 7500.6168) + * exp( + m.anta[compon] + - m.antb[compon] / (value(m.t[67]) * 100.0 + m.antc[compon]) + ) + ) + m.vp[67, compon].setub( + (1.0 / 7500.6168) + * exp( + m.anta[compon] + - m.antb[compon] / (value(m.t[67]) * 100.0 + m.antc[compon]) + ) + ) + + flashdata_file = os.path.join(dir_path, 'flashdata.csv') flash = pd.read_csv(flashdata_file, header=0) number = flash.iloc[:, [4]].dropna().values two_digit_number = flash.iloc[:, [0]].dropna().values two_digit_compon = flash.iloc[:, [1]].dropna().values for i in range(len(two_digit_number)): m.eflsh[two_digit_number[i, 0], two_digit_compon[i, 0]].setlb( - flash.iloc[:, [2]].dropna().values[i, 0]) + flash.iloc[:, [2]].dropna().values[i, 0] + ) m.eflsh[two_digit_number[i, 0], two_digit_compon[i, 0]].setub( - flash.iloc[:, [3]].dropna().values[i, 0]) + flash.iloc[:, [3]].dropna().values[i, 0] + ) for i in range(len(number)): m.flshp[number[i, 0]].setlb(flash.iloc[:, [5]].dropna().values[i, 0]) m.flshp[number[i, 0]].setub(flash.iloc[:, [6]].dropna().values[i, 0]) @@ -622,10 +969,20 @@ def boundsofe(m): for stream in m.str: for compon in m.compon: - m.vp[stream, compon].setlb((1. / 7500.6168) * exp( - m.anta[compon] - m.antb[compon] / (m.t[stream].lb * 100. + m.antc[compon]))) - m.vp[stream, compon].setub((1. / 7500.6168) * exp( - m.anta[compon] - m.antb[compon] / (m.t[stream].ub * 100. + m.antc[compon]))) + m.vp[stream, compon].setlb( + (1.0 / 7500.6168) + * exp( + m.anta[compon] + - m.antb[compon] / (m.t[stream].lb * 100.0 + m.antc[compon]) + ) + ) + m.vp[stream, compon].setub( + (1.0 / 7500.6168) + * exp( + m.anta[compon] + - m.antb[compon] / (m.t[stream].ub * 100.0 + m.antc[compon]) + ) + ) m.p[1].setub(3.93) m.p[1].setlb(3.93) @@ -641,16 +998,14 @@ def boundsofe(m): if (dist, stream) in m.ldist and (dist, compon) in m.dlkey: m.avevlt[dist].setlb(m.vp[stream, compon].ub) if (dist, stream) in m.ldist and (dist, compon) in m.dhkey: - m.avevlt[dist].setlb( - m.avevlt[dist].lb/m.vp[stream, compon].ub) + m.avevlt[dist].setlb(m.avevlt[dist].lb / m.vp[stream, compon].ub) for dist in m.dist: for stream in m.str: for compon in m.compon: if (dist, stream) in m.vdist and (dist, compon) in m.dlkey: m.avevlt[dist].setub(m.vp[stream, compon].lb) if (dist, stream) in m.vdist and (dist, compon) in m.dhkey: - m.avevlt[dist].setub( - m.avevlt[dist].ub/m.vp[stream, compon].lb) + m.avevlt[dist].setub(m.avevlt[dist].ub / m.vp[stream, compon].lb) # ## initialization procedure @@ -677,7 +1032,7 @@ def boundsofe(m): m.qfuel[1] = 0.0475341 m.q[2] = 54.3002 - file_1 = os.path.join(dir_path,'GAMS_init_stream_data.csv') + file_1 = os.path.join(dir_path, 'GAMS_init_stream_data.csv') stream = pd.read_csv(file_1, usecols=[0]) data = pd.read_csv(file_1, usecols=[1]) temp = pd.read_csv(file_1, usecols=[3]) @@ -691,7 +1046,7 @@ def boundsofe(m): m.f[stream.to_numpy()[i, 0]] = flow.to_numpy()[i, 0] m.e[stream.to_numpy()[i, 0]] = e.to_numpy()[i, 0] - file_2 = os.path.join(dir_path,'GAMS_init_stream_compon_data.csv') + file_2 = os.path.join(dir_path, 'GAMS_init_stream_compon_data.csv') streamfc = pd.read_csv(file_2, usecols=[0]) comp = pd.read_csv(file_2, usecols=[1]) fc = pd.read_csv(file_2, usecols=[2]) @@ -700,12 +1055,10 @@ def boundsofe(m): vp = pd.read_csv(file_2, usecols=[5]) for i in range(len(streamfc)): - m.fc[streamfc.to_numpy()[i, 0], comp.to_numpy()[ - i, 0]] = fc.to_numpy()[i, 0] - m.vp[streamvp.to_numpy()[i, 0], compvp.to_numpy()[ - i, 0]] = vp.to_numpy()[i, 0] + m.fc[streamfc.to_numpy()[i, 0], comp.to_numpy()[i, 0]] = fc.to_numpy()[i, 0] + m.vp[streamvp.to_numpy()[i, 0], compvp.to_numpy()[i, 0]] = vp.to_numpy()[i, 0] - file_3 = os.path.join(dir_path,'GAMS_init_data.csv') + file_3 = os.path.join(dir_path, 'GAMS_init_data.csv') stream3 = pd.read_csv(file_3, usecols=[0]) a = pd.read_csv(file_3, usecols=[1]) avevlt = pd.read_csv(file_3, usecols=[3]) @@ -736,680 +1089,1187 @@ def boundsofe(m): splp = pd.read_csv(file_3, usecols=[51]) splt = pd.read_csv(file_3, usecols=[53]) - for i in range(2): - m.rctp[i+1] = rctp.to_numpy()[i, 0] - m.rctt[i+1] = rctt.to_numpy()[i, 0] - m.rctvol[i+1] = rctvol.to_numpy()[i, 0] - m.sel[i+1] = sel.to_numpy()[i, 0] - m.krct[i+1] = krct.to_numpy()[i, 0] - m.consum[i+1, 'tol'] = consum.to_numpy()[i, 0] - m.conv[i+1, 'tol'] = conv.to_numpy()[i, 0] + m.rctp[i + 1] = rctp.to_numpy()[i, 0] + m.rctt[i + 1] = rctt.to_numpy()[i, 0] + m.rctvol[i + 1] = rctvol.to_numpy()[i, 0] + m.sel[i + 1] = sel.to_numpy()[i, 0] + m.krct[i + 1] = krct.to_numpy()[i, 0] + m.consum[i + 1, 'tol'] = consum.to_numpy()[i, 0] + m.conv[i + 1, 'tol'] = conv.to_numpy()[i, 0] m.a[stream3.to_numpy()[i, 0]] = a.to_numpy()[i, 0] - m.qc[i+1] = qc.to_numpy()[i, 0] + m.qc[i + 1] = qc.to_numpy()[i, 0] for i in range(3): - m.avevlt[i+1] = avevlt.to_numpy()[i, 0] - m.distp[i+1] = disp.to_numpy()[i, 0] - m.flshp[i+1] = flshp.to_numpy()[i, 0] - m.flsht[i+1] = flsht.to_numpy()[i, 0] - m.ndist[i+1] = ndist.to_numpy()[i, 0] - m.nmin[i+1] = nmin.to_numpy()[i, 0] - m.reflux[i+1] = reflux.to_numpy()[i, 0] - m.rmin[i+1] = rmin.to_numpy()[i, 0] - m.splp[i+1] = splp.to_numpy()[i, 0] - m.splt[i+1] = splt.to_numpy()[i, 0] + m.avevlt[i + 1] = avevlt.to_numpy()[i, 0] + m.distp[i + 1] = disp.to_numpy()[i, 0] + m.flshp[i + 1] = flshp.to_numpy()[i, 0] + m.flsht[i + 1] = flsht.to_numpy()[i, 0] + m.ndist[i + 1] = ndist.to_numpy()[i, 0] + m.nmin[i + 1] = nmin.to_numpy()[i, 0] + m.reflux[i + 1] = reflux.to_numpy()[i, 0] + m.rmin[i + 1] = rmin.to_numpy()[i, 0] + m.splp[i + 1] = splp.to_numpy()[i, 0] + m.splt[i + 1] = splt.to_numpy()[i, 0] for i in range(5): m.beta[1, comp1.to_numpy()[i, 0]] = beta.to_numpy()[i, 0] - m.mxrp[i+1] = mxrp.to_numpy()[i, 0] + m.mxrp[i + 1] = mxrp.to_numpy()[i, 0] for i in range(4): - m.qh[i+1] = qh.to_numpy()[i, 0] + m.qh[i + 1] = qh.to_numpy()[i, 0] for i in range(len(stream4)): - m.eflsh[stream4.to_numpy()[i, 0], comp2.to_numpy()[ - i, 0]] = eflsh.to_numpy()[i, 0] + m.eflsh[stream4.to_numpy()[i, 0], comp2.to_numpy()[i, 0]] = eflsh.to_numpy()[ + i, 0 + ] for i in range(6): - m.spl1p[i+1] = spl1p.to_numpy()[i, 0] - m.spl1t[i+1] = spl1t.to_numpy()[i, 0] + m.spl1p[i + 1] = spl1p.to_numpy()[i, 0] + m.spl1t[i + 1] = spl1t.to_numpy()[i, 0] # ## constraints - m.specrec = Constraint(expr=m.fc[72, 'h2'] >= 0.5 * m.f[72]) m.specprod = Constraint(expr=m.fc[31, 'ben'] >= 0.9997 * m.f[31]) def Fbal(_m, stream): return m.f[stream] == sum(m.fc[stream, compon] for compon in m.compon) + m.fbal = Constraint(m.str, rule=Fbal) def H2feed(m, compon): return m.fc[1, compon] == m.f[1] * m.f1comp[compon] + m.h2feed = Constraint(m.compon, rule=H2feed) def Tolfeed(_m, compon): return m.fc[66, compon] == m.f[66] * m.f66comp[compon] + m.tolfeed = Constraint(m.compon, rule=Tolfeed) def Tolabs(_m, compon): return m.fc[67, compon] == m.f[67] * m.f67comp[compon] + m.tolabs = Constraint(m.compon, rule=Tolabs) def build_absorber(b, absorber): - " Function for absorber" + "Function for absorber" + def Absfact(_m, i, compon): if (i, compon) in m.anorm: - return sum(m.f[stream] * m.p[stream] for (absb, stream) in m.ilabs if absb == i) == sum(m.f[stream] for (absc, stream) in m.ivabs if absc == i) * m.aabs[compon] * sum(m.vp[stream, compon] for (absd, stream) in m.ilabs if absd == i) + return sum( + m.f[stream] * m.p[stream] for (absb, stream) in m.ilabs if absb == i + ) == sum( + m.f[stream] for (absc, stream) in m.ivabs if absc == i + ) * m.aabs[ + compon + ] * sum( + m.vp[stream, compon] for (absd, stream) in m.ilabs if absd == i + ) return Constraint.Skip + b.absfact = Constraint( - [absorber], m.compon, rule=Absfact, doc='absorbption factor equation') + [absorber], m.compon, rule=Absfact, doc='absorbption factor equation' + ) def Gameqn(_m, i, compon): if (i, compon) in m.asolv: - return m.gamma[i, compon] == log((1 - m.aabs[compon] ** (m.nabs[i] * m.abseff + m.eps1)) / (1 - m.aabs[compon])) + return m.gamma[i, compon] == log( + (1 - m.aabs[compon] ** (m.nabs[i] * m.abseff + m.eps1)) + / (1 - m.aabs[compon]) + ) return Constraint.Skip - b.gameqn = Constraint([absorber], m.compon, - rule=Gameqn, doc='definition of gamma') + + b.gameqn = Constraint( + [absorber], m.compon, rule=Gameqn, doc='definition of gamma' + ) def Betaeqn(_m, i, compon): if (i, compon) not in m.asimp: - return m.beta[i, compon] == log((1 - m.aabs[compon] ** (m.nabs[i] * m.abseff + 1)) / (1 - m.aabs[compon])) + return m.beta[i, compon] == log( + (1 - m.aabs[compon] ** (m.nabs[i] * m.abseff + 1)) + / (1 - m.aabs[compon]) + ) return Constraint.Skip def Abssvrec(_m, i, compon): if (i, compon) in m.asolv: - return sum(m.fc[stream, compon] for (i, stream) in m.ovabs) * exp(m.beta[i, compon]) == sum(m.fc[stream, compon] for (i_, stream) in m.ivabs) + exp(m.gamma[i, compon]) * sum(m.fc[stream, compon] for (i_, stream) in m.ilabs) + return sum(m.fc[stream, compon] for (i, stream) in m.ovabs) * exp( + m.beta[i, compon] + ) == sum(m.fc[stream, compon] for (i_, stream) in m.ivabs) + exp( + m.gamma[i, compon] + ) * sum( + m.fc[stream, compon] for (i_, stream) in m.ilabs + ) return Constraint.Skip def Absrec(_m, i, compon): if (i, compon) in m.anorm: - return sum(m.fc[i, compon] for (abs, i) in m.ovabs) * exp(m.beta[i, compon]) == sum(m.fc[i, compon] for(abs, i) in m.ivabs) + return sum(m.fc[i, compon] for (abs, i) in m.ovabs) * exp( + m.beta[i, compon] + ) == sum(m.fc[i, compon] for (abs, i) in m.ivabs) return Constraint.Skip def abssimp(_m, absorb, compon): if (absorb, compon) in m.asimp: - return sum(m.fc[i, compon] for (absorb, i) in m.ovabs) == sum(m.fc[i, compon] for (absorb, i) in m.ivabs) / m.cbeta[compon] + return ( + sum(m.fc[i, compon] for (absorb, i) in m.ovabs) + == sum(m.fc[i, compon] for (absorb, i) in m.ivabs) / m.cbeta[compon] + ) return Constraint.Skip def Abscmb(_m, i, compon): - return sum(m.fc[stream, compon] for (i, stream) in m.ilabs) + sum(m.fc[stream, compon] for (i, stream) in m.ivabs) == sum(m.fc[stream, compon] for (i, stream) in m.olabs) + sum(m.fc[stream, compon] for (i, stream) in m.ovabs) - b.abscmb = Constraint([absorber], m.compon, rule=Abscmb, - doc='overall component mass balance') + return sum(m.fc[stream, compon] for (i, stream) in m.ilabs) + sum( + m.fc[stream, compon] for (i, stream) in m.ivabs + ) == sum(m.fc[stream, compon] for (i, stream) in m.olabs) + sum( + m.fc[stream, compon] for (i, stream) in m.ovabs + ) + + b.abscmb = Constraint( + [absorber], m.compon, rule=Abscmb, doc='overall component mass balance' + ) def Abspl(_m, i): - return sum(m.p[stream] for (_, stream) in m.ilabs) == sum(m.p[stream] for (_, stream) in m.olabs) - b.abspl = Constraint([absorber], rule=Abspl, - doc='pressure relation for liquid') + return sum(m.p[stream] for (_, stream) in m.ilabs) == sum( + m.p[stream] for (_, stream) in m.olabs + ) + + b.abspl = Constraint([absorber], rule=Abspl, doc='pressure relation for liquid') def Abstl(_m, i): - return sum(m.t[stream] for (_, stream) in m.ilabs) == sum(m.t[stream] for (_, stream) in m.olabs) - b.abstl = Constraint([absorber], rule=Abstl, - doc=' temperature relation for liquid') + return sum(m.t[stream] for (_, stream) in m.ilabs) == sum( + m.t[stream] for (_, stream) in m.olabs + ) + + b.abstl = Constraint( + [absorber], rule=Abstl, doc=' temperature relation for liquid' + ) def Abspv(_m, i): - return sum(m.p[stream] for (_, stream) in m.ivabs) == sum(m.p[stream] for (_, stream) in m.ovabs) - b.abspv = Constraint([absorber], rule=Abspv, - doc=' pressure relation for vapor') + return sum(m.p[stream] for (_, stream) in m.ivabs) == sum( + m.p[stream] for (_, stream) in m.ovabs + ) + + b.abspv = Constraint([absorber], rule=Abspv, doc=' pressure relation for vapor') def Abspin(_m, i): - return sum(m.p[stream] for (_, stream) in m.ilabs) == sum(m.p[stream] for (_, stream) in m.ivabs) + return sum(m.p[stream] for (_, stream) in m.ilabs) == sum( + m.p[stream] for (_, stream) in m.ivabs + ) + b.absp = Constraint([absorber], rule=Abspin) def Absttop(_m, i): - return sum(m.t[stream] for (_, stream) in m.ilabs) == sum(m.t[stream] for (_, stream) in m.ovabs) - b.abst = Constraint([absorber], rule=Absttop, - doc='temperature relation at top') + return sum(m.t[stream] for (_, stream) in m.ilabs) == sum( + m.t[stream] for (_, stream) in m.ovabs + ) + + b.abst = Constraint([absorber], rule=Absttop, doc='temperature relation at top') b.abssimp = Constraint( - [absorber], m.compon, rule=abssimp, doc=' recovery of simplified components') - b.absrec = Constraint([absorber], m.compon, - rule=Absrec, doc='recovery of non-solvent') - b.abssvrec = Constraint([absorber], m.compon, - rule=Abssvrec, doc='recovery of solvent') - b.betaeqn = Constraint([absorber], m.compon, - rule=Betaeqn, doc='definition of beta') + [absorber], m.compon, rule=abssimp, doc=' recovery of simplified components' + ) + b.absrec = Constraint( + [absorber], m.compon, rule=Absrec, doc='recovery of non-solvent' + ) + b.abssvrec = Constraint( + [absorber], m.compon, rule=Abssvrec, doc='recovery of solvent' + ) + b.betaeqn = Constraint( + [absorber], m.compon, rule=Betaeqn, doc='definition of beta' + ) def build_compressor(b, comp): def Compcmb(_m, comp1, compon): if comp1 == comp: - return sum(m.fc[stream, compon] for (comp_, stream) in m.ocomp if comp_ == comp1) == sum(m.fc[stream, compon] for (comp_, stream) in m.icomp if comp_ == comp1) + return sum( + m.fc[stream, compon] + for (comp_, stream) in m.ocomp + if comp_ == comp1 + ) == sum( + m.fc[stream, compon] + for (comp_, stream) in m.icomp + if comp_ == comp1 + ) return Constraint.Skip + b.compcmb = Constraint( - [comp], m.compon, rule=Compcmb, doc='component balance for compressor') + [comp], m.compon, rule=Compcmb, doc='component balance for compressor' + ) def Comphb(_m, comp1): if comp1 == comp: - return sum(m.t[stream] for (_, stream) in m.ocomp if _ == comp) == m.presrat[comp] * sum(m.t[stream] for (_, stream) in m.icomp if _ == comp) + return sum( + m.t[stream] for (_, stream) in m.ocomp if _ == comp + ) == m.presrat[comp] * sum( + m.t[stream] for (_, stream) in m.icomp if _ == comp + ) return Constraint.Skip - b.comphb = Constraint([comp], rule=Comphb, - doc='heat balance for compressor') + + b.comphb = Constraint([comp], rule=Comphb, doc='heat balance for compressor') def Compelec(_m, comp_): if comp_ == comp: - return m.elec[comp_] == m.alpha * (m.presrat[comp_] - 1) * sum(100. * m.t[stream] * m.f[stream] / 60. * (1./m.compeff) * (m.gam / (m.gam - 1.)) for (comp1, stream) in m.icomp if comp_ == comp1) - return Constraint.Skip - b.compelec = Constraint([comp], rule=Compelec, - doc="energy balance for compressor") + return m.elec[comp_] == m.alpha * (m.presrat[comp_] - 1) * sum( + 100.0 + * m.t[stream] + * m.f[stream] + / 60.0 + * (1.0 / m.compeff) + * (m.gam / (m.gam - 1.0)) + for (comp1, stream) in m.icomp + if comp_ == comp1 + ) + return Constraint.Skip + + b.compelec = Constraint( + [comp], rule=Compelec, doc="energy balance for compressor" + ) def Ratio(_m, comp_): if comp == comp_: - return m.presrat[comp_] ** (m.gam/(m.gam-1.)) == sum(m.p[stream] for (comp1, stream) in m.ocomp if comp_ == comp1) / sum(m.p[stream] for (comp1, stream) in m.icomp if comp1 == comp_) + return m.presrat[comp_] ** (m.gam / (m.gam - 1.0)) == sum( + m.p[stream] for (comp1, stream) in m.ocomp if comp_ == comp1 + ) / sum(m.p[stream] for (comp1, stream) in m.icomp if comp1 == comp_) return Constraint.Skip - b.ratio = Constraint([comp], rule=Ratio, - doc='pressure ratio (out to in)') + b.ratio = Constraint([comp], rule=Ratio, doc='pressure ratio (out to in)') m.vapor_pressure_unit_match = Param( - initialize=7500.6168, doc="unit match coeffieicnt for vapor pressure calculation") - m.actual_reflux_ratio = Param( - initialize=1.2, doc="actual reflux ratio coeffieicnt") + initialize=7500.6168, + doc="unit match coeffieicnt for vapor pressure calculation", + ) + m.actual_reflux_ratio = Param(initialize=1.2, doc="actual reflux ratio coeffieicnt") m.recovery_specification_coeffieicnt = Param( - initialize=0.05, doc="recovery specification coeffieicnt") + initialize=0.05, doc="recovery specification coeffieicnt" + ) def build_distillation(b, dist): def Antdistb(_m, dist_, stream, compon): - if (dist_, stream) in m.ldist and (dist_, compon) in m.dkey and dist_ == dist: - return log(m.vp[stream, compon] * m.vapor_pressure_unit_match) == m.anta[compon] - m.antb[compon] / (m.t[stream] * 100. + m.antc[compon]) + if ( + (dist_, stream) in m.ldist + and (dist_, compon) in m.dkey + and dist_ == dist + ): + return log( + m.vp[stream, compon] * m.vapor_pressure_unit_match + ) == m.anta[compon] - m.antb[compon] / ( + m.t[stream] * 100.0 + m.antc[compon] + ) return Constraint.Skip + b.antdistb = Constraint( - [dist], m.str, m.compon, rule=Antdistb, doc=' vapor pressure correlation (bot)') + [dist], + m.str, + m.compon, + rule=Antdistb, + doc=' vapor pressure correlation (bot)', + ) def Antdistt(_m, dist_, stream, compon): - if (dist_, stream) in m.vdist and (dist_, compon) in m.dkey and dist == dist_: - return log(m.vp[stream, compon] * m.vapor_pressure_unit_match) == m.anta[compon] - m.antb[compon] / (m.t[stream] * 100. + m.antc[compon]) + if ( + (dist_, stream) in m.vdist + and (dist_, compon) in m.dkey + and dist == dist_ + ): + return log( + m.vp[stream, compon] * m.vapor_pressure_unit_match + ) == m.anta[compon] - m.antb[compon] / ( + m.t[stream] * 100.0 + m.antc[compon] + ) return Constraint.Skip + b.antdistt = Constraint( - [dist], m.str, m.compon, rule=Antdistt, doc='vapor pressure correlation (top)') + [dist], + m.str, + m.compon, + rule=Antdistt, + doc='vapor pressure correlation (top)', + ) def Relvol(_m, dist_): if dist == dist_: - divided1 = sum(sum(m.vp[stream, compon] for (dist_, compon) in m.dlkey if dist_ == dist)/sum(m.vp[stream, compon] - for (dist_, compon) in m.dhkey if dist_ == dist) for (dist_, stream) in m.vdist if dist_ == dist) - divided2 = sum(sum(m.vp[stream, compon] for (dist_, compon) in m.dlkey if dist_ == dist)/sum(m.vp[stream, compon] - for (dist_, compon) in m.dhkey if dist_ == dist) for (dist_, stream) in m.ldist if dist_ == dist) + divided1 = sum( + sum( + m.vp[stream, compon] + for (dist_, compon) in m.dlkey + if dist_ == dist + ) + / sum( + m.vp[stream, compon] + for (dist_, compon) in m.dhkey + if dist_ == dist + ) + for (dist_, stream) in m.vdist + if dist_ == dist + ) + divided2 = sum( + sum( + m.vp[stream, compon] + for (dist_, compon) in m.dlkey + if dist_ == dist + ) + / sum( + m.vp[stream, compon] + for (dist_, compon) in m.dhkey + if dist_ == dist + ) + for (dist_, stream) in m.ldist + if dist_ == dist + ) return m.avevlt[dist] == sqrt(divided1 * divided2) return Constraint.Skip - b.relvol = Constraint([dist], rule=Relvol, - doc='average relative volatilty') + + b.relvol = Constraint([dist], rule=Relvol, doc='average relative volatilty') def Undwood(_m, dist_): if dist_ == dist: - return sum(m.fc[stream, compon] for (dist1, compon) in m.dlkey if dist1 == dist_ for (dist1, stream) in m.idist if dist1 == dist_) * m.rmin[dist_] * (m.avevlt[dist_] - 1) == sum(m.f[stream] for (dist1, stream) in m.idist if dist1 == dist_) + return sum( + m.fc[stream, compon] + for (dist1, compon) in m.dlkey + if dist1 == dist_ + for (dist1, stream) in m.idist + if dist1 == dist_ + ) * m.rmin[dist_] * (m.avevlt[dist_] - 1) == sum( + m.f[stream] for (dist1, stream) in m.idist if dist1 == dist_ + ) return Constraint.Skip - b.undwood = Constraint([dist], rule=Undwood, - doc='minimum reflux ratio equation') + + b.undwood = Constraint( + [dist], rule=Undwood, doc='minimum reflux ratio equation' + ) def Actreflux(_m, dist_): if dist_ == dist: return m.reflux[dist_] == m.actual_reflux_ratio * m.rmin[dist_] return Constraint.Skip - b.actreflux = Constraint( - [dist], rule=Actreflux, doc='actual reflux ratio') + + b.actreflux = Constraint([dist], rule=Actreflux, doc='actual reflux ratio') def Fenske(_m, dist_): if dist == dist_: - sum1 = sum((m.f[stream] + m.eps1)/(m.fc[stream, compon] + m.eps1) for (dist1, compon) - in m.dhkey if dist1 == dist_ for (dist1, stream) in m.vdist if dist1 == dist_) - sum2 = sum((m.f[stream] + m.eps1)/(m.fc[stream, compon] + m.eps1) for (dist1, compon) - in m.dlkey if dist1 == dist_ for (dist1, stream) in m.ldist if dist1 == dist_) + sum1 = sum( + (m.f[stream] + m.eps1) / (m.fc[stream, compon] + m.eps1) + for (dist1, compon) in m.dhkey + if dist1 == dist_ + for (dist1, stream) in m.vdist + if dist1 == dist_ + ) + sum2 = sum( + (m.f[stream] + m.eps1) / (m.fc[stream, compon] + m.eps1) + for (dist1, compon) in m.dlkey + if dist1 == dist_ + for (dist1, stream) in m.ldist + if dist1 == dist_ + ) return m.nmin[dist_] * log(m.avevlt[dist_]) == log(sum1 * sum2) return Constraint.Skip - b.fenske = Constraint([dist], rule=Fenske, - doc='minimum number of trays') + + b.fenske = Constraint([dist], rule=Fenske, doc='minimum number of trays') def Acttray(_m, dist_): if dist == dist_: - return m.ndist[dist_] == m.nmin[dist_] * 2. / m.disteff + return m.ndist[dist_] == m.nmin[dist_] * 2.0 / m.disteff return Constraint.Skip - b.acttray = Constraint([dist], rule=Acttray, - doc='actual number of trays') + + b.acttray = Constraint([dist], rule=Acttray, doc='actual number of trays') def Distspec(_m, dist_, stream, compon): - if (dist_, stream) in m.vdist and (dist_, compon) in m.dhkey and dist_ == dist: - return m.fc[stream, compon] <= m.recovery_specification_coeffieicnt * sum(m.fc[str2, compon] for (dist_, str2) in m.idist if dist == dist_) - return Constraint.Skip - b.distspec = Constraint([dist], m.str, m.compon, - rule=Distspec, doc='recovery specification') + if ( + (dist_, stream) in m.vdist + and (dist_, compon) in m.dhkey + and dist_ == dist + ): + return m.fc[ + stream, compon + ] <= m.recovery_specification_coeffieicnt * sum( + m.fc[str2, compon] for (dist_, str2) in m.idist if dist == dist_ + ) + return Constraint.Skip + + b.distspec = Constraint( + [dist], m.str, m.compon, rule=Distspec, doc='recovery specification' + ) def Distheav(_m, dist_, compon): if (dist_, compon) in m.dh and dist == dist_: - return sum(m.fc[str2, compon] for (dist_, str2) in m.idist if dist_ == dist) == sum(m.fc[str2, compon] for (dist_, str2) in m.ldist if dist_ == dist) + return sum( + m.fc[str2, compon] for (dist_, str2) in m.idist if dist_ == dist + ) == sum( + m.fc[str2, compon] for (dist_, str2) in m.ldist if dist_ == dist + ) return Constraint.Skip - b.distheav = Constraint( - [dist], m.compon, rule=Distheav, doc='heavy components') + + b.distheav = Constraint([dist], m.compon, rule=Distheav, doc='heavy components') def Distlite(_m, dist_, compon): if (dist_, compon) in m.dl and dist_ == dist: - return sum(m.fc[str2, compon] for (dist_, str2) in m.idist if dist == dist_) == sum(m.fc[str2, compon] for (dist_, str2) in m.vdist if dist == dist_) + return sum( + m.fc[str2, compon] for (dist_, str2) in m.idist if dist == dist_ + ) == sum( + m.fc[str2, compon] for (dist_, str2) in m.vdist if dist == dist_ + ) return Constraint.Skip - b.distlite = Constraint( - [dist], m.compon, rule=Distlite, doc='light components') + + b.distlite = Constraint([dist], m.compon, rule=Distlite, doc='light components') def Distpi(_m, dist_, stream): if (dist_, stream) in m.idist and dist_ == dist: return m.distp[dist_] <= m.p[stream] return Constraint.Skip - b.distpi = Constraint([dist], m.str, rule=Distpi, - doc='inlet pressure relation') + + b.distpi = Constraint([dist], m.str, rule=Distpi, doc='inlet pressure relation') def Distvpl(_m, dist_, stream): if (dist_, stream) in m.ldist and dist == dist_: - return m.distp[dist_] == sum(m.vp[stream, compon] for (dist_, compon) in m.dhkey if dist_ == dist) + return m.distp[dist_] == sum( + m.vp[stream, compon] for (dist_, compon) in m.dhkey if dist_ == dist + ) return Constraint.Skip - b.distvpl = Constraint([dist], m.str, rule=Distvpl, - doc='bottom vapor pressure relation') + + b.distvpl = Constraint( + [dist], m.str, rule=Distvpl, doc='bottom vapor pressure relation' + ) def Distvpv(_m, dist_, stream): if dist > 1 and (dist, stream) in m.vdist and dist_ == dist: - return m.distp[dist_] == sum(m.vp[stream, compon] for (dist_, compon) in m.dlkey if dist_ == dist) + return m.distp[dist_] == sum( + m.vp[stream, compon] for (dist_, compon) in m.dlkey if dist_ == dist + ) return Constraint.Skip - b.distvpv = Constraint([dist], m.str, rule=Distvpv, - doc='top vapor pressure relation') + + b.distvpv = Constraint( + [dist], m.str, rule=Distvpv, doc='top vapor pressure relation' + ) def Distpl(_m, dist_, stream): if (dist_, stream) in m.ldist and dist_ == dist: return m.distp[dist_] == m.p[stream] return Constraint.Skip - b.distpl = Constraint([dist], m.str, rule=Distpl, - doc='outlet pressure relation(liquid)') + + b.distpl = Constraint( + [dist], m.str, rule=Distpl, doc='outlet pressure relation(liquid)' + ) def Distpv(_m, dist_, stream): if (dist_, stream) in m.vdist and dist == dist_: return m.distp[dist_] == m.p[stream] return Constraint.Skip - b.distpv = Constraint([dist], m.str, rule=Distpv, - doc='outlet pressure relation(vapor)') + + b.distpv = Constraint( + [dist], m.str, rule=Distpv, doc='outlet pressure relation(vapor)' + ) def Distcmb(_m, dist_, compon): if dist_ == dist: - return sum(m.fc[stream, compon] for (dist1, stream) in m.idist if dist1 == dist_) == sum(m.fc[stream, compon] for (dist1, stream) in m.vdist if dist1 == dist_) + sum(m.fc[stream, compon] for (dist1, stream) in m.ldist if dist1 == dist_) + return sum( + m.fc[stream, compon] + for (dist1, stream) in m.idist + if dist1 == dist_ + ) == sum( + m.fc[stream, compon] + for (dist1, stream) in m.vdist + if dist1 == dist_ + ) + sum( + m.fc[stream, compon] + for (dist1, stream) in m.ldist + if dist1 == dist_ + ) return Constraint.Skip - b.distcmb = Constraint( - [dist], m.compon, rule=Distcmb, doc='component mass balance') - + b.distcmb = Constraint( + [dist], m.compon, rule=Distcmb, doc='component mass balance' + ) def build_flash(b, flsh): def Flshcmb(_m, flsh_, compon): if flsh_ in m.flsh and compon in m.compon and flsh_ == flsh: - return sum(m.fc[stream, compon] for (flsh1, stream) in m.iflsh if flsh1 == flsh_) == sum(m.fc[stream, compon] for (flsh1, stream) in m.vflsh if flsh1 == flsh_) + sum(m.fc[stream, compon] for (flsh1, stream) in m.lflsh if flsh1 == flsh_) + return sum( + m.fc[stream, compon] + for (flsh1, stream) in m.iflsh + if flsh1 == flsh_ + ) == sum( + m.fc[stream, compon] + for (flsh1, stream) in m.vflsh + if flsh1 == flsh_ + ) + sum( + m.fc[stream, compon] + for (flsh1, stream) in m.lflsh + if flsh1 == flsh_ + ) return Constraint.Skip + b.flshcmb = Constraint( - [flsh], m.compon, rule=Flshcmb, doc='component mass balance') + [flsh], m.compon, rule=Flshcmb, doc='component mass balance' + ) def Antflsh(_m, flsh_, stream, compon): if (flsh_, stream) in m.lflsh and flsh_ == flsh: - return log(m.vp[stream, compon] * m.vapor_pressure_unit_match) == m.anta[compon] - m.antb[compon] / (m.t[stream] * 100. + m.antc[compon]) + return log( + m.vp[stream, compon] * m.vapor_pressure_unit_match + ) == m.anta[compon] - m.antb[compon] / ( + m.t[stream] * 100.0 + m.antc[compon] + ) return Constraint.Skip - b.antflsh = Constraint([flsh], m.str, m.compon, - rule=Antflsh, doc='flash pressure relation') + + b.antflsh = Constraint( + [flsh], m.str, m.compon, rule=Antflsh, doc='flash pressure relation' + ) def Flshrec(_m, flsh_, stream, compon): if (flsh_, stream) in m.lflsh and flsh_ == flsh: - return sum(m.eflsh[flsh1, compon2] for (flsh1, compon2) in m.fkey if flsh1 == flsh_) * (m.eflsh[flsh_, compon] * sum(m.vp[stream, compon2] for (flsh1, compon2) in m.fkey if flsh_ == flsh1) + (1. - m.eflsh[flsh_, compon]) * m.vp[stream, compon]) == sum(m.vp[stream, compon2] for (flsh1, compon2) in m.fkey if flsh_ == flsh1) * m.eflsh[flsh_, compon] + return ( + sum( + m.eflsh[flsh1, compon2] + for (flsh1, compon2) in m.fkey + if flsh1 == flsh_ + ) + * ( + m.eflsh[flsh_, compon] + * sum( + m.vp[stream, compon2] + for (flsh1, compon2) in m.fkey + if flsh_ == flsh1 + ) + + (1.0 - m.eflsh[flsh_, compon]) * m.vp[stream, compon] + ) + == sum( + m.vp[stream, compon2] + for (flsh1, compon2) in m.fkey + if flsh_ == flsh1 + ) + * m.eflsh[flsh_, compon] + ) return Constraint.Skip - b.flshrec = Constraint([flsh], m.str, m.compon, - rule=Flshrec, doc='vapor recovery relation') + + b.flshrec = Constraint( + [flsh], m.str, m.compon, rule=Flshrec, doc='vapor recovery relation' + ) def Flsheql(_m, flsh_, compon): if flsh in m.flsh and compon in m.compon and flsh_ == flsh: - return sum(m.fc[stream, compon] for (flsh1, stream) in m.vflsh if flsh1 == flsh_) == sum(m.fc[stream, compon] for (flsh1, stream) in m.iflsh if flsh1 == flsh_) * m.eflsh[flsh, compon] + return ( + sum( + m.fc[stream, compon] + for (flsh1, stream) in m.vflsh + if flsh1 == flsh_ + ) + == sum( + m.fc[stream, compon] + for (flsh1, stream) in m.iflsh + if flsh1 == flsh_ + ) + * m.eflsh[flsh, compon] + ) return Constraint.Skip + b.flsheql = Constraint( - [flsh], m.compon, rule=Flsheql, doc='equilibrium relation') + [flsh], m.compon, rule=Flsheql, doc='equilibrium relation' + ) def Flshpr(_m, flsh_, stream): if (flsh_, stream) in m.lflsh and flsh_ == flsh: - return m.flshp[flsh_] * m.f[stream] == sum(m.vp[stream, compon] * m.fc[stream, compon] for compon in m.compon) + return m.flshp[flsh_] * m.f[stream] == sum( + m.vp[stream, compon] * m.fc[stream, compon] for compon in m.compon + ) return Constraint.Skip - b.flshpr = Constraint([flsh], m.str, rule=Flshpr, - doc='flash pressure relation') + + b.flshpr = Constraint([flsh], m.str, rule=Flshpr, doc='flash pressure relation') def Flshpi(_m, flsh_, stream): if (flsh_, stream) in m.iflsh and flsh_ == flsh: return m.flshp[flsh_] == m.p[stream] return Constraint.Skip - b.flshpi = Constraint([flsh], m.str, rule=Flshpi, - doc='inlet pressure relation') + + b.flshpi = Constraint([flsh], m.str, rule=Flshpi, doc='inlet pressure relation') def Flshpl(_m, flsh_, stream): if (flsh_, stream) in m.lflsh and flsh_ == flsh: return m.flshp[flsh_] == m.p[stream] return Constraint.Skip - b.flshpl = Constraint([flsh], m.str, rule=Flshpl, - doc='outlet pressure relation(liquid)') + + b.flshpl = Constraint( + [flsh], m.str, rule=Flshpl, doc='outlet pressure relation(liquid)' + ) def Flshpv(_m, flsh_, stream): if (flsh_, stream) in m.vflsh and flsh_ == flsh: return m.flshp[flsh_] == m.p[stream] return Constraint.Skip - b.flshpv = Constraint([flsh], m.str, rule=Flshpv, - doc='outlet pressure relation(vapor)') + + b.flshpv = Constraint( + [flsh], m.str, rule=Flshpv, doc='outlet pressure relation(vapor)' + ) def Flshti(_m, flsh_, stream): if (flsh_, stream) in m.iflsh and flsh_ == flsh: return m.flsht[flsh_] == m.t[stream] return Constraint.Skip - b.flshti = Constraint([flsh], m.str, rule=Flshti, - doc='inlet temp. relation') + + b.flshti = Constraint([flsh], m.str, rule=Flshti, doc='inlet temp. relation') def Flshtl(_m, flsh_, stream): if (flsh_, stream) in m.lflsh and flsh_ == flsh: return m.flsht[flsh_] == m.t[stream] return Constraint.Skip - b.flshtl = Constraint([flsh], m.str, rule=Flshtl, - doc='outlet temp. relation(liquid)') + + b.flshtl = Constraint( + [flsh], m.str, rule=Flshtl, doc='outlet temp. relation(liquid)' + ) def Flshtv(_m, flsh_, stream): if (flsh_, stream) in m.vflsh and flsh_ == flsh: return m.flsht[flsh_] == m.t[stream] return Constraint.Skip - b.flshtv = Constraint([flsh], m.str, rule=Flshtv, - doc='outlet temp. relation(vapor)') - + b.flshtv = Constraint( + [flsh], m.str, rule=Flshtv, doc='outlet temp. relation(vapor)' + ) + m.heat_unit_match = Param( - initialize=3600. * 8500. * 1.0e-12 / 60., doc="unit change on temp") + initialize=3600.0 * 8500.0 * 1.0e-12 / 60.0, doc="unit change on temp" + ) def build_furnace(b, furnace): def Furnhb(_m, furn): if furn == furnace: - return m.qfuel[furn] == (sum(m.cp[stream] * m.f[stream] * 100. * m.t[stream] for (furn, stream) in m.ofurn) - sum(m.cp[stream] * m.f[stream] * 100. * m.t[stream] for (furn, stream) in m.ifurn)) * m.heat_unit_match + return ( + m.qfuel[furn] + == ( + sum( + m.cp[stream] * m.f[stream] * 100.0 * m.t[stream] + for (furn, stream) in m.ofurn + ) + - sum( + m.cp[stream] * m.f[stream] * 100.0 * m.t[stream] + for (furn, stream) in m.ifurn + ) + ) + * m.heat_unit_match + ) return Constraint.Skip + b.furnhb = Constraint([furnace], rule=Furnhb, doc='heat balance') def Furncmb(_m, furn, compon): if furn == furnace: - return sum(m.fc[stream, compon] for (furn, stream) in m.ofurn) == sum(m.fc[stream, compon] for (furn, stream) in m.ifurn) + return sum(m.fc[stream, compon] for (furn, stream) in m.ofurn) == sum( + m.fc[stream, compon] for (furn, stream) in m.ifurn + ) return Constraint.Skip - b.furncmb = Constraint([furnace], m.compon, - rule=Furncmb, doc='component mass balance') + + b.furncmb = Constraint( + [furnace], m.compon, rule=Furncmb, doc='component mass balance' + ) def Furnp(_m, furn): if furn == furnace: - return sum(m.p[stream] for (furn, stream) in m.ofurn) == sum(m.p[stream] for (furn, stream) in m.ifurn) - m.furnpdrop + return ( + sum(m.p[stream] for (furn, stream) in m.ofurn) + == sum(m.p[stream] for (furn, stream) in m.ifurn) - m.furnpdrop + ) return Constraint.Skip - b.furnp = Constraint([furnace], rule=Furnp, doc=' pressure relation ') - + b.furnp = Constraint([furnace], rule=Furnp, doc=' pressure relation ') def build_cooler(b, cooler): def Heccmb(_m, hec, compon): - return sum(m.fc[stream, compon] for (hec_, stream) in m.ohec if hec_ == hec) == sum(m.fc[stream, compon] for (hec_, stream) in m.ihec if hec_ == hec) - b.heccmb = Constraint([cooler], m.compon, - rule=Heccmb, doc='heat balance') + return sum( + m.fc[stream, compon] for (hec_, stream) in m.ohec if hec_ == hec + ) == sum(m.fc[stream, compon] for (hec_, stream) in m.ihec if hec_ == hec) + + b.heccmb = Constraint([cooler], m.compon, rule=Heccmb, doc='heat balance') def Hechb(_m, hec): - return m.qc[hec] == (sum(m.cp[stream] * m.f[stream] * 100. * m.t[stream] for (hec_, stream) in m.ihec if hec_ == hec) - sum(m.cp[stream] * m.f[stream] * 100. * m.t[stream] for (hec_, stream) in m.ohec if hec_ == hec)) * m.heat_unit_match - b.hechb = Constraint([cooler], rule=Hechb, - doc='component mass balance') + return ( + m.qc[hec] + == ( + sum( + m.cp[stream] * m.f[stream] * 100.0 * m.t[stream] + for (hec_, stream) in m.ihec + if hec_ == hec + ) + - sum( + m.cp[stream] * m.f[stream] * 100.0 * m.t[stream] + for (hec_, stream) in m.ohec + if hec_ == hec + ) + ) + * m.heat_unit_match + ) + + b.hechb = Constraint([cooler], rule=Hechb, doc='component mass balance') def Hecp(_m, hec): - return sum(m.p[stream] for(hec_, stream) in m.ihec if hec_ == hec) == sum(m.p[stream] for(hec_, stream) in m.ohec if hec_ == hec) - b.hecp = Constraint([cooler], rule=Hecp, doc='pressure relation') + return sum(m.p[stream] for (hec_, stream) in m.ihec if hec_ == hec) == sum( + m.p[stream] for (hec_, stream) in m.ohec if hec_ == hec + ) - + b.hecp = Constraint([cooler], rule=Hecp, doc='pressure relation') def build_heater(b, heater): def Hehcmb(_m, heh, compon): if heh == heater and compon in m.compon: - return sum(m.fc[stream, compon] for (heh_, stream) in m.oheh if heh_ == heh) == sum(m.fc[stream, compon] for (heh_, stream) in m.iheh if heh == heh_) + return sum( + m.fc[stream, compon] for (heh_, stream) in m.oheh if heh_ == heh + ) == sum( + m.fc[stream, compon] for (heh_, stream) in m.iheh if heh == heh_ + ) return Constraint.Skip - b.hehcmb = Constraint(Set( - initialize=[heater]), m.compon, rule=Hehcmb, doc='component balance in heater') + + b.hehcmb = Constraint( + Set(initialize=[heater]), + m.compon, + rule=Hehcmb, + doc='component balance in heater', + ) def Hehhb(_m, heh): if heh == heater: - return m.qh[heh] == (sum(m.cp[stream] * m.f[stream] * 100. * m.t[stream] for (heh_, stream) in m.oheh if heh_ == heh) - - sum(m.cp[stream] * m.f[stream] * 100. * m.t[stream] for (heh_, stream) in m.iheh if heh_ == heh)) * m.heat_unit_match + return ( + m.qh[heh] + == ( + sum( + m.cp[stream] * m.f[stream] * 100.0 * m.t[stream] + for (heh_, stream) in m.oheh + if heh_ == heh + ) + - sum( + m.cp[stream] * m.f[stream] * 100.0 * m.t[stream] + for (heh_, stream) in m.iheh + if heh_ == heh + ) + ) + * m.heat_unit_match + ) return Constraint.Skip + b.hehhb = Constraint( - Set(initialize=[heater]), rule=Hehhb, doc='heat balance for heater') + Set(initialize=[heater]), rule=Hehhb, doc='heat balance for heater' + ) def hehp(_m, heh): if heh == heater: - return sum(m.p[stream] for(heh_, stream) in m.iheh if heh_ == heh) == sum(m.p[stream] for(heh_, stream) in m.oheh if heh == heh_) + return sum( + m.p[stream] for (heh_, stream) in m.iheh if heh_ == heh + ) == sum(m.p[stream] for (heh_, stream) in m.oheh if heh == heh_) return Constraint.Skip + b.Hehp = Constraint( - Set(initialize=[heater]), rule=hehp, doc='no pressure drop thru heater') + Set(initialize=[heater]), rule=hehp, doc='no pressure drop thru heater' + ) - m.exchanger_temp_drop = Param(initialize=0.25) def build_exchanger(b, exchanger): def Exchcmbc(_m, exch, compon): if exch in m.exch and compon in m.compon: - return sum(m.fc[stream, compon] for (exch_, stream) in m.ocexch if exch == exch_) == sum(m.fc[stream, compon] for (exch_, stream) in m.icexch if exch == exch_) + return sum( + m.fc[stream, compon] + for (exch_, stream) in m.ocexch + if exch == exch_ + ) == sum( + m.fc[stream, compon] + for (exch_, stream) in m.icexch + if exch == exch_ + ) return Constraint.Skip - b.exchcmbc = Constraint([exchanger], m.compon, - rule=Exchcmbc, doc='component balance (cold)') + + b.exchcmbc = Constraint( + [exchanger], m.compon, rule=Exchcmbc, doc='component balance (cold)' + ) def Exchcmbh(_m, exch, compon): if exch in m.exch and compon in m.compon: - return sum(m.fc[stream, compon] for (exch_, stream) in m.ohexch if exch == exch_) == sum(m.fc[stream, compon] for (exch_, stream) in m.ihexch if exch == exch_) + return sum( + m.fc[stream, compon] + for (exch_, stream) in m.ohexch + if exch == exch_ + ) == sum( + m.fc[stream, compon] + for (exch_, stream) in m.ihexch + if exch == exch_ + ) return Constraint.Skip - b.exchcmbh = Constraint([exchanger], m.compon, - rule=Exchcmbh, doc='component balance (hot)') + + b.exchcmbh = Constraint( + [exchanger], m.compon, rule=Exchcmbh, doc='component balance (hot)' + ) def Exchhbc(_m, exch): if exch in m.exch: - return (sum(m.cp[stream] * m.f[stream] * 100. * m.t[stream] for (exch_, stream) in m.ocexch if exch == exch_) - sum(m.cp[stream] * m.f[stream] * 100. * m.t[stream] for (exch_, stream) in m.icexch if exch == exch_)) * m.heat_unit_match == m.qexch[exch] + return ( + sum( + m.cp[stream] * m.f[stream] * 100.0 * m.t[stream] + for (exch_, stream) in m.ocexch + if exch == exch_ + ) + - sum( + m.cp[stream] * m.f[stream] * 100.0 * m.t[stream] + for (exch_, stream) in m.icexch + if exch == exch_ + ) + ) * m.heat_unit_match == m.qexch[exch] return Constraint.Skip - b.exchhbc = Constraint([exchanger], rule=Exchhbc, - doc='heat balance for cold stream') + + b.exchhbc = Constraint( + [exchanger], rule=Exchhbc, doc='heat balance for cold stream' + ) def Exchhbh(_m, exch): - return (sum(m.cp[stream] * m.f[stream] * 100. * m.t[stream] for (exch, stream) in m.ihexch) - sum(m.cp[stream] * m.f[stream] * 100. * m.t[stream] for (exch, stream) in m.ohexch)) * m.heat_unit_match == m.qexch[exch] - b.exchhbh = Constraint([exchanger], rule=Exchhbh, - doc='heat balance for hot stream') + return ( + sum( + m.cp[stream] * m.f[stream] * 100.0 * m.t[stream] + for (exch, stream) in m.ihexch + ) + - sum( + m.cp[stream] * m.f[stream] * 100.0 * m.t[stream] + for (exch, stream) in m.ohexch + ) + ) * m.heat_unit_match == m.qexch[exch] + + b.exchhbh = Constraint( + [exchanger], rule=Exchhbh, doc='heat balance for hot stream' + ) def Exchdtm1(_m, exch): - return sum(m.t[stream] for (exch, stream) in m.ohexch) >= sum(m.t[stream] for (exch, stream) in m.icexch) + m.exchanger_temp_drop - b.exchdtm1 = Constraint( - [exchanger], rule=Exchdtm1, doc='delta t min condition') + return ( + sum(m.t[stream] for (exch, stream) in m.ohexch) + >= sum(m.t[stream] for (exch, stream) in m.icexch) + + m.exchanger_temp_drop + ) + + b.exchdtm1 = Constraint([exchanger], rule=Exchdtm1, doc='delta t min condition') def Exchdtm2(_m, exch): - return sum(m.t[stream] for (exch, stream) in m.ocexch) <= sum(m.t[stream] for (exch, stream) in m.ihexch) - m.exchanger_temp_drop - b.exchdtm2 = Constraint( - [exchanger], rule=Exchdtm2, doc='delta t min condition') + return ( + sum(m.t[stream] for (exch, stream) in m.ocexch) + <= sum(m.t[stream] for (exch, stream) in m.ihexch) + - m.exchanger_temp_drop + ) + + b.exchdtm2 = Constraint([exchanger], rule=Exchdtm2, doc='delta t min condition') def Exchpc(_m, exch): - return sum(m.p[stream] for (exch, stream) in m.ocexch) == sum(m.p[stream] for (exch, stream) in m.icexch) - b.exchpc = Constraint([exchanger], rule=Exchpc, - doc='pressure relation (cold)') + return sum(m.p[stream] for (exch, stream) in m.ocexch) == sum( + m.p[stream] for (exch, stream) in m.icexch + ) + + b.exchpc = Constraint([exchanger], rule=Exchpc, doc='pressure relation (cold)') def Exchph(_m, exch): - return sum(m.p[stream] for (exch, stream) in m.ohexch) == sum(m.p[stream] for (exch, stream) in m.ihexch) - b.exchph = Constraint([exchanger], rule=Exchph, - doc='pressure relation (hot)') + return sum(m.p[stream] for (exch, stream) in m.ohexch) == sum( + m.p[stream] for (exch, stream) in m.ihexch + ) + + b.exchph = Constraint([exchanger], rule=Exchph, doc='pressure relation (hot)') - m.membrane_recovery_sepc = Param(initialize=0.50) m.membrane_purity_sepc = Param(initialize=0.50) def build_membrane(b, membrane): def Memcmb(_m, memb, stream, compon): if (memb, stream) in m.imemb and memb == membrane: - return m.fc[stream, compon] == sum(m.fc[stream, compon] for (memb_, stream) in m.pmemb if memb == memb_) + sum(m.fc[stream, compon] for (memb_, stream) in m.nmemb if memb == memb_) + return m.fc[stream, compon] == sum( + m.fc[stream, compon] for (memb_, stream) in m.pmemb if memb == memb_ + ) + sum( + m.fc[stream, compon] for (memb_, stream) in m.nmemb if memb == memb_ + ) return Constraint.Skip - b.memcmb = Constraint([membrane], m.str, m.compon, - rule=Memcmb, doc='component mass balance') + + b.memcmb = Constraint( + [membrane], m.str, m.compon, rule=Memcmb, doc='component mass balance' + ) def Flux(_m, memb, stream, compon): - if (memb, stream) in m.pmemb and (memb, compon) in m.mnorm and memb == membrane: - return m.fc[stream, compon] == m.a[memb] * m.perm[compon] / 2.0 * (sum(m.p[stream2] for (memb_, stream2) in m.imemb if memb_ == memb) * (sum((m.fc[stream2, compon] + m.eps1)/(m.f[stream2] + m.eps1) for (memb_, stream2) in m.imemb if memb_ == memb) + sum((m.fc[stream2, compon] + m.eps1)/(m.f[stream2] + m.eps1) for (memb_, stream2) in m.nmemb if memb_ == memb)) - 2.0 * m.p[stream] * (m.fc[stream, compon] + m.eps1) / (m.f[stream] + m.eps1)) + if ( + (memb, stream) in m.pmemb + and (memb, compon) in m.mnorm + and memb == membrane + ): + return m.fc[stream, compon] == m.a[memb] * m.perm[compon] / 2.0 * ( + sum(m.p[stream2] for (memb_, stream2) in m.imemb if memb_ == memb) + * ( + sum( + (m.fc[stream2, compon] + m.eps1) / (m.f[stream2] + m.eps1) + for (memb_, stream2) in m.imemb + if memb_ == memb + ) + + sum( + (m.fc[stream2, compon] + m.eps1) / (m.f[stream2] + m.eps1) + for (memb_, stream2) in m.nmemb + if memb_ == memb + ) + ) + - 2.0 + * m.p[stream] + * (m.fc[stream, compon] + m.eps1) + / (m.f[stream] + m.eps1) + ) return Constraint.Skip - b.flux = Constraint([membrane], m.str, m.compon, - rule=Flux, doc='mass flux relation') + + b.flux = Constraint( + [membrane], m.str, m.compon, rule=Flux, doc='mass flux relation' + ) def Simp(_m, memb, stream, compon): - if (memb, stream) in m.pmemb and (memb, compon) in m.msimp and memb == membrane: + if ( + (memb, stream) in m.pmemb + and (memb, compon) in m.msimp + and memb == membrane + ): return m.fc[stream, compon] == 0.0 return Constraint.Skip - b.simp = Constraint([membrane], m.str, m.compon, - rule=Simp, doc='mass flux relation (simplified)') + + b.simp = Constraint( + [membrane], + m.str, + m.compon, + rule=Simp, + doc='mass flux relation (simplified)', + ) def Memtp(_m, memb, stream): if (memb, stream) in m.pmemb and memb == membrane: - return m.t[stream] == sum(m.t[stream2] for (memb, stream2) in m.imemb if memb == membrane) + return m.t[stream] == sum( + m.t[stream2] for (memb, stream2) in m.imemb if memb == membrane + ) return Constraint.Skip - b.memtp = Constraint([membrane], m.str, rule=Memtp, - doc='temp relation for permeate') + + b.memtp = Constraint( + [membrane], m.str, rule=Memtp, doc='temp relation for permeate' + ) def Mempp(_m, memb, stream): if (memb, stream) in m.pmemb and memb == membrane: - return m.p[stream] <= sum(m.p[stream2] for (memb, stream2) in m.imemb if memb == membrane) + return m.p[stream] <= sum( + m.p[stream2] for (memb, stream2) in m.imemb if memb == membrane + ) return Constraint.Skip - b.mempp = Constraint([membrane], m.str, rule=Mempp, - doc='pressure relation for permeate') + + b.mempp = Constraint( + [membrane], m.str, rule=Mempp, doc='pressure relation for permeate' + ) def Memtn(_m, memb, stream): if (memb, stream) in m.nmemb and memb == membrane: - return m.t[stream] == sum(m.t[stream2] for (memb, stream2) in m.imemb if memb == membrane) + return m.t[stream] == sum( + m.t[stream2] for (memb, stream2) in m.imemb if memb == membrane + ) return Constraint.Skip - b.Memtn = Constraint([membrane], m.str, rule=Memtn, - doc='temp relation for non-permeate') + + b.Memtn = Constraint( + [membrane], m.str, rule=Memtn, doc='temp relation for non-permeate' + ) def Mempn(_m, memb, stream): if (memb, stream) in m.nmemb and memb == membrane: - return m.p[stream] == sum(m.p[stream] for (memb_, stream) in m.imemb if memb_ == memb) + return m.p[stream] == sum( + m.p[stream] for (memb_, stream) in m.imemb if memb_ == memb + ) return Constraint.Skip - b.Mempn = Constraint([membrane], m.str, rule=Mempn, - doc='pressure relation for non-permeate') + + b.Mempn = Constraint( + [membrane], m.str, rule=Mempn, doc='pressure relation for non-permeate' + ) def Rec(_m, memb_, stream): if (memb_, stream) in m.pmemb and memb_ == membrane: - return m.fc[stream, 'h2'] >= m.membrane_recovery_sepc * sum(m.fc[stream, 'h2'] for (memb, stream) in m.imemb if memb == memb_) + return m.fc[stream, 'h2'] >= m.membrane_recovery_sepc * sum( + m.fc[stream, 'h2'] for (memb, stream) in m.imemb if memb == memb_ + ) return Constraint.Skip + b.rec = Constraint([membrane], m.str, rule=Rec, doc='recovery spec') def Pure(_m, memb, stream): if (memb, stream) in m.pmemb and memb == membrane: return m.fc[stream, 'h2'] >= m.membrane_purity_sepc * m.f[stream] return Constraint.Skip - b.pure = Constraint([membrane], m.str, rule=Pure, doc='purity spec') - + b.pure = Constraint([membrane], m.str, rule=Pure, doc='purity spec') def build_multiple_mixer(b, multiple_mxr): def Mxrcmb(_b, mxr, compon): if mxr == multiple_mxr: - return sum(m.fc[stream, compon] for (mxr_, stream) in m.omxr if mxr == mxr_) == sum(m.fc[stream, compon] for (mxr_, stream) in m.imxr if mxr == mxr_) + return sum( + m.fc[stream, compon] for (mxr_, stream) in m.omxr if mxr == mxr_ + ) == sum( + m.fc[stream, compon] for (mxr_, stream) in m.imxr if mxr == mxr_ + ) return Constraint.Skip - b.mxrcmb = Constraint([multiple_mxr], m.compon, - rule=Mxrcmb, doc='component balance in mixer') + + b.mxrcmb = Constraint( + [multiple_mxr], m.compon, rule=Mxrcmb, doc='component balance in mixer' + ) def Mxrhb(_b, mxr): if mxr == multiple_mxr and mxr != 2: - return sum(m.f[stream] * m.t[stream] * m.cp[stream] for (mxr_, stream) in m.imxr if mxr == mxr_) == sum(m.f[stream] * m.t[stream] * m.cp[stream] for (mxr_, stream) in m.omxr if mxr == mxr_) + return sum( + m.f[stream] * m.t[stream] * m.cp[stream] + for (mxr_, stream) in m.imxr + if mxr == mxr_ + ) == sum( + m.f[stream] * m.t[stream] * m.cp[stream] + for (mxr_, stream) in m.omxr + if mxr == mxr_ + ) return Constraint.Skip - b.mxrhb = Constraint([multiple_mxr], rule=Mxrhb, - doc="heat balance in mixer") + + b.mxrhb = Constraint([multiple_mxr], rule=Mxrhb, doc="heat balance in mixer") def Mxrhbq(_b, mxr): if mxr == 2 and mxr == multiple_mxr: - return m.f[16] * m.t[16] == m.f[15] * m.t[15] - (m.fc[20, 'ben'] + m.fc[20, 'tol']) * m.heatvap['tol'] / (100. * m.cp[15]) + return m.f[16] * m.t[16] == m.f[15] * m.t[15] - ( + m.fc[20, 'ben'] + m.fc[20, 'tol'] + ) * m.heatvap['tol'] / (100.0 * m.cp[15]) return Constraint.Skip - b.mxrhbq = Constraint([multiple_mxr], rule=Mxrhbq, - doc=' heat balance in quench') + + b.mxrhbq = Constraint( + [multiple_mxr], rule=Mxrhbq, doc=' heat balance in quench' + ) def Mxrpi(_b, mxr, stream): if (mxr, stream) in m.imxr and mxr == multiple_mxr: return m.mxrp[mxr] == m.p[stream] return Constraint.Skip - b.mxrpi = Constraint([multiple_mxr], m.str, - rule=Mxrpi, doc='inlet pressure relation') + + b.mxrpi = Constraint( + [multiple_mxr], m.str, rule=Mxrpi, doc='inlet pressure relation' + ) def Mxrpo(_b, mxr, stream): if (mxr, stream) in m.omxr and mxr == multiple_mxr: return m.mxrp[mxr] == m.p[stream] return Constraint.Skip - b.mxrpo = Constraint([multiple_mxr], m.str, - rule=Mxrpo, doc='outlet pressure relation') - + b.mxrpo = Constraint( + [multiple_mxr], m.str, rule=Mxrpo, doc='outlet pressure relation' + ) def build_pump(b, pump_): def Pumpcmb(_m, pump, compon): if pump == pump_ and compon in m.compon: - return sum(m.fc[stream, compon] for (pump_, stream) in m.opump if pump == pump_) == sum(m.fc[stream, compon] for (pump_, stream) in m.ipump if pump_ == pump) + return sum( + m.fc[stream, compon] for (pump_, stream) in m.opump if pump == pump_ + ) == sum( + m.fc[stream, compon] for (pump_, stream) in m.ipump if pump_ == pump + ) return Constraint.Skip - b.pumpcmb = Constraint( - [pump_], m.compon, rule=Pumpcmb, doc='component balance') + + b.pumpcmb = Constraint([pump_], m.compon, rule=Pumpcmb, doc='component balance') def Pumphb(_m, pump): if pump == pump_: - return sum(m.t[stream] for (pump_, stream) in m.opump if pump == pump_) == sum(m.t[stream] for (pump_, stream) in m.ipump if pump == pump_) + return sum( + m.t[stream] for (pump_, stream) in m.opump if pump == pump_ + ) == sum(m.t[stream] for (pump_, stream) in m.ipump if pump == pump_) return Constraint.Skip + b.pumphb = Constraint([pump_], rule=Pumphb, doc='heat balance') def Pumppr(_m, pump): if pump == pump_: - return sum(m.p[stream] for (pump_, stream) in m.opump if pump == pump_) >= sum(m.p[stream] for (pump_, stream) in m.ipump if pump == pump_) + return sum( + m.p[stream] for (pump_, stream) in m.opump if pump == pump_ + ) >= sum(m.p[stream] for (pump_, stream) in m.ipump if pump == pump_) return Constraint.Skip - b.pumppr = Constraint([pump_], rule=Pumppr, doc='pressure relation') - + b.pumppr = Constraint([pump_], rule=Pumppr, doc='pressure relation') def build_multiple_splitter(b, multi_splitter): def Splcmb(_m, spl, stream, compon): if (spl, stream) in m.ospl and spl == multi_splitter: - return m.fc[stream, compon] == sum(m.e[stream]*m.fc[str2, compon] for (spl_, str2) in m.ispl if spl == spl_) + return m.fc[stream, compon] == sum( + m.e[stream] * m.fc[str2, compon] + for (spl_, str2) in m.ispl + if spl == spl_ + ) return Constraint.Skip - b.splcmb = Constraint([multi_splitter], m.str, m.compon, - rule=Splcmb, doc='component balance in splitter') + + b.splcmb = Constraint( + [multi_splitter], + m.str, + m.compon, + rule=Splcmb, + doc='component balance in splitter', + ) def Esum(_m, spl): if spl in m.spl and spl == multi_splitter: - return sum(m.e[stream] for (spl_, stream) in m.ospl if spl_ == spl) == 1.0 + return ( + sum(m.e[stream] for (spl_, stream) in m.ospl if spl_ == spl) == 1.0 + ) return Constraint.Skip - b.esum = Constraint([multi_splitter], rule=Esum, - doc='split fraction relation') + + b.esum = Constraint([multi_splitter], rule=Esum, doc='split fraction relation') def Splpi(_m, spl, stream): if (spl, stream) in m.ispl and spl == multi_splitter: return m.splp[spl] == m.p[stream] return Constraint.Skip - b.splpi = Constraint([multi_splitter], m.str, - rule=Splpi, doc='inlet pressure relation') + + b.splpi = Constraint( + [multi_splitter], m.str, rule=Splpi, doc='inlet pressure relation' + ) def Splpo(_m, spl, stream): if (spl, stream) in m.ospl and spl == multi_splitter: return m.splp[spl] == m.p[stream] return Constraint.Skip - b.splpo = Constraint([multi_splitter], m.str, - rule=Splpo, doc='outlet pressure relation') + + b.splpo = Constraint( + [multi_splitter], m.str, rule=Splpo, doc='outlet pressure relation' + ) def Splti(_m, spl, stream): if (spl, stream) in m.ispl and spl == multi_splitter: return m.splt[spl] == m.t[stream] return Constraint.Skip - b.splti = Constraint([multi_splitter], m.str, - rule=Splti, doc='inlet temperature relation') + + b.splti = Constraint( + [multi_splitter], m.str, rule=Splti, doc='inlet temperature relation' + ) def Splto(_m, spl, stream): if (spl, stream) in m.ospl and spl == multi_splitter: return m.splt[spl] == m.t[stream] return Constraint.Skip - b.splto = Constraint([multi_splitter], m.str, - rule=Splto, doc='outlet temperature relation') - + b.splto = Constraint( + [multi_splitter], m.str, rule=Splto, doc='outlet temperature relation' + ) def build_valve(b, valve_): def Valcmb(_m, valve, compon): - return sum(m.fc[stream, compon] for (valve_, stream) in m.oval if valve == valve_) == sum(m.fc[stream, compon] for (valve_, stream) in m.ival if valve == valve_) + return sum( + m.fc[stream, compon] for (valve_, stream) in m.oval if valve == valve_ + ) == sum( + m.fc[stream, compon] for (valve_, stream) in m.ival if valve == valve_ + ) + b.valcmb = Constraint([valve_], m.compon, rule=Valcmb, doc='valcmb') def Valt(_m, valve): - return sum(m.t[stream] / (m.p[stream] ** ((m.gam - 1.) / m.gam)) for (valv, stream) in m.oval if valv == valve) == sum(m.t[stream] / (m.p[stream] ** ((m.gam - 1.) / m.gam)) for (valv, stream) in m.ival if valv == valve) + return sum( + m.t[stream] / (m.p[stream] ** ((m.gam - 1.0) / m.gam)) + for (valv, stream) in m.oval + if valv == valve + ) == sum( + m.t[stream] / (m.p[stream] ** ((m.gam - 1.0) / m.gam)) + for (valv, stream) in m.ival + if valv == valve + ) + b.valt = Constraint([valve_], rule=Valt, doc='temperature relation') def Valp(_m, valve): - return sum(m.p[stream] for (valv, stream) in m.oval if valv == valve) <= sum(m.p[stream] for (valv, stream) in m.ival if valv == valve) - b.valp = Constraint([valve_], rule=Valp, doc='pressure relation') + return sum( + m.p[stream] for (valv, stream) in m.oval if valv == valve + ) <= sum(m.p[stream] for (valv, stream) in m.ival if valv == valve) + b.valp = Constraint([valve_], rule=Valp, doc='pressure relation') m.Prereference_factor = Param( - initialize=6.3e+10, doc="Pre-reference factor for reaction rate constant") - m.Ea_R = Param(initialize=-26167.) + initialize=6.3e10, doc="Pre-reference factor for reaction rate constant" + ) + m.Ea_R = Param(initialize=-26167.0) m.pressure_drop = Param(initialize=0.20684) m.selectivity_1 = Param(initialize=0.0036) m.selectivity_2 = Param(initialize=-1.544) @@ -1418,150 +2278,250 @@ def Valp(_m, valve): def build_reactor(b, rct): def rctspec(_m, rct, stream): if (rct, stream) in m.irct: - return m.fc[stream, 'h2'] >= 5 * (m.fc[stream, 'ben'] + m.fc[stream, 'tol'] + m.fc[stream, 'dip']) + return m.fc[stream, 'h2'] >= 5 * ( + m.fc[stream, 'ben'] + m.fc[stream, 'tol'] + m.fc[stream, 'dip'] + ) return Constraint.Skip - b.Rctspec = Constraint([rct], m.str, rule=rctspec, - doc='spec. on reactor feed stream') + + b.Rctspec = Constraint( + [rct], m.str, rule=rctspec, doc='spec. on reactor feed stream' + ) def rxnrate(_m, rct): - return m.krct[rct] == m.Prereference_factor * exp(m.Ea_R / (m.rctt[rct] * 100.)) - b.Rxnrate = Constraint([rct], rule=rxnrate, - doc='reaction rate constant') + return m.krct[rct] == m.Prereference_factor * exp( + m.Ea_R / (m.rctt[rct] * 100.0) + ) + + b.Rxnrate = Constraint([rct], rule=rxnrate, doc='reaction rate constant') def rctconv(_m, rct, stream, compon): if (rct, compon) in m.rkey and (rct, stream) in m.irct: - return 1. - m.conv[rct, compon] == (1. / (1. + m.conversion_coefficient * m.krct[rct] * m.rctvol[rct] * sqrt(m.fc[stream, compon] / 60 + m.eps1) * (m.f[stream] / 60. + m.eps1) ** (-3./2.))) ** 2. + return ( + 1.0 - m.conv[rct, compon] + == ( + 1.0 + / ( + 1.0 + + m.conversion_coefficient + * m.krct[rct] + * m.rctvol[rct] + * sqrt(m.fc[stream, compon] / 60 + m.eps1) + * (m.f[stream] / 60.0 + m.eps1) ** (-3.0 / 2.0) + ) + ) + ** 2.0 + ) return Constraint.Skip - b.Rctconv = Constraint([rct], m.str, m.compon, - rule=rctconv, doc="conversion of key component") + + b.Rctconv = Constraint( + [rct], m.str, m.compon, rule=rctconv, doc="conversion of key component" + ) def rctsel(_m, rct): - return (1. - m.sel[rct]) == m.selectivity_1 * (1. - m.conv[rct, 'tol']) ** m.selectivity_2 - b.Rctsel = Constraint([rct], rule=rctsel, - doc=' selectivity to benzene') + return (1.0 - m.sel[rct]) == m.selectivity_1 * ( + 1.0 - m.conv[rct, 'tol'] + ) ** m.selectivity_2 + + b.Rctsel = Constraint([rct], rule=rctsel, doc=' selectivity to benzene') def rctcns(_m, rct, stream, compon): if (rct, compon) in m.rkey and (rct, stream) in m.irct: - return m.consum[rct, compon] == m.conv[rct, compon] * m.fc[stream, compon] + return ( + m.consum[rct, compon] == m.conv[rct, compon] * m.fc[stream, compon] + ) return Constraint.Skip - b.Rctcns = Constraint([rct], m.str, m.compon, - rule=rctcns, doc='consumption rate of key comp.') + + b.Rctcns = Constraint( + [rct], m.str, m.compon, rule=rctcns, doc='consumption rate of key comp.' + ) def rctmbtol(_m, rct): - return sum(m.fc[stream, 'tol'] for (rct_, stream) in m.orct if rct_ == rct) == sum(m.fc[stream, 'tol'] for (rct_, stream) in m.irct if rct_ == rct) - m.consum[rct, 'tol'] - b.Rctmbtol = Constraint([rct], rule=rctmbtol, - doc='mass balance in reactor (tol)') + return ( + sum(m.fc[stream, 'tol'] for (rct_, stream) in m.orct if rct_ == rct) + == sum(m.fc[stream, 'tol'] for (rct_, stream) in m.irct if rct_ == rct) + - m.consum[rct, 'tol'] + ) + + b.Rctmbtol = Constraint( + [rct], rule=rctmbtol, doc='mass balance in reactor (tol)' + ) def rctmbben(_m, rct): - return sum(m.fc[stream, 'ben'] for (rct_, stream) in m.orct if rct_ == rct) == sum(m.fc[stream, 'ben'] for (rct_, stream) in m.irct if rct_ == rct) + m.consum[rct, 'tol'] * m.sel[rct] + return ( + sum(m.fc[stream, 'ben'] for (rct_, stream) in m.orct if rct_ == rct) + == sum(m.fc[stream, 'ben'] for (rct_, stream) in m.irct if rct_ == rct) + + m.consum[rct, 'tol'] * m.sel[rct] + ) + b.Rctmbben = Constraint([rct], rule=rctmbben) def rctmbdip(_m, rct): - return sum(m.fc[stream, 'dip'] for (rct1, stream) in m.orct if rct1 == rct) == sum(m.fc[stream, 'dip'] for (rct1, stream) in m.irct if rct1 == rct) + m.consum[rct, 'tol'] * 0.5 + (sum(m.fc[stream, 'ben'] for (rct1, stream) in m.irct if rct1 == rct) - sum(m.fc[stream, 'ben'] for (rct1, stream) in m.orct if rct1 == rct)) * 0.5 + return ( + sum(m.fc[stream, 'dip'] for (rct1, stream) in m.orct if rct1 == rct) + == sum(m.fc[stream, 'dip'] for (rct1, stream) in m.irct if rct1 == rct) + + m.consum[rct, 'tol'] * 0.5 + + ( + sum(m.fc[stream, 'ben'] for (rct1, stream) in m.irct if rct1 == rct) + - sum( + m.fc[stream, 'ben'] for (rct1, stream) in m.orct if rct1 == rct + ) + ) + * 0.5 + ) + b.Rctmbdip = Constraint([rct], rule=rctmbdip) def rctmbh2(_m, rct): - return sum(m.fc[stream, 'h2'] for (rct1, stream) in m.orct if rct1 == rct) == sum(m.fc[stream, 'h2'] for (rct1, stream) in m.irct if rct1 == rct) - m.consum[rct, 'tol'] - sum(m.fc[stream, 'dip'] for (rct1, stream) in m.irct if rct1 == rct) + sum(m.fc[stream, 'dip'] for (rct1, stream) in m.orct if rct1 == rct) + return sum( + m.fc[stream, 'h2'] for (rct1, stream) in m.orct if rct1 == rct + ) == sum( + m.fc[stream, 'h2'] for (rct1, stream) in m.irct if rct1 == rct + ) - m.consum[ + rct, 'tol' + ] - sum( + m.fc[stream, 'dip'] for (rct1, stream) in m.irct if rct1 == rct + ) + sum( + m.fc[stream, 'dip'] for (rct1, stream) in m.orct if rct1 == rct + ) + b.Rctmbh2 = Constraint([rct], rule=rctmbh2) def rctpi(_m, rct, stream): if (rct, stream) in m.irct: return m.rctp[rct] == m.p[stream] return Constraint.Skip - b.Rctpi = Constraint([rct], m.str, rule=rctpi, - doc='inlet pressure relation') + + b.Rctpi = Constraint([rct], m.str, rule=rctpi, doc='inlet pressure relation') def rctpo(_m, rct, stream): if (rct, stream) in m.orct: return m.rctp[rct] - m.pressure_drop == m.p[stream] return Constraint.Skip - b.Rctpo = Constraint([rct], m.str, rule=rctpo, - doc='outlet pressure relation') + + b.Rctpo = Constraint([rct], m.str, rule=rctpo, doc='outlet pressure relation') def rcttave(_m, rct): - return m.rctt[rct] == (sum(m.t[stream] for (rct1, stream) in m.irct if rct1 == rct) + sum(m.t[stream] for (rct1, stream) in m.orct if rct1 == rct))/2 - b.Rcttave = Constraint([rct], rule=rcttave, - doc='average temperature relation ') + return ( + m.rctt[rct] + == ( + sum(m.t[stream] for (rct1, stream) in m.irct if rct1 == rct) + + sum(m.t[stream] for (rct1, stream) in m.orct if rct1 == rct) + ) + / 2 + ) + + b.Rcttave = Constraint([rct], rule=rcttave, doc='average temperature relation ') def Rctmbch4(_m, rct): - return sum(m.fc[stream, 'ch4'] for (rct_, stream) in m.orct if rct_ == rct) == sum(m.fc[stream, 'ch4'] for (rct_, stream) in m.irct if rct == rct_) + m.consum[rct, 'tol'] - b.rctmbch4 = Constraint([rct], rule=Rctmbch4, - doc='mass balance in reactor (ch4)') + return ( + sum(m.fc[stream, 'ch4'] for (rct_, stream) in m.orct if rct_ == rct) + == sum(m.fc[stream, 'ch4'] for (rct_, stream) in m.irct if rct == rct_) + + m.consum[rct, 'tol'] + ) + + b.rctmbch4 = Constraint( + [rct], rule=Rctmbch4, doc='mass balance in reactor (ch4)' + ) def Rcthbadb(_m, rct): if rct == 1: - return m.heatrxn[rct] * m.consum[rct, 'tol'] / 100. == sum(m.cp[stream] * m.f[stream] * m.t[stream] for (rct_, stream) in m.orct if rct_ == rct) - sum(m.cp[stream] * m.f[stream] * m.t[stream] for (rct_, stream) in m.irct if rct_ == rct) + return m.heatrxn[rct] * m.consum[rct, 'tol'] / 100.0 == sum( + m.cp[stream] * m.f[stream] * m.t[stream] + for (rct_, stream) in m.orct + if rct_ == rct + ) - sum( + m.cp[stream] * m.f[stream] * m.t[stream] + for (rct_, stream) in m.irct + if rct_ == rct + ) return Constraint.Skip - b.rcthbadb = Constraint([rct], rule=Rcthbadb, - doc='heat balance (adiabatic)') + + b.rcthbadb = Constraint([rct], rule=Rcthbadb, doc='heat balance (adiabatic)') def Rcthbiso(_m, rct): if rct == 2: - return m.heatrxn[rct] * m.consum[rct, 'tol'] * 60. * 8500 * 1.0e-09 == m.q[rct] + return ( + m.heatrxn[rct] * m.consum[rct, 'tol'] * 60.0 * 8500 * 1.0e-09 + == m.q[rct] + ) return Constraint.Skip - b.rcthbiso = Constraint([rct], rule=Rcthbiso, - doc='temp relation (isothermal)') + + b.rcthbiso = Constraint([rct], rule=Rcthbiso, doc='temp relation (isothermal)') def Rctisot(_m, rct): if rct == 2: - return sum(m.t[stream] for (rct_, stream) in m.irct if rct_ == rct) == sum(m.t[stream] for (rct_, stream) in m.orct if rct_ == rct) + return sum( + m.t[stream] for (rct_, stream) in m.irct if rct_ == rct + ) == sum(m.t[stream] for (rct_, stream) in m.orct if rct_ == rct) return Constraint.Skip - b.rctisot = Constraint([rct], rule=Rctisot, - doc='temp relation (isothermal)') - + b.rctisot = Constraint([rct], rule=Rctisot, doc='temp relation (isothermal)') def build_single_mixer(b, mixer): def Mxr1cmb(m_, mxr1, str1, compon): if (mxr1, str1) in m.omxr1 and mxr1 == mixer: - return m.fc[str1, compon] == sum(m.fc[str2, compon] for (mxr1_, str2) in m.imxr1 if mxr1_ == mxr1) + return m.fc[str1, compon] == sum( + m.fc[str2, compon] for (mxr1_, str2) in m.imxr1 if mxr1_ == mxr1 + ) return Constraint.Skip - b.mxr1cmb = Constraint([mixer], m.str, m.compon, - rule=Mxr1cmb, doc='component balance in mixer') - m.single_mixer = Block(m.mxr1, rule=build_single_mixer) - + b.mxr1cmb = Constraint( + [mixer], m.str, m.compon, rule=Mxr1cmb, doc='component balance in mixer' + ) + + m.single_mixer = Block(m.mxr1, rule=build_single_mixer) # single output splitter def build_single_splitter(b, splitter): def Spl1cmb(m_, spl1, compon): - return sum(m.fc[str1, compon] for (spl1_, str1) in m.ospl1 if spl1_ == spl1) == sum(m.fc[str1, compon] for (spl1_, str1) in m.ispl1 if spl1_ == spl1) + return sum( + m.fc[str1, compon] for (spl1_, str1) in m.ospl1 if spl1_ == spl1 + ) == sum(m.fc[str1, compon] for (spl1_, str1) in m.ispl1 if spl1_ == spl1) + b.spl1cmb = Constraint( - [splitter], m.compon, rule=Spl1cmb, doc='component balance in splitter') + [splitter], m.compon, rule=Spl1cmb, doc='component balance in splitter' + ) def Spl1pi(m_, spl1, str1): if (spl1, str1) in m.ispl1: return m.spl1p[spl1] == m.p[str1] return Constraint.Skip - b.spl1pi = Constraint([splitter], m.str, rule=Spl1pi, - doc='inlet pressure relation') + + b.spl1pi = Constraint( + [splitter], m.str, rule=Spl1pi, doc='inlet pressure relation' + ) def Spl1po(m_, spl1, str1): if (spl1, str1) in m.ospl1: return m.spl1p[spl1] == m.p[str1] return Constraint.Skip - b.spl1po = Constraint([splitter], m.str, rule=Spl1po, - doc='outlet pressure relation') + + b.spl1po = Constraint( + [splitter], m.str, rule=Spl1po, doc='outlet pressure relation' + ) def Spl1ti(m_, spl1, str1): if (spl1, str1) in m.ispl1: return m.spl1t[spl1] == m.t[str1] return Constraint.Skip - b.spl1ti = Constraint([splitter], m.str, rule=Spl1ti, - doc='inlet temperature relation') + + b.spl1ti = Constraint( + [splitter], m.str, rule=Spl1ti, doc='inlet temperature relation' + ) def Spl1to(m_, spl1, str1): if (spl1, str1) in m.ospl1: return m.spl1t[spl1] == m.t[str1] return Constraint.Skip - b.spl1to = Constraint([splitter], m.str, rule=Spl1to, - doc='outlet temperature relation') + + b.spl1to = Constraint( + [splitter], m.str, rule=Spl1to, doc='outlet temperature relation' + ) + m.single_splitter = Block(m.spl1, rule=build_single_splitter) # ## GDP formulation - - m.one = Set(initialize=[1]) m.two = Set(initialize=[2]) m.three = Set(initialize=[3]) @@ -1569,7 +2529,6 @@ def Spl1to(m_, spl1, str1): m.five = Set(initialize=[5]) m.six = Set(initialize=[6]) - # first disjunction: Purify H2 inlet or not @m.Disjunct() def purify_H2(disj): @@ -1590,7 +2549,7 @@ def no_purify_H2(disj): @m.Disjunction() def inlet_treatment(m): - return[m.purify_H2, m.no_purify_H2] + return [m.purify_H2, m.no_purify_H2] m.multi_mixer_1 = Block(m.one, rule=build_multiple_mixer) m.furnace_1 = Block(m.one, rule=build_furnace) @@ -1615,9 +2574,7 @@ def isothermal_reactor(disj): @m.Disjunction() def reactor_selection(m): - return[m.adiabatic_reactor, m.isothermal_reactor] - - + return [m.adiabatic_reactor, m.isothermal_reactor] m.valve_3 = Block(m.three, rule=build_valve) m.multi_mixer_2 = Block(m.two, rule=build_multiple_mixer) @@ -1626,7 +2583,7 @@ def reactor_selection(m): m.flash_1 = Block(m.one, rule=build_flash) m.multi_splitter_2 = Block(m.two, rule=build_multiple_splitter) - # thrid disjunction: recycle methane with membrane or purge it + # third disjunction: recycle methane with membrane or purge it @m.Disjunct() def recycle_methane_purge(disj): disj.no_flow_54 = Constraint(expr=m.f[54] == 0) @@ -1641,11 +2598,10 @@ def recycle_methane_membrane(disj): disj.compressor_4 = Block(m.four, rule=build_compressor) @m.Disjunction() - def methane_treatmet(m): - return[m.recycle_methane_purge, m.recycle_methane_membrane] + def methane_treatments(m): + return [m.recycle_methane_purge, m.recycle_methane_membrane] - - # fourth disjunction: recycle hydrogen with absorber or not + # fourth disjunction: recycle hydrogen with absorber or not @m.Disjunct() def recycle_hydrogen(disj): disj.no_flow_61 = Constraint(expr=m.f[61] == 0) @@ -1676,16 +2632,11 @@ def absorber_hydrogen(disj): def recycle_selection(m): return [m.recycle_hydrogen, m.absorber_hydrogen] - - m.multi_mixer_5 = Block(m.five, rule=build_multiple_mixer) - - m.multi_mixer_3 = Block(m.three, rule=build_multiple_mixer) m.multi_splitter_1 = Block(m.one, rule=build_multiple_splitter) - # fifth disjunction: methane stabilizing selection @m.Disjunct() def methane_distillation_column(disj): @@ -1728,11 +2679,8 @@ def methane_flash_separation(disj): def H2_selection(m): return [m.methane_distillation_column, m.methane_flash_separation] - - m.benzene_column = Block(m.two, rule=build_distillation) - # sixth disjunction: toluene stabilizing selection @m.Disjunct() def toluene_distillation_column(disj): @@ -1760,52 +2708,177 @@ def toluene_flash_separation(disj): def toluene_selection(m): return [m.toluene_distillation_column, m.toluene_flash_separation] - - m.pump_1 = Block(m.one, rule=build_pump) m.abound = Constraint(expr=m.a[1] >= 0.0) # ## objective function - m.hydrogen_purge_value = Param(initialize = 1.08,doc = "heating value of hydrogen purge") - m.electricity_cost = Param(initialize = 0.04 * 24 * 365 / 1000 , doc ="electricity cost, value is 0.04 with the unit of kw/h, now is kw/yr/k$") - m.meathane_purge_value = Param(initialize = 3.37, doc = "heating value of meathane purge") - m.heating_cost = Param(initialize = 8000., doc = "Heating cost(steam) with unit 1e6 KJ") - m.cooling_cost = Param(initialize = 700.0, doc = "heating cost (water) with unit 1e6 KJ") - m.fuel_cost = Param(initialize = 4000.0 ,doc = "fuel cost with unit 1e6 KJ") - m.abs_fixed_cost = Param(initialize = 13, doc = "fixed cost of absober ($1e3 per year)") - m.abs_linear_coeffcient = Param(initialize = 1.2, doc = "linear coeffcient of absorber (times tray number) ($1e3 per year)") - m.compressor_fixed_cost = Param(initialize = 7.155, doc = "compressor fixed cost ($1e3 per year)") - m.compressor_fixed_cost_4 = Param(initialize = 4.866, doc = "compressor fixed cost for compressor 4 ($1e3 per year)") - m.compressor_linear_coeffcient = Param(initialize = 0.815 ,doc = "compressor linear coeffcient (vaporflow rate) ($1e3 per year)") - m.compressor_linear_coeffcient_4 = Param(initialize = 0.887 ,doc = "compressor linear coeffcient (vaporflow rate) ($1e3 per year)") - m.stabilizing_column_fixed_cost = Param(initialize = 1.126 ,doc = "stabilizing column fixed cost ($1e3 per year)") - m.stabilizing_column_linear_coeffcient = Param(initialize = 0.375 ,doc = "stabilizing column linear coeffcient (times number of trays) ($1e3 per year)") - m.benzene_column_fixed_cost = Param(initialize = 16.3 ,doc = "benzene column fixed cost ($1e3 per year)") - m.benzene_column_linear_coeffcient = Param(initialize = 1.55 ,doc = "benzene column linear coeffcient (times number of trays) ($1e3 per year)") - m.toluene_column_fixed_cost = Param(initialize = 3.9 ,doc = "toluene column fixed cost ($1e3 per year)") - m.toluene_column_linear_coeffcient = Param(initialize = 1.12 ,doc = "toluene column linear coeffcient (times number of trays) ($1e3 per year)") - m.furnace_fixed_cost = Param(initialize = 6.20 ,doc = "toluene column fixed cost ($1e3 per year)") - m.furnace_linear_coeffcient = Param(initialize = 1171.7 ,doc = "furnace column linear coeffcient (1e9KJ/yr) ($1e3 per year)") - m.membrane_seperator_fixed_cost = Param(initialize = 43.24 ,doc = "membrane seperator fixed cost ($1e3 per year)") - m.membrane_seperator_linear_coeffcient = Param(initialize = 49.0 ,doc = "furnace column linear coeffcient (times inlet flowrate) ($1e3 per year)") - m.adiabtic_reactor_fixed_cost = Param(initialize = 74.3 ,doc = "adiabtic reactor fixed cost ($1e3 per year)") - m.adiabtic_reactor_linear_coeffcient = Param(initialize = 1.257 ,doc = "adiabtic reactor linear coeffcient (times reactor volumn) ($1e3 per year)") - m.isothermal_reactor_fixed_cost = Param(initialize = 92.875 ,doc = "isothermal reactor fixed cost ($1e3 per year)") - m.isothermal_reactor_linear_coeffcient = Param(initialize = 1.57125 ,doc = "isothermal reactor linear coeffcient (times reactor volumn) ($1e3 per year)") - m.h2_feed_cost = Param(initialize = 2.5, doc = "h2 feed cost (95% h2,5% Ch4)") - m.toluene_feed_cost = Param(initialize = 14., doc = "toluene feed cost (100% toluene)") - m.benzene_product = Param(initialize = 19.9,doc = "benzene product profit(benzene >= 99.97%)") - m.diphenyl_product = Param(initialize = 11.84,doc= "diphenyl product profit(diphenyl = 100%)") + m.hydrogen_purge_value = Param( + initialize=1.08, doc="heating value of hydrogen purge" + ) + m.electricity_cost = Param( + initialize=0.04 * 24 * 365 / 1000, + doc="electricity cost, value is 0.04 with the unit of kw/h, now is kw/yr/k$", + ) + m.meathane_purge_value = Param( + initialize=3.37, doc="heating value of meathane purge" + ) + m.heating_cost = Param( + initialize=8000.0, doc="Heating cost(steam) with unit 1e6 KJ" + ) + m.cooling_cost = Param( + initialize=700.0, doc="heating cost (water) with unit 1e6 KJ" + ) + m.fuel_cost = Param(initialize=4000.0, doc="fuel cost with unit 1e6 KJ") + m.abs_fixed_cost = Param(initialize=13, doc="fixed cost of absober ($1e3 per year)") + m.abs_linear_coefficient = Param( + initialize=1.2, + doc="linear coefficient of absorber (times tray number) ($1e3 per year)", + ) + m.compressor_fixed_cost = Param( + initialize=7.155, doc="compressor fixed cost ($1e3 per year)" + ) + m.compressor_fixed_cost_4 = Param( + initialize=4.866, doc="compressor fixed cost for compressor 4 ($1e3 per year)" + ) + m.compressor_linear_coefficient = Param( + initialize=0.815, + doc="compressor linear coefficient (vaporflow rate) ($1e3 per year)", + ) + m.compressor_linear_coefficient_4 = Param( + initialize=0.887, + doc="compressor linear coefficient (vaporflow rate) ($1e3 per year)", + ) + m.stabilizing_column_fixed_cost = Param( + initialize=1.126, doc="stabilizing column fixed cost ($1e3 per year)" + ) + m.stabilizing_column_linear_coefficient = Param( + initialize=0.375, + doc="stabilizing column linear coefficient (times number of trays) ($1e3 per year)", + ) + m.benzene_column_fixed_cost = Param( + initialize=16.3, doc="benzene column fixed cost ($1e3 per year)" + ) + m.benzene_column_linear_coefficient = Param( + initialize=1.55, + doc="benzene column linear coefficient (times number of trays) ($1e3 per year)", + ) + m.toluene_column_fixed_cost = Param( + initialize=3.9, doc="toluene column fixed cost ($1e3 per year)" + ) + m.toluene_column_linear_coefficient = Param( + initialize=1.12, + doc="toluene column linear coefficient (times number of trays) ($1e3 per year)", + ) + m.furnace_fixed_cost = Param( + initialize=6.20, doc="toluene column fixed cost ($1e3 per year)" + ) + m.furnace_linear_coefficient = Param( + initialize=1171.7, + doc="furnace column linear coefficient (1e9KJ/yr) ($1e3 per year)", + ) + m.membrane_separator_fixed_cost = Param( + initialize=43.24, doc="membrane separator fixed cost ($1e3 per year)" + ) + m.membrane_separator_linear_coefficient = Param( + initialize=49.0, + doc="furnace column linear coefficient (times inlet flowrate) ($1e3 per year)", + ) + m.adiabtic_reactor_fixed_cost = Param( + initialize=74.3, doc="adiabtic reactor fixed cost ($1e3 per year)" + ) + m.adiabtic_reactor_linear_coefficient = Param( + initialize=1.257, + doc="adiabtic reactor linear coefficient (times reactor volume) ($1e3 per year)", + ) + m.isothermal_reactor_fixed_cost = Param( + initialize=92.875, doc="isothermal reactor fixed cost ($1e3 per year)" + ) + m.isothermal_reactor_linear_coefficient = Param( + initialize=1.57125, + doc="isothermal reactor linear coefficient (times reactor volume) ($1e3 per year)", + ) + m.h2_feed_cost = Param(initialize=2.5, doc="h2 feed cost (95% h2,5% Ch4)") + m.toluene_feed_cost = Param(initialize=14.0, doc="toluene feed cost (100% toluene)") + m.benzene_product = Param( + initialize=19.9, doc="benzene product profit(benzene >= 99.97%)" + ) + m.diphenyl_product = Param( + initialize=11.84, doc="diphenyl product profit(diphenyl = 100%)" + ) def profits_from_paper(m): - return 510. * (- m.h2_feed_cost * m.f[1] - m.toluene_feed_cost * (m.f[66] + m.f[67]) + m.benzene_product * m.f[31] + m.diphenyl_product * m.f[35] + m.hydrogen_purge_value * (m.fc[4, 'h2'] + m.fc[28, 'h2'] + m.fc[53, 'h2'] + m.fc[55, 'h2']) + m.meathane_purge_value * (m.fc[4, 'ch4'] + m.fc[28, 'ch4'] + m.fc[53, 'ch4'] + m.fc[55, 'ch4'])) - m.compressor_linear_coeffcient * (m.elec[1] + m.elec[2] + m.elec[3]) - m.compressor_linear_coeffcient * m.elec[4] - m.compressor_fixed_cost * (m.purify_H2.binary_indicator_var + m.recycle_hydrogen.binary_indicator_var + m.absorber_hydrogen.binary_indicator_var) - m.compressor_fixed_cost * m.recycle_methane_membrane.binary_indicator_var - sum((m.electricity_cost * m.elec[comp]) for comp in m.comp) - (m.adiabtic_reactor_fixed_cost * m.adiabatic_reactor.binary_indicator_var + m.adiabtic_reactor_linear_coeffcient * m.rctvol[1]) - (m.isothermal_reactor_fixed_cost * m.isothermal_reactor.binary_indicator_var + m.isothermal_reactor_linear_coeffcient * m.rctvol[2]) - m.cooling_cost/1000 * m.q[2] - (m.stabilizing_column_fixed_cost * m.methane_distillation_column.binary_indicator_var +m.stabilizing_column_linear_coeffcient * m.ndist[1]) - (m.benzene_column_fixed_cost+ m.benzene_column_linear_coeffcient * m.ndist[2]) - (m.toluene_column_fixed_cost * m.toluene_distillation_column.binary_indicator_var + m.toluene_column_linear_coeffcient * m.ndist[3]) - (m.membrane_seperator_fixed_cost * m.purify_H2.binary_indicator_var + m.membrane_seperator_linear_coeffcient * m.f[3]) - (m.membrane_seperator_fixed_cost * m.recycle_methane_membrane.binary_indicator_var + m.membrane_seperator_linear_coeffcient * m.f[54]) - (m.abs_fixed_cost * m.absorber_hydrogen.binary_indicator_var + m.abs_linear_coeffcient * m.nabs[1]) - ( m.fuel_cost * m.qfuel[1] + m.furnace_linear_coeffcient* m.qfuel[1] ) - sum(m.cooling_cost * m.qc[hec] for hec in m.hec) - sum(m.heating_cost * m.qh[heh] for heh in m.heh) - m.furnace_fixed_cost - m.obj = Objective(rule = profits_from_paper, sense=maximize) + return ( + 510.0 + * ( + -m.h2_feed_cost * m.f[1] + - m.toluene_feed_cost * (m.f[66] + m.f[67]) + + m.benzene_product * m.f[31] + + m.diphenyl_product * m.f[35] + + m.hydrogen_purge_value + * (m.fc[4, 'h2'] + m.fc[28, 'h2'] + m.fc[53, 'h2'] + m.fc[55, 'h2']) + + m.meathane_purge_value + * (m.fc[4, 'ch4'] + m.fc[28, 'ch4'] + m.fc[53, 'ch4'] + m.fc[55, 'ch4']) + ) + - m.compressor_linear_coefficient * (m.elec[1] + m.elec[2] + m.elec[3]) + - m.compressor_linear_coefficient * m.elec[4] + - m.compressor_fixed_cost + * ( + m.purify_H2.binary_indicator_var + + m.recycle_hydrogen.binary_indicator_var + + m.absorber_hydrogen.binary_indicator_var + ) + - m.compressor_fixed_cost * m.recycle_methane_membrane.binary_indicator_var + - sum((m.electricity_cost * m.elec[comp]) for comp in m.comp) + - ( + m.adiabtic_reactor_fixed_cost * m.adiabatic_reactor.binary_indicator_var + + m.adiabtic_reactor_linear_coefficient * m.rctvol[1] + ) + - ( + m.isothermal_reactor_fixed_cost + * m.isothermal_reactor.binary_indicator_var + + m.isothermal_reactor_linear_coefficient * m.rctvol[2] + ) + - m.cooling_cost / 1000 * m.q[2] + - ( + m.stabilizing_column_fixed_cost + * m.methane_distillation_column.binary_indicator_var + + m.stabilizing_column_linear_coefficient * m.ndist[1] + ) + - ( + m.benzene_column_fixed_cost + + m.benzene_column_linear_coefficient * m.ndist[2] + ) + - ( + m.toluene_column_fixed_cost + * m.toluene_distillation_column.binary_indicator_var + + m.toluene_column_linear_coefficient * m.ndist[3] + ) + - ( + m.membrane_separator_fixed_cost * m.purify_H2.binary_indicator_var + + m.membrane_separator_linear_coefficient * m.f[3] + ) + - ( + m.membrane_separator_fixed_cost + * m.recycle_methane_membrane.binary_indicator_var + + m.membrane_separator_linear_coefficient * m.f[54] + ) + - ( + m.abs_fixed_cost * m.absorber_hydrogen.binary_indicator_var + + m.abs_linear_coefficient * m.nabs[1] + ) + - (m.fuel_cost * m.qfuel[1] + m.furnace_linear_coefficient * m.qfuel[1]) + - sum(m.cooling_cost * m.qc[hec] for hec in m.hec) + - sum(m.heating_cost * m.qh[heh] for heh in m.heh) + - m.furnace_fixed_cost + ) + + m.obj = Objective(rule=profits_from_paper, sense=maximize) # def profits_GAMS_file(m): # "there are several differences between the data from GAMS file and the paper: 1. all the compressor share the same fixed and linear cost in paper but in GAMS they have different fixed and linear cost in GAMS file. 2. the fixed cost for absorber in GAMS file is 3.0 but in the paper is 13.0, but they are getting the same results 3. the electricity cost is not the same" - # return 510. * (- m.h2_feed_cost * m.f[1] - m.toluene_feed_cost * (m.f[66] + m.f[67]) + m.benzene_product * m.f[31] + m.diphenyl_product * m.f[35] + m.hydrogen_purge_value * (m.fc[4, 'h2'] + m.fc[28, 'h2'] + m.fc[53, 'h2'] + m.fc[55, 'h2']) + m.meathane_purge_value * (m.fc[4, 'ch4'] + m.fc[28, 'ch4'] + m.fc[53, 'ch4'] + m.fc[55, 'ch4'])) - m.compressor_linear_coeffcient * (m.elec[1] + m.elec[2] + m.elec[3]) - m.compressor_linear_coeffcient_4 * m.elec[4] - m.compressor_fixed_cost * (m.purify_H2.binary_indicator_var + m.recycle_hydrogen.binary_indicator_var + m.absorber_hydrogen.binary_indicator_var) - m.compressor_fixed_cost_4 * m.recycle_methane_membrane.binary_indicator_var - sum((m.costelec * m.elec[comp]) for comp in m.comp) - (m.adiabtic_reactor_fixed_cost * m.adiabatic_reactor.binary_indicator_var + m.adiabtic_reactor_linear_coeffcient * m.rctvol[1]) - (m.isothermal_reactor_fixed_cost * m.isothermal_reactor.binary_indicator_var + m.isothermal_reactor_linear_coeffcient * m.rctvol[2]) - m.cooling_cost/1000 * m.q[2] - (m.stabilizing_column_fixed_cost * m.methane_distillation_column.binary_indicator_var +m.stabilizing_column_linear_coeffcient * m.ndist[1]) - (m.benzene_column_fixed_cost + m.benzene_column_linear_coeffcient * m.ndist[2]) - (m.toluene_column_fixed_cost * m.toluene_distillation_column.binary_indicator_var + m.toluene_column_linear_coeffcient * m.ndist[3]) - (m.membrane_seperator_fixed_cost * m.purify_H2.binary_indicator_var + m.membrane_seperator_linear_coeffcient * m.f[3]) - (m.membrane_seperator_fixed_cost * m.recycle_methane_membrane.binary_indicator_var + m.membrane_seperator_linear_coeffcient * m.f[54]) - (3.0 * m.absorber_hydrogen.binary_indicator_var + m.abs_linear_coeffcient * m.nabs[1]) - (m.fuel_cost * m.qfuel[1] + m.furnace_linear_coeffcient* m.qfuel[1]) - sum(m.cooling_cost * m.qc[hec] for hec in m.hec) - sum(m.heating_cost * m.qh[heh] for heh in m.heh) - m.furnace_fixed_cost + # return 510. * (- m.h2_feed_cost * m.f[1] - m.toluene_feed_cost * (m.f[66] + m.f[67]) + m.benzene_product * m.f[31] + m.diphenyl_product * m.f[35] + m.hydrogen_purge_value * (m.fc[4, 'h2'] + m.fc[28, 'h2'] + m.fc[53, 'h2'] + m.fc[55, 'h2']) + m.meathane_purge_value * (m.fc[4, 'ch4'] + m.fc[28, 'ch4'] + m.fc[53, 'ch4'] + m.fc[55, 'ch4'])) - m.compressor_linear_coefficient * (m.elec[1] + m.elec[2] + m.elec[3]) - m.compressor_linear_coefficient_4 * m.elec[4] - m.compressor_fixed_cost * (m.purify_H2.binary_indicator_var + m.recycle_hydrogen.binary_indicator_var + m.absorber_hydrogen.binary_indicator_var) - m.compressor_fixed_cost_4 * m.recycle_methane_membrane.binary_indicator_var - sum((m.costelec * m.elec[comp]) for comp in m.comp) - (m.adiabtic_reactor_fixed_cost * m.adiabatic_reactor.binary_indicator_var + m.adiabtic_reactor_linear_coefficient * m.rctvol[1]) - (m.isothermal_reactor_fixed_cost * m.isothermal_reactor.binary_indicator_var + m.isothermal_reactor_linear_coefficient * m.rctvol[2]) - m.cooling_cost/1000 * m.q[2] - (m.stabilizing_column_fixed_cost * m.methane_distillation_column.binary_indicator_var +m.stabilizing_column_linear_coefficient * m.ndist[1]) - (m.benzene_column_fixed_cost + m.benzene_column_linear_coefficient * m.ndist[2]) - (m.toluene_column_fixed_cost * m.toluene_distillation_column.binary_indicator_var + m.toluene_column_linear_coefficient * m.ndist[3]) - (m.membrane_separator_fixed_cost * m.purify_H2.binary_indicator_var + m.membrane_separator_linear_coefficient * m.f[3]) - (m.membrane_separator_fixed_cost * m.recycle_methane_membrane.binary_indicator_var + m.membrane_separator_linear_coefficient * m.f[54]) - (3.0 * m.absorber_hydrogen.binary_indicator_var + m.abs_linear_coefficient * m.nabs[1]) - (m.fuel_cost * m.qfuel[1] + m.furnace_linear_coefficient* m.qfuel[1]) - sum(m.cooling_cost * m.qc[hec] for hec in m.hec) - sum(m.heating_cost * m.qh[heh] for heh in m.heh) - m.furnace_fixed_cost # m.obj = Objective(rule=profits_GAMS_file, sense=maximize) return m @@ -1813,50 +2886,51 @@ def profits_from_paper(m): # %% + def solve_with_gdpopt(m): ''' This function solves model m using GDPOpt ''' opt = SolverFactory('gdpopt') - res = opt.solve(m, tee=True, - strategy='LOA', - # strategy='GLOA', - time_limit=3600, - mip_solver='gams', - mip_solver_args=dict(solver='cplex', warmstart=True), - nlp_solver='gams', - nlp_solver_args=dict(solver='ipopth', warmstart=True,), - minlp_solver='gams', - minlp_solver_args=dict(solver='dicopt', warmstart=True), - subproblem_presolve=False, - # init_strategy='no_init', - set_cover_iterlim=20, - # calc_disjunctive_bounds=True - ) + res = opt.solve( + m, + tee=True, + strategy='LOA', + # strategy='GLOA', + time_limit=3600, + mip_solver='gams', + mip_solver_args=dict(solver='cplex', warmstart=True), + nlp_solver='gams', + nlp_solver_args=dict(solver='ipopth', warmstart=True), + minlp_solver='gams', + minlp_solver_args=dict(solver='dicopt', warmstart=True), + subproblem_presolve=False, + # init_strategy='no_init', + set_cover_iterlim=20, + # calc_disjunctive_bounds=True + ) return res + def solve_with_minlp(m): ''' - This function solves model m using minlp transformation by either Big-M or convex hull + This function solves model m using minlp transformation by either Big-M or convex hull ''' TransformationFactory('gdp.bigm').apply_to(m, bigM=60) # TransformationFactory('gdp.hull').apply_to(m) # result = SolverFactory('baron').solve(m, tee=True) result = SolverFactory('gams').solve( - m, solver='baron', tee=True, - add_options=[ - 'option reslim=120;' - ] - ); + m, solver='baron', tee=True, add_options=['option reslim=120;'] + ) return result - # %% + def infeasible_constraints(m): ''' This function checks infeasible constraint in the model @@ -1864,20 +2938,19 @@ def infeasible_constraints(m): log_infeasible_constraints(m) - # %% # enumeration each possible route selection by fixing binary variable values in every disjunctions -def enumerate_solutions(m): - H2_treatments = ['purify','none_purify'] - Reactor_selections = ['adiabatic_reactor','isothermal_reactor'] - Methane_recycle_selections = ['recycle_membrane','recycle_purge'] - Absorber_recycle_selections = ['no_absorber','yes_absorber'] - Methane_product_selections = ['methane_flash','methane_column'] - Toluene_product_selections = ['toluene_flash','toluene_column'] +def enumerate_solutions(m): + H2_treatments = ['purify', 'none_purify'] + Reactor_selections = ['adiabatic_reactor', 'isothermal_reactor'] + Methane_recycle_selections = ['recycle_membrane', 'recycle_purge'] + Absorber_recycle_selections = ['no_absorber', 'yes_absorber'] + Methane_product_selections = ['methane_flash', 'methane_column'] + Toluene_product_selections = ['toluene_flash', 'toluene_column'] for H2_treatment in H2_treatments: for Reactor_selection in Reactor_selections: @@ -1922,20 +2995,39 @@ def enumerate_solutions(m): m.toluene_flash_separation.indicator_var.fix(True) m.toluene_distillation_column.indicator_var.fix(False) opt = SolverFactory('gdpopt') - res = opt.solve(m,tee =False, - strategy = 'LOA', - time_limit = 3600, - mip_solver = 'gams', - mip_solver_args= dict(solver = 'gurobi', warmstart=True), - nlp_solver = 'gams', - nlp_solver_args= dict(solver = 'ipopth', add_options = ['option optcr = 0'],warmstart=True), - minlp_solver = 'gams', - minlp_solver_args= dict(solver = 'dicopt', warmstart=True), - subproblem_presolve=False, - init_strategy = 'no_init', - set_cover_iterlim = 20 - ) - print('{0:<30}{1:<30}{2:<30}{3:<30}{4:<30}{5:<30}{6:<30}{7:<30}'.format(H2_treatment, Reactor_selection, Methane_recycle_selection,Absorber_recycle_selection,Methane_product_selection,Toluene_product_selection,str(res.solver.termination_condition),value(m.obj))) + res = opt.solve( + m, + tee=False, + strategy='LOA', + time_limit=3600, + mip_solver='gams', + mip_solver_args=dict(solver='gurobi', warmstart=True), + nlp_solver='gams', + nlp_solver_args=dict( + solver='ipopth', + add_options=['option optcr = 0'], + warmstart=True, + ), + minlp_solver='gams', + minlp_solver_args=dict(solver='dicopt', warmstart=True), + subproblem_presolve=False, + init_strategy='no_init', + set_cover_iterlim=20, + ) + print( + '{0:<30}{1:<30}{2:<30}{3:<30}{4:<30}{5:<30}{6:<30}{7:<30}'.format( + H2_treatment, + Reactor_selection, + Methane_recycle_selection, + Absorber_recycle_selection, + Methane_product_selection, + Toluene_product_selection, + str(res.solver.termination_condition), + value(m.obj), + ) + ) + + # %% def show_decision(m): ''' @@ -1965,6 +3057,8 @@ def show_decision(m): print("toluene_column") else: print("toluene_flash") + + # %% @@ -1982,7 +3076,7 @@ def show_decision(m): # Check if constraints are violated infeasible_constraints(m) - # show optimial flowsheet selection + # show optimal flowsheet selection # show_decision(m) print(res) diff --git a/gdplib/jobshop/__init__.py b/gdplib/jobshop/__init__.py new file mode 100644 index 0000000..6361ccd --- /dev/null +++ b/gdplib/jobshop/__init__.py @@ -0,0 +1,3 @@ +from .jobshop import build_model + +__all__ = ['build_model'] diff --git a/gdplib/pyomo_examples/jobshop-small.dat b/gdplib/jobshop/jobshop-small.dat similarity index 100% rename from gdplib/pyomo_examples/jobshop-small.dat rename to gdplib/jobshop/jobshop-small.dat diff --git a/gdplib/jobshop/jobshop.py b/gdplib/jobshop/jobshop.py new file mode 100644 index 0000000..d4d0dd6 --- /dev/null +++ b/gdplib/jobshop/jobshop.py @@ -0,0 +1,241 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * +from pyomo.gdp import * +from pyomo.common.fileutils import this_file_dir +from os.path import join + +# +# Jobshop example from http://www.gams.com/modlib/libhtml/logmip4.htm +# +# This model solves a jobshop scheduling, which has a set of jobs +# which must be processed in sequence of stages but not all jobs +# require all stages. A zero wait transfer policy is assumed between +# stages. To obtain a feasible solution it is necessary to eliminate +# all clashes between jobs. It requires that no two jobs be performed +# at any stage at any time. The objective is to minimize the makespan, +# the time to complete all jobs. +# +# References: +# +# Raman & Grossmann, Modelling and computational techniques for logic based integer programming, Computers and Chemical Engineering 18, 7, p.563-578, 1994. DOI: 10.1016/0098-1354(93)E0010-7. +# +# Aldo Vecchietti, LogMIP User's Manual, http://www.logmip.ceride.gov.ar/, 2007 +# + + +def build_model(): + """ + Build and return a jobshop scheduling model. + + This function constructs a Pyomo abstract model for jobshop scheduling, aiming to minimize the makespan. + It includes sets of jobs and stages, with the assumption of a zero-wait policy between stages. + The model enforces constraints to avoid job clashes at any stage and minimizes the total completion time. + + + Parameters + ---------- + None + + Returns + ------- + model : Pyomo.AbstractModel + The jobshop scheduling model, which has a set of jobs which must be processed in sequence of stages but not all jobs require all stages. + + References + ---------- + Raman & Grossmann, Modelling and computational techniques for logic based integer programming, Computers and Chemical Engineering 18, 7, p.563-578, 1994. + Aldo Vecchietti, LogMIP User's Manual, http://www.logmip.ceride.gov.ar/, 2007 + """ + model = AbstractModel('Jobshop Scheduling Model') + + model.JOBS = Set(ordered=True, doc='Set of jobs') + model.STAGES = Set(ordered=True, doc='Set of stages') + model.I_BEFORE_K = RangeSet(0, 1) + + # Task durations + model.tau = Param(model.JOBS, model.STAGES, default=0) + + # Total Makespan (this will be the objective) + model.ms = Var() + + # Start time of each job + def t_bounds(model, I): + """ + Calculate the time bounds for the start time of each job in a scheduling model. + + Parameters + ---------- + model : Pyomo.Abstractmodel + The job shop scheduling model, which has a set of jobs which must be processed in sequence of stages but not all jobs require all stages. + A zero wait transfer policy is assumed between stages. + I : str + The index of the job index + + Returns + ------- + tuple + A tuple containing the lower and upper bounds for the start time of the job. + """ + return (0, sum(value(model.tau[idx]) for idx in model.tau)) + + model.t = Var( + model.JOBS, + within=NonNegativeReals, + bounds=t_bounds, + doc='Start time of each job', + ) + + # Auto-generate the L set (potential collisions between 2 jobs at any stage. + def _L_filter(model, I, K, J): + """ + Filter for the L set (potential collisions between 2 jobs at any stage). + + Parameters + ---------- + model : Pyomo.Abstractmodel + The jobshop scheduling model, which has a set of jobs which must be processed in sequence of stages but not all jobs require all stages. + A zero wait transfer policy is assumed between stages. + I : str + job index + K : str + job index that is greater than I (After I) + J : int + stage index + + Returns + ------- + bool + Returns `True` if job `I` precedes job `K` and both jobs require processing at stage `J`, indicating a potential scheduling clash. + 'False' otherwise. + """ + return I < K and model.tau[I, J] and model.tau[K, J] + + model.L = Set( + initialize=model.JOBS * model.JOBS * model.STAGES, + dimen=3, + filter=_L_filter, + doc='Set of potential collisions between 2 jobs at any stage', + ) + + # Makespan is greater than the start time of every job + that job's + # total duration + def _feas(model, I): + """ + This function creates a constraint that ensures the makespan is greater than the sum of the start time of every job and that job's total duration. + + Parameters + ---------- + model : Pyomo.Abstractmodel + The jobshop scheduling model, which has a set of jobs which must be processed in sequence of stages but not all jobs require all stages. + A zero wait transfer policy is assumed between stages. + I : str + job index + + Returns + ------- + Pyomo.Constraint.Expression + A constraint expression that ensures the makespan is greater than or equal to the sum of the start time and total duration for the job. + """ + return model.ms >= model.t[I] + sum(model.tau[I, M] for M in model.STAGES) + + model.Feas = Constraint( + model.JOBS, + rule=_feas, + doc='Makespan is greater than the start time of every job + that job' + 's total duration', + ) + + # Disjunctions to prevent clashes at a stage: This creates a set of + # disjunct pairs: one if job I occurs before job K and the other if job + # K occurs before job I. + def _NoClash(disjunct, I, K, J, IthenK): + """ + Disjunctions to prevent clashes at a stage: This creates a set of disjunct pairs: one if job I occurs before job K and the other if job K occurs before job I. + + Parameters + ---------- + model : Pyomo.Disjunct + The disjunction of the model. + I : str + job index + K : str + job index that is greater than I (After I) + J : int + stage index + IthenK : bool + A boolean flag indicating if job I is scheduled before job K (`True`) or vice versa (`False`). + + Returns + ------- + None + However, a constraint is added to the disjunction to prevent clashes at a stage. + """ + model = disjunct.model() + lhs = model.t[I] + sum([M < J and model.tau[I, M] or 0 for M in model.STAGES]) + rhs = model.t[K] + sum([M < J and model.tau[K, M] or 0 for M in model.STAGES]) + if IthenK: + disjunct.c = Constraint(expr=lhs + model.tau[I, J] <= rhs) + else: + disjunct.c = Constraint(expr=rhs + model.tau[K, J] <= lhs) + + model.NoClash = Disjunct( + model.L, + model.I_BEFORE_K, + rule=_NoClash, + doc='Disjunctions to prevent clashes at a stage', + ) + + # Define the disjunctions: either job I occurs before K or K before I + def _disj(model, I, K, J): + """ + Define the disjunctions: either job I occurs before K or K before I + + Parameters + ---------- + model : Pyomo.Abstractmodel + jobshop scheduling model, which has a set of jobs which must be processed in sequence of stages but not all jobs require all stages. + I : str + job index + K : str + job index that is greater than I (After I) + J : int + stage index + + Returns + ------- + list of Pyomo.Disjunct + A list of disjunctions for the given jobs and stage, enforcing that one job must precede the other to avoid clashes. + """ + return [model.NoClash[I, K, J, IthenK] for IthenK in model.I_BEFORE_K] + + model.disj = Disjunction( + model.L, + rule=_disj, + doc='Define the disjunctions: either job I occurs before K or K before I', + ) + + # minimize makespan + model.makespan = Objective( + expr=model.ms, doc='Objective Function: Minimize the makespan' + ) + model = model.create_instance(join(this_file_dir(), 'jobshop-small.dat')) + return model + + +if __name__ == "__main__": + m = build_model() + TransformationFactory('gdp.bigm').apply_to(m) + SolverFactory('gams').solve( + m, solver='baron', tee=True, add_options=['option optcr=1e-6;'] + ) + m.makespan.display() diff --git a/gdplib/kaibel/kaibel_init.py b/gdplib/kaibel/kaibel_init.py index 921204c..3e3cf86 100644 --- a/gdplib/kaibel/kaibel_init.py +++ b/gdplib/kaibel/kaibel_init.py @@ -25,7 +25,19 @@ Figure 2. Sequence of columns for the separation of a quaternary mixture """ -from pyomo.environ import (exp, log10, minimize, NonNegativeReals, Objective, RangeSet, SolverFactory, value, Var) +from __future__ import division + +from pyomo.environ import ( + exp, + log10, + minimize, + NonNegativeReals, + Objective, + RangeSet, + SolverFactory, + value, + Var, +) from gdplib.kaibel.kaibel_prop import get_model_with_properties # from .kaibel_prop import get_model_with_properties @@ -44,27 +56,27 @@ def initialize_kaibel(): ## Get the model with properties from kaibel_prop.py m = get_model_with_properties() - + ## Operating conditions - m.Preb = 1.2 # Reboiler pressure in bar - m.Pcon = 1.05 # Condenser pressure in bar - m.Pf = 1.02 # Column pressure in bar + m.Preb = 1.2 # Reboiler pressure in bar + m.Pcon = 1.05 # Condenser pressure in bar + m.Pf = 1.02 - Pnmin = {} # Pressure in bars - Pnmin[1] = m.Preb # Reboiler pressure in bars - Pnmin[3] = m.Pcon # Distillate pressure in bars - Pnmin[2] = m.Pf # Side feed pressure in bars + Pnmin = {} # Pressure in bars + Pnmin[1] = m.Preb # Reboiler pressure in bars + Pnmin[3] = m.Pcon # Distillate pressure in bars + Pnmin[2] = m.Pf # Side feed pressure in bars - xi_nmin = {} # Initial liquid composition: first number = column and - # second number = 1 reboiler, 2 side feed, and - # 3 for condenser + xi_nmin = {} # Initial liquid composition: first number = column and + # second number = 1 reboiler, 2 side feed, and + # 3 for condenser ## Column 1 - c_c1 = 4 # Components in Column 1 - lc_c1 = 3 # Ligh component in Column 1 - hc_c1 = 4 # Heavy component in Column 1 - inter1_c1 = 1 # Intermediate component in Column 1 - inter2_c1 = 2 # Intermediate component in Column 1 + c_c1 = 4 # Components in Column 1 + lc_c1 = 3 # Light component in Column 1 + hc_c1 = 4 # Heavy component in Column 1 + inter1_c1 = 1 # Intermediate component in Column 1 + inter2_c1 = 2 # Intermediate component in Column 1 xi_nmin[1, 1, hc_c1] = 0.999 xi_nmin[1, 1, lc_c1] = (1 - xi_nmin[1, 1, hc_c1]) / (c_c1 - 1) @@ -73,18 +85,19 @@ def initialize_kaibel(): xi_nmin[1, 3, lc_c1] = 0.33 xi_nmin[1, 3, inter1_c1] = 0.33 xi_nmin[1, 3, inter2_c1] = 0.33 - xi_nmin[1, 3, hc_c1] = 1 - (xi_nmin[1, 3, lc_c1] + xi_nmin[1, 3, inter1_c1] + - xi_nmin[1, 3, inter2_c1]) + xi_nmin[1, 3, hc_c1] = 1 - ( + xi_nmin[1, 3, lc_c1] + xi_nmin[1, 3, inter1_c1] + xi_nmin[1, 3, inter2_c1] + ) xi_nmin[1, 2, lc_c1] = 1 / c_c1 xi_nmin[1, 2, inter1_c1] = 1 / c_c1 xi_nmin[1, 2, inter2_c1] = 1 / c_c1 xi_nmin[1, 2, hc_c1] = 1 / c_c1 ## Column 2 - c_c2 = 3 # Light components in Column 2 - lc_c2 = 2 # Light component in Column 2 - hc_c2 = 3 # Heavy component in Column 2 - inter_c2 = 1 # Intermediate component in Column 2 + c_c2 = 3 # Light components in Column 2 + lc_c2 = 2 # Light component in Column 2 + hc_c2 = 3 # Heavy component in Column 2 + inter_c2 = 1 # Intermediate component in Column 2 xi_nmin[2, 1, hc_c2] = 0.999 xi_nmin[2, 1, lc_c2] = (1 - xi_nmin[2, 1, hc_c2]) / (c_c2 - 1) @@ -96,11 +109,10 @@ def initialize_kaibel(): xi_nmin[2, 2, inter_c2] = 1 / c_c2 xi_nmin[2, 2, hc_c2] = 1 / c_c2 - ## Column 3 - c_c3 = 2 # Components in Column 3 - lc_c3 = 1 # Light component in Column 3 - hc_c3 = 2 # Heavy component in Column 3 + c_c3 = 2 # Components in Column 3 + lc_c3 = 1 # Light component in Column 3 + hc_c3 = 2 # Heavy component in Column 3 xi_nmin[3, 1, hc_c3] = 0.999 xi_nmin[3, 1, lc_c3] = 1 - xi_nmin[3, 1, hc_c3] @@ -109,44 +121,51 @@ def initialize_kaibel(): xi_nmin[3, 2, lc_c3] = 0.50 xi_nmin[3, 2, hc_c3] = 0.50 - - #### mn = m.clone() # Clone the model to add the initialization code mn.name = "Initialization Code" - mn.cols = RangeSet(3, - doc='Number of columns ') - mn.sec = RangeSet(3, - doc='Sections in column: 1 reb, 2 side feed, 3 cond') - mn.nc1 = RangeSet(c_c1, - doc='Number of components in Column 1') - mn.nc2 = RangeSet(c_c2, - doc='Number of components in Column 2') - mn.nc3 = RangeSet(c_c3, - doc='Number of components in Column 3') - - mn.Tnmin = Var(mn.cols, mn.sec, - doc='Temperature in K', bounds=(0, 500), - domain=NonNegativeReals) - mn.Tr1nmin = Var(mn.cols, mn.sec, mn.nc1, - doc='Temperature term for vapor pressure column 1', - domain=NonNegativeReals, - bounds=(0, None)) - mn.Tr2nmin = Var(mn.cols, mn.sec, mn.nc2, - doc='Temperature term for vapor pressure column 2', - domain=NonNegativeReals, - bounds=(0, None)) - mn.Tr3nmin = Var(mn.cols, mn.sec, mn.nc3, - doc='Temperature term for vapor pressure column 3', - domain=NonNegativeReals, - bounds=(0, None)) - - - @mn.Constraint(mn.cols, mn.sec, mn.nc1, - doc="Temperature term for vapor pressure column 1") + mn.cols = RangeSet(3, doc='Number of columns ') + mn.sec = RangeSet(3, doc='Sections in column: 1 reb, 2 side feed, 3 cond') + mn.nc1 = RangeSet(c_c1, doc='Number of components in Column 1') + mn.nc2 = RangeSet(c_c2, doc='Number of components in Column 2') + mn.nc3 = RangeSet(c_c3, doc='Number of components in Column 3') + + mn.Tnmin = Var( + mn.cols, + mn.sec, + doc='Temperature in K', + bounds=(0, 500), + domain=NonNegativeReals, + ) + mn.Tr1nmin = Var( + mn.cols, + mn.sec, + mn.nc1, + doc='Temperature term for vapor pressure', + domain=NonNegativeReals, + bounds=(0, None), + ) + mn.Tr2nmin = Var( + mn.cols, + mn.sec, + mn.nc2, + doc='Temperature term for vapor pressure', + domain=NonNegativeReals, + bounds=(0, None), + ) + mn.Tr3nmin = Var( + mn.cols, + mn.sec, + mn.nc3, + doc='Temperature term for vapor pressure', + domain=NonNegativeReals, + bounds=(0, None), + ) + + @mn.Constraint(mn.cols, mn.sec, mn.nc1, doc="Temperature term for vapor pressure") def _column1_reduced_temperature(mn, col, sec, nc): """Calculate the reduced temperature for column 1. @@ -170,9 +189,7 @@ def _column1_reduced_temperature(mn, col, sec, nc): """ return mn.Tr1nmin[col, sec, nc] * mn.Tnmin[col, sec] == mn.prop[nc, 'TC'] - - @mn.Constraint(mn.cols, mn.sec, mn.nc2, - doc="Temperature term for vapor pressure column 2") + @mn.Constraint(mn.cols, mn.sec, mn.nc2, doc="Temperature term for vapor pressure") def _column2_reduced_temperature(mn, col, sec, nc): """Calculate the reduced temperature for column 2. @@ -196,9 +213,7 @@ def _column2_reduced_temperature(mn, col, sec, nc): """ return mn.Tr2nmin[col, sec, nc] * mn.Tnmin[col, sec] == mn.prop[nc, 'TC'] - - @mn.Constraint(mn.cols, mn.sec, mn.nc3, - doc="Temperature term for vapor pressure column 3") + @mn.Constraint(mn.cols, mn.sec, mn.nc3, doc="Temperature term for vapor pressure") def _column3_reduced_temperature(mn, col, sec, nc): """Calculate the reduced temperature for column 3. @@ -222,7 +237,6 @@ def _column3_reduced_temperature(mn, col, sec, nc): """ return mn.Tr3nmin[col, sec, nc] * mn.Tnmin[col, sec] == mn.prop[nc, 'TC'] - @mn.Constraint(mn.cols, mn.sec, doc="Boiling point temperature") def _equilibrium_equation(mn, col, sec): """Equilibrium equations for a given column and section. @@ -250,46 +264,51 @@ def _equilibrium_equation(mn, col, sec): elif col == 3: a = mn.Tr3nmin b = mn.nc3 - return sum( - xi_nmin[col, sec, nc] * mn.prop[nc, 'PC'] * exp( - a[col, sec, nc] * ( - mn.prop[nc, 'vpA'] * \ - (1 - mn.Tnmin[col, sec] / mn.prop[nc, 'TC']) - + mn.prop[nc, 'vpB'] * \ - (1 - mn.Tnmin[col, sec] / mn.prop[nc, 'TC'])**1.5 - + mn.prop[nc, 'vpC'] * \ - (1 - mn.Tnmin[col, sec] / mn.prop[nc, 'TC'])**3 - + mn.prop[nc, 'vpD'] * \ - (1 - mn.Tnmin[col, sec] / mn.prop[nc, 'TC'])**6 + return ( + sum( + xi_nmin[col, sec, nc] + * mn.prop[nc, 'PC'] + * exp( + a[col, sec, nc] + * ( + mn.prop[nc, 'vpA'] + * (1 - mn.Tnmin[col, sec] / mn.prop[nc, 'TC']) + + mn.prop[nc, 'vpB'] + * (1 - mn.Tnmin[col, sec] / mn.prop[nc, 'TC']) ** 1.5 + + mn.prop[nc, 'vpC'] + * (1 - mn.Tnmin[col, sec] / mn.prop[nc, 'TC']) ** 3 + + mn.prop[nc, 'vpD'] + * (1 - mn.Tnmin[col, sec] / mn.prop[nc, 'TC']) ** 6 + ) ) - ) / Pnmin[sec] for nc in b - ) == 1 + / Pnmin[sec] + for nc in b + ) + == 1 + ) - mn.OBJ = Objective(expr=1, sense=minimize) - #### SolverFactory('ipopt').solve(mn) - - yc = {} # Vapor composition - kl = {} # Light key component - kh = {} # Heavy key component - alpha = {} # Relative volatility of kl - ter = {} # Term to calculate the minimum number of trays - Nmin = {} # Minimum number of stages - Nminopt = {} # Total optimal minimum number of trays - Nfeed = {} # Side feed optimal location using Kirkbride's method: - # 1 = number of trays in rectifying section and - # 2 = number of trays in stripping section - side_feed = {} # Side feed location - av_alpha = {} # Average relative volatilities - xi_lhc = {} # Liquid composition in columns - rel = mn.Bdes / mn.Ddes # Ratio between products flowrates - ln = {} # Light component for the different columns - hn = {} # Heavy component for the different columns + yc = {} # Vapor composition + kl = {} # Light key component + kh = {} # Heavy key component + alpha = {} # Relative volatility of kl + ter = {} # Term to calculate the minimum number of trays + Nmin = {} # Minimum number of stages + Nminopt = {} # Total optimal minimum number of trays + Nfeed = {} # Side feed optimal location using Kirkbride's method: + # 1 = number of trays in rectifying section and + # 2 = number of trays in stripping section + side_feed = {} # Side feed location + av_alpha = {} # Average relative volatilities + xi_lhc = {} # Liquid composition in columns + rel = mn.Bdes / mn.Ddes # Ratio between products flowrates + ln = {} # Light component for the different columns + hn = {} # Heavy component for the different columns ln[1] = lc_c1 ln[2] = lc_c2 ln[3] = lc_c3 @@ -297,7 +316,6 @@ def _equilibrium_equation(mn, col, sec): hn[2] = hc_c2 hn[3] = hc_c3 - for col in mn.cols: if col == 1: b = mn.nc1 @@ -308,16 +326,22 @@ def _equilibrium_equation(mn, col, sec): # For each component in the column and section calculate the vapor composition with the Peng-Robinson equation of state for sec in mn.sec: for nc in b: - yc[col, sec, nc] = xi_nmin[col, sec, nc] * mn.prop[nc, 'PC'] * exp( - mn.prop[nc, 'TC'] / value(mn.Tnmin[col, sec]) * ( - mn.prop[nc, 'vpA'] * \ - (1 - value(mn.Tnmin[col, sec]) / mn.prop[nc, 'TC']) - + mn.prop[nc, 'vpB'] * \ - (1 - value(mn.Tnmin[col, sec]) / mn.prop[nc, 'TC'])**1.5 - + mn.prop[nc, 'vpC'] * \ - (1 - value(mn.Tnmin[col, sec]) / mn.prop[nc, 'TC'])**3 - + mn.prop[nc, 'vpD'] * \ - (1 - value(mn.Tnmin[col, sec]) / mn.prop[nc, 'TC'])**6 + yc[col, sec, nc] = ( + xi_nmin[col, sec, nc] + * mn.prop[nc, 'PC'] + * exp( + mn.prop[nc, 'TC'] + / value(mn.Tnmin[col, sec]) + * ( + mn.prop[nc, 'vpA'] + * (1 - value(mn.Tnmin[col, sec]) / mn.prop[nc, 'TC']) + + mn.prop[nc, 'vpB'] + * (1 - value(mn.Tnmin[col, sec]) / mn.prop[nc, 'TC']) ** 1.5 + + mn.prop[nc, 'vpC'] + * (1 - value(mn.Tnmin[col, sec]) / mn.prop[nc, 'TC']) ** 3 + + mn.prop[nc, 'vpD'] + * (1 - value(mn.Tnmin[col, sec]) / mn.prop[nc, 'TC']) ** 6 + ) ) ) / Pnmin[sec] # Vapor composition in the different sections for the different components in the columns @@ -353,7 +377,6 @@ def _equilibrium_equation(mn, col, sec): m.Tf0 = value(mn.Tnmin[1, 2]) # Side feed temperature in K in column 1 m.TD0 = value(mn.Tnmin[2, 3]) # Distillate temperature in K in column 2 - return m diff --git a/gdplib/kaibel/kaibel_prop.py b/gdplib/kaibel/kaibel_prop.py index dc853d4..3cae0f1 100644 --- a/gdplib/kaibel/kaibel_prop.py +++ b/gdplib/kaibel/kaibel_prop.py @@ -89,9 +89,9 @@ def get_model_with_properties(): # # ------------------------------------------------------------------ - m.prop = {} # Properties of components: - cpL = {} # Ruczika-D method for liquid heat capacity calculation - # (Reference A, page 6.20) + m.prop = {} # Properties of components: + cpL = {} # Ruczika-D method for liquid heat capacity calculation + # (Reference A, page 6.20) sumA = {} sumB = {} sumC = {} diff --git a/gdplib/logical/positioning.py b/gdplib/logical/positioning.py index b9e610c..8047c21 100644 --- a/gdplib/logical/positioning.py +++ b/gdplib/logical/positioning.py @@ -16,17 +16,36 @@ from pyomo.environ import * from pyomo.gdp import * from pyomo.core.expr.logical_expr import * -from pyomo.core.plugins.transform.logical_to_linear import update_boolean_vars_from_binary +from pyomo.core.plugins.transform.logical_to_linear import ( + update_boolean_vars_from_binary, +) from six import StringIO import pandas as pd def build_model(): + """ + Constructs and returns a Pyomo ConcreteModel object configured to solve the optimal product positioning problem. The model seeks to position a new product in a way that optimizes its location in a multidimensional attribute space, balancing consumer satisfaction against production and positioning costs. + + Returns + ------- + m : Pyomo.ConcreteModel + The optimal positioning model that includes variables for product locations, consumer satisfaction indicators, and the constraints and objective function necessary for finding the optimal product positioning strategy. + + Notes + ----- + The model uses a disjunctive programming approach to handle the binary nature of consumer decisions and includes quadratic cost functions to represent varying cost behaviors associated with different product positioning strategies. + + References + ---------- + [1] Duran, M. A., & Grossmann, I. E. (1986). An outer-approximation algorithm for a class of mixed-integer nonlinear programs. Mathematical programming, 36, 307-339. https://doi.org/10.1007/BF02592064 + [2] Gavish, B., Horsky, D., & Srikanth, K. (1983). An approach to the optimal positioning of a new product. Management Science, 29(11), 1277-1297. https://doi.org/10.1287/mnsc.29.11.1277 + """ m = ConcreteModel() - m.locations = RangeSet(5) - m.consumers = RangeSet(25) - m.products = RangeSet(10) + m.locations = RangeSet(5) # 5 locations + m.consumers = RangeSet(25) # 25 consumers + m.products = RangeSet(10) # 10 products fixed_profit_data = { 1: 1, @@ -55,7 +74,9 @@ def build_model(): 24: 0.7, 25: 0.7, } - m.fixed_profit = Param(m.consumers, initialize=fixed_profit_data) + m.fixed_profit = Param( + m.consumers, initialize=fixed_profit_data, doc='Fixed profit for each consumer' + ) # fixed profit for each consumer minimum_weights_data = { 1: 77.84, @@ -84,16 +105,17 @@ def build_model(): 24: 340.581, 25: 407.52, } - m.minimum_weights = Param(m.consumers, initialize=minimum_weights_data) - location_bounds = { - 1: (2, 4.5), - 2: (0, 8.0), - 3: (3, 9.0), - 4: (0, 5.0), - 5: (4, 10), - } + m.minimum_weights = Param( + m.consumers, + initialize=minimum_weights_data, + doc='Minimum weights for each consumer', + ) # minimum weights for each consumer + + # Bounds for the locations + location_bounds = {1: (2, 4.5), 2: (0, 8.0), 3: (3, 9.0), 4: (0, 5.0), 5: (4, 10)} - ideal_points_data = StringIO(""" + ideal_points_data = StringIO( + """ 1 2 3 4 5 1 2.26 5.15 4.03 1.74 4.74 2 5.51 9.01 3.84 1.47 9.92 @@ -120,12 +142,23 @@ def build_model(): 23 1.37 0.54 1.55 5.56 5.85 24 8.79 5.04 4.83 6.94 0.38 25 2.66 4.19 6.49 8.04 1.66 - """) - ideal_points_table = pd.read_csv(ideal_points_data, delimiter=r'\s+') - ideal_points_dict = {(k[0], int(k[1])): v for k, v in ideal_points_table.stack().to_dict().items()} - m.ideal_points = Param(m.consumers, m.locations, initialize=ideal_points_dict) + """ + ) + ideal_points_table = pd.read_csv( + ideal_points_data, delimiter=r'\s+' + ) # ideal points for each consumer and product + ideal_points_dict = { + (k[0], int(k[1])): v for k, v in ideal_points_table.stack().to_dict().items() + } + m.ideal_points = Param( + m.consumers, + m.locations, + initialize=ideal_points_dict, + doc='Ideal points for each consumer and product', + ) - weights_data = StringIO(""" + weights_data = StringIO( + """ 1 2 3 4 5 1 9.57 2.74 9.75 3.96 8.67 2 8.38 3.93 5.18 5.2 7.82 @@ -152,12 +185,21 @@ def build_model(): 23 1.47 5.71 6.95 1.42 3.49 24 5.4 3.12 5.37 6.1 3.71 25 6.32 0.81 6.12 6.73 7.93 - """) + """ + ) weights_table = pd.read_csv(weights_data, delimiter=r'\s+') - weights_dict = {(k[0], int(k[1])): v for k, v in weights_table.stack().to_dict().items()} - m.weights = Param(m.consumers, m.locations, initialize=weights_dict) + weights_dict = { + (k[0], int(k[1])): v for k, v in weights_table.stack().to_dict().items() + } + m.weights = Param( + m.consumers, + m.locations, + initialize=weights_dict, + doc='Weights for each consumer and product', + ) - existing_products_data = StringIO(""" + existing_products_data = StringIO( + """ 1 2 3 4 5 1 0.62 5.06 7.82 0.22 4.42 2 5.21 2.66 9.54 5.03 8.01 @@ -169,29 +211,74 @@ def build_model(): 8 8.35 3.79 1.19 1.96 5.88 9 6.44 0.17 9.93 6.8 9.75 10 6.49 1.92 0.05 4.89 6.43 - """) + """ + ) existing_products_table = pd.read_csv(existing_products_data, delimiter=r'\s+') - existing_products_dict = {(k[0], int(k[1])): v for k, v in existing_products_table.stack().to_dict().items()} - m.existing_products = Param(m.products, m.locations, initialize=existing_products_dict) + existing_products_dict = { + (k[0], int(k[1])): v + for k, v in existing_products_table.stack().to_dict().items() + } + m.existing_products = Param( + m.products, + m.locations, + initialize=existing_products_dict, + doc='Existing products for each location and product', + ) # m.consumers * m.products - rr = {(i, j): sum(m.weights[i, k] * (m.existing_products[j, k] - m.ideal_points[i, k]) * ( - m.existing_products[j, k] - m.ideal_points[i, k]) - for k in m.locations) - for (i, j) in m.consumers * m.products} + # Squared weighted Euclidean distance between the existing products and the ideal points + rr = { + (i, j): sum( + m.weights[i, k] + * (m.existing_products[j, k] - m.ideal_points[i, k]) + * (m.existing_products[j, k] - m.ideal_points[i, k]) + for k in m.locations + ) + for (i, j) in m.consumers * m.products + } + # minimum dissimilarity between the existing products and consumers' ideal points r = {i: min(rr[i, j] for j in m.products) for i in m.consumers} - m.x = Var(m.locations) - m.Y = BooleanVar(m.consumers) - m.H = Param(initialize=1000) - m.U = Var(bounds=(0, 5000)) + m.x = Var(m.locations, doc='Location of each product') + m.Y = BooleanVar( + m.consumers, + doc='Indicates if consumer positioning is satisfactory based on distance to ideal points.', + ) + m.H = Param(initialize=1000, doc='Big M value') + m.U = Var( + bounds=(0, 5000), + doc='Upper bound on the sum of the distances to the ideal points', + ) # Slack variable @m.Disjunction(m.consumers) def d(m, i): + """ + Define the disjunction that model whether the distance squared sum of weights and deviations from ideal points for consumer i is less than or equal to a utility threshold (r[i]) adjusted by a slack variable m.U. + + Parameters + ---------- + m : Pyomo.ConcreteModel + The optimal positioning model + i : int + Index for consumers + + Returns + ------- + Pyomo.Disjunction + A Pyomo Disjunction object that evaluates if the consumer's preference is met (`true` scenario) with a single Pyomo expression, or not met (`false` scenario) where the list is empty. + """ return [ - [sum(m.weights[i, k] * (m.x[k] - m.ideal_points[i, k]) ** 2 for k in m.locations) - r[i] <= m.U], - [] + [ + sum( + m.weights[i, k] * (m.x[k] - m.ideal_points[i, k]) ** 2 + for k in m.locations + ) + - r[i] + <= m.U + ], + [], ] + for i in m.consumers: m.Y[i].associate_binary_var(m.d[i].disjuncts[0].binary_indicator_var) for k in m.locations: @@ -199,15 +286,33 @@ def d(m, i): m.x[k].setlb(lb) m.x[k].setub(ub) - m.c1 = Constraint(expr=m.x[1] - m.x[2] + m.x[3] + m.x[4] + m.x[5] <= 10) - m.c2 = Constraint(expr=0.6 * m.x[1] - 0.9 * m.x[2] - 0.5 * m.x[3] + 0.1 * m.x[4] + m.x[5] <= -0.64) - m.c3 = Constraint(expr=m.x[1] - m.x[2] + m.x[3] - m.x[4] + m.x[5] >= 0.69) - m.c4 = Constraint(expr=0.157 * m.x[1] + 0.05 * m.x[2] <= 1.5) - m.c5 = Constraint(expr=0.25 * m.x[2] + 1.05 * m.x[4] - 0.3 * m.x[5] >= 4.5) + m.c1 = Constraint( + expr=m.x[1] - m.x[2] + m.x[3] + m.x[4] + m.x[5] <= 10, doc='Constraint 1' + ) + m.c2 = Constraint( + expr=0.6 * m.x[1] - 0.9 * m.x[2] - 0.5 * m.x[3] + 0.1 * m.x[4] + m.x[5] + <= -0.64, + doc='Constraint 2', + ) + m.c3 = Constraint( + expr=m.x[1] - m.x[2] + m.x[3] - m.x[4] + m.x[5] >= 0.69, doc='Constraint 3' + ) + m.c4 = Constraint(expr=0.157 * m.x[1] + 0.05 * m.x[2] <= 1.5, doc='Constraint 4') + m.c5 = Constraint( + expr=0.25 * m.x[2] + 1.05 * m.x[4] - 0.3 * m.x[5] >= 4.5, doc='Constraint 5' + ) + # Minimizes total adjusted dissimilarity costs and maximizes consumer-based fixed profit, while balancing quadratic and linear operational costs across multiple product locations. m.obj = Objective( - expr=10 * m.U - sum(m.fixed_profit[i] * m.Y[i].get_associated_binary() for i in m.consumers) + 0.6 * m.x[1] ** 2 - 0.9 * m.x[ - 2] - 0.5 * m.x[3] + 0.1 * m.x[4] ** 2 + m.x[5]) + expr=10 * m.U + - sum(m.fixed_profit[i] * m.Y[i].get_associated_binary() for i in m.consumers) + + 0.6 * m.x[1] ** 2 + - 0.9 * m.x[2] + - 0.5 * m.x[3] + + 0.1 * m.x[4] ** 2 + + m.x[5], + doc='Objective function', + ) return m @@ -217,6 +322,8 @@ def d(m, i): TransformationFactory('core.logical_to_linear').apply_to(m) # res = SolverFactory('gdpopt').solve(m, tee=True, nlp_solver='gams') TransformationFactory('gdp.bigm').apply_to(m) - SolverFactory('gams').solve(m, tee=True, solver='baron', add_options=['option optcr=0;']) + SolverFactory('gams').solve( + m, tee=True, solver='baron', add_options=['option optcr=0;'] + ) update_boolean_vars_from_binary(m) m.Y.display() diff --git a/gdplib/logical/spectralog.py b/gdplib/logical/spectralog.py index 9bb6b17..ea0c7f3 100644 --- a/gdplib/logical/spectralog.py +++ b/gdplib/logical/spectralog.py @@ -1,27 +1,47 @@ -# coding: utf-8 +""" +spectrolog.py +IR Spectroscopy Parameter Estimation -# # [Pyomo.GDP](./index.ipynb) Logical Expression System Demo - IR Spectroscopy Parameter Estimation -# -# This is a reproduction of the IR spectroscopy parameter estimation problem found in: -# -# > Vecchietti A. & Grossmann I. E. -# > LOGMIP: A disjunctive 0-1 non-linear optimizer for process system models, -# > *Comp. & Chem Eng.* 23, p. 555-565, 1999. -# -# This code relies on the logic-v1 branch at https://github.com/qtothec/pyomo/tree/logic-v1 +This is a reproduction of the IR spectroscopy parameter estimation problem found in: -# Optimal value: 12.0893 +[1] Vecchietti, A., & Grossmann, I. E. (1997). LOGMIP: a disjunctive 0-1 nonlinear optimizer for process systems models. Computers & chemical engineering, 21, S427-S432. https://doi.org/10.1016/S0098-1354(97)87539-4 +[2] Brink, A., & Westerlund, T. (1995). The joint problem of model structure determination and parameter estimation in quantitative IR spectroscopy. Chemometrics and intelligent laboratory systems, 29(1), 29-36. https://doi.org/10.1016/0169-7439(95)00033-3 + +Optimal value: 12.0893 +""" from pyomo.environ import * from pyomo.gdp import * from pyomo.core.expr.logical_expr import * -from pyomo.core.plugins.transform.logical_to_linear import update_boolean_vars_from_binary +from pyomo.core.plugins.transform.logical_to_linear import ( + update_boolean_vars_from_binary, +) from six import StringIO import pandas as pd def build_model(): - spectroscopic_data = StringIO(""" + """ + Constructs and returns a Pyomo Concrete Model for IR spectroscopy parameter estimation. + + Returns + ------- + Pyomo.ConcreteModel + A Pyomo model representing the IR spectroscopy parameter estimation problem. + + Notes + ----- + - The model uses a disjunctive programming approach where decision variables can trigger different sets of constraints, + representing different physcochemical conditions. + + References + ---------- + [1] Vecchietti, A., & Grossmann, I. E. (1997). LOGMIP: a disjunctive 0-1 nonlinear optimizer for process systems models. Computers & chemical engineering, 21, S427-S432. https://doi.org/10.1016/S0098-1354(97)87539-4 + [2] Brink, A., & Westerlund, T. (1995). The joint problem of model structure determination and parameter estimation in quantitative IR spectroscopy. Chemometrics and intelligent laboratory systems, 29(1), 29-36. https://doi.org/10.1016/0169-7439(95)00033-3 + """ + # Matrix of absorbance values across different wave numbers (rows) and spectra numbers (columns) + spectroscopic_data = StringIO( + """ 1 2 3 4 5 6 7 8 1 0.0003 0.0764 0.0318 0.0007 0.0534 0.0773 0.0536 0.0320 2 0.0007 0.0003 0.0004 0.0009 0.0005 0.0009 0.0005 0.0003 @@ -33,68 +53,152 @@ def build_model(): 8 0.0507 0.0361 0.0433 0.0635 0.0048 0.0891 0.0213 0.0310 9 0.0905 0.0600 0.0754 0.1098 0.0038 0.1443 0.0420 0.0574 10 0.0016 0.0209 0.0063 0.0010 0.0132 0.0203 0.0139 0.0057 - """) + """ + ) # Note: this could come from an external data file spectroscopic_data_table = pd.read_csv(spectroscopic_data, delimiter=r'\s+') flat_spectro_data = spectroscopic_data_table.stack() - spectro_data_dict = {(k[0], int(k[1])): v for k, v in flat_spectro_data.to_dict().items()} # column labels to integer - - c_data = StringIO(""" + spectro_data_dict = { + (k[0], int(k[1])): v for k, v in flat_spectro_data.to_dict().items() + } # column labels to integer + + # Measured concentration data for each compound(row) and spectra number(column) + # Units for concentration for each component 1, 2, and 3 are ppm, ppm, and % for CO, NO, and CO2, respectively. + c_data = StringIO( + """ 1 2 3 4 5 6 7 8 1 502 204 353 702 0 1016 104 204 2 97 351 351 351 700 0 201 97 3 0 22 8 0 14 22 14 8 - """) + """ + ) c_data_table = pd.read_csv(c_data, delimiter=r'\s+') - c_data_dict = {(k[0], int(k[1])): v for k, v in c_data_table.stack().to_dict().items()} + c_data_dict = { + (k[0], int(k[1])): v for k, v in c_data_table.stack().to_dict().items() + } - # Covariance matrix - r_data = StringIO(""" + # Covariance matrix; It is assumed to be known that it is equal to the identity matrix at first problem iteration + r_data = StringIO( + """ 1 2 3 1 1 0 0 2 0 1 0 3 0 0 1 - """) + """ + ) r_data_table = pd.read_csv(r_data, delimiter=r'\s+') - r_data_dict = {(k[0], int(k[1])): v for k, v in r_data_table.stack().to_dict().items()} + r_data_dict = { + (k[0], int(k[1])): v for k, v in r_data_table.stack().to_dict().items() + } m = ConcreteModel(name="IR spectroscopy parameter estimation") - m.wave_number = RangeSet(10) - m.spectra_data = RangeSet(8) - m.compounds = RangeSet(3) - - m.A = Param(m.wave_number, m.spectra_data, initialize=spectro_data_dict) - m.C = Param(m.compounds, m.spectra_data, initialize=c_data_dict) - m.R = Param(m.compounds, m.compounds, initialize=r_data_dict) - - m.val = Var(m.spectra_data) - m.ent = Var(m.compounds, m.wave_number, bounds=(0, 1)) - m.Y = BooleanVar(m.compounds, m.wave_number) - m.P = Var(m.compounds, m.wave_number, bounds=(0, 1000)) + m.wave_number = RangeSet(10) # 10 wave numbers + m.spectra_data = RangeSet(8) # 8 spectra data points + m.compounds = RangeSet( + 3 + ) # 3 compounds; 1, 2, 3 refer to CO, NO, and CO2, respectively + + m.A = Param( + m.wave_number, + m.spectra_data, + initialize=spectro_data_dict, + doc='Absorbance data', + ) + m.C = Param( + m.compounds, m.spectra_data, initialize=c_data_dict, doc='Concentration data' + ) + m.R = Param( + m.compounds, m.compounds, initialize=r_data_dict, doc='Covariance matrix' + ) + + m.val = Var( + m.spectra_data, doc='Calculated objective values for each spectra data point' + ) + m.ent = Var( + m.compounds, + m.wave_number, + bounds=(0, 1), + doc='Binary variables affecting the objective function and constraints.', + ) + m.Y = BooleanVar( + m.compounds, + m.wave_number, + doc='Boolean decisions for compound presence at each wave number.', + ) + m.P = Var( + m.compounds, + m.wave_number, + bounds=(0, 1000), + doc='Continuous variables estimating the concentration level of each compound at each wave number.', + ) @m.Disjunction(m.compounds, m.wave_number) def d(m, k, i): + """_summary_ + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo model representing the IR spectroscopy parameter estimation problem. + k : int + Index of compounds. + i : int + Index of wave numbers. + + Returns + ------- + Pyomo.Disjunction + A disjunctive constraint that specifies the operational conditions for each compound-wave number pair based on the model's parameters. + """ return [ - [m.P[k, i] <= 1000, m.P[k, i] >= 0, m.ent[k, i] == 1], - [m.P[k, i] == 0, m.ent[k, i] == 0] + [ + m.P[k, i] <= 1000, + m.P[k, i] >= 0, + m.ent[k, i] == 1, + ], # Conditions for the compound being active + [ + m.P[k, i] == 0, + m.ent[k, i] == 0, + ], # Conditions for the compound being inactive ] + # Associate each Boolean variable with a corresponding binary variable to handle logical conditions. for k, i in m.compounds * m.wave_number: - m.Y[k, i].associate_binary_var( - m.d[k, i].disjuncts[0].binary_indicator_var) + m.Y[k, i].associate_binary_var(m.d[k, i].disjuncts[0].binary_indicator_var) @m.Constraint(m.spectra_data) def eq1(m, j): + """ + Defines a disjunction for each compound and wave number that determines whether a compound is active at a particular wave number based on the parameter estimates. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo model representing the IR spectroscopy parameter estimation problem. + j : int + Index for the spectra data points, representing different experimental conditions. + + Returns + ------- + Pyomo.Constraint + An expression that equates the calculated value for each spectra data point with a mathematically derived expression from the model. + """ return m.val[j] == sum( - sum((m.C[kk, j] / 100 - sum(m.P[kk, i] * m.A[i, j] for i in m.wave_number)) + sum( + (m.C[kk, j] / 100 - sum(m.P[kk, i] * m.A[i, j] for i in m.wave_number)) * m.R[kk, k] - for kk in m.compounds) + for kk in m.compounds + ) * (m.C[k, j] / 100 - sum(m.P[k, i] * m.A[i, j] for i in m.wave_number)) for k in m.compounds ) m.profit = Objective( - expr=sum(m.val[j] for j in m.spectra_data) + 2 * sum(m.ent[k, i] for k in m.compounds for i in m.wave_number)) + expr=sum(m.val[j] for j in m.spectra_data) + + 2 * sum(m.ent[k, i] for k in m.compounds for i in m.wave_number), + doc='Maximizes the total spectroscopic agreement across data points and promotes the activation of compound-wave number pairs.', + ) + # The first sum represents total spectroscopic value across data points, and the second weighted sum promotes activation of compound-wave number pairs. return m @@ -109,5 +213,3 @@ def eq1(m, j): m.profit.display() m.Y.display() m.P.display() - - diff --git a/gdplib/med_term_purchasing/__init__.py b/gdplib/med_term_purchasing/__init__.py new file mode 100644 index 0000000..fb2e2da --- /dev/null +++ b/gdplib/med_term_purchasing/__init__.py @@ -0,0 +1,3 @@ +from .med_term_purchasing import build_model + +__all__ = ['build_model'] diff --git a/gdplib/pyomo_examples/med_term_purchasing.dat b/gdplib/med_term_purchasing/med_term_purchasing.dat similarity index 100% rename from gdplib/pyomo_examples/med_term_purchasing.dat rename to gdplib/med_term_purchasing/med_term_purchasing.dat diff --git a/gdplib/med_term_purchasing/med_term_purchasing.py b/gdplib/med_term_purchasing/med_term_purchasing.py new file mode 100644 index 0000000..73bb3c8 --- /dev/null +++ b/gdplib/med_term_purchasing/med_term_purchasing.py @@ -0,0 +1,1973 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * +from pyomo.gdp import * +from pyomo.common.fileutils import this_file_dir +from os.path import join + +# Medium-term Purchasing Contracts problem from https://www.minlp.org/library/problem/index.php?i=129 +# This model maximizes profit in a short-term horizon in which various contracts +# are available for purchasing raw materials. The model decides inventory levels, +# amounts to purchase, amount sold, and flows through the process nodes while +# maximizing profit. The four different contracts available are: +# FIXED PRICE CONTRACT: buy as much as you want at constant price +# DISCOUNT CONTRACT: quantities below minimum amount cost RegPrice. Any additional quantity +# above min amount costs DiscoutPrice. +# BULK CONTRACT: If more than min amount is purchased, whole purchase is at discount price. +# FIXED DURATION CONTRACT: Depending on length of time contract is valid, there is a purchase +# price during that time and min quantity that must be purchased + + +# This version of the model is a literal transcription of what is in +# ShortTermContractCH.gms from the website. Some data is hardcoded into this model, +# most notably the process structure itself and the mass balance information. + + +def build_model(): + """ + Build a Pyomo abstract model for the medium-term purchasing contracts problem. + + Returns + ------- + Pyomo.AbstractModel + Pyomo abstract model for medium-term purchasing contracts problem. + + References + ---------- + [1] Vecchietti, A., & Grossmann, I. (2004). Computational experience with logmip solving linear and nonlinear disjunctive programming problems. Proc. of FOCAPD, 587-590. + [2] Park, M., Park, S., Mele, F. D., & Grossmann, I. E. (2006). Modeling of purchase and sales contracts in supply chain optimization. Industrial and Engineering Chemistry Research, 45(14), 5013-5026. DOI: 10.1021/ie0513144 + """ + model = AbstractModel("Medium-term Purchasing Contracts Problem") + + # Constants (data that was hard-coded in GAMS model) + AMOUNT_UB = 1000 + COST_UB = 1e4 + MAX_AMOUNT_FP = 1000 + MIN_AMOUNT_FD_1MONTH = 0 + + RandomConst_Line264 = 0.17 + RandomConst_Line265 = 0.83 + + ################### + # Sets + ################### + + # T + # t in GAMS + model.TimePeriods = Set(ordered=True, doc="Set of time periods") + + # Available length contracts + # p in GAMS + model.Contracts_Length = Set(doc="Set of available length contracts") + + # JP + # final(j) in GAMS + # Finished products + model.Products = Set(doc="Set of finished products") + + # JM + # rawmat(J) in GAMS + # Set of Raw Materials-- raw materials, intermediate products, and final products partition J + model.RawMaterials = Set( + doc="Set of raw materials, intermediate products, and final products" + ) + + # C + # c in GAMS + model.Contracts = Set(doc='Set of available contracts') + + # I + # i in GAMS + model.Processes = Set(doc='Set of processes in the network') + + # J + # j in GAMS + model.Streams = Set(doc='Set of streams in the network') + + ################## + # Parameters + ################## + + # Q_it + # excap(i) in GAMS + model.Capacity = Param(model.Processes, doc='Capacity of process i') + + # u_ijt + # cov(i) in GAMS + model.ProcessConstants = Param(model.Processes, doc='Process constants') + + # a_jt^U and d_jt^U + # spdm(j,t) in GAMS + model.SupplyAndDemandUBs = Param( + model.Streams, + model.TimePeriods, + default=0, + doc='Supply and demand upper bounds', + ) + + # d_jt^L + # lbdm(j, t) in GAMS + model.DemandLB = Param( + model.Streams, model.TimePeriods, default=0, doc='Demand lower bounds' + ) + + # delta_it + # delta(i, t) in GAMS + # operating cost of process i at time t + model.OperatingCosts = Param( + model.Processes, model.TimePeriods, doc='Operating cost of process i at time t' + ) + + # prices of raw materials under FP contract and selling prices of products + # pf(j, t) in GAMS + # omega_jt and pf_jt + model.Prices = Param( + model.Streams, + model.TimePeriods, + default=0, + doc='Prices of raw materials under FP contract and selling prices of products', + ) + + # Price for quantities less than min amount under discount contract + # pd1(j, t) in GAMS + model.RegPrice_Discount = Param( + model.Streams, + model.TimePeriod, + doc='Price for quantities less than min amount under discount contract', + ) + + # Discounted price for the quantity purchased exceeding the min amount + # pd2(j,t0 in GAMS + model.DiscountPrice_Discount = Param( + model.Streams, + model.TimePeriods, + doc='Discounted price for the quantity purchased exceeding the min amount', + ) + + # Price for quantities below min amount + # pb1(j,t) in GAMS + model.RegPrice_Bulk = Param( + model.Streams, + model.TimePeriods, + doc='Price for quantities below min amount under bulk contract', + ) + + # Price for quantities above min amount + # pb2(j, t) in GAMS + model.DiscountPrice_Bulk = Param( + model.Streams, + model.TimePeriods, + doc='Price for quantities above minimum amount under bulk contract', + ) + + # prices with length contract + # pl(j, p, t) in GAMS + model.Prices_Length = Param( + model.Streams, + model.Contracts_Length, + model.TimePeriods, + default=0, + doc='Prices with length contract', + ) + + # sigmad_jt + # sigmad(j, t) in GAMS + # Minimum quantity of chemical j that must be bought before receiving a Discount under discount contract + model.MinAmount_Discount = Param( + model.Streams, + model.TimePeriods, + default=0, + doc='Minimum quantity of chemical j that must be bought before receiving a Discount under discount contract', + ) + + # min quantity to receive discount under bulk contract + # sigmab(j, t) in GAMS + model.MinAmount_Bulk = Param( + model.Streams, + model.TimePeriods, + default=0, + doc='Minimum quantity of chemical j that must be bought before receiving a Discount under bulk contract', + ) + + # min quantity to receive discount under length contract + # sigmal(j, p) in GAMS + model.MinAmount_Length = Param( + model.Streams, + model.Contracts_Length, + default=0, + doc='Minimum quantity of chemical j that must be bought before receiving a Discount under length contract', + ) + + # main products of process i + # These are 1 (true) if stream j is the main product of process i, false otherwise. + # jm(j, i) in GAMS + model.MainProducts = Param( + model.Streams, model.Processes, default=0, doc='Main products of process i' + ) + + # theta_jt + # psf(j, t) in GAMS + # Shortfall penalty of product j at time t + model.ShortfallPenalty = Param( + model.Products, + model.TimePeriods, + doc='Shortfall penalty of product j at time t', + ) + + # shortfall upper bound + # sfub(j, t) in GAMS + model.ShortfallUB = Param( + model.Products, model.TimePeriods, default=0, doc='Shortfall upper bound' + ) + + # epsilon_jt + # cinv(j, t) in GAMS + # inventory cost of material j at time t + model.InventoryCost = Param( + model.Streams, model.TimePeriods, doc='Inventory cost of material j at time t' + ) + + # invub(j, t) in GAMS + # inventory upper bound + model.InventoryLevelUB = Param( + model.Streams, model.TimePeriods, default=0, doc='Inventory upper bound' + ) + + ## UPPER BOUNDS HARDCODED INTO GAMS MODEL + + # All of these upper bounds are hardcoded. So I am just leaving them that way. + # This means they all have to be the same as each other right now. + def getAmountUBs(model, j, t): + """ + Retrieves the upper bound for the amount that can be purchased or processed for a given material j and time period t. + + Parameters + ---------- + model : Pyomo.AbstractModel + Pyomo abstract model for medium-term purchasing contracts problem. + j : int + Index of materials. + t : int + Index of time period. + + Returns + ------- + float + The hardcoded upper bound on the amount for any material and time period, defined by the global variable `AMOUNT_UB`. + """ + return AMOUNT_UB + + def getCostUBs(model, j, t): + """ + Retrieves the upper bound for the cost associated with purchasing or processing a given material in a specific time period. + + Parameters + ---------- + model : Pyomo.AbstractModel + Pyomo abstract model for medium-term purchasing contracts problem. + j : int + Index of materials. + t : int + Index of time period. + + Returns + ------- + float + The hardcoded upper bound on costs for any material and time period, specified by the global variable `COST_UB`. + """ + return COST_UB + + model.AmountPurchasedUB_FP = Param( + model.Streams, + model.TimePeriods, + initialize=getAmountUBs, + doc='Upper bound on amount purchased under fixed price contract', + ) + model.AmountPurchasedUB_Discount = Param( + model.Streams, + model.TimePeriods, + initialize=getAmountUBs, + doc='Upper bound on amount purchased under discount contract', + ) + model.AmountPurchasedBelowMinUB_Discount = Param( + model.Streams, + model.TimePeriods, + initialize=getAmountUBs, + doc='Upper bound on amount purchased below min amount for discount under discount contract', + ) + model.AmountPurchasedAboveMinUB_Discount = Param( + model.Streams, + model.TimePeriods, + initialize=getAmountUBs, + doc='Upper bound on amount purchased above min amount for discount under discount contract', + ) + model.AmountPurchasedUB_FD = Param( + model.Streams, + model.TimePeriods, + initialize=getAmountUBs, + doc='Upper bound on amount purchased under fixed duration contract', + ) + model.AmountPurchasedUB_Bulk = Param( + model.Streams, + model.TimePeriods, + initialize=getAmountUBs, + doc='Upper bound on amount purchased under bulk contract', + ) + + model.CostUB_FP = Param( + model.Streams, + model.TimePeriods, + initialize=getCostUBs, + doc='Upper bound on cost of fixed price contract', + ) + model.CostUB_FD = Param( + model.Streams, + model.TimePeriods, + initialize=getCostUBs, + doc='Upper bound on cost of fixed duration contract', + ) + model.CostUB_Discount = Param( + model.Streams, + model.TimePeriods, + initialize=getCostUBs, + DOC='Upper bound on cost of discount contract', + ) + model.CostUB_Bulk = Param( + model.Streams, + model.TimePeriods, + initialize=getCostUBs, + doc='Upper bound on cost of bulk contract', + ) + + #################### + # VARIABLES + #################### + + # prof in GAMS + # will be objective + model.Profit = Var(doc='Profit') + + # f(j, t) in GAMS + # mass flow rates in tons per time interval t + model.FlowRate = Var( + model.Streams, + model.TimePeriods, + within=NonNegativeReals, + doc='Mass flow rates in tons per time interval t', + ) + + # V_jt + # inv(j, t) in GAMS + # inventory level of chemical j at time period t + def getInventoryBounds(model, i, j): + """ + Defines the lower and upper bounds for the inventory level of material j associated with process i. + + Parameters + ---------- + model : Pyomo.AbstractModel + Pyomo abstract model for medium-term purchasing contracts problem. + i : int + Index of processes. + j : int + Index of materials. + + Returns + ------- + tuple + A tuple containing two floats: the lower bound (0) and the upper bound for the inventory level of material j. + The upper bound is retrieved from the model's 'InventoryLevelUB' parameter using indices i and j. + """ + return (0, model.InventoryLevelUB[i, j]) + + model.InventoryLevel = Var( + model.Streams, + model.TimePeriods, + bounds=getInventoryBounds, + doc='Inventory level of material j at time period t', + ) + + # SF_jt + # sf(j, t) in GAMS + # Shortfall of demand for chemical j at time period t + def getShortfallBounds(model, i, j): + """ + Defines the lower and upper bounds for the shortfall in demand for material j during a specific time period, associated with process i. + + Parameters + ---------- + model : Pyomo.AbstractModel + Pyomo abstract model for medium-term purchasing contracts problem. + i : int + Index of processes. + j : int + Index of materials. + + Returns + ------- + tuple + A tuple (0, upper_bound), where 0 is the lower bound (nonnegative shortfalls) and 'upper_bound' is extracted from 'model.ShortfallUB[i, j]'. + 'model.ShortfallUB[i, j]' represents the maximal permitted shortfall for material i in the context of process i. + """ + return (0, model.ShortfallUB[i, j]) + + model.Shortfall = Var( + model.Products, + model.TimePeriods, + bounds=getShortfallBounds, + doc='Shortfall of demand for material j at time period t', + ) + + # amounts purchased under different contracts + + # spf(j, t) in GAMS + # Amount of raw material j bought under fixed price contract at time period t + def get_FP_bounds(model, j, t): + """ + Determines the bounds for the amount of raw material 'j' that can be purchased under a fixed price contract during time period 't'. + + Parameters + ---------- + model : Pyomo.AbstractModel + Pyomo abstract model for medium-term purchasing contracts problem. + j : int + Index of materials. + t : int + Index of time period. + + Returns + ------- + tuple + A tuple (0, upper_bound), where '0' is the lower bound (non-negative constraint) and 'upper_bound' is sourced from 'model.AmountPurchasedUB_FP[j,t]'. + 'model.AmountPurchasedUB_FP[j,t]' represents the maximum allowed purchase amount for material 'j' in period 't' + """ + return (0, model.AmountPurchasedUB_FP[j, t]) + + model.AmountPurchased_FP = Var( + model.Streams, + model.TimePeriods, + bounds=get_FP_bounds, + doc='Amount of raw material j bought under fixed price contract at time period t', + ) + + # spd(j, t) in GAMS + def get_Discount_Total_bounds(model, j, t): + """ + Determines the lower and upper bounds for the total amount of material j that can be purchased under discount contracts during time period t. + + Parameters + ---------- + model : Pyomo.AbstractModel + Pyomo abstract model for medium-term purchasing contracts problem. + j : int + Index of materials. + t : int + Index of time period. + + Returns + ------- + tuple + A tuple containing two elements: the lower bound (0) and the upper bound for the total amount of material j that can be purchased under discount contracts in period t. + The upper bound is retrieved from 'model.AmountPurchasedUB_Discount[j, t]'. + """ + return (0, model.AmountPurchasedUB_Discount[j, t]) + + model.AmountPurchasedTotal_Discount = Var( + model.Streams, + model.TimePeriods, + bounds=get_Discount_Total_bounds, + doc='Total amount of material j bought under discount contract at time period t', + ) + + # Amount purchased below min amount for discount under discount contract + # spd1(j, t) in GAMS + def get_Discount_BelowMin_bounds(model, j, t): + """ + Determines the lower and upper bounds for the amount of material j purchased below the minimum quantity for discounts under discount contracts during time period t. + + Parameters + ---------- + model : Pyomo.AbstractModel + Pyomo abstract model for medium-term purchasing contracts problem. + j : int + Index of materials. + t : int + Index of time period. + + Returns + ------- + tuple + A tuple (0, upper_bound), where '0' is the lower bound, and 'upper_bound' is 'model.AmountPurchasedBelowMinUB_Discount[j, t]'. + 'model.AmountPurchasedBelowMinUB_Discount[j, t] indicates the maximal purchasable amount below the discount threshold for material j in period't. + """ + return (0, model.AmountPurchasedBelowMinUB_Discount[j, t]) + + model.AmountPurchasedBelowMin_Discount = Var( + model.Streams, + model.TimePeriods, + bounds=get_Discount_BelowMin_bounds, + doc='Amount purchased below min amount for discount under discount contract', + ) + + # spd2(j, t) in GAMS + # Amount purchased above min amount for discount under discount contract + def get_Discount_AboveMin_bounds(model, j, t): + """ + Determines the lower and upper bounds for the amount of material j purchased above the minimum quantity eligible for discounts under discount contracts during time period t. + + Parameters + ---------- + model : Pyomo.AbstractModel + Pyomo abstract model for medium-term purchasing contracts problem. + j : int + Index of materials. + t : int + Index of time period. + + Returns + ------- + tuple + A tuple (0, upper_bound), where '0' is the lower bound and 'upper_bound' is sourced from 'model.AmountPurchasedBelowMinUB_Discount[j, t]'. + 'model.AmountPurchasedBelowMinUB_Discount[j, t]' indicates the maximum amount that can be purchased above the discount threshold for material j in period t. + """ + return (0, model.AmountPurchasedBelowMinUB_Discount[j, t]) + + model.AmountPurchasedAboveMin_Discount = Var( + model.Streams, + model.TimePeriods, + bounds=get_Discount_AboveMin_bounds, + doc='Amount purchased above min amount for discount under discount contract', + ) + + # Amount purchased under bulk contract + # spb(j, t) in GAMS + def get_bulk_bounds(model, j, t): + """ + Sets the lower and upper bounds for the amount of material j that can be purchased under a bulk contract during time period t. + + Parameters + ---------- + model : Pyomo.AbstractModel + Pyomo abstract model for medium-term purchasing contracts problem. + j : int + Index of materials. + t : int + Index of time period. + + Returns + ------- + tuple + A tuple (0, upper_bound), where '0' is the lower bound and 'upper_bound' is sourced from 'model.AmountPurchasedUB_Bulk[j,t]'. + 'model.AmountPurchasedUB_Bulk[j,t]' indicates the maximal allowable bulk purchase amount for material 'j' in period 't'. + """ + return (0, model.AmountPurchasedUB_Bulk[j, t]) + + model.AmountPurchased_Bulk = Var( + model.Streams, + model.TimePeriods, + bounds=get_bulk_bounds, + doc='Amount purchased under bulk contract', + ) + + # spl(j, t) in GAMS + # Amount purchased under Fixed Duration contract + def get_FD_bounds(model, j, t): + """ + Sets the lower and upper bounds for the quantity of material j that can be acquired under a fixed duration contract during the time period t. + + Parameters + ---------- + model : Pyomo.AbstractModel + Pyomo abstract model for medium-term purchasing contracts problem. + j : int + Index of materials. + t : int + Index of time period. + + Returns + ------- + tuple + A tuple (0, upper_bound), where '0' is the lower bound and upper_bound is taken from 'model.AmountPurchasedUB_FD[j, t]'. + 'model.AmountPurchasedUB_FD[j, t]' indicates the maximum permissible purchase quantity for material j under a fixed duration contract in time period t. + """ + return (0, model.AmountPurchasedUB_FD[j, t]) + + model.AmountPurchased_FD = Var( + model.Streams, + model.TimePeriods, + bounds=get_FD_bounds, + doc='Amount purchased under Fixed Duration contract', + ) + + # costs + + # costpl(j, t) in GAMS + # cost of variable length contract + def get_CostUBs_FD(model, j, t): + """ + Build the lower and upper bounds for the cost of purchasing material j under a fixed duration contract during time period t. + + Parameters + ---------- + model : Pyomo.AbstractModel + Pyomo abstract model for medium-term purchasing contracts problem. + j : int + Index of materials. + t : int + Index of time period. + + Returns + ------- + tuple + A tuple (0, upper_bound), where '0' is the lower bound, the minimal cost scenario, and 'upper_bound' is retrieved from 'model.CostUB_FD[j, t]', + 'model.CostUB_FD[j, t]' represents the highest permissible cost for purchasing material j under a fixed duration contract in time period t. + """ + return (0, model.CostUB_FD[j, t]) + + model.Cost_FD = Var( + model.Streams, + model.TimePeriods, + bounds=get_CostUBs_FD, + doc='Cost of variable length contract', + ) + + # costpf(j, t) in GAMS + # cost of fixed duration contract + def get_CostUBs_FP(model, j, t): + """ + Sets the lower and upper bounds for the cost of purchasing material j under a fixed price contract during time period t. + + Parameters + ---------- + model : Pyomo.AbstractModel + Pyomo abstract model for medium-term purchasing contracts problem. + j : int + Index of materials. + t : int + Index of time period. + + Returns + ------- + tuple + A tuple (0, upper_bound), where '0' is the lower bound and upper_bound is sourced from 'model.CostUB_FP[j, t]'. + 'model.CostUB_FP[j, t]' denotes the highest permissible cost for purchasing material 'j' under a fixed price contract in time period 't' + """ + return (0, model.CostUB_FP[j, t]) + + model.Cost_FP = Var( + model.Streams, + model.TimePeriods, + bounds=get_CostUBs_FP, + doc='Cost of fixed price contract', + ) + + # costpd(j, t) in GAMS + # cost of discount contract + def get_CostUBs_Discount(model, j, t): + """ + Set the lower and upper bounds for the cost of acquiring material j under discount contracts during time period t. + + Parameters + ---------- + model : Pyomo.AbstractModel + Pyomo abstract model for medium-term purchasing contracts problem. + j : int + Index of materials. + t : int + Index of time period. + + Returns + ------- + tuple + A tuple (0, upper bound), where 0' represents the lower bound, indicating the lowest possible cost condition, and upper bound is the 'model.CostUB_Discount[j, t]'. + 'model.CostUB_Discount[j, t]' represents the maximum allowed cost for purchasing material j under a discount contract in the given time period t. + """ + return (0, model.CostUB_Discount[j, t]) + + model.Cost_Discount = Var( + model.Streams, + model.TimePeriods, + bounds=get_CostUBs_Discount, + doc='Cost of discount contract', + ) + + # costpb(j, t) in GAMS + # cost of bulk contract + def get_CostUBs_Bulk(model, j, t): + """ + Set the lower and upper bounds for the cost incurred from purchasing material j under bulk contracts during time period t. + + Parameters + ---------- + model : Pyomo.AbstractModel + Pyomo abstract model for medium-term purchasing contracts problem. + j : int + Index of materials. + t : int + Index of time period. + + Returns + ------- + tuple + A tuple with 0 as the lower bound and 'model.CostUB_Bulk[j, t]' as the upper bound. + 'model.CostUB_Bulk[j, t]' represents the maximum cost for purchasing material j under a bulk contract in time period t. + """ + return (0, model.CostUB_Bulk[j, t]) + + model.Cost_Bulk = Var( + model.Streams, + model.TimePeriods, + bounds=get_CostUBs_Bulk, + doc='Cost of bulk contract', + ) + + # binary variables + + model.BuyFPContract = RangeSet(0, 1) # buy fixed price contract + model.BuyDiscountContract = Set( + initialize=('BelowMin', 'AboveMin', 'NotSelected'), doc='Buy discount contract' + ) + model.BuyBulkContract = Set( + initialize=('BelowMin', 'AboveMin', 'NotSelected'), doc='Buy bulk contract' + ) + model.BuyFDContract = Set( + initialize=('1Month', '2Month', '3Month', 'NotSelected'), + doc='Buy fixed duration contract', + ) + + ################ + # CONSTRAINTS + ################ + + # Objective: maximize profit + def profit_rule(model): + """ + Objective function: maximize profit of the medium-term purchasing contracts problem. + The profit is given by sales revenues, operating costs, purchasing costs, inventory costs, and shortfall penalties + + Parameters + ---------- + model : Pyomo.AbstractModel + Pyomo abstract model for medium-term purchasing contracts problem. + + Returns + ------- + Pyomo.Objective + Objective function: maximize profit of the medium-term purchasing contracts problem. + """ + salesIncome = sum( + model.Prices[j, t] * model.FlowRate[j, t] + for j in model.Products + for t in model.TimePeriods + ) + purchaseCost = ( + sum( + model.Cost_FD[j, t] + for j in model.RawMaterials + for t in model.TimePeriods + ) + + sum( + model.Cost_Discount[j, t] + for j in model.RawMaterials + for t in model.TimePeriods + ) + + sum( + model.Cost_Bulk[j, t] + for j in model.RawMaterials + for t in model.TimePeriods + ) + + sum( + model.Cost_FP[j, t] + for j in model.RawMaterials + for t in model.TimePeriods + ) + ) + productionCost = sum( + model.OperatingCosts[i, t] + * sum( + model.FlowRate[j, t] for j in model.Streams if model.MainProducts[j, i] + ) + for i in model.Processes + for t in model.TimePeriods + ) + shortfallCost = sum( + model.Shortfall[j, t] * model.ShortfallPenalty[j, t] + for j in model.Products + for t in model.TimePeriods + ) + inventoryCost = sum( + model.InventoryCost[j, t] * model.InventoryLevel[j, t] + for j in model.Products + for t in model.TimePeriods + ) + return ( + salesIncome - purchaseCost - productionCost - inventoryCost - shortfallCost + ) + + model.profit = Objective(rule=profit_rule, sense=maximize, doc='Maximize profit') + + # flow of raw materials is the total amount purchased (across all contracts) + def raw_material_flow_rule(model, j, t): + """ + Ensures the total flow of raw material j in time period t equals the sum of amounts purchased under all contract types. + + Parameters + ---------- + model : Pyomo.AbstractModel + Pyomo abstract model for medium-term purchasing contracts problem. + j : int + Index of materials. + t : int + Index of time period. + + Returns + ------- + Pyomo.Constraint + An equality constraint ensuring the material flow balance for each raw material j in each time period t. + """ + return ( + model.FlowRate[j, t] + == model.AmountPurchased_FD[j, t] + + model.AmountPurchased_FP[j, t] + + model.AmountPurchased_Bulk[j, t] + + model.AmountPurchasedTotal_Discount[j, t] + ) + + model.raw_material_flow = Constraint( + model.RawMaterials, + model.TimePeriods, + rule=raw_material_flow_rule, + doc='Material flow balance for each raw material j in each time period t', + ) + + def discount_amount_total_rule(model, j, t): + """ + Balances the total amount of material j purchased under discount contracts in time period t with the sum of amounts purchased below and above the minimum discount threshold. + + Parameters + ---------- + model : Pyomo.AbstractModel + Pyomo abstract model for medium-term purchasing contracts problem. + j : int + Index of materials. + t : int + Index of time period. + + Returns + ------- + Pyomo.Constraint + An equality constraint that ensures the total discounted purchase amount of material j in time period t is the sum of amounts bought below and above the discount threshold. + """ + return ( + model.AmountPurchasedTotal_Discount[j, t] + == model.AmountPurchasedBelowMin_Discount[j, t] + + model.AmountPurchasedAboveMin_Discount[j, t] + ) + + model.discount_amount_total_rule = Constraint( + model.RawMaterials, + model.TimePeriods, + rule=discount_amount_total_rule, + doc='Total discounted purchase amount of material j in time period t is the sum of amounts bought below and above the discount threshold', + ) + + # mass balance equations for each node + # these are specific to the process network in this example. + def mass_balance_rule1(model, t): + """ + Represents the mass balance equation for the first node in the process network. + + Stream 1 is the inlet stream, and streams 2 and 3 are the outlet streams. + The mass balance equation states that the total flow rate into the node (stream 1) is equal to the sum of the flow rates out of the node (streams 2 and 3). + + Parameters + ---------- + model : Pyomo.AbstractModel + Pyomo abstract model for medium-term purchasing contracts problem. + t : int + Index of time period. + + Returns + ------- + Pyomo.Constraint + A constraint that enforces the mass balance equation for the first node in the process network. + """ + return model.FlowRate[1, t] == model.FlowRate[2, t] + model.FlowRate[3, t] + + model.mass_balance1 = Constraint( + model.TimePeriods, + rule=mass_balance_rule1, + doc='Mass balance equation for the first node in the process network', + ) + + def mass_balance_rule2(model, t): + """ + Represents the mass balance equation for the second node in the process network. + + Stream 4 and 8 are the inlet streams, and stream 5 is the outlet stream. + The mass balance equation states that the total flow rate into the node (streams 4 and 8) is equal to the flow rate out of the node (stream 5). + + Parameters + ---------- + model : Pyomo.AbstractModel + Pyomo abstract model for medium-term purchasing contracts problem. + t : int + Index of time period. + + Returns + ------- + Pyomo.Constraint + A constraint that enforces the mass balance equation for the second node in the process network. + """ + return model.FlowRate[5, t] == model.FlowRate[4, t] + model.FlowRate[8, t] + + model.mass_balance2 = Constraint( + model.TimePeriods, + rule=mass_balance_rule2, + doc='Mass balance equation for the second node in the process network', + ) + + def mass_balance_rule3(model, t): + """ + Represents the mass balance equation for the third node in the process network. + + Stream 6 is the inlet stream, and stream 7 is the outlet stream. + The mass balance equation states that the total flow rate into the node (stream 6) is equal to the flow rate out of the node (stream 7). + + Parameters + ---------- + model : Pyomo.AbstractModel + Pyomo abstract model for medium-term purchasing contracts problem. + t : int + Index of time period. + + Returns + ------- + Pyomo.Constraint + A constraint that enforces the mass balance equation for the third node in the process network. + """ + return model.FlowRate[6, t] == model.FlowRate[7, t] + + model.mass_balance3 = Constraint( + model.TimePeriods, + rule=mass_balance_rule3, + doc='Mass balance equation for the third node in the process network', + ) + + def mass_balance_rule4(model, t): + """ + Represents the mass balance equation for Process 2 in the process network. + + Stream 3 and Stream 5 are the inlet of Process 2. + The mass flowrate of Stream 3 is 10 times the mass flowrate of Stream 5. + + Parameters + ---------- + model : Pyomo.AbstractModel + Pyomo abstract model for medium-term purchasing contracts problem. + t : int + Index of time period. + + Returns + ------- + Pyomo.Constraint + A constraint that enforces the mass balance equation for Process 2 in the process network. + """ + return model.FlowRate[3, t] == 10 * model.FlowRate[5, t] + + model.mass_balance4 = Constraint( + model.TimePeriods, + rule=mass_balance_rule4, + doc='Mass balance equation for Process 2 in the process network', + ) + + # process input/output constraints + # these are also totally specific to the process network + def process_balance_rule1(model, t): + """ + Represents the input/output balance equation for Process 1 in the process network. + + Process 1 has input Streams 2 and output Stream 9. + + Parameters + ---------- + model : Pyomo.AbstractModel + Pyomo abstract model for medium-term purchasing contracts problem. + t : int + Index of time period. + + Returns + ------- + Pyomo.Constraint + A constraint that enforces the input/output balance equation for Process 1 in the process network. + """ + return model.FlowRate[9, t] == model.ProcessConstants[1] * model.FlowRate[2, t] + + model.process_balance1 = Constraint( + model.TimePeriods, + rule=process_balance_rule1, + doc='Input/output balance equation for Process 1 in the process network', + ) + + def process_balance_rule2(model, t): + """ + Represents the input/output balance equation for Process 2 in the process network. + + Process 2 has input Streams 5 and 3 and output Stream 10. + + Parameters + ---------- + model : Pyomo.AbstractModel + Pyomo abstract model for medium-term purchasing contracts problem. + t : int + Index of time period. + + Returns + ------- + Pyomo.Constraint + A constraint that enforces the input/output balance equation for Process 2 in the process network. + """ + return model.FlowRate[10, t] == model.ProcessConstants[2] * ( + model.FlowRate[5, t] + model.FlowRate[3, t] + ) + + model.process_balance2 = Constraint( + model.TimePeriods, + rule=process_balance_rule2, + doc='Input/output balance equation for Process 2 in the process network', + ) + + def process_balance_rule3(model, t): + """ + Represents the input/output balance equation for Process 3 in the process network. + + Process 3 has input Stream 7 and outputs Streams 11 and 8. + RandomConst_Line264 is a hardcoded constant and determines the portion of Stream 7 that goes to Stream 8. + + Parameters + ---------- + model : Pyomo.AbstractModel + Pyomo abstract model for medium-term purchasing contracts problem. + t : int + Index of time period. + + Returns + ------- + Pyomo.Constraint + A constraint that enforces the input/output balance equation for Process 3 in the process network. + """ + return ( + model.FlowRate[8, t] + == RandomConst_Line264 * model.ProcessConstants[3] * model.FlowRate[7, t] + ) + + model.process_balance3 = Constraint( + model.TimePeriods, + rule=process_balance_rule3, + doc='Input/output balance equation 1 for Process 3 in the process network', + ) + + def process_balance_rule4(model, t): + """ + Represents the input/output balance equation for Process 3 in the process network. + + Process 3 has input Stream 7 and outputs Streams 11 and 8. + RandomConst_Line265 is a hardcoded constant and determines the portion of Stream 7 that goes to Stream 11. + + Parameters + ---------- + model : Pyomo.AbstractModel + Pyomo abstract model for medium-term purchasing contracts problem. + t : int + Index of time period. + + Returns + ------- + Pyomo.Constraint + A constraint that enforces the input/output balance equation for Process 3 in the process network. + """ + return ( + model.FlowRate[11, t] + == RandomConst_Line265 * model.ProcessConstants[3] * model.FlowRate[7, t] + ) + + model.process_balance4 = Constraint( + model.TimePeriods, + rule=process_balance_rule4, + doc='Input/output balance equation 2 for Process 3 in the process network', + ) + + # process capacity constraints + # these are hardcoded based on the three processes and the process flow structure + def process_capacity_rule1(model, t): + """ + Set the capacity constraint for Process 1 in the process network. + + Process 1 has a capacity constraint on Stream 9, which is the output stream of Process 1. + + Parameters + ---------- + model : Pyomo.AbstractModel + Pyomo abstract model for medium-term purchasing contracts problem. + t : int + Index of time period. + + Returns + ------- + Pyomo.Constraint + A constraint that enforces the capacity constraint for Process 1 in the process network. + """ + return model.FlowRate[9, t] <= model.Capacity[1] + + model.process_capacity1 = Constraint( + model.TimePeriods, + rule=process_capacity_rule1, + doc='Capacity constraint for Process 1 in the process network', + ) + + def process_capacity_rule2(model, t): + """ + Set the capacity constraint for Process 2 in the process network. + + Process 2 has a capacity constraint on Stream 10, which is the output stream of Process 2. + + Parameters + ---------- + model : Pyomo.AbstractModel + Pyomo abstract model for medium-term purchasing contracts problem. + t : int + Index of time period. + + Returns + ------- + Pyomo.Constraint + A constraint that enforces the capacity constraint for Process 2 in the process network. + """ + return model.FlowRate[10, t] <= model.Capacity[2] + + model.process_capacity2 = Constraint( + model.TimePeriods, + rule=process_capacity_rule2, + doc='Capacity constraint for Process 2 in the process network', + ) + + def process_capacity_rule3(model, t): + """ + Set the capacity constraint for Process 3 in the process network. + + Process 3 has capacity constraints on Streams 11 and 8, which are the output streams of Process 3. + + Parameters + ---------- + model : Pyomo.AbstractModel + Pyomo abstract model for medium-term purchasing contracts problem. + t : int + Index of time period. + + Returns + ------- + Pyomo.Constraint + A constraint that enforces the capacity constraint for Process 3 in the process network. + """ + return model.FlowRate[11, t] + model.FlowRate[8, t] <= model.Capacity[3] + + model.process_capacity3 = Constraint( + model.TimePeriods, + rule=process_capacity_rule3, + doc='Capacity constraint for Process 3 in the process network', + ) + + # Inventory balance of final products + # again, these are hardcoded. + + def inventory_balance1(model, t): + """ + Maintains inventory balance for the material associated with stream 12 at the first inventory node across time periods. + + This constraint ensures that the inventory level of the material at the beginning of each time period t, combined with the incoming flow from stream 9, equals the sum of the outflow to the next process (or demand) represented by stream 12 and the inventory level at the end of the time period. For the initial time period, the previous inventory is assumed to be zero. + This balance is vital for tracking inventory levels accurately, allowing the model to make informed decisions about production, storage, and sales to maximize overall profit. + + Parameters + ---------- + model : Pyomo.AbstractModel + Pyomo abstract model for medium-term purchasing contracts problem. + t : int + Index of time period. + + Returns + ------- + Pyomo.Constraint + A constraint enforcing the balance of inventory levels for the material flowing through stream 12 at the first inventory node, taking into account the material inflows and outflows as well as changes in inventory from the previous to the current time period. + """ + prev = 0 if t == min(model.TimePeriods) else model.InventoryLevel[12, t - 1] + return ( + prev + model.FlowRate[9, t] + == model.FlowRate[12, t] + model.InventoryLevel[12, t] + ) + + model.inventory_balance1 = Constraint( + model.TimePeriods, + rule=inventory_balance1, + doc='Inventory balance for material associated with stream 12 at the first inventory node', + ) + + def inventory_balance_rule2(model, t): + """ + Ensures inventory balance for the material associated with stream 13 at the second inventory node for the first time period. + + This constraint is applied only to the first time period (t=1) and ensures that the sum of incoming flows from streams 10 and 11 equals the sum of the outflow to the next process represented by stream 13 and the inventory level at the end of the period. + For periods beyond the first, this constraint is skipped, as the balance for these periods may be governed by other conditions or constraints within the model. This selective application is crucial for accurately modeling the startup phase of the inventory system, where initial conditions significantly impact subsequent operations. + + Parameters + ---------- + model : Pyomo.AbstractModel + Pyomo abstract model for medium-term purchasing contracts problem. + t : int + Index of time period. + + Returns + ------- + Pyomo.Constraint or Constraint.Skip + A constraint enforcing the inventory balance for the material flowing through stream 13 at the second inventory node during the first time period. + For all other periods, the function returns `Constraint.Skip`, indicating no constraint is applied. + """ + if t != 1: + return Constraint.Skip + return ( + model.FlowRate[10, t] + model.FlowRate[11, t] + == model.InventoryLevel[13, t] + model.FlowRate[13, t] + ) + + model.inventory_balance2 = Constraint( + model.TimePeriods, + rule=inventory_balance_rule2, + doc='Inventory balance for material associated with stream 13 at the second inventory node', + ) + + def inventory_balance_rule3(model, t): + """ + Maintains the inventory balance for material associated with stream 13 at the second inventory node for all time periods after the first. + + This constraint is crucial for modeling the dynamic behavior of inventory levels over time, ensuring that the sum of the previous period's inventory level and the current period's inflows from streams 10 and 11 equals the current period's outflow (through stream 13) and ending inventory level. + It reflects the principle of inventory continuity, accounting for inflows, outflows, and storage from one period to the next. + The constraint is skipped for the first period (t=1) to accommodate initial conditions or startup behaviors specific to the model's context. + + Parameters + ---------- + model : Pyomo.AbstractModel + Pyomo abstract model for medium-term purchasing contracts problem. + t : int + Index of time period. + + Returns + ------- + Pyomo.Constraint or Constraint.Skip + A constraint enforcing the inventory balance for the material flowing through stream 13 at the second inventory node from the second period onwards. + For the first period, the function returns `Constraint.Skip`, indicating the constraint does not apply. + """ + if t <= 1: + return Constraint.Skip + return ( + model.InventoryLevel[13, t - 1] + + model.FlowRate[10, t] + + model.FlowRate[11, t] + == model.InventoryLevel[13, t] + model.FlowRate[13, t] + ) + + model.inventory_balance3 = Constraint( + model.TimePeriods, + rule=inventory_balance_rule3, + doc='Inventory balance for material associated with stream 13 at the second inventory node', + ) + + # Max capacities of inventories + def inventory_capacity_rule(model, j, t): + """ + Sets the maximum inventory capacity for each material j at each time period t. + + Parameters + ---------- + model : Pyomo.AbstractModel + Pyomo abstract model for medium-term purchasing contracts problem. + j : int + Index of materials. + t : int + Index of time period. + + Returns + ------- + Pyomo.Constraint + A constraint that sets the maximum permissible inventory level for material j in time period t, ensuring that the inventory does not exceed the predefined upper bound 'InventoryLevelUB[j, t]'. + This maintains the model's alignment with practical storage limitations. + """ + return model.InventoryLevel[j, t] <= model.InventoryLevelUB[j, t] + + model.inventory_capacity_rule = Constraint( + model.Products, + model.TimePeriods, + rule=inventory_capacity_rule, + doc='Maximum inventory capacity for each material j at each time period t', + ) + + # Shortfall calculation + def shortfall_rule(model, j, t): + """ + Calculates the shortfall for each product 'j' in each time period 't'. + + Parameters + ---------- + model : Pyomo.AbstractModel + Pyomo abstract model for medium-term purchasing contracts problem. + j : int + Index of materials. + t : int + Index of time period. + + Returns + ------- + Pyomo.Constraint + A constraint defining the shortfall for product j during time period t as the difference between the supply or demand upper bound and the actual flow rate. + This calculation is pivotal for evaluating performance and identifying bottlenecks or excess capacities within the supply chain. + """ + return ( + model.Shortfall[j, t] + == model.SupplyAndDemandUBs[j, t] - model.FlowRate[j, t] + ) + + model.shortfall = Constraint( + model.Products, + model.TimePeriods, + rule=shortfall_rule, + doc='Shortfall calculation for each product j in each time period t', + ) + + # maximum shortfall allowed + def shortfall_max_rule(model, j, t): + """ + Imposes an upper limit on the shortfall allowed for each product j in each time period t. + + Parameters + ---------- + model : Pyomo.AbstractModel + Pyomo abstract model for medium-term purchasing contracts problem. + j : int + Index of materials. + t : int + Index of time period. + + Returns + ------- + Pyomo.Constraint + A constraint that limits the shortfall for product j during time period t to a maximum value specified by 'ShortfallUB[j, t]'. + This constraint is instrumental in aligning the model's solutions with real-world operational constraints and strategic objectives. + """ + return model.Shortfall[j, t] <= model.ShortfallUB[j, t] + + model.shortfall_max = Constraint( + model.Products, + model.TimePeriods, + rule=shortfall_max_rule, + doc='Maximum shortfall allowed for each product j in each time period t', + ) + + # maximum capacities of suppliers + def supplier_capacity_rule(model, j, t): + """ + Enforces the upper limits on the supply capacity for each raw material j provided by suppliers in each time period t. + + Parameters + ---------- + model : Pyomo.AbstractModel + Pyomo abstract model for medium-term purchasing contracts problem. + j : int + Index of materials. + t : int + Index of time period. + + Returns + ------- + Pyomo.Constraint + A constraint that limits the flow rate of raw material j from suppliers in time period t to not exceed the predefined upper bound 'SupplyAndDemandUBs[j, t]'. + This constraint is crucial for ensuring the feasibility of the supply chain model and its alignment with practical supply capabilities. + """ + return model.FlowRate[j, t] <= model.SupplyAndDemandUBs[j, t] + + model.supplier_capacity = Constraint( + model.RawMaterials, + model.TimePeriods, + rule=supplier_capacity_rule, + doc='Maximum supply capacity for each raw material j in each time period t', + ) + + # demand upper bound + def demand_UB_rule(model, j, t): + """ + Ensures that the supply of each product j does not exceed its maximum demand in each time period t. + + Parameters + ---------- + model : Pyomo.AbstractModel + Pyomo abstract model for medium-term purchasing contracts problem. + j : int + Index of materials. + t : int + Index of time period. + + Returns + ------- + Pyomo.Constraint + A constraint limiting the flow rate of product j to not exceed the predefined maximum demand 'SupplyAndDemandUBs[j, t]' in time period t, ensuring production is demand-driven. + """ + return model.FlowRate[j, t] <= model.SupplyAndDemandUBs[j, t] + + model.demand_UB = Constraint( + model.Products, + model.TimePeriods, + rule=demand_UB_rule, + doc='Maximum demand allowed for each product j in each time period t', + ) + + # demand lower bound + def demand_LB_rule(model, j, t): + """ + Ensures that the supply of each product j meets at least the minimum demand in each time period t. + + Parameters + ---------- + model : Pyomo.AbstractModel + Pyomo abstract model for medium-term purchasing contracts problem. + j : int + Index of materials. + t : int + Index of time period. + + Returns + ------- + Pyomo.Constraint + A constraint ensuring that the flow rate of product j meets or exceeds the minimum demand 'DemandLB[j, t]' in time period t, supporting effective market engagement. + """ + return model.FlowRate[j, t] >= model.DemandLB[j, t] + + model.demand_LB = Constraint( + model.Products, + model.TimePeriods, + rule=demand_LB_rule, + doc='Minimum demand required for each product j in each time period t', + ) + + # FIXED PRICE CONTRACT + + # Disjunction for Fixed Price contract buying options + def FP_contract_disjunct_rule(disjunct, j, t, buy): + """ + Defines disjunctive constraints for procurement decisions under a Fixed Price (FP) contract for material j in time period t. + + A decision must be made whether to engage in purchasing under the contract terms or not for each material in each time period. + This function encapsulates the disjunctive nature of this decision: if the decision is to buy ('buy' parameter is True), + the amount purchased under the FP contract is limited by a predefined maximum ('MAX_AMOUNT_FP'); + otherwise, no purchase is made under the FP contract for that material and period. + This disjunctive approach allows for modeling complex decision-making processes in procurement strategies. + + Parameters + ---------- + disjunct : Pyomo.Disjunct + A Pyomo Disjunct object representing a part of the disjunction. It encapsulates the constraints that are valid under a specific scenario ('buy' or not buy). + j : int + Index of materials. + t : int + Index of time period. + buy : str + A decision parameter indicating whether to purchase ('buy' is True) under the FP contract or not ('buy' is False) for material j in time period t + + Notes + ----- + The 'buy' parameter is treated as a binary variable in the context of the model, where True indicates a decision to engage in purchasing under the FP contract, and False indicates otherwise. + """ + model = disjunct.model() + if buy: + disjunct.c = Constraint( + expr=model.AmountPurchased_FP[j, t] <= MAX_AMOUNT_FP + ) + else: + disjunct.c = Constraint(expr=model.AmountPurchased_FP[j, t] == 0) + + model.FP_contract_disjunct = Disjunct( + model.RawMaterials, + model.TimePeriods, + model.BuyFPContract, + rule=FP_contract_disjunct_rule, + doc='Disjunctive constraints for Fixed Price contract buying options', + ) + + # Fixed price disjunction + def FP_contract_rule(model, j, t): + """ + Creates a choice between buying or not buying materials under a Fixed Price contract for each material 'j' and time 't'. + + This function sets up a disjunction, which is like a crossroads for the model: for each material and time period, it can choose one of the paths defined in the 'FP_contract_disjunct_rule'. + + Parameters + ---------- + model : Pyomo.AbstractModel + Pyomo abstract model for medium-term purchasing contracts problem. + j : int + Index of materials. + t : int + Index of time period. + + Returns + ------- + Pyomo.Disjunction + A disjunction that represents the decision-making point for FP contract purchases, contributing to the model's overall procurement strategy. + """ + return [model.FP_contract_disjunct[j, t, buy] for buy in model.BuyFPContract] + + model.FP_disjunction = Disjunction( + model.RawMaterials, + model.TimePeriods, + rule=FP_contract_rule, + doc='Disjunction for Fixed Price contract buying options', + ) + + # cost constraint for fixed price contract (independent constraint) + def FP_contract_cost_rule(model, j, t): + """_summary_ + + Parameters + ---------- + model : Pyomo.AbstractModel + Pyomo abstract model for medium-term purchasing contracts problem. + j : int + Index of materials. + t : int + Index of time period. + + Returns + ------- + Pyomo.Constraint + A constraint equating the FP contract cost for material j in period t to the product of the purchased amount and its price, ensuring accurate financial accounting in the model. + """ + return ( + model.Cost_FP[j, t] == model.AmountPurchased_FP[j, t] * model.Prices[j, t] + ) + + model.FP_contract_cost = Constraint( + model.RawMaterials, + model.TimePeriods, + rule=FP_contract_cost_rule, + doc='Cost constraint for Fixed Price contract', + ) + + # DISCOUNT CONTRACT + + # Disjunction for Discount contract + def discount_contract_disjunct_rule(disjunct, j, t, buy): + """ + Sets rules for purchasing materials j in time t under discount contracts based on the buying decision 'buy'. + + For discount contracts, the decision involves purchasing below or above a minimum amount for a discount, or not selecting the contract at all. + This rule reflects these choices by adjusting purchasing amounts and enforcing corresponding constraints. + + Parameters + ---------- + disjunct : Pyomo.Disjunct + A Pyomo Disjunct object representing a part of the disjunction. It encapsulates the constraints that are valid under a specific scenario ('BelowMin', 'AboveMin', or 'NotSelected'). + Index of materials. + t : int + Index of time period. + buy : str + Decision on purchasing strategy: 'BelowMin', 'AboveMin', or 'NotSelected'. + + """ + model = disjunct.model() + if buy == 'BelowMin': + disjunct.belowMin = Constraint( + expr=model.AmountPurchasedBelowMin_Discount[j, t] + <= model.MinAmount_Discount[j, t] + ) + disjunct.aboveMin = Constraint( + expr=model.AmountPurchasedAboveMin_Discount[j, t] == 0 + ) + elif buy == 'AboveMin': + disjunct.belowMin = Constraint( + expr=model.AmountPurchasedBelowMin_Discount[j, t] + == model.MinAmount_Discount[j, t] + ) + disjunct.aboveMin = Constraint( + expr=model.AmountPurchasedAboveMin_Discount[j, t] >= 0 + ) + elif buy == 'NotSelected': + disjunct.belowMin = Constraint( + expr=model.AmountPurchasedBelowMin_Discount[j, t] == 0 + ) + disjunct.aboveMin = Constraint( + expr=model.AmountPurchasedAboveMin_Discount[j, t] == 0 + ) + else: + raise RuntimeError("Unrecognized choice for discount contract: %s" % buy) + + model.discount_contract_disjunct = Disjunct( + model.RawMaterials, + model.TimePeriods, + model.BuyDiscountContract, + rule=discount_contract_disjunct_rule, + doc='Disjunctive constraints for Discount contract buying options', + ) + + # Discount contract disjunction + def discount_contract_rule(model, j, t): + """ + Determines the disjunction for purchasing under discount contracts for each material and time period, based on available decisions. + + This function sets up the model to choose among different discount purchasing strategies, enhancing the flexibility in procurement planning. + + Parameters + ---------- + model : Pyomo.AbstractModel + Pyomo abstract model for medium-term purchasing contracts problem. + j : int + Index of materials. + t : int + Index of time period. + + Returns + ------- + Pyomo.Disjunction + The disjunction representing the choice among discount contract purchasing strategies. + """ + return [ + model.discount_contract_disjunct[j, t, buy] + for buy in model.BuyDiscountContract + ] + + model.discount_contract = Disjunction( + model.RawMaterials, + model.TimePeriods, + rule=discount_contract_rule, + doc='Disjunction for Discount contract buying options', + ) + + # cost constraint for discount contract (independent constraint) + def discount_cost_rule(model, j, t): + """ + Calculates the cost of purchasing material 'j' in time 't' under a discount contract, accounting for different price levels. + + This constraint ensures the model correctly accounts for the total cost of purchases under discount contracts, which may involve different prices based on quantity thresholds. + + Parameters + ---------- + model : Pyomo.AbstractModel + Pyomo abstract model for medium-term purchasing contracts problem. + j : int + Index of materials. + t : int + Index of time period. + + Returns + ------- + Pyomo.Constraint + The constraint that calculates the total cost of purchases under discount contracts. + """ + return ( + model.Cost_Discount[j, t] + == model.RegPrice_Discount[j, t] + * model.AmountPurchasedBelowMin_Discount[j, t] + + model.DiscountPrice_Discount[j, t] + * model.AmountPurchasedAboveMin_Discount[j, t] + ) + + model.discount_cost = Constraint( + model.RawMaterials, + model.TimePeriods, + rule=discount_cost_rule, + doc='Cost constraint for Discount contract', + ) + + # BULK CONTRACT + + # Bulk contract buying options disjunct + def bulk_contract_disjunct_rule(disjunct, j, t, buy): + """ + Defines conditions for bulk purchases of material j at time t based on the decision 'buy'. + + This rule determines how much of a material is bought under a bulk contract and at what price, based on whether purchases are below or above a specified minimum amount, or if the bulk option is not selected. + It enforces different constraints for the amount and cost of materials under these scenarios. + + Parameters + ---------- + disjunct : Pyomo.Disjunct + A Pyomo Disjunct object representing a part of the disjunction. It encapsulates the constraints that are valid under a specific scenario ('BelowMin', 'AboveMin', or 'NotSelected'). + j : int + Index of materials. + t : int + Index of time period. + buy : str + The decision on how to engage with the bulk contract: 'BelowMin', 'AboveMin', or 'NotSelected'. + + """ + model = disjunct.model() + if buy == 'BelowMin': + disjunct.amount = Constraint( + expr=model.AmountPurchased_Bulk[j, t] <= model.MinAmount_Bulk[j, t] + ) + disjunct.price = Constraint( + expr=model.Cost_Bulk[j, t] + == model.RegPrice_Bulk[j, t] * model.AmountPurchased_Bulk[j, t] + ) + elif buy == 'AboveMin': + disjunct.amount = Constraint( + expr=model.AmountPurchased_Bulk[j, t] >= model.MinAmount_Bulk[j, t] + ) + disjunct.price = Constraint( + expr=model.Cost_Bulk[j, t] + == model.DiscountPrice_Bulk[j, t] * model.AmountPurchased_Bulk[j, t] + ) + elif buy == 'NotSelected': + disjunct.amount = Constraint(expr=model.AmountPurchased_Bulk[j, t] == 0) + disjunct.price = Constraint(expr=model.Cost_Bulk[j, t] == 0) + else: + raise RuntimeError("Unrecognized choice for bulk contract: %s" % buy) + + model.bulk_contract_disjunct = Disjunct( + model.RawMaterials, + model.TimePeriods, + model.BuyBulkContract, + rule=bulk_contract_disjunct_rule, + doc='Disjunctive constraints for Bulk contract buying options', + ) + + # Bulk contract disjunction + def bulk_contract_rule(model, j, t): + """ + Establishes a decision-making framework for bulk purchases, allowing the model to choose among predefined scenarios. + + This function sets up a flexible structure for deciding on bulk purchases. + Each material and time period can be evaluated independently, allowing the model to adapt to various conditions and optimize procurement strategies under bulk contracts. + + Parameters + ---------- + model : Pyomo.AbstractModel + Pyomo abstract model for medium-term purchasing contracts problem. + j : int + Index of materials. + t : int + Index of time period. + + Returns + ------- + Pyomo.Disjunction + A set of disjunctive conditions that the model can choose from when making bulk purchasing decisions. + """ + return [ + model.bulk_contract_disjunct[j, t, buy] for buy in model.BuyBulkContract + ] + + model.bulk_contract = Disjunction( + model.RawMaterials, + model.TimePeriods, + rule=bulk_contract_rule, + doc='Disjunction for Bulk contract buying options', + ) + + # FIXED DURATION CONTRACT + + def FD_1mo_contract(disjunct, j, t): + """ + Defines the constraints for engaging in a 1-month fixed duration contract for material j at time t. + This includes a minimum purchase amount and the cost calculation based on contract-specific prices. + + Parameters + ---------- + disjunct : Pyomo.Disjunct + A component representing the 1-month contract scenario. + j : int + Index of materials. + t : int + Index of time period. + """ + model = disjunct.model() + disjunct.amount1 = Constraint( + expr=model.AmountPurchased_FD[j, t] >= MIN_AMOUNT_FD_1MONTH + ) + disjunct.price1 = Constraint( + expr=model.Cost_FD[j, t] + == model.Prices_Length[j, 1, t] * model.AmountPurchased_FD[j, t] + ) + + model.FD_1mo_contract = Disjunct( + model.RawMaterials, + model.TimePeriods, + rule=FD_1mo_contract, + doc='1-month fixed duration contract', + ) + + def FD_2mo_contract(disjunct, j, t): + """ + Establishes conditions for a 2-month fixed duration contract. + This involves a minimum purchase requirement for two consecutive periods and corresponding cost calculations. + + Parameters + ---------- + disjunct : Pyomo.Disjunct + The 2-month contract scenario component + j : int + Index of materials. + t : int + Index of time period. + """ + model = disjunct.model() + disjunct.amount1 = Constraint( + expr=model.AmountPurchased_FD[j, t] >= model.MinAmount_Length[j, 2] + ) + disjunct.price1 = Constraint( + expr=model.Cost_FD[j, t] + == model.Prices_Length[j, 2, t] * model.AmountPurchased_FD[j, t] + ) + # only enforce these if we aren't in the last time period + if t < model.TimePeriods[-1]: + disjunct.amount2 = Constraint( + expr=model.AmountPurchased_FD[j, t + 1] >= model.MinAmount_Length[j, 2] + ) + disjunct.price2 = Constraint( + expr=model.Cost_FD[j, t + 1] + == model.Prices_Length[j, 2, t] * model.AmountPurchased_FD[j, t + 1] + ) + + model.FD_2mo_contract = Disjunct( + model.RawMaterials, + model.TimePeriods, + rule=FD_2mo_contract, + doc='2-month fixed duration contract', + ) + + def FD_3mo_contract(disjunct, j, t): + """ + Sets up a 3-month fixed duration contract scenario with minimum purchase requirements extending over three periods and the cost calculation. + + Parameters + ---------- + disjunct : Pyomo.Disjunct + The 3-month contract scenario component. + j : int + Index of materials. + t : int + Index of time period. + """ + model = disjunct.model() + # NOTE: I think there is a mistake in the GAMS file in line 327. + # they use the bulk minamount rather than the length one. + # I am doing the same here for validation purposes. + disjunct.amount1 = Constraint( + expr=model.AmountPurchased_FD[j, t] >= model.MinAmount_Bulk[j, 3] + ) + disjunct.cost1 = Constraint( + expr=model.Cost_FD[j, t] + == model.Prices_Length[j, 3, t] * model.AmountPurchased_FD[j, t] + ) + # check we aren't in one of the last two time periods + if t < model.TimePeriods[-1]: + disjunct.amount2 = Constraint( + expr=model.AmountPurchased_FD[j, t + 1] >= model.MinAmount_Length[j, 3] + ) + disjunct.cost2 = Constraint( + expr=model.Cost_FD[j, t + 1] + == model.Prices_Length[j, 3, t] * model.AmountPurchased_FD[j, t + 1] + ) + if t < model.TimePeriods[-2]: + disjunct.amount3 = Constraint( + expr=model.AmountPurchased_FD[j, t + 2] >= model.MinAmount_Length[j, 3] + ) + disjunct.cost3 = Constraint( + expr=model.Cost_FD[j, t + 2] + == model.Prices_Length[j, 3, t] * model.AmountPurchased_FD[j, t + 2] + ) + + model.FD_3mo_contract = Disjunct( + model.RawMaterials, + model.TimePeriods, + rule=FD_3mo_contract, + doc='3-month fixed duration contract', + ) + + def FD_no_contract(disjunct, j, t): + """ + Represents the scenario where no fixed duration contract is selected for material j at time t. + Ensures no purchases or costs are accounted for under FD contracts. + + Parameters + ---------- + disjunct : Pyomo.Disjunct + The 'no contract' scenario component. + j : int + Index of materials. + t : int + Index of time period. + """ + model = disjunct.model() + disjunct.amount1 = Constraint(expr=model.AmountPurchased_FD[j, t] == 0) + disjunct.cost1 = Constraint(expr=model.Cost_FD[j, t] == 0) + if t < model.TimePeriods[-1]: + disjunct.amount2 = Constraint(expr=model.AmountPurchased_FD[j, t + 1] == 0) + disjunct.cost2 = Constraint(expr=model.Cost_FD[j, t + 1] == 0) + if t < model.TimePeriods[-2]: + disjunct.amount3 = Constraint(expr=model.AmountPurchased_FD[j, t + 2] == 0) + disjunct.cost3 = Constraint(expr=model.Cost_FD[j, t + 2] == 0) + + model.FD_no_contract = Disjunct( + model.RawMaterials, + model.TimePeriods, + rule=FD_no_contract, + doc='No fixed duration contract', + ) + + def FD_contract(model, j, t): + """ + Consolidates the FD contract scenarios into a single decision framework, allowing the model to choose the most optimal contract length or to not select an FD contract for each material and time period. + + Parameters + ---------- + model : Pyomo.AbstractModel + Pyomo abstract model for medium-term purchasing contracts problem. + j : int + Index of materials. + t : int + Index of time period. + + Returns + ------- + Pyomo.Disjunction + The disjunctive decision structure for FD contracts. + """ + return [ + model.FD_1mo_contract[j, t], + model.FD_2mo_contract[j, t], + model.FD_3mo_contract[j, t], + model.FD_no_contract[j, t], + ] + + model.FD_contract = Disjunction( + model.RawMaterials, + model.TimePeriods, + rule=FD_contract, + doc='Fixed duration contract scenarios', + ) + model = model.create_instance(join(this_file_dir(), 'med_term_purchasing.dat')) + return model + + +if __name__ == "__main__": + m = build_model() + TransformationFactory('gdp.bigm').apply_to(m) + SolverFactory('gams').solve( + m, solver='baron', tee=True, add_options=['option optcr=1e-6;'] + ) + m.profit.display() diff --git a/gdplib/methanol/methanol.py b/gdplib/methanol/methanol.py index 16f802a..fe85918 100644 --- a/gdplib/methanol/methanol.py +++ b/gdplib/methanol/methanol.py @@ -22,9 +22,13 @@ def fix_vars_with_equal_bounds(m, tol=1e-8): if lb is None or ub is None: continue if lb > ub + tol: - raise InfeasibleError('Variable lb is larger than ub: {0} lb: {1} ub: {2}'.format(v.name, lb, ub)) + raise InfeasibleError( + 'Variable lb is larger than ub: {0} lb: {1} ub: {2}'.format( + v.name, lb, ub + ) + ) elif abs(ub - lb) <= tol: - v.fix(0.5*(lb+ub)) + v.fix(0.5 * (lb + ub)) class MethanolModel(object): @@ -44,8 +48,8 @@ def __init__(self): self.electricity_cost = 0.255 self.cooling_cost = 700 self.heating_cost = 8000 - self.purity_demand = 0.9 #purity demand in product stream - self.demand = 1.0 # flowrate restriction on product flow + self.purity_demand = 0.9 # purity demand in product stream + self.demand = 1.0 # flowrate restriction on product flow self.flow_feed_lb = 0.5 self.flow_feed_ub = 5 self.flow_feed_temp = 3 @@ -58,7 +62,7 @@ def __init__(self): self.cheap_reactor_variable_cost = 5 self.expensive_reactor_fixed_cost = 250 self.expensive_reactor_variable_cost = 10 - self.heat_unit_match = 0.00306 + self.heat_unit_match = 0.00306 self.capacity_redundancy = 1.2 self.antoine_unit_trans = 7500.6168 self.K = 0.415 @@ -66,7 +70,7 @@ def __init__(self): self.reactor_relation = 0.9 self.purity_demand = 0.9 self.fix_electricity_cost = 175 - self.two_stage_fix_cost = 50 + self.two_stage_fix_cost = 50 m.streams = pe.Set(initialize=list(range(1, 34)), ordered=True) m.components = pe.Set(initialize=['H2', 'CO', 'CH3OH', 'CH4'], ordered=True) @@ -79,12 +83,12 @@ def __init__(self): flow_1['H2'] = 0.6 flow_1['CO'] = 0.25 flow_1['CH4'] = 0.15 - m.flow_1_composition = pe.Param(m.components,initialize = flow_1,default = 0) + m.flow_1_composition = pe.Param(m.components, initialize=flow_1, default=0) flow_2 = dict() flow_2['H2'] = 0.65 flow_2['CO'] = 0.30 flow_2['CH4'] = 0.05 - m.flow_2_composition = pe.Param(m.components,initialize = flow_2,default = 0) + m.flow_2_composition = pe.Param(m.components, initialize=flow_2, default=0) m.pressures[13].setlb(2.5) m.temps[13].setlb(4.23) @@ -158,11 +162,15 @@ def __init__(self): self.liquid_outlets[13] = 22 def _total_flow(_m, _s): - return _m.flows[_s] == sum(_m.component_flows[_s, _c] for _c in _m.components) + return _m.flows[_s] == sum( + _m.component_flows[_s, _c] for _c in _m.components + ) + m.total_flow_con = pe.Constraint(m.streams, rule=_total_flow) - m.purity_con = pe.Constraint(expr=m.component_flows[23, 'CH3OH'] >= self.purity_demand * m.flows[23]) - + m.purity_con = pe.Constraint( + expr=m.component_flows[23, 'CH3OH'] >= self.purity_demand * m.flows[23] + ) # ************************************ # Feed @@ -172,7 +180,7 @@ def _total_flow(_m, _s): self.build_stream_doesnt_exist_con(m.cheap_feed_disjunct, 2) m.cheap_feed_disjunct.feed_cons = c = pe.ConstraintList() c.add(m.component_flows[1, 'H2'] == m.flow_1_composition['H2'] * m.flows[1]) - c.add(m.component_flows[1, 'CO'] == m.flow_1_composition['CO']* m.flows[1]) + c.add(m.component_flows[1, 'CO'] == m.flow_1_composition['CO'] * m.flows[1]) c.add(m.component_flows[1, 'CH4'] == m.flow_1_composition['CH4'] * m.flows[1]) c.add(m.flows[1] >= self.flow_feed_lb) c.add(m.flows[1] <= self.flow_feed_ub) @@ -191,7 +199,9 @@ def _total_flow(_m, _s): c.add(m.temps[2] == self.flow_feed_temp) c.add(m.pressures[2] == self.flow_feed_pressure) - m.feed_disjunctions = gdp.Disjunction(expr=[m.cheap_feed_disjunct, m.expensive_feed_disjunct]) + m.feed_disjunctions = gdp.Disjunction( + expr=[m.cheap_feed_disjunct, m.expensive_feed_disjunct] + ) # ************************************ # Feed compressors @@ -213,11 +223,21 @@ def _total_flow(_m, _s): self.build_compressor(m.two_stage_feed_compressor_disjunct, 4) self.build_cooler(m.two_stage_feed_compressor_disjunct, 5) self.build_compressor(m.two_stage_feed_compressor_disjunct, 6) - m.two_stage_feed_compressor_disjunct.equal_electric_requirements = pe.Constraint(expr=m.two_stage_feed_compressor_disjunct.compressor_4.electricity_requirement == m.two_stage_feed_compressor_disjunct.compressor_6.electricity_requirement) + m.two_stage_feed_compressor_disjunct.equal_electric_requirements = pe.Constraint( + expr=m.two_stage_feed_compressor_disjunct.compressor_4.electricity_requirement + == m.two_stage_feed_compressor_disjunct.compressor_6.electricity_requirement + ) m.two_stage_feed_compressor_disjunct.exists = pe.Var(bounds=(0, 1)) - m.two_stage_feed_compressor_disjunct.exists_con = pe.Constraint(expr=m.two_stage_feed_compressor_disjunct.exists == 1) + m.two_stage_feed_compressor_disjunct.exists_con = pe.Constraint( + expr=m.two_stage_feed_compressor_disjunct.exists == 1 + ) - m.feed_compressor_disjunction = gdp.Disjunction(expr=[m.single_stage_feed_compressor_disjunct, m.two_stage_feed_compressor_disjunct]) + m.feed_compressor_disjunction = gdp.Disjunction( + expr=[ + m.single_stage_feed_compressor_disjunct, + m.two_stage_feed_compressor_disjunct, + ] + ) self.build_mixer(m, 'recycle_feed_mixer') self.build_cooler(m, 7) @@ -233,7 +253,9 @@ def _total_flow(_m, _s): self.build_stream_doesnt_exist_con(m.expensive_reactor, 16) self.build_reactor(m.expensive_reactor, 9) m.expensive_reactor.exists = pe.Var(bounds=(0, 1)) - m.expensive_reactor.exists_con = pe.Constraint(expr=m.expensive_reactor.exists == 1) + m.expensive_reactor.exists_con = pe.Constraint( + expr=m.expensive_reactor.exists == 1 + ) m.expensive_reactor.composition_cons = c = pe.ConstraintList() for _comp in m.components: c.add(m.component_flows[17, _comp] >= 0.01) @@ -250,7 +272,9 @@ def _total_flow(_m, _s): for _comp in m.components: c.add(m.component_flows[16, _comp] >= 0.01) - m.reactor_disjunction = gdp.Disjunction(expr=[m.expensive_reactor, m.cheap_reactor]) + m.reactor_disjunction = gdp.Disjunction( + expr=[m.expensive_reactor, m.cheap_reactor] + ) self.build_expansion_valve(m, 11) self.build_cooler(m, 12) @@ -265,10 +289,18 @@ def _total_flow(_m, _s): m.single_stage_recycle_compressor_disjunct = gdp.Disjunct() self.build_equal_streams(m.single_stage_recycle_compressor_disjunct, 26, 27) self.build_equal_streams(m.single_stage_recycle_compressor_disjunct, 29, 33) - self.build_stream_doesnt_exist_con(m.single_stage_recycle_compressor_disjunct, 28) - self.build_stream_doesnt_exist_con(m.single_stage_recycle_compressor_disjunct, 30) - self.build_stream_doesnt_exist_con(m.single_stage_recycle_compressor_disjunct, 31) - self.build_stream_doesnt_exist_con(m.single_stage_recycle_compressor_disjunct, 32) + self.build_stream_doesnt_exist_con( + m.single_stage_recycle_compressor_disjunct, 28 + ) + self.build_stream_doesnt_exist_con( + m.single_stage_recycle_compressor_disjunct, 30 + ) + self.build_stream_doesnt_exist_con( + m.single_stage_recycle_compressor_disjunct, 31 + ) + self.build_stream_doesnt_exist_con( + m.single_stage_recycle_compressor_disjunct, 32 + ) self.build_compressor(m.single_stage_recycle_compressor_disjunct, 16) m.two_stage_recycle_compressor_disjunct = gdp.Disjunct() @@ -279,35 +311,74 @@ def _total_flow(_m, _s): self.build_compressor(m.two_stage_recycle_compressor_disjunct, 17) self.build_cooler(m.two_stage_recycle_compressor_disjunct, 18) self.build_compressor(m.two_stage_recycle_compressor_disjunct, 19) - m.two_stage_recycle_compressor_disjunct.equal_electric_requirements = pe.Constraint(expr=m.two_stage_recycle_compressor_disjunct.compressor_17.electricity_requirement == m.two_stage_recycle_compressor_disjunct.compressor_19.electricity_requirement) + m.two_stage_recycle_compressor_disjunct.equal_electric_requirements = pe.Constraint( + expr=m.two_stage_recycle_compressor_disjunct.compressor_17.electricity_requirement + == m.two_stage_recycle_compressor_disjunct.compressor_19.electricity_requirement + ) m.two_stage_recycle_compressor_disjunct.exists = pe.Var(bounds=(0, 1)) - m.two_stage_recycle_compressor_disjunct.exists_con = pe.Constraint(expr=m.two_stage_recycle_compressor_disjunct.exists == 1) + m.two_stage_recycle_compressor_disjunct.exists_con = pe.Constraint( + expr=m.two_stage_recycle_compressor_disjunct.exists == 1 + ) - m.recycle_compressor_disjunction = gdp.Disjunction(expr=[m.single_stage_recycle_compressor_disjunct, m.two_stage_recycle_compressor_disjunct]) + m.recycle_compressor_disjunction = gdp.Disjunction( + expr=[ + m.single_stage_recycle_compressor_disjunct, + m.two_stage_recycle_compressor_disjunct, + ] + ) # ************************************ # Objective # ************************************ - + e = 0 e -= self.cost_flow_1 * m.flows[1] e -= self.cost_flow_2 * m.flows[2] e += self.price_of_product * m.flows[23] e += self.price_of_byproduct * m.flows[25] - e -= self.cheap_reactor_variable_cost * self.reactor_volume * m.cheap_reactor.exists + e -= ( + self.cheap_reactor_variable_cost + * self.reactor_volume + * m.cheap_reactor.exists + ) e -= self.cheap_reactor_fixed_cost * m.cheap_reactor.exists - e -= self.expensive_reactor_variable_cost * self.reactor_volume * m.expensive_reactor.exists + e -= ( + self.expensive_reactor_variable_cost + * self.reactor_volume + * m.expensive_reactor.exists + ) e -= self.expensive_reactor_fixed_cost * m.expensive_reactor.exists - e -= ( self.fix_electricity_cost+ self.electricity_cost) * m.single_stage_feed_compressor_disjunct.compressor_3.electricity_requirement + e -= ( + (self.fix_electricity_cost + self.electricity_cost) + * m.single_stage_feed_compressor_disjunct.compressor_3.electricity_requirement + ) e -= self.two_stage_fix_cost * m.two_stage_feed_compressor_disjunct.exists - e -= (self.fix_electricity_cost + self.electricity_cost) * m.two_stage_feed_compressor_disjunct.compressor_4.electricity_requirement - e -= (self.fix_electricity_cost + self.electricity_cost) * m.two_stage_feed_compressor_disjunct.compressor_6.electricity_requirement + e -= ( + (self.fix_electricity_cost + self.electricity_cost) + * m.two_stage_feed_compressor_disjunct.compressor_4.electricity_requirement + ) + e -= ( + (self.fix_electricity_cost + self.electricity_cost) + * m.two_stage_feed_compressor_disjunct.compressor_6.electricity_requirement + ) e -= self.cooling_cost * m.two_stage_feed_compressor_disjunct.cooler_5.heat_duty - e -= (self.fix_electricity_cost + self.electricity_cost) * m.single_stage_recycle_compressor_disjunct.compressor_16.electricity_requirement + e -= ( + (self.fix_electricity_cost + self.electricity_cost) + * m.single_stage_recycle_compressor_disjunct.compressor_16.electricity_requirement + ) e -= self.two_stage_fix_cost * m.two_stage_recycle_compressor_disjunct.exists - e -= (self.fix_electricity_cost + self.electricity_cost) * m.two_stage_recycle_compressor_disjunct.compressor_17.electricity_requirement - e -= (self.fix_electricity_cost + self.electricity_cost) * m.two_stage_recycle_compressor_disjunct.compressor_19.electricity_requirement - e -= self.cooling_cost * m.two_stage_recycle_compressor_disjunct.cooler_18.heat_duty + e -= ( + (self.fix_electricity_cost + self.electricity_cost) + * m.two_stage_recycle_compressor_disjunct.compressor_17.electricity_requirement + ) + e -= ( + (self.fix_electricity_cost + self.electricity_cost) + * m.two_stage_recycle_compressor_disjunct.compressor_19.electricity_requirement + ) + e -= ( + self.cooling_cost + * m.two_stage_recycle_compressor_disjunct.cooler_18.heat_duty + ) e -= self.cooling_cost * m.cooler_7.heat_duty e -= self.heating_cost * m.heater_8.heat_duty e -= self.cooling_cost * m.cooler_12.heat_duty @@ -324,18 +395,28 @@ def build_compressor(self, block, unit_number): out_stream = self.outlet_streams[u] b = pe.Block() - setattr(block, 'compressor_'+str(u), b) + setattr(block, 'compressor_' + str(u), b) b.p_ratio = pe.Var(bounds=(0, 1.74)) b.electricity_requirement = pe.Var(bounds=(0, 50)) def _component_balances(_b, _c): return m.component_flows[out_stream, _c] == m.component_flows[in_stream, _c] + b.component_balances = pe.Constraint(m.components, rule=_component_balances) b.t_ratio_con = pe.Constraint(expr=t[out_stream] == b.p_ratio * t[in_stream]) - b.electricity_requirement_con = pe.Constraint(expr=(b.electricity_requirement == self.alpha * - (b.p_ratio - 1) * t[in_stream] * m.flows[in_stream] / - (10.0 * self.eta * self.gamma))) - b.p_ratio_con = pe.Constraint(expr=p[out_stream]**self.gamma == b.p_ratio * p[in_stream]**self.gamma) + b.electricity_requirement_con = pe.Constraint( + expr=( + b.electricity_requirement + == self.alpha + * (b.p_ratio - 1) + * t[in_stream] + * m.flows[in_stream] + / (10.0 * self.eta * self.gamma) + ) + ) + b.p_ratio_con = pe.Constraint( + expr=p[out_stream] ** self.gamma == b.p_ratio * p[in_stream] ** self.gamma + ) def build_expansion_valve(self, block, unit_number): u = unit_number @@ -345,12 +426,16 @@ def build_expansion_valve(self, block, unit_number): in_stream = self.inlet_streams[u] out_stream = self.outlet_streams[u] b = pe.Block() - setattr(block, 'expansion_valve_'+str(u), b) + setattr(block, 'expansion_valve_' + str(u), b) def _component_balances(_b, _c): return m.component_flows[out_stream, _c] == m.component_flows[in_stream, _c] + b.component_balances = pe.Constraint(m.components, rule=_component_balances) - b.ratio_con = pe.Constraint(expr=t[out_stream] * p[in_stream] ** self.gamma == t[in_stream] * p[out_stream] ** self.gamma) + b.ratio_con = pe.Constraint( + expr=t[out_stream] * p[in_stream] ** self.gamma + == t[in_stream] * p[out_stream] ** self.gamma + ) b.expansion_con = pe.Constraint(expr=p[out_stream] <= p[in_stream]) def build_cooler(self, block, unit_number): @@ -362,13 +447,19 @@ def build_cooler(self, block, unit_number): in_stream = self.inlet_streams[u] out_stream = self.outlet_streams[u] b = pe.Block() - setattr(block, 'cooler_'+str(u), b) + setattr(block, 'cooler_' + str(u), b) b.heat_duty = pe.Var(bounds=(0, 50)) def _component_balances(_b, _c): return m.component_flows[out_stream, _c] == m.component_flows[in_stream, _c] + b.component_balances = pe.Constraint(m.components, rule=_component_balances) - b.heat_duty_con = pe.Constraint(expr=b.heat_duty == self.heat_unit_match * self.cp * (f[in_stream] * t[in_stream] - f[out_stream] * t[out_stream])) + b.heat_duty_con = pe.Constraint( + expr=b.heat_duty + == self.heat_unit_match + * self.cp + * (f[in_stream] * t[in_stream] - f[out_stream] * t[out_stream]) + ) b.pressure_con = pe.Constraint(expr=p[out_stream] == p[in_stream]) def build_heater(self, block, unit_number): @@ -380,13 +471,19 @@ def build_heater(self, block, unit_number): in_stream = self.inlet_streams[u] out_stream = self.outlet_streams[u] b = pe.Block() - setattr(block, 'heater_'+str(u), b) + setattr(block, 'heater_' + str(u), b) b.heat_duty = pe.Var(bounds=(0, 50)) def _component_balances(_b, _c): return m.component_flows[out_stream, _c] == m.component_flows[in_stream, _c] + b.component_balances = pe.Constraint(m.components, rule=_component_balances) - b.heat_duty_con = pe.Constraint(expr=b.heat_duty == self.heat_unit_match * self.cp * (f[out_stream] * t[out_stream] - f[in_stream] * t[in_stream])) + b.heat_duty_con = pe.Constraint( + expr=b.heat_duty + == self.heat_unit_match + * self.cp + * (f[out_stream] * t[out_stream] - f[in_stream] * t[in_stream]) + ) b.pressure_con = pe.Constraint(expr=p[out_stream] == p[in_stream]) def build_mixer(self, block, unit_number): @@ -398,13 +495,21 @@ def build_mixer(self, block, unit_number): in_stream1, in_stream2 = self.inlet_streams[u] out_stream = self.outlet_streams[u] b = pe.Block() - setattr(block, 'mixer_'+str(u), b) + setattr(block, 'mixer_' + str(u), b) def _component_balances(_b, _c): - return m.component_flows[out_stream, _c] == m.component_flows[in_stream1, _c] + m.component_flows[in_stream2, _c] + return ( + m.component_flows[out_stream, _c] + == m.component_flows[in_stream1, _c] + m.component_flows[in_stream2, _c] + ) + b.component_balances = pe.Constraint(m.components, rule=_component_balances) - b.average_temp = pe.Constraint(expr=(t[out_stream] * f[out_stream] == (t[in_stream1] * f[in_stream1] + - t[in_stream2] * f[in_stream2]))) + b.average_temp = pe.Constraint( + expr=( + t[out_stream] * f[out_stream] + == (t[in_stream1] * f[in_stream1] + t[in_stream2] * f[in_stream2]) + ) + ) b.pressure_con1 = pe.Constraint(expr=p[in_stream1] == p[out_stream]) b.pressure_con2 = pe.Constraint(expr=p[in_stream2] == p[out_stream]) @@ -416,18 +521,27 @@ def build_splitter(self, block, unit_number): in_stream = self.inlet_streams[u] out_stream1, out_stream2 = self.outlet_streams[u] b = pe.Block() - setattr(block, 'splitter_'+str(u), b) + setattr(block, 'splitter_' + str(u), b) b.split_fraction = pe.Var(bounds=(0, 1)) if unit_number == 'purge_splitter': b.split_fraction.setlb(0.01) b.split_fraction.setub(0.99) def _split_frac_rule(_b, _c): - return m.component_flows[out_stream1, _c] == b.split_fraction * m.component_flows[in_stream, _c] + return ( + m.component_flows[out_stream1, _c] + == b.split_fraction * m.component_flows[in_stream, _c] + ) + b.split_frac_con = pe.Constraint(m.components, rule=_split_frac_rule) def _component_balances(_b, _c): - return m.component_flows[in_stream, _c] == m.component_flows[out_stream1, _c] + m.component_flows[out_stream2, _c] + return ( + m.component_flows[in_stream, _c] + == m.component_flows[out_stream1, _c] + + m.component_flows[out_stream2, _c] + ) + b.component_balances = pe.Constraint(m.components, rule=_component_balances) b.temp_con1 = pe.Constraint(expr=t[in_stream] == t[out_stream1]) b.temp_con2 = pe.Constraint(expr=t[in_stream] == t[out_stream2]) @@ -443,6 +557,7 @@ def build_equal_streams(self, block, stream1, stream2): def _component_balances(_b, _c): return m.component_flows[stream2, _c] == m.component_flows[stream1, _c] + b.component_balances = pe.Constraint(m.components, rule=_component_balances) b.temp_con = pe.Constraint(expr=t[stream1] == t[stream2]) b.pressure_con = pe.Constraint(expr=p[stream1] == p[stream2]) @@ -458,7 +573,7 @@ def build_reactor(self, block, unit_number): out_stream = self.outlet_streams[u] b = pe.Block() - setattr(block, 'reactor_'+str(u), b) + setattr(block, 'reactor_' + str(u), b) b.consumption_rate = pe.Var(bounds=(0, 5)) b.conversion = pe.Var(bounds=(0, 0.42)) b.equilibrium_conversion = pe.Var(bounds=(0, 0.42)) @@ -473,25 +588,46 @@ def build_reactor(self, block, unit_number): b.t_inv_con = pe.Constraint(expr=b.temp * b.t_inv == 1) fbbt(b.p_sq_inv_con) # just getting bounds on p_sq_inv fbbt(b.t_inv_con) # just getting bounds on t_inv - b.conversion_consumption_con = pe.Constraint(expr=b.consumption_rate == b.conversion * component_f[in_stream, - key]) - b.energy_balance = pe.Constraint(expr=(f[in_stream]*t[in_stream] - f[out_stream]*t[ - out_stream])*self.cp == 0.01*self.heat_of_reaction*b.consumption_rate) - b.H2_balance = pe.Constraint(expr=component_f[out_stream,'H2'] == component_f[in_stream,'H2'] - - b.consumption_rate) - b.CO_balance = pe.Constraint(expr=component_f[out_stream,'CO'] == component_f[in_stream,'CO'] - - 0.5*b.consumption_rate) - b.CH3OH_balance = pe.Constraint(expr=component_f[out_stream,'CH3OH'] == component_f[in_stream,'CH3OH'] + - 0.5*b.consumption_rate) - b.CH4_balance = pe.Constraint(expr=component_f[out_stream,'CH4'] == component_f[in_stream,'CH4']) - b.eq_conversion_con = pe.Constraint(expr=b.equilibrium_conversion == self.K * - (1 - (self.delta_H*pe.exp(-18*b.t_inv)*b.p_sq_inv))) - b.conversion_con = pe.Constraint(expr=b.conversion * f[in_stream] == b.equilibrium_conversion * - (1-pe.exp(-self.volume_conversion[u]*self.reactor_volume)) * - (component_f[in_stream,'H2'] + component_f[in_stream, 'CO'] + - component_f[in_stream, 'CH3OH'])) + b.conversion_consumption_con = pe.Constraint( + expr=b.consumption_rate == b.conversion * component_f[in_stream, key] + ) + b.energy_balance = pe.Constraint( + expr=(f[in_stream] * t[in_stream] - f[out_stream] * t[out_stream]) * self.cp + == 0.01 * self.heat_of_reaction * b.consumption_rate + ) + b.H2_balance = pe.Constraint( + expr=component_f[out_stream, 'H2'] + == component_f[in_stream, 'H2'] - b.consumption_rate + ) + b.CO_balance = pe.Constraint( + expr=component_f[out_stream, 'CO'] + == component_f[in_stream, 'CO'] - 0.5 * b.consumption_rate + ) + b.CH3OH_balance = pe.Constraint( + expr=component_f[out_stream, 'CH3OH'] + == component_f[in_stream, 'CH3OH'] + 0.5 * b.consumption_rate + ) + b.CH4_balance = pe.Constraint( + expr=component_f[out_stream, 'CH4'] == component_f[in_stream, 'CH4'] + ) + b.eq_conversion_con = pe.Constraint( + expr=b.equilibrium_conversion + == self.K * (1 - (self.delta_H * pe.exp(-18 * b.t_inv) * b.p_sq_inv)) + ) + b.conversion_con = pe.Constraint( + expr=b.conversion * f[in_stream] + == b.equilibrium_conversion + * (1 - pe.exp(-self.volume_conversion[u] * self.reactor_volume)) + * ( + component_f[in_stream, 'H2'] + + component_f[in_stream, 'CO'] + + component_f[in_stream, 'CH3OH'] + ) + ) b.pressure_con1 = pe.Constraint(expr=b.pressure == p[in_stream]) - b.pressure_con2 = pe.Constraint(expr=p[out_stream] == self.reactor_relation*b.pressure) + b.pressure_con2 = pe.Constraint( + expr=p[out_stream] == self.reactor_relation * b.pressure + ) b.temp_con = pe.Constraint(expr=b.temp == t[out_stream]) def build_flash(self, block, unit_number): @@ -504,7 +640,7 @@ def build_flash(self, block, unit_number): vapor_stream = self.vapor_outlets[u] liquid_stream = self.liquid_outlets[u] b = pe.Block() - setattr(block, 'flash_'+str(u), b) + setattr(block, 'flash_' + str(u), b) b.vapor_pressure = pe.Var(m.components, bounds=(0.001, 80)) b.flash_t = pe.Var(bounds=(3, 5)) @@ -528,23 +664,50 @@ def build_flash(self, block, unit_number): b.antoine_C['CH4'] = -7.16 def _component_balances(_b, _c): - return m.component_flows[in_stream, _c] == m.component_flows[vapor_stream, _c] + m.component_flows[liquid_stream, _c] + return ( + m.component_flows[in_stream, _c] + == m.component_flows[vapor_stream, _c] + + m.component_flows[liquid_stream, _c] + ) + b.component_balances = pe.Constraint(m.components, rule=_component_balances) def _antoine(_b, _c): - return (_b.antoine_A[_c] - pe.log(self.antoine_unit_trans * _b.vapor_pressure[_c])) * (100 * _b.flash_t - _b.antoine_C[_c]) == _b.antoine_B[_c] + return ( + _b.antoine_A[_c] + - pe.log(self.antoine_unit_trans * _b.vapor_pressure[_c]) + ) * (100 * _b.flash_t - _b.antoine_C[_c]) == _b.antoine_B[_c] + b.antoine_con = pe.Constraint(m.components, rule=_antoine) def _vle(_b, _c): - return _b.vapor_recovery['H2'] * (_b.vapor_recovery[_c] * _b.vapor_pressure['H2'] + (1 - _b.vapor_recovery[_c]) * _b.vapor_pressure[_c]) == _b.vapor_pressure['H2'] * _b.vapor_recovery[_c] + return ( + _b.vapor_recovery['H2'] + * ( + _b.vapor_recovery[_c] * _b.vapor_pressure['H2'] + + (1 - _b.vapor_recovery[_c]) * _b.vapor_pressure[_c] + ) + == _b.vapor_pressure['H2'] * _b.vapor_recovery[_c] + ) + b.vle_set = pe.Set(initialize=['CO', 'CH3OH', 'CH4'], ordered=True) b.vle_con = pe.Constraint(b.vle_set, rule=_vle) def _vapor_recovery(_b, _c): - return m.component_flows[vapor_stream, _c] == _b.vapor_recovery[_c] * m.component_flows[in_stream, _c] + return ( + m.component_flows[vapor_stream, _c] + == _b.vapor_recovery[_c] * m.component_flows[in_stream, _c] + ) + b.vapor_recovery_con = pe.Constraint(m.components, rule=_vapor_recovery) - b.total_p_con = pe.Constraint(expr=b.flash_p*f[liquid_stream] == sum(b.vapor_pressure[_c] * m.component_flows[liquid_stream, _c] for _c in m.components)) + b.total_p_con = pe.Constraint( + expr=b.flash_p * f[liquid_stream] + == sum( + b.vapor_pressure[_c] * m.component_flows[liquid_stream, _c] + for _c in m.components + ) + ) b.flash_p_con = pe.ConstraintList() b.flash_p_con.add(b.flash_p == p[in_stream]) @@ -561,22 +724,35 @@ def build_stream_doesnt_exist_con(self, block, stream): b = pe.Block() setattr(block, 'stream_doesnt_exist_con_' + str(stream), b) b.zero_flow_con = pe.Constraint(expr=m.flows[stream] == 0) + def _zero_component_flows(_b, _c): return m.component_flows[stream, _c] == 0 - b.zero_component_flows_con = pe.Constraint(m.components, rule=_zero_component_flows) + + b.zero_component_flows_con = pe.Constraint( + m.components, rule=_zero_component_flows + ) b.fixed_temp_con = pe.Constraint(expr=m.temps[stream] == 3) b.fixed_pressure_con = pe.Constraint(expr=m.pressures[stream] == 1) def enumerate_solutions(): import time + feed_choices = ['cheap', 'expensive'] feed_compressor_choices = ['single_stage', 'two_stage'] reactor_choices = ['cheap', 'expensive'] recycle_compressor_choices = ['single_stage', 'two_stage'] - print('{0:<20}{1:<20}{2:<20}{3:<20}{4:<20}{5:<20}'.format('feed choice', 'feed compressor', 'reactor choice', - 'recycle compressor', 'termination cond', 'profit')) + print( + '{0:<20}{1:<20}{2:<20}{3:<20}{4:<20}{5:<20}'.format( + 'feed choice', + 'feed compressor', + 'reactor choice', + 'recycle compressor', + 'termination cond', + 'profit', + ) + ) since = time.time() for feed_choice in feed_choices: for feed_compressor_choice in feed_compressor_choices: @@ -584,13 +760,16 @@ def enumerate_solutions(): for recycle_compressor_choice in recycle_compressor_choices: m = MethanolModel() m = m.model - for _d in m.component_data_objects(gdp.Disjunct, descend_into=True, active=True, sort=True): + for _d in m.component_data_objects( + gdp.Disjunct, descend_into=True, active=True, sort=True + ): _d.BigM = pe.Suffix() - for _c in _d.component_data_objects(pe.Constraint, descend_into=True, active=True, sort=True): + for _c in _d.component_data_objects( + pe.Constraint, descend_into=True, active=True, sort=True + ): lb, ub = compute_bounds_on_expr(_c.body) _d.BigM[_c] = max(abs(lb), abs(ub)) - if feed_choice == 'cheap': m.cheap_feed_disjunct.indicator_var.fix(1) m.expensive_feed_disjunct.indicator_var.fix(0) @@ -633,29 +812,38 @@ def enumerate_solutions(): opt = pe.SolverFactory('ipopt') res = opt.solve(m, tee=False) - print('{0:<20}{1:<20}{2:<20}{3:<20}{4:<20}{5:<20}'.format(feed_choice, feed_compressor_choice, - reactor_choice, recycle_compressor_choice, - str(res.solver.termination_condition), - str(-pe.value(m.objective)))) - time_elapsed = time.time() - since + print( + '{0:<20}{1:<20}{2:<20}{3:<20}{4:<20}{5:<20}'.format( + feed_choice, + feed_compressor_choice, + reactor_choice, + recycle_compressor_choice, + str(res.solver.termination_condition), + str(-pe.value(m.objective)), + ) + ) + time_elapsed = time.time() - since print('The code run {:.0f}m {:.0f}s'.format(time_elapsed // 60, time_elapsed % 60)) return m - + + def solve_with_gdp_opt(): m = MethanolModel().model - for _d in m.component_data_objects(gdp.Disjunct, descend_into=True, active=True, sort=True): + for _d in m.component_data_objects( + gdp.Disjunct, descend_into=True, active=True, sort=True + ): _d.BigM = pe.Suffix() - for _c in _d.component_data_objects(pe.Constraint, descend_into=True, active=True, sort=True): + for _c in _d.component_data_objects( + pe.Constraint, descend_into=True, active=True, sort=True + ): lb, ub = compute_bounds_on_expr(_c.body) _d.BigM[_c] = max(abs(lb), abs(ub)) opt = pe.SolverFactory('gdpopt') - opt.CONFIG.strategy = 'LOA' - opt.CONFIG.mip_solver = 'gams' - opt.CONFIG.nlp_solver = 'gams' - opt.CONFIG.tee = True - res = opt.solve(m) - for d in m.component_data_objects(ctype=gdp.Disjunct, active=True, sort=True, descend_into=True): + res = opt.solve(m, algorithm='LOA', mip_solver='gams', nlp_solver='gams', tee=True) + for d in m.component_data_objects( + ctype=gdp.Disjunct, active=True, sort=True, descend_into=True + ): if d.indicator_var.value == 1: print(d.name) print(res) @@ -665,8 +853,8 @@ def solve_with_gdp_opt(): if __name__ == '__main__': logging.basicConfig(level=logging.DEBUG, filename='out.log') - from pyomo.util.model_size import * + from pyomo.util.model_size import * + # m = enumerate() m = solve_with_gdp_opt() print(build_model_size_report(m)) - \ No newline at end of file diff --git a/gdplib/mod_hens/__init__.py b/gdplib/mod_hens/__init__.py index 7ca419a..96bc5d3 100644 --- a/gdplib/mod_hens/__init__.py +++ b/gdplib/mod_hens/__init__.py @@ -1,11 +1,16 @@ from functools import partial from .conventional import build_conventional as _conv -from .modular_discrete import build_modular_option as _disc_opt, build_require_modular as _disc_mod +from .modular_discrete import ( + build_modular_option as _disc_opt, + build_require_modular as _disc_mod, +) from .modular_discrete_single_module import build_single_module as _disc_sing from .modular_integer import ( - build_modular_option as _int_opt, build_require_modular as _int_mod, - build_single_module as _int_sing, ) + build_modular_option as _int_opt, + build_require_modular as _int_mod, + build_single_module as _int_sing, +) # These are the functions that we want to expose as public build_conventional = partial(_conv, cafaro_approx=True, num_stages=4) @@ -16,6 +21,12 @@ build_discrete_require_modular = partial(_disc_mod, cafaro_approx=True, num_stages=4) build_discrete_modular_option = partial(_disc_opt, cafaro_approx=True, num_stages=4) -__all__ = ['build_conventional', 'build_integer_single_module', 'build_integer_require_modular', - 'build_integer_modular_option', 'build_discrete_single_module', 'build_discrete_require_modular', - 'build_discrete_modular_option'] +__all__ = [ + 'build_conventional', + 'build_integer_single_module', + 'build_integer_require_modular', + 'build_integer_modular_option', + 'build_discrete_single_module', + 'build_discrete_require_modular', + 'build_discrete_modular_option', +] diff --git a/gdplib/mod_hens/cafaro_approx.py b/gdplib/mod_hens/cafaro_approx.py index 89d4491..236544f 100644 --- a/gdplib/mod_hens/cafaro_approx.py +++ b/gdplib/mod_hens/cafaro_approx.py @@ -1,7 +1,7 @@ """Cafaro approximation parameter estimation. Rather than use the cost relation (1), Cafaro & Grossmann, 2014 (DOI: -10.1016/j.compchemeng.2013.10.001) proposes using (2), which has much better +https://doi.org/10.1016/j.compchemeng.2013.10.001) proposes using (2), which has much better behaved derivative values near x=0. However, we need to use parameter estimation in order to derive the correct values of k and b. @@ -11,13 +11,23 @@ (2) cost = factor * k * ln(bx + 1) """ + from __future__ import division -from pyomo.environ import (ConcreteModel, Constraint, log, NonNegativeReals, SolverFactory, value, Var) +from pyomo.environ import ( + ConcreteModel, + Constraint, + log, + NonNegativeReals, + SolverFactory, + value, + Var, +) def calculate_cafaro_coefficients(area1, area2, exponent): - """Calculate the coefficients for the Cafaro approximation. + """ + Calculate the coefficients for the Cafaro approximation. Gives the coefficients k and b to approximate a function x^exponent such that at the given areas, the following relations apply: @@ -25,19 +35,30 @@ def calculate_cafaro_coefficients(area1, area2, exponent): area1 ^ exponent = k * ln(b * area1 + 1) area2 ^ exponent = k * ln(b * area2 + 1) - Args: - area1 (float): area to use as the first regression point - area2 (float): area to use as the second regression point - exponent (float): exponent to approximate + Parameters + ---------- + area1 : float + The area to use as the first regression point. + area2 : float + The area to use as the second regression point. + exponent : float + The exponent to approximate. + + Returns + ------- + tuple of float + A tuple containing the coefficients `k` and `b`. + + References + ---------- + [1] Cafaro, D. C., & Grossmann, I. E. (2014). Alternate approximation of concave cost functions for process design and supply chain optimization problems. Computers & chemical engineering, 60, 376-380. https://doi.org/10.1016/j.compchemeng.2013.10.001 """ m = ConcreteModel() m.k = Var(domain=NonNegativeReals) m.b = Var(domain=NonNegativeReals) - m.c1 = Constraint( - expr=area1 ** exponent == m.k * log(m.b * area1 + 1)) - m.c2 = Constraint( - expr=area2 ** exponent == m.k * log(m.b * area2 + 1)) + m.c1 = Constraint(expr=area1**exponent == m.k * log(m.b * area1 + 1)) + m.c2 = Constraint(expr=area2**exponent == m.k * log(m.b * area2 + 1)) SolverFactory('ipopt').solve(m) diff --git a/gdplib/mod_hens/common.py b/gdplib/mod_hens/common.py index c3203c7..2bc7c5f 100644 --- a/gdplib/mod_hens/common.py +++ b/gdplib/mod_hens/common.py @@ -1,105 +1,153 @@ -"""Heat integration case study. +""" +Heat integration case study. + +This is example 1 of the Yee & Grossmann, 1990 paper "Simultaneous optimization models for heat integration--II". DOI: 10.1016/0098-1354(90)85010-8 -This is example 1 of the Yee & Grossmann, 1990 paper "Simultaneous optimization -models for heat integration--II". -DOI: 10.1016/0098-1354(90)85010-8 +This file provides common modeling elements of the heat exchanger network. +The model utilizes sets to organize hot and cold process streams, utility streams, and stages of heat exchange, with parameters defining the essential properties like temperatures and flow capacities. This structure facilitates detailed modeling of the heat transfer process across different stages and stream types. +Disjunctions are employed to model the binary decision of either installing or not installing a heat exchanger between specific stream pairs at each stage, enhancing the model's flexibility and ability to find an optimal solution that balances cost and efficiency. +The objective function aims to minimize the total cost of the heat exchanger network, which includes the costs associated with utility usage and the capital and operational expenses of the heat exchangers, ensuring economic feasibility alongside energy optimization. -This file provides common modeling elements. +Given the common.py, the model can be shown as the conventional model, or can be modified into single module type, integer or discretized formulation, and other various formulations. +References: + Yee, T. F., & Grossmann, I. E. (1990). Simultaneous optimization models for heat integration—II. Heat exchanger network synthesis. Computers & Chemical Engineering, 14(10), 1165–1184. https://doi.org/10.1016/0098-1354(90)85010-8 """ + from __future__ import division from pyomo.environ import ( - ConcreteModel, Constraint, minimize, NonNegativeReals, Objective, Param, RangeSet, Set, Suffix, value, Var, ) + ConcreteModel, + Constraint, + minimize, + NonNegativeReals, + Objective, + Param, + RangeSet, + Set, + Suffix, + value, + Var, +) from pyomo.gdp import Disjunct, Disjunction from .cafaro_approx import calculate_cafaro_coefficients def build_model(use_cafaro_approximation, num_stages): - """Build the model.""" + """ + Constructs a Pyomo concrete model for heat integration optimization. This model incorporates various components including process and utility streams, heat exchangers, and stages of heat exchange, with optional application of the Cafaro approximation for certain calculations. + + Parameters + ---------- + use_cafaro_approximation : bool + A Boolean flag indicating whether the Cafaro approximation method should be used + to calculate certain coefficients in the model + num_stages : int + The number of stages in the heat exchange model. + + Returns + ------- + Pyomo.ConcreteModel + A Pyomo concrete model representing the heat exchange model based on the specified number of stages and the use of Cafaro approximation, if applicable. The model is ready to be solved using an optimization solver to determine optimal heat integration strategies. + """ m = ConcreteModel() - m.hot_process_streams = Set(initialize=['H1', 'H2']) - m.cold_process_streams = Set(initialize=['C1', 'C2']) - m.process_streams = m.hot_process_streams | m.cold_process_streams - m.hot_utility_streams = Set(initialize=['steam']) - m.cold_utility_streams = Set(initialize=['water']) + m.hot_process_streams = Set(initialize=['H1', 'H2'], doc="Hot process streams") + m.cold_process_streams = Set(initialize=['C1', 'C2'], doc="Cold process streams") + m.process_streams = ( + m.hot_process_streams | m.cold_process_streams + ) # All process streams + m.hot_utility_streams = Set(initialize=['steam'], doc="Hot utility streams") + m.cold_utility_streams = Set(initialize=['water'], doc="Cold utility streams") m.hot_streams = Set( - initialize=m.hot_process_streams | m.hot_utility_streams) + initialize=m.hot_process_streams | m.hot_utility_streams, doc="Hot streams" + ) m.cold_streams = Set( - initialize=m.cold_process_streams | m.cold_utility_streams) + initialize=m.cold_process_streams | m.cold_utility_streams, doc="Cold streams" + ) m.utility_streams = Set( - initialize=m.hot_utility_streams | m.cold_utility_streams) - m.streams = Set( - initialize=m.process_streams | m.utility_streams) + initialize=m.hot_utility_streams | m.cold_utility_streams, doc="Utility streams" + ) + m.streams = Set(initialize=m.process_streams | m.utility_streams, doc="All streams") m.valid_matches = Set( - initialize=(m.hot_process_streams * m.cold_streams) | - (m.hot_utility_streams * m.cold_process_streams), + initialize=(m.hot_process_streams * m.cold_streams) + | (m.hot_utility_streams * m.cold_process_streams), doc="Match all hot streams to cold streams, but exclude " - "matches between hot and cold utilities.") + "matches between hot and cold utilities.", + ) # m.EMAT = Param(doc="Exchanger minimum approach temperature [K]", # initialize=1) # Unused right now, but could be used for variable bound tightening # in the LMTD calculation. - m.stages = RangeSet(num_stages) + m.stages = RangeSet(num_stages, doc="Number of stages") m.T_in = Param( - m.streams, doc="Inlet temperature of stream [K]", - initialize={'H1': 443, - 'H2': 423, - 'C1': 293, - 'C2': 353, - 'steam': 450, - 'water': 293}) + m.streams, + doc="Inlet temperature of stream [K]", + initialize={ + 'H1': 443, + 'H2': 423, + 'C1': 293, + 'C2': 353, + 'steam': 450, + 'water': 293, + }, + ) m.T_out = Param( - m.streams, doc="Outlet temperature of stream [K]", - initialize={'H1': 333, - 'H2': 303, - 'C1': 408, - 'C2': 413, - 'steam': 450, - 'water': 313}) + m.streams, + doc="Outlet temperature of stream [K]", + initialize={ + 'H1': 333, + 'H2': 303, + 'C1': 408, + 'C2': 413, + 'steam': 450, + 'water': 313, + }, + ) m.heat_exchanged = Var( - m.valid_matches, m.stages, + m.valid_matches, + m.stages, domain=NonNegativeReals, doc="Heat exchanged from hot stream to cold stream in stage [kW]", - initialize=1, bounds=(0, 5000)) + initialize=1, + bounds=(0, 5000), + ) m.overall_FCp = Param( m.process_streams, doc="Flow times heat capacity of stream [kW / K]", - initialize={'H1': 30, - 'H2': 15, - 'C1': 20, - 'C2': 40}) + initialize={'H1': 30, 'H2': 15, 'C1': 20, 'C2': 40}, + ) m.utility_usage = Var( m.utility_streams, doc="Hot or cold utility used [kW]", - domain=NonNegativeReals, initialize=1, bounds=(0, 5000)) + domain=NonNegativeReals, + initialize=1, + bounds=(0, 5000), + ) m.stage_entry_T = Var( - m.streams, m.stages, - doc="Temperature of stream at stage entry.", + m.streams, + m.stages, + doc="Temperature of stream at stage entry [K].", initialize=350, - bounds=(293, 450) # TODO set to be equal to min and max temps + bounds=(293, 450), # TODO set to be equal to min and max temps ) m.stage_exit_T = Var( - m.streams, m.stages, - doc="Temperature of stream at stage exit.", + m.streams, + m.stages, + doc="Temperature of stream at stage exit [K].", initialize=350, - bounds=(293, 450) # TODO set to be equal to min and max temps + bounds=(293, 450), # TODO set to be equal to min and max temps ) # Improve bounds on stage entry and exit temperatures for strm, stg in m.process_streams * m.stages: - m.stage_entry_T[strm, stg].setlb( - min(value(m.T_in[strm]), value(m.T_out[strm]))) - m.stage_exit_T[strm, stg].setlb( - min(value(m.T_in[strm]), value(m.T_out[strm]))) - m.stage_entry_T[strm, stg].setub( - max(value(m.T_in[strm]), value(m.T_out[strm]))) - m.stage_exit_T[strm, stg].setub( - max(value(m.T_in[strm]), value(m.T_out[strm]))) + m.stage_entry_T[strm, stg].setlb(min(value(m.T_in[strm]), value(m.T_out[strm]))) + m.stage_exit_T[strm, stg].setlb(min(value(m.T_in[strm]), value(m.T_out[strm]))) + m.stage_entry_T[strm, stg].setub(max(value(m.T_in[strm]), value(m.T_out[strm]))) + m.stage_exit_T[strm, stg].setub(max(value(m.T_in[strm]), value(m.T_out[strm]))) for strm, stg in m.utility_streams * m.stages: _fix_and_bound(m.stage_entry_T[strm, stg], m.T_in[strm]) _fix_and_bound(m.stage_exit_T[strm, stg], m.T_out[strm]) @@ -115,191 +163,477 @@ def build_model(use_cafaro_approximation, num_stages): m.utility_unit_cost = Param( m.utility_streams, doc="Annual unit cost of utilities [$/kW]", - initialize={'steam': 80, 'water': 20}) + initialize={'steam': 80, 'water': 20}, + ) - m.module_sizes = Set(initialize=[10, 50, 100]) - m.max_num_modules = Param(m.module_sizes, initialize={ - # 5: 100, - 10: 50, - 50: 10, - 100: 5, - # 250: 2 - }, doc="maximum number of each module size available.") + m.module_sizes = Set(initialize=[10, 50, 100], doc="Available module sizes.") + m.max_num_modules = Param( + m.module_sizes, + initialize={ + # 5: 100, + 10: 50, + 50: 10, + 100: 5, + # 250: 2 + }, + doc="maximum number of each module size available.", + ) m.exchanger_fixed_unit_cost = Param( - m.valid_matches, default=2000) + m.valid_matches, default=2000, doc="exchanger fixed cost [$/kW]" + ) m.exchanger_area_cost_factor = Param( - m.valid_matches, default=1000, - initialize={ - ('steam', cold): 1200 - for cold in m.cold_process_streams}, - doc="1200 for heaters. 1000 for all other exchangers.") - m.area_cost_exponent = Param(default=0.6) + m.valid_matches, + default=1000, + initialize={('steam', cold): 1200 for cold in m.cold_process_streams}, + doc="1200 for heaters. 1000 for all other exchangers.", + ) + m.area_cost_exponent = Param(default=0.6, doc="Area cost exponent.") - if use_cafaro_approximation: + if use_cafaro_approximation: # Use Cafaro approximation for coefficients if True k, b = calculate_cafaro_coefficients(10, 500, m.area_cost_exponent) m.cafaro_k = Param(default=k) m.cafaro_b = Param(default=b) - @m.Param(m.valid_matches, m.module_sizes, - doc="Area cost factor for modular exchangers.") + @m.Param( + m.valid_matches, m.module_sizes, doc="Area cost factor for modular exchangers." + ) def module_area_cost_factor(m, hot, cold, area): + """ + Determines the area cost factor for modular exchangers within the heat integration model. The cost factor is based on the specified module size and stream pair, with different values for steam and other hot streams. The unit is [$/(m^2)^0.6]. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo concrete model representing the heat exchange model. + hot : str + The index for the hot stream involved in the heat exchanger. + cold : str + The index for the cold stream involved in the heat exchanger. + area : float + The modular area size of the heat exchanger. + + Returns + ------- + Pyomo.Parameter + The area cost factor for the specified module size and stream pair. It returns a higher value for steam (1300) compared to other hot streams (1100), reflecting specific cost adjustments based on utility type. + """ if hot == 'steam': return 1300 else: return 1100 - m.module_fixed_unit_cost = Param(default=0) - m.module_area_cost_exponent = Param(default=0.6) + m.module_fixed_unit_cost = Param(default=0, doc="Fixed cost for a module.") + m.module_area_cost_exponent = Param(default=0.6, doc="Area cost exponent.") - @m.Param(m.valid_matches, m.module_sizes, - doc="Cost of a module with a particular area.") + @m.Param( + m.valid_matches, m.module_sizes, doc="Cost of a module with a particular area." + ) def module_area_cost(m, hot, cold, area): - return (m.module_area_cost_factor[hot, cold, area] - * area ** m.module_area_cost_exponent) + """ + Determines the cost of a module with a specified area size for a given hot and cold stream pair. The cost is calculated based on the area cost factor and exponent for the module size and stream pair. The unit is [$]. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo concrete model representing the heat exchange model. + hot : str + The index for the hot stream involved in the heat exchanger. + cold : str + The index for the cold stream involved in the heat exchanger. + area : float + The modular area size of the heat exchanger. + + Returns + ------- + Pyomo.Parameter + The cost of a module with the specified area size for the given hot and cold stream pair. The cost is calculated based on the area cost factor and exponent for the module size and stream pair. + """ + return ( + m.module_area_cost_factor[hot, cold, area] + * area**m.module_area_cost_exponent + ) m.U = Param( m.valid_matches, default=0.8, - initialize={ - ('steam', cold): 1.2 - for cold in m.cold_process_streams}, + initialize={('steam', cold): 1.2 for cold in m.cold_process_streams}, doc="Overall heat transfer coefficient." - "1.2 for heaters. 0.8 for everything else.") + "1.2 for heaters. 0.8 for everything else. The unit is [kW/m^2/K].", + ) m.exchanger_hot_side_approach_T = Var( - m.valid_matches, m.stages, + m.valid_matches, + m.stages, doc="Temperature difference between the hot stream inlet and cold " - "stream outlet of the exchanger.", - bounds=(0.1, 500), initialize=10 + "stream outlet of the exchanger. The unit is [K].", + bounds=(0.1, 500), + initialize=10, ) m.exchanger_cold_side_approach_T = Var( - m.valid_matches, m.stages, + m.valid_matches, + m.stages, doc="Temperature difference between the hot stream outlet and cold " - "stream inlet of the exchanger.", - bounds=(0.1, 500), initialize=10 + "stream inlet of the exchanger. The unit is [K].", + bounds=(0.1, 500), + initialize=10, ) m.LMTD = Var( - m.valid_matches, m.stages, + m.valid_matches, + m.stages, doc="Log mean temperature difference across the exchanger.", - bounds=(1, 500), initialize=10 + bounds=(1, 500), + initialize=10, ) # Improve LMTD bounds based on T values for hot, cold, stg in m.valid_matches * m.stages: - hot_side_dT_LB = max(0, value( - m.stage_entry_T[hot, stg].lb - m.stage_exit_T[cold, stg].ub)) - hot_side_dT_UB = max(0, value( - m.stage_entry_T[hot, stg].ub - m.stage_exit_T[cold, stg].lb)) - cold_side_dT_LB = max(0, value( - m.stage_exit_T[hot, stg].lb - m.stage_entry_T[cold, stg].ub)) - cold_side_dT_UB = max(0, value( - m.stage_exit_T[hot, stg].ub - m.stage_entry_T[cold, stg].lb)) - m.LMTD[hot, cold, stg].setlb(( - hot_side_dT_LB * cold_side_dT_LB * ( - hot_side_dT_LB + cold_side_dT_LB) / 2) ** (1 / 3) + hot_side_dT_LB = max( + 0, value(m.stage_entry_T[hot, stg].lb - m.stage_exit_T[cold, stg].ub) + ) + hot_side_dT_UB = max( + 0, value(m.stage_entry_T[hot, stg].ub - m.stage_exit_T[cold, stg].lb) + ) + cold_side_dT_LB = max( + 0, value(m.stage_exit_T[hot, stg].lb - m.stage_entry_T[cold, stg].ub) + ) + cold_side_dT_UB = max( + 0, value(m.stage_exit_T[hot, stg].ub - m.stage_entry_T[cold, stg].lb) ) - m.LMTD[hot, cold, stg].setub(( - hot_side_dT_UB * cold_side_dT_UB * ( - hot_side_dT_UB + cold_side_dT_UB) / 2) ** (1 / 3) + m.LMTD[hot, cold, stg].setlb( + (hot_side_dT_LB * cold_side_dT_LB * (hot_side_dT_LB + cold_side_dT_LB) / 2) + ** (1 / 3) + ) + m.LMTD[hot, cold, stg].setub( + (hot_side_dT_UB * cold_side_dT_UB * (hot_side_dT_UB + cold_side_dT_UB) / 2) + ** (1 / 3) ) m.exchanger_fixed_cost = Var( - m.stages, m.valid_matches, + m.stages, + m.valid_matches, doc="Fixed cost for an exchanger between a hot and cold stream.", - domain=NonNegativeReals, bounds=(0, 1E5), initialize=0) + domain=NonNegativeReals, + bounds=(0, 1e5), + initialize=0, + ) m.exchanger_area = Var( - m.stages, m.valid_matches, + m.stages, + m.valid_matches, doc="Area for an exchanger between a hot and cold stream.", - domain=NonNegativeReals, bounds=(0, 500), initialize=5) + domain=NonNegativeReals, + bounds=(0, 500), + initialize=5, + ) m.exchanger_area_cost = Var( - m.stages, m.valid_matches, + m.stages, + m.valid_matches, doc="Capital cost contribution from exchanger area.", - domain=NonNegativeReals, bounds=(0, 1E5), initialize=1000) + domain=NonNegativeReals, + bounds=(0, 1e5), + initialize=1000, + ) @m.Constraint(m.hot_process_streams) def overall_hot_stream_heat_balance(m, strm): + """ + Enforces the heat balance for a hot process stream within the model. This constraint ensures that the total heat loss from the hot stream equals the sum of heat transferred to all paired cold streams across all stages. The heat loss is calculated based on the temperature difference between the stream outlet and inlet, multiplied by the overall flow times heat capacity of the stream. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo concrete model representing the heat exchange model. + strm : str + The index for the hot stream involved in the heat exchanger. + + Returns + ------- + Pyomo.Constraint + A constraint object that ensures the heat balance across the specified hot stream over all stages and cold stream interactions. + """ return (m.T_in[strm] - m.T_out[strm]) * m.overall_FCp[strm] == ( - sum(m.heat_exchanged[strm, cold, stg] - for cold in m.cold_streams for stg in m.stages)) + sum( + m.heat_exchanged[strm, cold, stg] + for cold in m.cold_streams + for stg in m.stages + ) + ) @m.Constraint(m.cold_process_streams) def overall_cold_stream_heat_balance(m, strm): + """ + Enforces the heat balance for a cold process stream within the model. This constraint ensures that the total heat gain for the cold stream equals the sum of heat received from all paired hot streams across all stages. The heat gain is calculated based on the temperature difference between the stream outlet and inlet, multiplied by the overall flow times heat capacity of the stream. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo concrete model representing the heat exchange model. + strm : str + The index for the cold stream involved in the heat exchanger. + + Returns + ------- + Pyomo.Constraint + A constraint object that ensures the heat balance across the specified cold stream over all stages and hot stream interactions. + """ return (m.T_out[strm] - m.T_in[strm]) * m.overall_FCp[strm] == ( - sum(m.heat_exchanged[hot, strm, stg] - for hot in m.hot_streams for stg in m.stages)) + sum( + m.heat_exchanged[hot, strm, stg] + for hot in m.hot_streams + for stg in m.stages + ) + ) @m.Constraint(m.utility_streams) def overall_utility_stream_usage(m, strm): + """ + Ensures the total utility usage for each utility stream matches the sum of heat exchanged involving that utility across all stages. This constraint separates the calculations for hot and cold utility streams. For cold utility streams, it sums the heat exchanged from all hot process streams to the utility, and for hot utility streams, it sums the heat exchanged from the utility to all cold process streams. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo concrete model representing the heat exchange model. + strm : str + The index for the utility stream involved in the heat exchanger. This can be a hot or cold utility, and the constraint dynamically adjusts to sum the appropriate heat transfers based on this classification. + + Returns + ------- + Pyomo.Constraint + A constraint object that ensures the total calculated utility usage for the specified utility stream accurately reflects the sum of relevant heat exchanges in the system. This helps maintain energy balance specifically for utility streams within the overall heat exchange model. + """ return m.utility_usage[strm] == ( - sum(m.heat_exchanged[hot, strm, stg] + sum( + m.heat_exchanged[hot, strm, stg] for hot in m.hot_process_streams for stg in m.stages - ) if strm in m.cold_utility_streams else 0 + - sum(m.heat_exchanged[strm, cold, stg] - for cold in m.cold_process_streams - for stg in m.stages - ) if strm in m.hot_utility_streams else 0 + ) + if strm in m.cold_utility_streams + else ( + 0 + + sum( + m.heat_exchanged[strm, cold, stg] + for cold in m.cold_process_streams + for stg in m.stages + ) + if strm in m.hot_utility_streams + else 0 + ) ) - @m.Constraint(m.stages, m.hot_process_streams, - doc="Hot side overall heat balance for a stage.") + @m.Constraint( + m.stages, + m.hot_process_streams, + doc="Hot side overall heat balance for a stage.", + ) def hot_stage_overall_heat_balance(m, stg, strm): - return ((m.stage_entry_T[strm, stg] - m.stage_exit_T[strm, stg]) - * m.overall_FCp[strm]) == sum( - m.heat_exchanged[strm, cold, stg] - for cold in m.cold_streams) - - @m.Constraint(m.stages, m.cold_process_streams, - doc="Cold side overall heat balance for a stage.") + """ + Establishes an overall heat balance for a specific hot stream within a particular stage of the heat exchange process. This constraint ensures that the heat loss from the hot stream, calculated as the product of the temperature drop across the stage and the flow capacity of the stream, equals the total heat transferred to all corresponding cold streams within the same stage. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo concrete model representing the heat exchange model. + stg : int + The index for the stage involved in the heat exchanger. + strm : str + The index for the hot stream involved in the heat exchanger. + + Returns + ------- + Pyomo.Constraint + A constraint object that enforces the heat balance for the specified hot stream at the given stage. This ensures that the heat output from this stream is appropriately accounted for and matched by heat intake by the cold streams, promoting efficient energy use. + """ + return ( + (m.stage_entry_T[strm, stg] - m.stage_exit_T[strm, stg]) + * m.overall_FCp[strm] + ) == sum(m.heat_exchanged[strm, cold, stg] for cold in m.cold_streams) + + @m.Constraint( + m.stages, + m.cold_process_streams, + doc="Cold side overall heat balance for a stage.", + ) def cold_stage_overall_heat_balance(m, stg, strm): - return ((m.stage_exit_T[strm, stg] - m.stage_entry_T[strm, stg]) - * m.overall_FCp[strm]) == sum( - m.heat_exchanged[hot, strm, stg] - for hot in m.hot_streams) + """ + Establishes an overall heat balance for a specific cold stream within a particular stage of the heat exchange process. This constraint ensures that the heat gain for the cold stream, calculated as the product of the temperature increase across the stage and the flow capacity of the stream, equals the total heat received from all corresponding hot streams within the same stage. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo concrete model representing the heat exchange model. + stg : int + The index for the stage involved in the heat exchanger. + strm : str + The index for the cold stream involved in the heat exchanger. + + Returns + ------- + Pyomo.Constraint + A constraint object that enforces the heat balance for the specified cold stream at the given stage. This ensures that the heat intake by this stream is appropriately accounted for and matched by heat output from the hot streams, promoting efficient energy use. + """ + return ( + (m.stage_exit_T[strm, stg] - m.stage_entry_T[strm, stg]) + * m.overall_FCp[strm] + ) == sum(m.heat_exchanged[hot, strm, stg] for hot in m.hot_streams) @m.Constraint(m.stages, m.hot_process_streams) def hot_stream_monotonic_T_decrease(m, stg, strm): + """ + Ensures that the temperature of a hot stream decreases monotonically across a given stage. This constraint is critical for modeling realistic heat exchange scenarios where hot streams naturally cool down as they transfer heat to colder streams. It enforces that the exit temperature of the hot stream from any stage is less than or equal to its entry temperature for that stage. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo concrete model representing the heat exchange model. + stg : int + The index for the stage involved in the heat exchanger. + strm : str + The index for the hot stream involved in the heat exchanger. + + Returns + ------- + Pyomo.Constraint + A constraint object that ensures the temperature of the hot stream does not increase as it passes through the stage, which is essential for maintaining the physical feasibility of the heat exchange process. + """ return m.stage_exit_T[strm, stg] <= m.stage_entry_T[strm, stg] @m.Constraint(m.stages, m.cold_process_streams) def cold_stream_monotonic_T_increase(m, stg, strm): + """ + Ensures that the temperature of a cold stream increases monotonically across a given stage. This constraint is essential for modeling realistic heat exchange scenarios where cold streams naturally warm up as they absorb heat from hotter streams. It enforces that the exit temperature of the cold stream from any stage is greater than or equal to its entry temperature for that stage. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo concrete model representing the heat exchange model. + stg : int + The index for the stage involved in the heat exchanger. + strm : str + The index for the cold stream involved in the heat exchanger. + + Returns + ------- + Pyomo.Constraint + A constraint object that ensures the temperature of the cold stream increases as it passes through the stage, reflecting the natural heat absorption process and maintaining the physical feasibility of the heat exchange model. + """ return m.stage_exit_T[strm, stg] >= m.stage_entry_T[strm, stg] @m.Constraint(m.stages, m.hot_process_streams) def hot_stream_stage_T_link(m, stg, strm): + """ + Links the exit temperature of a hot stream from one stage to the entry temperature of the same stream in the subsequent stage, ensuring continuity and consistency in temperature progression across stages. This constraint is vital for maintaining a coherent thermal profile within each hot stream as it progresses through the heat exchange stages. For the final stage, no constraint is applied since there is no subsequent stage. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo concrete model representing the heat exchange model. + stg : int + The index for the stage involved in the heat exchanger. + strm : str + The index for the hot stream involved in the heat exchanger. + + Returns + ------- + Pyomo.Constraint + A constraint object that ensures the exit temperature at the end of one stage matches the entry temperature at the beginning of the next stage for the hot streams. In the final stage, where there is no subsequent stage, no constraint is applied. + """ return ( - m.stage_exit_T[strm, stg] == m.stage_entry_T[strm, stg + 1] - ) if stg < num_stages else Constraint.NoConstraint + (m.stage_exit_T[strm, stg] == m.stage_entry_T[strm, stg + 1]) + if stg < num_stages + else Constraint.NoConstraint + ) @m.Constraint(m.stages, m.cold_process_streams) def cold_stream_stage_T_link(m, stg, strm): + """ + Ensures continuity in the temperature profiles of cold streams across stages in the heat exchange model by linking the exit temperature of a cold stream in one stage to its entry temperature in the following stage. This constraint is crucial for maintaining consistent and logical heat absorption sequences within the cold streams as they move through successive stages. For the final stage, no constraint is applied since there is no subsequent stage. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo concrete model representing the heat exchange model. + stg : int + The index for the stage involved in the heat exchanger. + strm : str + The index for the cold stream involved in the heat exchanger. + + Returns + ------- + Pyomo.Constraint + A constraint object that ensures the exit temperature at the end of one stage matches the entry temperature at the beginning of the next stage for cold streams. In the final stage, where there is no subsequent stage, no constraint is applied, reflecting the end of the process sequence. + """ return ( - m.stage_entry_T[strm, stg] == m.stage_exit_T[strm, stg + 1] - ) if stg < num_stages else Constraint.NoConstraint + (m.stage_entry_T[strm, stg] == m.stage_exit_T[strm, stg + 1]) + if stg < num_stages + else Constraint.NoConstraint + ) @m.Expression(m.valid_matches, m.stages) def exchanger_capacity(m, hot, cold, stg): + """ + Calculates the heat transfer capacity of an exchanger for a given hot stream, cold stream, and stage combination. This capacity is derived from the exchanger's area, the overall heat transfer coefficient, and the geometric mean of the approach temperatures at both sides of the exchanger. This expression is used to estimate the efficiency and effectiveness of heat transfer in each stage of the heat exchange process. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo concrete model representing the heat exchange model. + hot : str + The index for the hot stream involved in the heat exchanger. + cold : str + The index for the cold stream involved in the heat exchanger. + stg : int + The index for the stage involved in the heat exchanger. + + Returns + ------- + Pyomo.Expression + A Pyomo expression that quantifies the heat transfer capacity of the exchanger. This value is crucial for optimizing the heat exchange system, ensuring that each stage is designed to maximize heat recovery while adhering to operational constraints and physical laws. + """ return m.exchanger_area[stg, hot, cold] * ( - m.U[hot, cold] * ( - m.exchanger_hot_side_approach_T[hot, cold, stg] * - m.exchanger_cold_side_approach_T[hot, cold, stg] * - (m.exchanger_hot_side_approach_T[hot, cold, stg] + - m.exchanger_cold_side_approach_T[hot, cold, stg]) / 2 - ) ** (1 / 3)) + m.U[hot, cold] + * ( + m.exchanger_hot_side_approach_T[hot, cold, stg] + * m.exchanger_cold_side_approach_T[hot, cold, stg] + * ( + m.exchanger_hot_side_approach_T[hot, cold, stg] + + m.exchanger_cold_side_approach_T[hot, cold, stg] + ) + / 2 + ) + ** (1 / 3) + ) def _exchanger_exists(disj, hot, cold, stg): + """ + Defines the conditions and constraints for the existence of an exchanger between a specified hot and cold stream at a given stage. This function sets the disjunct's indicator variable to true and configures constraints that model the physical behavior of the heat exchanger, including the log mean temperature difference and approach temperatures. + + Parameters + ---------- + disj : Pyomo.Disjunct + The disjunct object representing a potential heat exchanger scenario between the specified hot and cold streams. + hot : str + The index for the hot stream involved in the heat exchanger. + cold : str + The index for the cold stream involved in the heat exchanger. + stg : int + The index for the stage involved in the heat exchanger. + """ disj.indicator_var.value = True # Log mean temperature difference calculation disj.LMTD_calc = Constraint( doc="Log mean temperature difference", - expr=m.LMTD[hot, cold, stg] == ( - m.exchanger_hot_side_approach_T[hot, cold, stg] * - m.exchanger_cold_side_approach_T[hot, cold, stg] * - (m.exchanger_hot_side_approach_T[hot, cold, stg] + - m.exchanger_cold_side_approach_T[hot, cold, stg]) / 2 - ) ** (1 / 3) + expr=m.LMTD[hot, cold, stg] + == ( + m.exchanger_hot_side_approach_T[hot, cold, stg] + * m.exchanger_cold_side_approach_T[hot, cold, stg] + * ( + m.exchanger_hot_side_approach_T[hot, cold, stg] + + m.exchanger_cold_side_approach_T[hot, cold, stg] + ) + / 2 + ) + ** (1 / 3), ) m.BigM[disj.LMTD_calc] = 160 @@ -313,56 +647,113 @@ def _exchanger_exists(disj, hot, cold, stg): # Calculation of the approach temperatures if hot in m.hot_utility_streams: disj.stage_hot_approach_temperature = Constraint( - expr=m.exchanger_hot_side_approach_T[hot, cold, stg] <= - m.T_in[hot] - m.stage_exit_T[cold, stg]) + expr=m.exchanger_hot_side_approach_T[hot, cold, stg] + <= m.T_in[hot] - m.stage_exit_T[cold, stg], + doc="Hot utility: hot side limit.", + ) disj.stage_cold_approach_temperature = Constraint( - expr=m.exchanger_cold_side_approach_T[hot, cold, stg] <= - m.T_out[hot] - m.stage_entry_T[cold, stg]) + expr=m.exchanger_cold_side_approach_T[hot, cold, stg] + <= m.T_out[hot] - m.stage_entry_T[cold, stg], + doc="Hot utility: cold side limit.", + ) elif cold in m.cold_utility_streams: disj.stage_hot_approach_temperature = Constraint( - expr=m.exchanger_hot_side_approach_T[hot, cold, stg] <= - m.stage_entry_T[hot, stg] - m.T_out[cold]) + expr=m.exchanger_hot_side_approach_T[hot, cold, stg] + <= m.stage_entry_T[hot, stg] - m.T_out[cold], + doc="Cold utility: hot side limit.", + ) disj.stage_cold_approach_temperature = Constraint( - expr=m.exchanger_cold_side_approach_T[hot, cold, stg] <= - m.stage_exit_T[hot, stg] - m.T_in[cold]) + expr=m.exchanger_cold_side_approach_T[hot, cold, stg] + <= m.stage_exit_T[hot, stg] - m.T_in[cold], + doc="Cold utility: cold side limit.", + ) else: disj.stage_hot_approach_temperature = Constraint( - expr=m.exchanger_hot_side_approach_T[hot, cold, stg] <= - m.stage_entry_T[hot, stg] - - m.stage_exit_T[cold, stg]) + expr=m.exchanger_hot_side_approach_T[hot, cold, stg] + <= m.stage_entry_T[hot, stg] - m.stage_exit_T[cold, stg], + doc="Process stream: hot side limit.", + ) disj.stage_cold_approach_temperature = Constraint( - expr=m.exchanger_cold_side_approach_T[hot, cold, stg] <= - m.stage_exit_T[hot, stg] - - m.stage_entry_T[cold, stg]) + expr=m.exchanger_cold_side_approach_T[hot, cold, stg] + <= m.stage_exit_T[hot, stg] - m.stage_entry_T[cold, stg], + doc="Process stream: cold side limit.", + ) def _exchanger_absent(disj, hot, cold, stg): + """ + Defines the conditions for the absence of a heat exchanger between a specified hot and cold stream at a given stage. This function sets the disjunct's indicator variable to false and ensures that all associated costs and heat exchanged values are set to zero, effectively removing the exchanger from the model for this configuration. + + Parameters + ---------- + disj : Pyomo.Disjunct + The disjunct object representing a scenario where no heat exchanger is present between the specified hot and cold streams at the given stage. + hot : str + The index for the hot stream involved in the heat exchanger. + cold : str + The index for the cold stream involved in the heat exchanger. + stg : int + The index for the stage involved in the heat exchanger. + """ disj.indicator_var.value = False disj.no_match_exchanger_cost = Constraint( - expr=m.exchanger_area_cost[stg, hot, cold] == 0) + expr=m.exchanger_area_cost[stg, hot, cold] == 0, doc="No exchanger cost." + ) disj.no_match_exchanger_area = Constraint( - expr=m.exchanger_area[stg, hot, cold] == 0) + expr=m.exchanger_area[stg, hot, cold] == 0, doc="No exchanger area." + ) disj.no_match_exchanger_fixed_cost = Constraint( - expr=m.exchanger_fixed_cost[stg, hot, cold] == 0) + expr=m.exchanger_fixed_cost[stg, hot, cold] == 0, + doc="No exchanger fixed cost.", + ) disj.no_heat_exchange = Constraint( - expr=m.heat_exchanged[hot, cold, stg] == 0) + expr=m.heat_exchanged[hot, cold, stg] == 0, doc="No heat exchange." + ) m.exchanger_exists = Disjunct( - m.valid_matches, m.stages, + m.valid_matches, + m.stages, doc="Disjunct for the presence of an exchanger between a " - "hot stream and a cold stream at a stage.", rule=_exchanger_exists) + "hot stream and a cold stream at a stage.", + rule=_exchanger_exists, + ) m.exchanger_absent = Disjunct( - m.valid_matches, m.stages, + m.valid_matches, + m.stages, doc="Disjunct for the absence of an exchanger between a " - "hot stream and a cold stream at a stage.", rule=_exchanger_absent) + "hot stream and a cold stream at a stage.", + rule=_exchanger_absent, + ) def _exchanger_exists_or_absent(m, hot, cold, stg): - return [m.exchanger_exists[hot, cold, stg], - m.exchanger_absent[hot, cold, stg]] + """ + Defines a disjunction to represent the decision between installing or not installing a heat exchanger between a specific hot and cold stream at a certain stage. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo concrete model representing the heat exchange model. + hot : str + The index for the hot stream involved in the heat exchanger. + cold : str + The index for the cold stream involved in the heat exchanger. + stg : int + The index for the stage involved in the heat exchanger. + + Returns + ------- + list + A list of Pyomo Disjunct objects, which includes the scenarios where the exchanger exists or is absent, allowing the model to explore different configurations for optimal energy use and cost efficiency. + """ + return [m.exchanger_exists[hot, cold, stg], m.exchanger_absent[hot, cold, stg]] + m.exchanger_exists_or_absent = Disjunction( - m.valid_matches, m.stages, + m.valid_matches, + m.stages, doc="Disjunction between presence or absence of an exchanger between " "a hot stream and a cold stream at a stage.", - rule=_exchanger_exists_or_absent, xor=True) + rule=_exchanger_exists_or_absent, + xor=True, + ) # Only hot utility matches in first stage and cold utility matches in last # stage for hot, cold in m.valid_matches: @@ -380,23 +771,53 @@ def _exchanger_exists_or_absent(m, hot, cold, stg): @m.Expression(m.utility_streams) def utility_cost(m, strm): + """ + alculates the cost associated with the usage of a utility stream within the heat exchange model. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo concrete model representing the heat exchange model. + strm : str + The index for the utility stream involved in the heat exchanger. This can be a hot or cold utility. + + Returns + ------- + Pyomo.Expression + An expression representing the total cost of using the specified utility stream within the model, computed as the product of unit cost and usage. This helps in assessing the economic impact of utility choices in the heat exchange system. + """ return m.utility_unit_cost[strm] * m.utility_usage[strm] m.total_cost = Objective( expr=sum(m.utility_cost[strm] for strm in m.utility_streams) - + sum(m.exchanger_fixed_cost[stg, hot, cold] - for stg in m.stages - for hot, cold in m.valid_matches) - + sum(m.exchanger_area_cost[stg, hot, cold] - for stg in m.stages - for hot, cold in m.valid_matches), - sense=minimize + + sum( + m.exchanger_fixed_cost[stg, hot, cold] + for stg in m.stages + for hot, cold in m.valid_matches + ) + + sum( + m.exchanger_area_cost[stg, hot, cold] + for stg in m.stages + for hot, cold in m.valid_matches + ), + sense=minimize, + doc="Total cost of the heat exchanger network.", ) return m def _fix_and_bound(var, val): + """ + Fix a Pyomo variable to a value and set bounds to that value. + + Parameters + ---------- + var : Pyomo.Var + The Pyomo variable to be fixed. + val : float + The value to fix the variable to. This value will also be used to set both the lower and upper bounds of the variable. + """ var.fix(val) var.setlb(val) var.setub(val) diff --git a/gdplib/mod_hens/conventional.py b/gdplib/mod_hens/conventional.py index 00046a9..a6f8080 100644 --- a/gdplib/mod_hens/conventional.py +++ b/gdplib/mod_hens/conventional.py @@ -7,6 +7,7 @@ This is an implementation of the conventional problem. """ + from __future__ import division from pyomo.environ import Constraint, log, value @@ -15,37 +16,78 @@ def build_conventional(cafaro_approx, num_stages): + """ + Builds a conventional heat integration model based on specified parameters, delegating to the common build_model function. + + Parameters + ---------- + cafaro_approx : bool + Specifies whether to use the Cafaro approximation in the model. + num_stages : int + The number of stages in the heat integration model. + + Returns + ------- + Pyomo.ConcreteModel + The constructed Pyomo concrete model for heat integration. + """ return build_model(cafaro_approx, num_stages) def build_model(use_cafaro_approximation, num_stages): - """Build the model.""" + """ + Builds and configures a heat integration model using either standard calculations or the Cafaro approximation for specific costs and heat exchange calculations, supplemented by constraints specific to the conventional scenario. + + Parameters + ---------- + use_cafaro_approximation : bool + Flag to determine whether to use the Cafaro approximation for cost calculations. + num_stages : int + Number of stages in the heat exchange model. + + Returns + ------- + Pyomo.ConcreteModel + A fully configured heat integration model with additional conventional-specific constraints. + """ m = common.build_model(use_cafaro_approximation, num_stages) for hot, cold, stg in m.valid_matches * m.stages: disj = m.exchanger_exists[hot, cold, stg] if not use_cafaro_approximation: disj.exchanger_area_cost = Constraint( - expr=m.exchanger_area_cost[stg, hot, cold] * 1E-3 >= - m.exchanger_area_cost_factor[hot, cold] * 1E-3 * - m.exchanger_area[stg, hot, cold] ** m.area_cost_exponent) + expr=m.exchanger_area_cost[stg, hot, cold] * 1e-3 + >= m.exchanger_area_cost_factor[hot, cold] + * 1e-3 + * m.exchanger_area[stg, hot, cold] ** m.area_cost_exponent, + doc="Ensures area cost meets the standard cost scaling.", + ) else: disj.exchanger_area_cost = Constraint( - expr=m.exchanger_area_cost[stg, hot, cold] * 1E-3 >= - m.exchanger_area_cost_factor[hot, cold] * 1E-3 * m.cafaro_k - * log(m.cafaro_b * m.exchanger_area[stg, hot, cold] + 1) + expr=m.exchanger_area_cost[stg, hot, cold] * 1e-3 + >= m.exchanger_area_cost_factor[hot, cold] + * 1e-3 + * m.cafaro_k + * log( + m.cafaro_b * m.exchanger_area[stg, hot, cold] + 1, + doc="Applies Cafaro's logarithmic cost scaling to area cost.", + ) ) m.BigM[disj.exchanger_area_cost] = 100 disj.exchanger_fixed_cost = Constraint( - expr=m.exchanger_fixed_cost[stg, hot, cold] == - m.exchanger_fixed_unit_cost[hot, cold]) + expr=m.exchanger_fixed_cost[stg, hot, cold] + == m.exchanger_fixed_unit_cost[hot, cold], + doc="Sets fixed cost for the exchanger based on unit costs.", + ) # Area requirement disj.exchanger_required_area = Constraint( - expr=m.exchanger_area[stg, hot, cold] * ( - m.U[hot, cold] * m.LMTD[hot, cold, stg]) >= - m.heat_exchanged[hot, cold, stg]) + expr=m.exchanger_area[stg, hot, cold] + * (m.U[hot, cold] * m.LMTD[hot, cold, stg]) + >= m.heat_exchanged[hot, cold, stg], + doc="Calculates the required area based on heat exchanged and LMTD.", + ) m.BigM[disj.exchanger_required_area] = 5000 return m diff --git a/gdplib/mod_hens/modular_discrete.py b/gdplib/mod_hens/modular_discrete.py index 66fdb46..6c70f1f 100644 --- a/gdplib/mod_hens/modular_discrete.py +++ b/gdplib/mod_hens/modular_discrete.py @@ -11,28 +11,50 @@ the nonlinear expressions. """ + from __future__ import division -from pyomo.environ import (Binary, Constraint, log, Set, Var) +from pyomo.environ import Binary, Constraint, log, Set, Var from pyomo.gdp import Disjunct, Disjunction from . import common def build_require_modular(cafaro_approx, num_stages): + """ + Builds a heat integration model requiring all exchangers to use modular configurations. + + Parameters + ---------- + cafaro_approx : bool + Specifies whether to use the Cafaro approximation in the model. + num_stages : int + The number of stages in the heat integration model. + + Returns + ------- + Pyomo.ConcreteModel + A Pyomo model configured to use only modular heat exchanger configurations. + """ m = build_model(cafaro_approx, num_stages) # Require modular + # Enforce modular configuration for all valid matches and stages for hot, cold, stg in m.valid_matches * m.stages: disj = m.exchanger_exists[hot, cold, stg] disj.modular.indicator_var.fix(True) disj.conventional.deactivate() + # Optimize modular configurations based on cost for hot, cold in m.valid_matches: lowest_price = float('inf') + # Determine the least costly configuration for each size for size in sorted(m.possible_sizes, reverse=True): - current_size_cost = (m.modular_size_cost[hot, cold, size] + - m.modular_fixed_cost[hot, cold, size]) + current_size_cost = ( + m.modular_size_cost[hot, cold, size] + + m.modular_fixed_cost[hot, cold, size] + ) if current_size_cost > lowest_price: + # Deactivate configurations that are not the least costly for stg in m.stages: m.module_size_active[hot, cold, stg, size].fix(0) else: @@ -42,13 +64,31 @@ def build_require_modular(cafaro_approx, num_stages): def build_modular_option(cafaro_approx, num_stages): + """ + Constructs a heat integration model with the option for using modular configurations based on cost optimization. + + Parameters + ---------- + cafaro_approx : bool + Specifies whether to use the Cafaro approximation in the model. + num_stages : int + The number of stages in the heat integration model. + + Returns + ------- + Pyomo.ConcreteModel + A Pyomo model that considers modular exchanger options based on cost efficiencies. + """ m = build_model(cafaro_approx, num_stages) + # Optimize for the least cost configuration across all stages and matche for hot, cold in m.valid_matches: lowest_price = float('inf') for size in sorted(m.possible_sizes, reverse=True): - current_size_cost = (m.modular_size_cost[hot, cold, size] + - m.modular_fixed_cost[hot, cold, size]) + current_size_cost = ( + m.modular_size_cost[hot, cold, size] + + m.modular_fixed_cost[hot, cold, size] + ) if current_size_cost > lowest_price: for stg in m.stages: m.module_size_active[hot, cold, stg, size].fix(0) @@ -59,14 +99,35 @@ def build_modular_option(cafaro_approx, num_stages): def build_model(use_cafaro_approximation, num_stages): - """Build the model.""" + """ + Initializes a base Pyomo model for heat integration using the common building blocks, with additional configuration for modular sizing. + + Parameters + ---------- + use_cafaro_approximation : bool + Specifies whether to use the Cafaro approximation in the model. + num_stages : int + The number of stages in the heat integration model. + + Returns + ------- + Pyomo.ConcreteModel + A Pyomo model configured for heat integration with modular exchanger options. + """ m = common.build_model(use_cafaro_approximation, num_stages) - m.possible_sizes = Set(initialize=[10 * (i + 1) for i in range(50)]) + m.possible_sizes = Set( + initialize=[10 * (i + 1) for i in range(50)], + doc="Set of possible module sizes, ranging from 10 to 500 in increments of 10.", + ) m.module_size_active = Var( - m.valid_matches, m.stages, m.possible_sizes, + m.valid_matches, + m.stages, + m.possible_sizes, doc="Total area of modular exchangers for each match.", - domain=Binary, initialize=0) + domain=Binary, + initialize=0, + ) num_modules_required = {} for size in m.possible_sizes: @@ -79,30 +140,106 @@ def build_model(use_cafaro_approximation, num_stages): num_modules_required[size, area] = remaining_size // area remaining_size = remaining_size % area - @m.Param(m.valid_matches, m.possible_sizes, m.module_sizes, - doc="Number of exchangers of each area required to " - "yield a certain total size.") + @m.Param( + m.valid_matches, + m.possible_sizes, + m.module_sizes, + doc="Number of exchangers of each area required to " + "yield a certain total size.", + ) def modular_num_exchangers(m, hot, cold, size, area): + """ + Returns the number of exchangers required for a given total module size and area. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo model configured for heat integration with modular exchanger options. + hot : str + The index for the hot stream involved in the heat exchanger. + cold : str + The index for the cold stream involved in the heat exchanger. + size : int + Module size under consideration. + area : float + The modular area size of the heat exchanger. + + Returns + ------- + Pyomo.Parameter + Number of modules of the specified area required to achieve the total size. + """ return num_modules_required[size, area] - @m.Param(m.valid_matches, m.possible_sizes, - doc="Area cost for each modular exchanger size.") + @m.Param( + m.valid_matches, + m.possible_sizes, + doc="Area cost for each modular exchanger size.", + ) def modular_size_cost(m, hot, cold, size): - return sum(m.modular_num_exchangers[hot, cold, size, area] * - m.module_area_cost[hot, cold, area] - for area in m.module_sizes) + """ + Returns the total area cost for a specified module size by summing costs of all required module areas. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo model configured for heat integration with modular exchanger options. + hot : str + The index for the hot stream involved in the heat exchanger. + cold : str + The index for the cold stream involved in the heat exchanger. + size : int + Module size under consideration. + + Returns + ------- + Pyomo.Parameter + Total area cost for the specified module size. + """ + return sum( + m.modular_num_exchangers[hot, cold, size, area] + * m.module_area_cost[hot, cold, area] + for area in m.module_sizes + ) - @m.Param(m.valid_matches, m.possible_sizes, - doc="Fixed cost for each modular exchanger size.") + @m.Param( + m.valid_matches, + m.possible_sizes, + doc="Fixed cost for each modular exchanger size.", + ) def modular_fixed_cost(m, hot, cold, size): - return sum(m.modular_num_exchangers[hot, cold, size, area] * - m.module_fixed_unit_cost - for area in m.module_sizes) + """ + Returns the total fixed cost associated with a specific modular exchanger size. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo model configured for heat integration with modular exchanger options. + hot : str + The index for the hot stream involved in the heat exchanger. + cold : str + The index for the cold stream involved in the heat exchanger. + size : int + Module size under consideration. + + Returns + ------- + Pyomo.Parameter + Total fixed cost for the specified module size. + """ + return sum( + m.modular_num_exchangers[hot, cold, size, area] * m.module_fixed_unit_cost + for area in m.module_sizes + ) m.LMTD_discretize = Var( - m.hot_streams, m.cold_streams, m.stages, m.possible_sizes, + m.hot_streams, + m.cold_streams, + m.stages, + m.possible_sizes, doc="Discretized log mean temperature difference", - bounds=(0, 500), initialize=0 + bounds=(0, 500), + initialize=0, ) for hot, cold, stg in m.valid_matches * m.stages: @@ -110,84 +247,159 @@ def modular_fixed_cost(m, hot, cold, size): disj.conventional = Disjunct() if not use_cafaro_approximation: disj.conventional.exchanger_area_cost = Constraint( - expr=m.exchanger_area_cost[stg, hot, cold] * 1E-3 >= - m.exchanger_area_cost_factor[hot, cold] * 1E-3 * - m.exchanger_area[stg, hot, cold] ** m.area_cost_exponent) + expr=m.exchanger_area_cost[stg, hot, cold] * 1e-3 + >= m.exchanger_area_cost_factor[hot, cold] + * 1e-3 + * m.exchanger_area[stg, hot, cold] ** m.area_cost_exponent, + doc="Ensures area cost meets the standard cost scaling.", + ) else: disj.conventional.exchanger_area_cost = Constraint( - expr=m.exchanger_area_cost[stg, hot, cold] * 1E-3 >= - m.exchanger_area_cost_factor[hot, cold] * 1E-3 * m.cafaro_k - * log(m.cafaro_b * m.exchanger_area[stg, hot, cold] + 1) + expr=m.exchanger_area_cost[stg, hot, cold] * 1e-3 + >= m.exchanger_area_cost_factor[hot, cold] + * 1e-3 + * m.cafaro_k + * log(m.cafaro_b * m.exchanger_area[stg, hot, cold] + 1), + doc="Ensures area cost meets the Cafaro approximation.", ) m.BigM[disj.conventional.exchanger_area_cost] = 100 disj.conventional.exchanger_fixed_cost = Constraint( - expr=m.exchanger_fixed_cost[stg, hot, cold] == - m.exchanger_fixed_unit_cost[hot, cold]) + expr=m.exchanger_fixed_cost[stg, hot, cold] + == m.exchanger_fixed_unit_cost[hot, cold], + doc="Sets the fixed cost for conventional exchangers.", + ) @disj.conventional.Constraint(m.possible_sizes) def no_modules(_, size): + """ + Ensures that no modules are active in the conventional configuration. + + Parameters + ---------- + _ : Pyomo.ConcreteModel + The Pyomo model instance, not used directly in the function. + size : int + Module size under consideration. + + Returns + ------- + Pyomo.Constraint + A constraint that forces the module size active variables to zero, ensuring no modular units are mistakenly considered in conventional configurations. + """ return m.module_size_active[hot, cold, stg, size] == 0 # Area requirement disj.conventional.exchanger_required_area = Constraint( - expr=m.exchanger_area[stg, hot, cold] * - m.U[hot, cold] * m.LMTD[hot, cold, stg] >= - m.heat_exchanged[hot, cold, stg]) + expr=m.exchanger_area[stg, hot, cold] + * m.U[hot, cold] + * m.LMTD[hot, cold, stg] + >= m.heat_exchanged[hot, cold, stg], + doc="Calculates the required area based on heat exchanged and LMTD.", + ) m.BigM[disj.conventional.exchanger_required_area] = 5000 disj.modular = Disjunct() disj.modular.choose_one_config = Constraint( expr=sum( - m.module_size_active[hot, cold, stg, size] - for size in m.possible_sizes) == 1 + m.module_size_active[hot, cold, stg, size] for size in m.possible_sizes + ) + == 1, + doc="Only one module size can be active.", ) disj.modular.exchanger_area_cost = Constraint( - expr=m.exchanger_area_cost[stg, hot, cold] * 1E-3 == - sum(m.modular_size_cost[hot, cold, size] * 1E-3 * - m.module_size_active[hot, cold, stg, size] - for size in m.possible_sizes) + expr=m.exchanger_area_cost[stg, hot, cold] * 1e-3 + == sum( + m.modular_size_cost[hot, cold, size] + * 1e-3 + * m.module_size_active[hot, cold, stg, size] + for size in m.possible_sizes + ), + doc="Area cost for modular exchangers.", ) disj.modular.exchanger_fixed_cost = Constraint( - expr=m.exchanger_fixed_cost[stg, hot, cold] == - sum(m.modular_fixed_cost[hot, cold, size] * 1E-3 * - m.module_size_active[hot, cold, stg, size] - for size in m.possible_sizes)) + expr=m.exchanger_fixed_cost[stg, hot, cold] + == sum( + m.modular_fixed_cost[hot, cold, size] + * 1e-3 + * m.module_size_active[hot, cold, stg, size] + for size in m.possible_sizes + ), + doc="Fixed cost for modular exchangers.", + ) disj.modular.discretize_area = Constraint( - expr=m.exchanger_area[stg, hot, cold] == sum( + expr=m.exchanger_area[stg, hot, cold] + == sum( area * m.module_size_active[hot, cold, stg, area] - for area in m.possible_sizes) + for area in m.possible_sizes + ), + doc="Total area of modular exchangers for each match.", ) disj.modular.discretized_LMTD = Constraint( - expr=m.LMTD[hot, cold, stg] == sum( - m.LMTD_discretize[hot, cold, stg, size] - for size in m.possible_sizes - ) + expr=m.LMTD[hot, cold, stg] + == sum( + m.LMTD_discretize[hot, cold, stg, size] for size in m.possible_sizes + ), + doc="Discretized LMTD for each match.", ) @disj.modular.Constraint(m.possible_sizes) def discretized_LMTD_LB(disj, size): + """ + Sets the lower bound on the discretized Log Mean Temperature Difference (LMTD) for each possible size. + + Parameters + ---------- + disj : Pyomo.Disjunct + The disjunct object representing a specific module size. + size : int + Module size under consideration. + + Returns + ------- + Pyomo.Constraint + A constraint that sets the lower limit for the discretized LMTD based on the module size active in the configuration. + """ return ( - m.LMTD[hot, cold, stg].lb - * m.module_size_active[hot, cold, stg, size] + m.LMTD[hot, cold, stg].lb * m.module_size_active[hot, cold, stg, size] ) <= m.LMTD_discretize[hot, cold, stg, size] @disj.modular.Constraint(m.possible_sizes) def discretized_LMTD_UB(disj, size): + """ + Sets the upper bound on the discretized Log Mean Temperature Difference (LMTD) for each possible size. + + Parameters + ---------- + disj : Pyomo.Disjunct + The disjunct object representing a specific module size. + size : int + Module size under consideration. + + Returns + ------- + Pyomo.Constraint + A constraint that sets the upper limit for the discretized LMTD based on the module size active in the configuration. + """ return m.LMTD_discretize[hot, cold, stg, size] <= ( - m.LMTD[hot, cold, stg].ub - * m.module_size_active[hot, cold, stg, size] + m.LMTD[hot, cold, stg].ub * m.module_size_active[hot, cold, stg, size] ) disj.modular.exchanger_required_area = Constraint( - expr=m.U[hot, cold] * sum( + expr=m.U[hot, cold] + * sum( area * m.LMTD_discretize[hot, cold, stg, area] - for area in m.possible_sizes) >= - m.heat_exchanged[hot, cold, stg]) + for area in m.possible_sizes + ) + >= m.heat_exchanged[hot, cold, stg], + doc="Calculates the required area based on heat exchanged and LMTD.", + ) disj.modular_or_not = Disjunction( - expr=[disj.modular, disj.conventional]) + expr=[disj.modular, disj.conventional], + doc="Disjunction between modular and conventional configurations.", + ) return m diff --git a/gdplib/mod_hens/modular_discrete_single_module.py b/gdplib/mod_hens/modular_discrete_single_module.py index be621be..0fbe980 100644 --- a/gdplib/mod_hens/modular_discrete_single_module.py +++ b/gdplib/mod_hens/modular_discrete_single_module.py @@ -12,6 +12,7 @@ exchanger module type (size). """ + from __future__ import division from pyomo.environ import Binary, Constraint, RangeSet, Var @@ -21,11 +22,40 @@ def build_single_module(cafaro_approx, num_stages): + """ + Builds a heat integration model tailored to handle single module types, with the option to utilize Cafaro's approximation for cost and efficiency calculations. + + Parameters + ---------- + cafaro_approx : bool + Specifies whether to use the Cafaro approximation in the model. + num_stages : int + The number of stages in the heat integration model. + + Returns + ------- + Pyomo.ConcreteModel + A Pyomo model configured with constraints and parameters specific to the requirements of using single module types in a discretized format. + """ return build_model(cafaro_approx, num_stages) def build_model(use_cafaro_approximation, num_stages): - """Build the model.""" + """ + Extends a base heat integration model by incorporating a module configuration approach. It allows only single exchanger module types, optimizing the model for specific operational constraints and simplifying the nonlinear terms through discretization. + + Parameters + ---------- + use_cafaro_approximation : bool + Specifies whether to use the Cafaro approximation in the model. + num_stages : int + The number of stages in the heat integration model. + + Returns + ------- + Pyomo.ConcreteModel + An enhanced heat integration model that supports module configurations with discretized area considerations to simplify calculations and improve optimization performance. + """ m = common.build_model(use_cafaro_approximation, num_stages) # list of tuples (num_modules, module_size) @@ -35,37 +65,101 @@ def build_model(use_cafaro_approximation, num_stages): configurations_list += configs # Map of config indx: (# modules, module size) - m.configurations_map = { - (k + 1): v for k, v in enumerate(configurations_list)} + m.configurations_map = {(k + 1): v for k, v in enumerate(configurations_list)} m.module_index_set = RangeSet(len(configurations_list)) m.module_config_active = Var( - m.valid_matches, m.stages, m.module_index_set, + m.valid_matches, + m.stages, + m.module_index_set, doc="Binary for if which module configuration is active for a match.", - domain=Binary, initialize=0) + domain=Binary, + initialize=0, + ) @m.Param(m.module_index_set, doc="Area of each configuration") def module_area(m, indx): + """ + Calculates the total area of a module configuration based on the number of modules and the size of each module. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo concrete model representing the heat exchange model modified to support discretization of area simplifying the nonlinear expressions, specialized to the case of allowing only a single exchanger module type (size). + indx : int + Index of the module configuration in the model. + + Returns + ------- + Pyomo.Parameter + The total area of the configuration corresponding to the given index. + """ num_modules, size = m.configurations_map[indx] return num_modules * size - @m.Param(m.valid_matches, m.module_index_set, - doc="Area cost for each modular configuration.") + @m.Param( + m.valid_matches, + m.module_index_set, + doc="Area cost for each modular configuration.", + ) def modular_size_cost(m, hot, cold, indx): + """ + Determines the cost associated with a specific modular configuration, taking into account the number of modules and their individual sizes. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo concrete model representing the heat exchange model modified for the case of allowing only a single exchanger module type (size). + hot : str + The index for the hot stream involved in the heat exchanger. + cold : str + The index for the cold stream involved in the heat exchanger. + indx : int + Index of the module configuration in the model. + + Returns + ------- + Pyomo.Parameter + Cost associated with the specified modular configuration. + """ num_modules, size = m.configurations_map[indx] return num_modules * m.module_area_cost[hot, cold, size] - @m.Param(m.valid_matches, m.module_index_set, - doc="Fixed cost for each modular exchanger size.") + @m.Param( + m.valid_matches, + m.module_index_set, + doc="Fixed cost for each modular exchanger size.", + ) def modular_fixed_cost(m, hot, cold, indx): + """ + Computes the fixed cost for a modular exchanger configuration, factoring in the number of modules and the set fixed cost per unit. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo concrete model representing the heat exchange model modified for the case of allowing only a single exchanger module type (size). + cold : str + The index for the cold stream involved in the heat exchanger. + indx : int + Index of the module configuration in the model. + + Returns + ------- + Pyomo.Parameter + Fixed cost for the given modular exchanger configuration. + """ num_modules, size = m.configurations_map[indx] return num_modules * m.module_fixed_unit_cost m.LMTD_discretize = Var( - m.hot_streams, m.cold_streams, m.stages, m.module_index_set, + m.hot_streams, + m.cold_streams, + m.stages, + m.module_index_set, doc="Discretized log mean temperature difference", - bounds=(0, 500), initialize=0 + bounds=(0, 500), + initialize=0, ) for hot, cold, stg in m.valid_matches * m.stages: @@ -73,72 +167,162 @@ def modular_fixed_cost(m, hot, cold, indx): disj.choose_one_config = Constraint( expr=sum( m.module_config_active[hot, cold, stg, indx] - for indx in m.module_index_set) == 1 + for indx in m.module_index_set + ) + == 1, + doc="Enforce a single active configuration per exchanger per stage.", ) disj.exchanger_area_cost = Constraint( - expr=m.exchanger_area_cost[stg, hot, cold] * 1E-3 == - sum(m.modular_size_cost[hot, cold, indx] * 1E-3 * - m.module_config_active[hot, cold, stg, indx] - for indx in m.module_index_set) + expr=m.exchanger_area_cost[stg, hot, cold] * 1e-3 + == sum( + m.modular_size_cost[hot, cold, indx] + * 1e-3 + * m.module_config_active[hot, cold, stg, indx] + for indx in m.module_index_set + ), + doc="Compute total area cost from active configurations.", ) disj.exchanger_fixed_cost = Constraint( - expr=m.exchanger_fixed_cost[stg, hot, cold] == - sum(m.modular_fixed_cost[hot, cold, indx] * 1E-3 * - m.module_config_active[hot, cold, stg, indx] - for indx in m.module_index_set)) + expr=m.exchanger_fixed_cost[stg, hot, cold] + == sum( + m.modular_fixed_cost[hot, cold, indx] + * 1e-3 + * m.module_config_active[hot, cold, stg, indx] + for indx in m.module_index_set + ), + doc="Sum fixed costs of active configurations for total investment.", + ) disj.discretize_area = Constraint( - expr=m.exchanger_area[stg, hot, cold] == sum( - m.module_area[indx] * - m.module_config_active[hot, cold, stg, indx] - for indx in m.module_index_set) + expr=m.exchanger_area[stg, hot, cold] + == sum( + m.module_area[indx] * m.module_config_active[hot, cold, stg, indx] + for indx in m.module_index_set + ), + doc="Match exchanger area with sum of active configuration areas.", ) disj.discretized_LMTD = Constraint( - expr=m.LMTD[hot, cold, stg] == sum( - m.LMTD_discretize[hot, cold, stg, indx] - for indx in m.module_index_set - ) + expr=m.LMTD[hot, cold, stg] + == sum( + m.LMTD_discretize[hot, cold, stg, indx] for indx in m.module_index_set + ), + doc="Aggregate LMTD from active configurations for thermal modeling.", ) @disj.Constraint(m.module_index_set) def discretized_LMTD_LB(disj, indx): + """ + Sets the lower bound on the discretized Log Mean Temperature Difference (LMTD) for each module configuration. + + Parameters + ---------- + disj : Pyomo.Disjunct + The disjunct object representing a specific module configuration. + indx : int + Index of the module configuration in the model. + + Returns + ------- + Pyomo.Constraint + A constraint ensuring that the discretized LMTD respects the specified lower bound for active configurations. + """ return ( - m.LMTD[hot, cold, stg].lb - * m.module_config_active[hot, cold, stg, indx] + m.LMTD[hot, cold, stg].lb * m.module_config_active[hot, cold, stg, indx] ) <= m.LMTD_discretize[hot, cold, stg, indx] @disj.Constraint(m.module_index_set) def discretized_LMTD_UB(disj, indx): + """ + Sets the upper bound on the discretized Log Mean Temperature Difference (LMTD) for each module configuration. + + Parameters + ---------- + disj : Pyomo.Disjunct + The disjunct object representing a specific module configuration. + indx : int + Index of the module configuration in the model. + + Returns + ------- + Pyomo.Constraint + A constraint ensuring that the discretized LMTD does not exceed the specified upper bound for active configurations. + """ return m.LMTD_discretize[hot, cold, stg, indx] <= ( - m.LMTD[hot, cold, stg].ub - * m.module_config_active[hot, cold, stg, indx] + m.LMTD[hot, cold, stg].ub * m.module_config_active[hot, cold, stg, indx] ) disj.exchanger_required_area = Constraint( - expr=m.U[hot, cold] * sum( + expr=m.U[hot, cold] + * sum( m.module_area[indx] * m.LMTD_discretize[hot, cold, stg, indx] - for indx in m.module_index_set) >= - m.heat_exchanged[hot, cold, stg]) + for indx in m.module_index_set + ) + >= m.heat_exchanged[hot, cold, stg], + doc="Ensures sufficient heat transfer capacity for required heat exchange.", + ) @m.Disjunct(m.module_sizes) def module_type(disj, size): - """Disjunct for selection of one module type.""" + """ + Disjunct for selecting a specific module size in the heat exchange model. This disjunct applies constraints to enforce that only the selected module size is active within any given configuration across all stages and matches. + + Parameters + ---------- + disj : Pyomo.Disjunct + The disjunct object associated with a specific module size. + size : int + The specific size of the module being considered in this disjunct. + + Returns + ------- + Pyomo.Disjunct + A Pyomo Disjunct object that contains constraints to limit the module configuration to a single size throughout the model. + """ + @disj.Constraint(m.valid_matches, m.stages, m.module_index_set) def no_other_module_types(_, hot, cold, stg, indx): + """ + Ensures only modules of the selected size are active, deactivating other sizes. + + Parameters + ---------- + _ : Pyomo.ConcreteModel + The Pyomo model instance, not used directly in the function. + hot : str + The index for the hot stream involved in the heat exchanger. + cold : str + The index for the cold stream involved in the heat exchanger. + stg : int + The index for the stage involved in the heat exchanger. + indx : int + Index of the module configuration in the model. + + Returns + ------- + Pyomo.Constraint + A constraint expression that ensures only modules of the specified size are active, effectively disabling other module sizes for the current configuration. + """ # num_modules, size = configurations_map[indx] if m.configurations_map[indx][1] != size: return m.module_config_active[hot, cold, stg, indx] == 0 else: return Constraint.NoConstraint + # disj.no_other_module_types = Constraint( # expr=sum( # m.module_config_active[hot, cold, stg, indx] # for indx in m.module_index_set - # if m.configurations_map[indx][1] != size) == 0 + # if m.configurations_map[indx][1] != size + # ) + # == 0, + # doc="Deactivates non-selected module sizes.", # ) + m.select_one_module_type = Disjunction( - expr=[m.module_type[area] for area in m.module_sizes]) + expr=[m.module_type[area] for area in m.module_sizes], + doc="Selects exactly one module size for use across all configurations.", + ) return m diff --git a/gdplib/mod_hens/modular_integer.py b/gdplib/mod_hens/modular_integer.py index 14dcf91..6a4ccee 100644 --- a/gdplib/mod_hens/modular_integer.py +++ b/gdplib/mod_hens/modular_integer.py @@ -8,15 +8,31 @@ modules using integer variables for module selection. """ + from __future__ import division -from pyomo.environ import (Constraint, Integers, log, Var) +from pyomo.environ import Constraint, Integers, log, Var from pyomo.gdp import Disjunct, Disjunction from gdplib.mod_hens import common def build_single_module(cafaro_approx, num_stages): + """ + Constructs a Pyomo model configured to exclusively use modular heat exchangers, forcing selection of a single module type per stage and stream match. This configuration utilizes the Cafaro approximation if specified. + + Parameters + ---------- + cafaro_approx : bool + Specifies whether to use the Cafaro approximation in the model. + num_stages : int + The number of stages in the heat integration model. + + Returns + ------- + Pyomo.ConcreteModel + A Pyomo ConcreteModel optimized with constraints for modular heat exchanger configurations, fixed to a single module type per valid match. + """ m = build_model(cafaro_approx, num_stages) # Require modular for hot, cold, stg in m.valid_matches * m.stages: @@ -27,20 +43,52 @@ def build_single_module(cafaro_approx, num_stages): # Must choose only one type of module @m.Disjunct(m.module_sizes) def module_type(disj, size): - """Disjunct for selection of one module type.""" + """ + Disjunct for selection of one module type. + + Parameters + ---------- + disj : Pyomo.Disjunct + _description_ + size : int + Module size under consideration. + """ disj.no_other_module_types = Constraint( - expr=sum(m.num_modules[hot, cold, stage, area] - for hot, cold in m.valid_matches - for stage in m.stages - for area in m.module_sizes - if area != size) == 0) + expr=sum( + m.num_modules[hot, cold, stage, area] + for hot, cold in m.valid_matches + for stage in m.stages + for area in m.module_sizes + if area != size + ) + == 0, + doc="Ensures no modules of other sizes are active when this size is selected.", + ) + m.select_one_module_type = Disjunction( - expr=[m.module_type[area] for area in m.module_sizes]) + expr=[m.module_type[area] for area in m.module_sizes], + doc="Select one module type", + ) return m def build_require_modular(cafaro_approx, num_stages): + """ + Builds a Pyomo model that requires the use of modular configurations for all heat exchangers within the model. This setup deactivates any conventional exchanger configurations. + + Parameters + ---------- + cafaro_approx : bool + Specifies whether to use the Cafaro approximation in the model. + num_stages : int + The number of stages in the heat integration model. + + Returns + ------- + Pyomo.ConcreteModel + A Pyomo model configured to require modular heat exchangers throughout the network. + """ m = build_model(cafaro_approx, num_stages) # Require modular for hot, cold, stg in m.valid_matches * m.stages: @@ -50,17 +98,51 @@ def build_require_modular(cafaro_approx, num_stages): def build_modular_option(cafaro_approx, num_stages): + """ + Builds a Pyomo model that can optionally use modular heat exchangers based on configuration decisions within the model. This function initializes a model using the Cafaro approximation as specified. + + Parameters + ---------- + cafaro_approx : bool + Specifies whether to use the Cafaro approximation in the model. + num_stages : int + The number of stages in the heat integration model. + + Returns + ------- + Pyomo.ConcreteModel + Returns a Pyomo model with the flexibility to choose modular heat exchanger configurations based on optimization results. + """ return build_model(cafaro_approx, num_stages) def build_model(use_cafaro_approximation, num_stages): - """Build the model.""" + """ + Base function for constructing a heat exchange network model with optional use of Cafaro approximation and integration of modular heat exchangers represented by integer variables. + + Parameters + ---------- + use_cafaro_approximation : bool + Specifies whether to use the Cafaro approximation in the model. + num_stages : int + The number of stages in the heat integration model. + + Returns + ------- + Pyomo.ConcreteModel + The initialized Pyomo model including both conventional and modular heat exchanger options. + """ m = common.build_model(use_cafaro_approximation, num_stages) m.num_modules = Var( - m.valid_matches, m.stages, m.module_sizes, + m.valid_matches, + m.stages, + m.module_sizes, doc="The number of modules of each size at each exchanger.", - domain=Integers, bounds=(0, 100), initialize=0) + domain=Integers, + bounds=(0, 100), + initialize=0, + ) # improve quality of bounds for size in m.module_sizes: for var in m.num_modules[:, :, :, size]: @@ -72,48 +154,84 @@ def build_model(use_cafaro_approximation, num_stages): disj.conventional = Disjunct() if not use_cafaro_approximation: disj.conventional.exchanger_area_cost = Constraint( - expr=m.exchanger_area_cost[stg, hot, cold] * 1E-3 >= - m.exchanger_area_cost_factor[hot, cold] * 1E-3 * - m.exchanger_area[stg, hot, cold] ** m.area_cost_exponent) + expr=m.exchanger_area_cost[stg, hot, cold] * 1e-3 + >= m.exchanger_area_cost_factor[hot, cold] + * 1e-3 + * m.exchanger_area[stg, hot, cold] ** m.area_cost_exponent, + doc="Ensures area cost meets the standard cost scaling.", + ) else: disj.conventional.exchanger_area_cost = Constraint( - expr=m.exchanger_area_cost[stg, hot, cold] * 1E-3 >= - m.exchanger_area_cost_factor[hot, cold] * 1E-3 * m.cafaro_k - * log(m.cafaro_b * m.exchanger_area[stg, hot, cold] + 1) + expr=m.exchanger_area_cost[stg, hot, cold] * 1e-3 + >= m.exchanger_area_cost_factor[hot, cold] + * 1e-3 + * m.cafaro_k + * log(m.cafaro_b * m.exchanger_area[stg, hot, cold] + 1), + doc="Applies Cafaro's logarithmic cost scaling to area cost.", ) m.BigM[disj.conventional.exchanger_area_cost] = 100 disj.conventional.exchanger_fixed_cost = Constraint( - expr=m.exchanger_fixed_cost[stg, hot, cold] == - m.exchanger_fixed_unit_cost[hot, cold]) + expr=m.exchanger_fixed_cost[stg, hot, cold] + == m.exchanger_fixed_unit_cost[hot, cold], + doc="Sets fixed cost for the exchanger based on unit costs.", + ) @disj.conventional.Constraint(m.module_sizes) def no_modules(_, area): + """ + Ensures that no modules are active in the conventional configuration. + + Parameters + ---------- + _ : Pyomo.ConcreteModel + The Pyomo model instance, not used directly in the function + area : float + The modular area size of the heat exchanger + + Returns + ------- + Pyomo.Constraint + A constraint that forces the module size active variables to zero, ensuring no modular units are mistakenly considered in conventional configurations. + """ return m.num_modules[hot, cold, stg, area] == 0 disj.modular = Disjunct() disj.modular.exchanger_area_cost = Constraint( - expr=m.exchanger_area_cost[stg, hot, cold] * 1E-3 == - sum(m.module_area_cost[hot, cold, area] + expr=m.exchanger_area_cost[stg, hot, cold] * 1e-3 + == sum( + m.module_area_cost[hot, cold, area] * m.num_modules[hot, cold, stg, area] - for area in m.module_sizes) - * 1E-3) + for area in m.module_sizes + ) + * 1e-3, + doc="Area cost for modular exchanger", + ) disj.modular.exchanger_fixed_cost = Constraint( - expr=m.exchanger_fixed_cost[stg, hot, cold] == - m.module_fixed_unit_cost * sum(m.num_modules[hot, cold, stg, area] - for area in m.module_sizes)) + expr=m.exchanger_fixed_cost[stg, hot, cold] + == m.module_fixed_unit_cost + * sum(m.num_modules[hot, cold, stg, area] for area in m.module_sizes), + doc="Fixed cost for modular exchanger", + ) disj.modular.exchanger_area = Constraint( - expr=m.exchanger_area[stg, hot, cold] == - sum(area * m.num_modules[hot, cold, stg, area] - for area in m.module_sizes)) + expr=m.exchanger_area[stg, hot, cold] + == sum( + area * m.num_modules[hot, cold, stg, area] for area in m.module_sizes + ), + doc="Area for modular exchanger", + ) disj.modular_or_not = Disjunction( - expr=[disj.modular, disj.conventional]) + expr=[disj.modular, disj.conventional], + doc="Module or conventional exchanger", + ) # Area requirement disj.exchanger_required_area = Constraint( - expr=m.exchanger_area[stg, hot, cold] * ( - m.U[hot, cold] * m.LMTD[hot, cold, stg]) >= - m.heat_exchanged[hot, cold, stg]) + expr=m.exchanger_area[stg, hot, cold] + * (m.U[hot, cold] * m.LMTD[hot, cold, stg]) + >= m.heat_exchanged[hot, cold, stg], + doc="Area requirement for exchanger", + ) m.BigM[disj.exchanger_required_area] = 5000 return m diff --git a/gdplib/modprodnet/__init__.py b/gdplib/modprodnet/__init__.py index 1bd64af..b0866e5 100644 --- a/gdplib/modprodnet/__init__.py +++ b/gdplib/modprodnet/__init__.py @@ -8,5 +8,10 @@ build_cap_expand_dip = partial(_capacity_expansion, case="Dip") build_cap_expand_decay = partial(_capacity_expansion, case="Decay") -__all__ = ['build_cap_expand_growth', 'build_cap_expand_dip', 'build_cap_expand_decay', 'build_distributed_model', - 'build_quarter_distributed_model'] +__all__ = [ + 'build_cap_expand_growth', + 'build_cap_expand_dip', + 'build_cap_expand_decay', + 'build_distributed_model', + 'build_quarter_distributed_model', +] diff --git a/gdplib/modprodnet/distributed.py b/gdplib/modprodnet/distributed.py index 1988b4f..e4fa585 100644 --- a/gdplib/modprodnet/distributed.py +++ b/gdplib/modprodnet/distributed.py @@ -19,7 +19,9 @@ def build_model(): xls_data = pd.read_excel( os.path.join(os.path.dirname(__file__), "multiple_market_size.xlsx"), - sheet_name=["demand", "locations"], index_col=0) + sheet_name=["demand", "locations"], + index_col=0, + ) @m.Param(m.markets, m.months) def market_demand(m, mkt, mo): @@ -31,20 +33,22 @@ def transport_cost(m, mo): m.route_fixed_cost = Param( initialize=100, - doc="Cost of establishing a route from a modular site to a market") + doc="Cost of establishing a route from a modular site to a market", + ) m.conv_x = Var(bounds=(0, 300), doc="x-coordinate of centralized plant.") m.conv_y = Var(bounds=(0, 300), doc="y-coordinate of centralized plant.") - m.conv_size = Var(bounds=(10, 500), initialize=10, - doc="Size of conventional plant.") + m.conv_size = Var( + bounds=(10, 500), initialize=10, doc="Size of conventional plant." + ) m.conv_cost = Var() m.conv_base_cost = Param(initialize=1000, doc="Cost for size 20") m.conv_exponent = Param(initialize=0.6) m.cost_calc = Constraint( - expr=m.conv_cost == ( - m.conv_base_cost * (m.conv_size / 20) ** m.conv_exponent)) + expr=m.conv_cost == (m.conv_base_cost * (m.conv_size / 20) ** m.conv_exponent) + ) @m.Param(m.markets) def mkt_x(m, mkt): @@ -58,17 +62,21 @@ def mkt_y(m, mkt): @m.Constraint(m.markets) def distance_calculation(m, mkt): - return m.dist_to_mkt[mkt] == sqrt( - (m.conv_x / 300 - m.mkt_x[mkt] / 300)**2 + - (m.conv_y / 300 - m.mkt_y[mkt] / 300)**2) * 300 + return ( + m.dist_to_mkt[mkt] + == sqrt( + (m.conv_x / 300 - m.mkt_x[mkt] / 300) ** 2 + + (m.conv_y / 300 - m.mkt_y[mkt] / 300) ** 2 + ) + * 300 + ) m.shipments_to_mkt = Var(m.markets, m.months, bounds=(0, 100)) m.production = Var(m.months, bounds=(0, 500)) @m.Constraint(m.months) def production_satisfaction(m, mo): - return m.production[mo] == sum(m.shipments_to_mkt[mkt, mo] - for mkt in m.markets) + return m.production[mo] == sum(m.shipments_to_mkt[mkt, mo] for mkt in m.markets) @m.Constraint(m.months) def size_requirement(m, mo): @@ -83,12 +91,15 @@ def demand_satisfaction(m, mkt, mo): m.variable_shipment_cost = Expression( expr=sum( - m.shipments_to_mkt[mkt, mo] * m.dist_to_mkt[mkt] * - m.transport_cost[mo] - for mkt in m.markets for mo in m.months)) + m.shipments_to_mkt[mkt, mo] * m.dist_to_mkt[mkt] * m.transport_cost[mo] + for mkt in m.markets + for mo in m.months + ) + ) m.total_cost = Objective( - expr=m.variable_shipment_cost + m.conv_cost + 5 * m.route_fixed_cost) + expr=m.variable_shipment_cost + m.conv_cost + 5 * m.route_fixed_cost + ) return m @@ -103,14 +114,15 @@ def build_modular_model(): m.markets = RangeSet(5) m.modular_sites = RangeSet(1, 3) m.site_pairs = Set( - initialize=m.modular_sites * m.modular_sites, - filter=lambda _, x, y: not x == y) - m.unique_site_pairs = Set( - initialize=m.site_pairs, filter=lambda _, x, y: x < y) + initialize=m.modular_sites * m.modular_sites, filter=lambda _, x, y: not x == y + ) + m.unique_site_pairs = Set(initialize=m.site_pairs, filter=lambda _, x, y: x < y) xls_data = pd.read_excel( os.path.join(os.path.dirname(__file__), "multiple_market_size.xlsx"), - sheet_name=["demand", "locations"], index_col=0) + sheet_name=["demand", "locations"], + index_col=0, + ) @m.Param(m.markets, m.months) def market_demand(m, mkt, mo): @@ -122,11 +134,13 @@ def transport_cost(m, mo): m.route_fixed_cost = Param( initialize=100, - doc="Cost of establishing a route from a modular site to a market") + doc="Cost of establishing a route from a modular site to a market", + ) @m.Param(m.months, doc="Cost of transporting a module one mile") def modular_transport_cost(m, mo): return 1 * (1 + m.discount_rate / 12) ** (-mo / 12) + m.modular_base_cost = Param(initialize=1000, doc="Cost for size 20") @m.Param(m.months) @@ -141,32 +155,27 @@ def mkt_x(m, mkt): def mkt_y(m, mkt): return float(xls_data["locations"]["y"]["market%s" % mkt]) - m.site_x = Var( - m.modular_sites, bounds=(0, 300), initialize={ - 1: 50, 2: 225, 3: 250}) - m.site_y = Var( - m.modular_sites, bounds=(0, 300), initialize={ - 1: 300, 2: 275, 3: 50}) + m.site_x = Var(m.modular_sites, bounds=(0, 300), initialize={1: 50, 2: 225, 3: 250}) + m.site_y = Var(m.modular_sites, bounds=(0, 300), initialize={1: 300, 2: 275, 3: 50}) - m.num_modules = Var( - m.modular_sites, m.months, domain=Integers, bounds=(0, 25)) + m.num_modules = Var(m.modular_sites, m.months, domain=Integers, bounds=(0, 25)) m.modules_transferred = Var( - m.site_pairs, m.months, - domain=Integers, bounds=(0, 25), - doc="Number of modules moved from one site to another in a month.") + m.site_pairs, + m.months, + domain=Integers, + bounds=(0, 25), + doc="Number of modules moved from one site to another in a month.", + ) # m.modules_transferred[...].fix(0) - m.modules_added = Var( - m.modular_sites, m.months, domain=Integers, bounds=(0, 25)) + m.modules_added = Var(m.modular_sites, m.months, domain=Integers, bounds=(0, 25)) - m.dist_to_mkt = Var( - m.modular_sites, m.markets, bounds=(0, sqrt(300**2 + 300**2))) + m.dist_to_mkt = Var(m.modular_sites, m.markets, bounds=(0, sqrt(300**2 + 300**2))) m.sqr_scaled_dist_to_mkt = Var( - m.modular_sites, m.markets, bounds=(0.001, 8), initialize=0.001) + m.modular_sites, m.markets, bounds=(0.001, 8), initialize=0.001 + ) m.dist_to_site = Var(m.site_pairs, bounds=(0, sqrt(300**2 + 300**2))) - m.sqr_scaled_dist_to_site = Var( - m.site_pairs, bounds=(0.001, 8), initialize=0.001) - m.shipments_to_mkt = Var( - m.modular_sites, m.markets, m.months, bounds=(0, 100)) + m.sqr_scaled_dist_to_site = Var(m.site_pairs, bounds=(0.001, 8), initialize=0.001) + m.shipments_to_mkt = Var(m.modular_sites, m.markets, m.months, bounds=(0, 100)) m.production = Var(m.modular_sites, m.months, bounds=(0, 500)) @m.Disjunct(m.modular_sites) @@ -174,7 +183,8 @@ def site_active(disj, site): @disj.Constraint(m.months) def production_satisfaction(site_disj, mo): return m.production[site, mo] == sum( - m.shipments_to_mkt[site, mkt, mo] for mkt in m.markets) + m.shipments_to_mkt[site, mkt, mo] for mkt in m.markets + ) @disj.Constraint(m.months) def production_limit(site_disj, mo): @@ -183,34 +193,49 @@ def production_limit(site_disj, mo): @disj.Constraint(m.months) def module_balance(site_disj, mo): existing_modules = m.num_modules[site, mo - 1] if mo >= 1 else 0 - new_modules = (m.modules_added[site, mo - m.modular_setup_time] - if mo > m.modular_setup_time else 0) + new_modules = ( + m.modules_added[site, mo - m.modular_setup_time] + if mo > m.modular_setup_time + else 0 + ) xfrd_in_modules = sum( - m.modules_transferred[from_site, - site, mo - m.modular_move_time] + m.modules_transferred[from_site, site, mo - m.modular_move_time] for from_site in m.modular_sites - if (not from_site == site) and mo > m.modular_move_time) + if (not from_site == site) and mo > m.modular_move_time + ) xfrd_out_modules = sum( m.modules_transferred[site, to_site, mo] - for to_site in m.modular_sites if not to_site == site) + for to_site in m.modular_sites + if not to_site == site + ) return m.num_modules[site, mo] == ( - new_modules + xfrd_in_modules - xfrd_out_modules + - existing_modules) + new_modules + xfrd_in_modules - xfrd_out_modules + existing_modules + ) @m.Disjunct(m.modular_sites) def site_inactive(disj, site): disj.no_modules = Constraint( - expr=sum(m.num_modules[site, mo] for mo in m.months) == 0) + expr=sum(m.num_modules[site, mo] for mo in m.months) == 0 + ) disj.no_module_transfer = Constraint( - expr=sum(m.modules_transferred[site1, site2, mo] - for site1, site2 in m.site_pairs - for mo in m.months - if site1 == site or site2 == site) == 0) + expr=sum( + m.modules_transferred[site1, site2, mo] + for site1, site2 in m.site_pairs + for mo in m.months + if site1 == site or site2 == site + ) + == 0 + ) disj.no_shipments = Constraint( - expr=sum(m.shipments_to_mkt[site, mkt, mo] - for mkt in m.markets for mo in m.months) == 0) + expr=sum( + m.shipments_to_mkt[site, mkt, mo] + for mkt in m.markets + for mo in m.months + ) + == 0 + ) @m.Disjunction(m.modular_sites) def site_active_or_not(m, site): @@ -219,40 +244,52 @@ def site_active_or_not(m, site): @m.Constraint(m.modular_sites, doc="Symmetry breaking for site activation") def site_active_ordering(m, site): if site + 1 <= max(m.modular_sites): - return (m.site_active[site].binary_indicator_var >= - m.site_active[site + 1].binary_indicator_var) + return ( + m.site_active[site].binary_indicator_var + >= m.site_active[site + 1].binary_indicator_var + ) else: return Constraint.NoConstraint @m.Disjunct(m.unique_site_pairs) def pair_active(disj, site1, site2): disj.site1_active = Constraint( - expr=m.site_active[site1].binary_indicator_var == 1) + expr=m.site_active[site1].binary_indicator_var == 1 + ) disj.site2_active = Constraint( - expr=m.site_active[site2].binary_indicator_var == 1) + expr=m.site_active[site2].binary_indicator_var == 1 + ) disj.site_distance_calc = Constraint( - expr=m.dist_to_site[site1, site2] == sqrt( - m.sqr_scaled_dist_to_site[site1, site2]) * 150) + expr=m.dist_to_site[site1, site2] + == sqrt(m.sqr_scaled_dist_to_site[site1, site2]) * 150 + ) disj.site_distance_symmetry = Constraint( - expr=m.dist_to_site[site1, site2] == m.dist_to_site[site2, site1]) + expr=m.dist_to_site[site1, site2] == m.dist_to_site[site2, site1] + ) disj.site_sqr_distance_calc = Constraint( - expr=m.sqr_scaled_dist_to_site[site1, site2] == ( - (m.site_x[site1] / 150 - m.site_x[site2] / 150)**2 + - (m.site_y[site1] / 150 - m.site_y[site2] / 150)**2)) + expr=m.sqr_scaled_dist_to_site[site1, site2] + == ( + (m.site_x[site1] / 150 - m.site_x[site2] / 150) ** 2 + + (m.site_y[site1] / 150 - m.site_y[site2] / 150) ** 2 + ) + ) disj.site_sqr_distance_symmetry = Constraint( - expr=m.sqr_scaled_dist_to_site[site1, site2] == - m.sqr_scaled_dist_to_site[site2, site1]) + expr=m.sqr_scaled_dist_to_site[site1, site2] + == m.sqr_scaled_dist_to_site[site2, site1] + ) @m.Disjunct(m.unique_site_pairs) def pair_inactive(disj, site1, site2): disj.site1_inactive = Constraint( - expr=m.site_active[site1].binary_indicator_var == 0) + expr=m.site_active[site1].binary_indicator_var == 0 + ) disj.site2_inactive = Constraint( - expr=m.site_active[site2].binary_indicator_var == 0) + expr=m.site_active[site2].binary_indicator_var == 0 + ) disj.no_module_transfer = Constraint( - expr=sum(m.modules_transferred[site1, site2, mo] - for mo in m.months) == 0) + expr=sum(m.modules_transferred[site1, site2, mo] for mo in m.months) == 0 + ) @m.Disjunction(m.unique_site_pairs) def site_pair_active_or_not(m, site1, site2): @@ -261,61 +298,82 @@ def site_pair_active_or_not(m, site1, site2): @m.Constraint(m.markets, m.months) def demand_satisfaction(m, mkt, mo): return m.market_demand[mkt, mo] <= sum( - m.shipments_to_mkt[site, mkt, mo] for site in m.modular_sites) + m.shipments_to_mkt[site, mkt, mo] for site in m.modular_sites + ) @m.Disjunct(m.modular_sites, m.markets) def product_route_active(disj, site, mkt): disj.site_active = Constraint( - expr=m.site_active[site].binary_indicator_var == 1) + expr=m.site_active[site].binary_indicator_var == 1 + ) @disj.Constraint() def market_distance_calculation(site_disj): - return m.dist_to_mkt[site, mkt] == sqrt( - m.sqr_scaled_dist_to_mkt[site, mkt]) * 150 + return ( + m.dist_to_mkt[site, mkt] + == sqrt(m.sqr_scaled_dist_to_mkt[site, mkt]) * 150 + ) @disj.Constraint() def market_sqr_distance_calc(site_disj): return m.sqr_scaled_dist_to_mkt[site, mkt] == ( - (m.site_x[site] / 150 - m.mkt_x[mkt] / 150)**2 + - (m.site_y[site] / 150 - m.mkt_y[mkt] / 150)**2) + (m.site_x[site] / 150 - m.mkt_x[mkt] / 150) ** 2 + + (m.site_y[site] / 150 - m.mkt_y[mkt] / 150) ** 2 + ) @m.Disjunct(m.modular_sites, m.markets) def product_route_inactive(disj, site, mkt): disj.no_shipments = Constraint( - expr=sum(m.shipments_to_mkt[site, mkt, mo] - for mo in m.months) == 0) + expr=sum(m.shipments_to_mkt[site, mkt, mo] for mo in m.months) == 0 + ) @m.Disjunction(m.modular_sites, m.markets) def product_route_active_or_not(m, site, mkt): - return [m.product_route_active[site, mkt], - m.product_route_inactive[site, mkt]] + return [m.product_route_active[site, mkt], m.product_route_inactive[site, mkt]] m.variable_shipment_cost = Expression( expr=sum( - m.shipments_to_mkt[site, mkt, mo] * m.dist_to_mkt[site, mkt] * - m.transport_cost[mo] - for site in m.modular_sites for mkt in m.markets - for mo in m.months)) + m.shipments_to_mkt[site, mkt, mo] + * m.dist_to_mkt[site, mkt] + * m.transport_cost[mo] + for site in m.modular_sites + for mkt in m.markets + for mo in m.months + ) + ) m.fixed_shipment_cost = Expression( - expr=sum(m.product_route_active[site, mkt].binary_indicator_var * - m.route_fixed_cost - for site in m.modular_sites for mkt in m.markets)) + expr=sum( + m.product_route_active[site, mkt].binary_indicator_var * m.route_fixed_cost + for site in m.modular_sites + for mkt in m.markets + ) + ) m.module_purchase_cost = Expression( - expr=sum(m.modules_added[site, mo] * m.modular_unit_cost[mo] - for site in m.modular_sites for mo in m.months)) + expr=sum( + m.modules_added[site, mo] * m.modular_unit_cost[mo] + for site in m.modular_sites + for mo in m.months + ) + ) - m.module_transfer_cost = Expression(expr=sum( - m.modules_transferred[site1, site2, mo] - * m.dist_to_site[site1, site2] * m.modular_transport_cost[mo] - for site1, site2 in m.site_pairs for mo in m.months)) + m.module_transfer_cost = Expression( + expr=sum( + m.modules_transferred[site1, site2, mo] + * m.dist_to_site[site1, site2] + * m.modular_transport_cost[mo] + for site1, site2 in m.site_pairs + for mo in m.months + ) + ) m.total_cost = Objective( expr=m.variable_shipment_cost + m.fixed_shipment_cost + m.module_purchase_cost - + m.module_transfer_cost) + + m.module_transfer_cost + ) return m @@ -343,8 +401,9 @@ def product_route_active_or_not(m, site, mkt): # 'option reslim=30;']}}) TransformationFactory('gdp.bigm').apply_to(m, bigM=10000) # TransformationFactory('gdp.chull').apply_to(m) - res = SolverFactory('gams').solve(m, tee=True, io_options={ - 'add_options': ['option reslim = 300;']}) + res = SolverFactory('gams').solve( + m, tee=True, io_options={'add_options': ['option reslim = 300;']} + ) # SolverFactory('gams').solve( # m, tee=True, # io_options={ @@ -358,13 +417,13 @@ def record_generator(): for mo in m.months: yield ( (mo,) - + tuple(m.num_modules[site, mo].value - for site in m.modular_sites) - + tuple(m.production[site, mo].value - for site in m.modular_sites) - + tuple(m.shipments_to_mkt[site, mkt, mo].value - for site in m.modular_sites - for mkt in m.markets) + + tuple(m.num_modules[site, mo].value for site in m.modular_sites) + + tuple(m.production[site, mo].value for site in m.modular_sites) + + tuple( + m.shipments_to_mkt[site, mkt, mo].value + for site in m.modular_sites + for mkt in m.markets + ) ) df = pd.DataFrame.from_records( @@ -372,8 +431,12 @@ def record_generator(): columns=("Month",) + tuple("Num Site%s" % site for site in m.modular_sites) + tuple("Prod Site%s" % site for site in m.modular_sites) - + tuple("Ship Site%s to Mkt%s" % (site, mkt) - for site in m.modular_sites for mkt in m.markets)) + + tuple( + "Ship Site%s to Mkt%s" % (site, mkt) + for site in m.modular_sites + for mkt in m.markets + ), + ) df.to_excel("multiple_modular_config.xlsx") print("Total cost: %s" % value(m.total_cost.expr)) print(" Variable ship cost: %s" % value(m.variable_shipment_cost)) @@ -381,40 +444,69 @@ def record_generator(): print(" Module buy cost: %s" % value(m.module_purchase_cost)) print(" Module xfer cost: %s" % value(m.module_transfer_cost)) for site in m.modular_sites: - print("Site {:1.0f} at ({:3.0f}, {:3.0f})".format( - site, m.site_x[site].value, m.site_y[site].value)) - print(" Supplies markets {}".format(tuple( - mkt for mkt in m.markets - if m.product_route_active[site, - mkt].binary_indicator_var.value == 1))) - + print( + "Site {:1.0f} at ({:3.0f}, {:3.0f})".format( + site, m.site_x[site].value, m.site_y[site].value + ) + ) + print( + " Supplies markets {}".format( + tuple( + mkt + for mkt in m.markets + if m.product_route_active[site, mkt].binary_indicator_var.value == 1 + ) + ) + ) if res.solver.termination_condition is not TerminationCondition.optimal: exit() - plt.plot([x.value for x in m.site_x.values()], - [y.value for y in m.site_y.values()], 'k.', markersize=12) - plt.plot([x for x in m.mkt_x.values()], - [y for y in m.mkt_y.values()], 'bo', markersize=12) + plt.plot( + [x.value for x in m.site_x.values()], + [y.value for y in m.site_y.values()], + 'k.', + markersize=12, + ) + plt.plot( + [x for x in m.mkt_x.values()], + [y for y in m.mkt_y.values()], + 'bo', + markersize=12, + ) for mkt in m.markets: - plt.annotate('mkt%s' % mkt, (m.mkt_x[mkt], m.mkt_y[mkt]), - (m.mkt_x[mkt] + 2, m.mkt_y[mkt] + 0)) + plt.annotate( + 'mkt%s' % mkt, + (m.mkt_x[mkt], m.mkt_y[mkt]), + (m.mkt_x[mkt] + 2, m.mkt_y[mkt] + 0), + ) for site in m.modular_sites: plt.annotate( - 'site%s' % site, (m.site_x[site].value, m.site_y[site].value), - (m.site_x[site].value + 2, m.site_y[site].value + 0)) + 'site%s' % site, + (m.site_x[site].value, m.site_y[site].value), + (m.site_x[site].value + 2, m.site_y[site].value + 0), + ) for site, mkt in m.modular_sites * m.markets: if m.product_route_active[site, mkt].binary_indicator_var.value == 1: - plt.arrow(m.site_x[site].value, m.site_y[site].value, - m.mkt_x[mkt] - m.site_x[site].value, - m.mkt_y[mkt] - m.site_y[site].value, - width=0.8, length_includes_head=True, color='r') + plt.arrow( + m.site_x[site].value, + m.site_y[site].value, + m.mkt_x[mkt] - m.site_x[site].value, + m.mkt_y[mkt] - m.site_y[site].value, + width=0.8, + length_includes_head=True, + color='r', + ) for site1, site2 in m.site_pairs: - if sum(m.modules_transferred[site1, site2, mo].value - for mo in m.months) > 0: - plt.arrow(m.site_x[site1].value, m.site_y[site1].value, - m.site_x[site2].value - m.site_x[site1].value, - m.site_y[site2].value - m.site_y[site1].value, - width=0.9, length_includes_head=True, - linestyle='dotted', color='k') + if sum(m.modules_transferred[site1, site2, mo].value for mo in m.months) > 0: + plt.arrow( + m.site_x[site1].value, + m.site_y[site1].value, + m.site_x[site2].value - m.site_x[site1].value, + m.site_y[site2].value - m.site_y[site1].value, + width=0.9, + length_includes_head=True, + linestyle='dotted', + color='k', + ) plt.show() diff --git a/gdplib/modprodnet/model.py b/gdplib/modprodnet/model.py index d5f30b0..562556d 100644 --- a/gdplib/modprodnet/model.py +++ b/gdplib/modprodnet/model.py @@ -4,8 +4,19 @@ import pandas as pd from pyomo.environ import ( - ConcreteModel, Constraint, Integers, maximize, Objective, Param, RangeSet, SolverFactory, summation, - TransformationFactory, value, Var, ) + ConcreteModel, + Constraint, + Integers, + maximize, + Objective, + Param, + RangeSet, + SolverFactory, + summation, + TransformationFactory, + value, + Var, +) from pyomo.gdp import Disjunct, Disjunction @@ -28,17 +39,21 @@ def production_value(m, mo): return (7 / 12) * m.discount_factor[mo] xls_data = pd.read_excel( - os.path.join(os.path.dirname(__file__), "market_size.xlsx"), sheet_name="single_market", index_col=0) + os.path.join(os.path.dirname(__file__), "market_size.xlsx"), + sheet_name="single_market", + index_col=0, + ) @m.Param(m.months) def market_demand(m, mo): return float(xls_data[case][mo]) - m.conv_size = Var(bounds=(25, 350), initialize=25, - doc="Size of conventional plant") + m.conv_size = Var(bounds=(25, 350), initialize=25, doc="Size of conventional plant") m.conv_base_cost = Param(initialize=1000, doc="Cost for size 25") m.conv_exponent = Param(initialize=0.6) - m.conv_cost = Var(bounds=(0, value(m.conv_base_cost * (m.conv_size.ub / 25) ** m.conv_exponent))) + m.conv_cost = Var( + bounds=(0, value(m.conv_base_cost * (m.conv_size.ub / 25) ** m.conv_exponent)) + ) m.module_base_cost = Param(initialize=1000, doc="Cost for size 25") m.production = Var(m.months, bounds=(0, 350)) @@ -68,12 +83,22 @@ def module_buy_cost(m, mo): @m.Expression(m.months) def module_sell_value(m, mo): - return m.module_base_cost * m.discount_factor[mo] * m.module_salvage_value * m.modules_sold[mo] + return ( + m.module_base_cost + * m.discount_factor[mo] + * m.module_salvage_value + * m.modules_sold[mo] + ) @m.Expression() def module_final_salvage(m): mo = max(m.months) - return m.module_base_cost * m.discount_factor[mo] * m.module_salvage_value * m.num_modules[mo] + return ( + m.module_base_cost + * m.discount_factor[mo] + * m.module_salvage_value + * m.num_modules[mo] + ) m.profit = Objective( expr=sum(m.revenue[:]) @@ -82,7 +107,8 @@ def module_final_salvage(m): - summation(m.module_buy_cost) + summation(m.module_sell_value) + m.module_final_salvage, - sense=maximize) + sense=maximize, + ) _build_conventional_disjunct(m) _build_modular_disjunct(m) @@ -95,8 +121,8 @@ def _build_conventional_disjunct(m): m.conventional = Disjunct() m.conventional.cost_calc = Constraint( - expr=m.conv_cost == ( - m.conv_base_cost * (m.conv_size / 25) ** m.conv_exponent)) + expr=m.conv_cost == (m.conv_base_cost * (m.conv_size / 25) ** m.conv_exponent) + ) @m.conventional.Constraint(m.months) def conv_production_limit(conv_disj, mo): @@ -107,7 +133,10 @@ def conv_production_limit(conv_disj, mo): @m.conventional.Constraint() def no_modules(conv_disj): - return sum(m.num_modules[:]) + sum(m.modules_purchased[:]) + sum(m.modules_sold[:]) == 0 + return ( + sum(m.num_modules[:]) + sum(m.modules_purchased[:]) + sum(m.modules_sold[:]) + == 0 + ) def _build_modular_disjunct(m): @@ -116,7 +145,11 @@ def _build_modular_disjunct(m): @m.modular.Constraint(m.months) def module_balance(disj, mo): existing_modules = 0 if mo == 0 else m.num_modules[mo - 1] - new_modules = 0 if mo < m.modular_setup_time else m.modules_purchased[mo - m.modular_setup_time] + new_modules = ( + 0 + if mo < m.modular_setup_time + else m.modules_purchased[mo - m.modular_setup_time] + ) sold_modules = m.modules_sold[mo] return m.num_modules[mo] == existing_modules + new_modules - sold_modules @@ -126,15 +159,22 @@ def modular_production_limit(mod_disj, mo): def display_conventional(m, writer, sheet_name): - df = pd.DataFrame( - list([ - mo, - m.production[mo].value, - m.market_demand[mo], - m.conv_size.value if mo >= m.conv_setup_time else 0 - ] for mo in m.months), - columns=("Month", "Production", "Demand", "Capacity") - ).set_index('Month').round(2) + df = ( + pd.DataFrame( + list( + [ + mo, + m.production[mo].value, + m.market_demand[mo], + m.conv_size.value if mo >= m.conv_setup_time else 0, + ] + for mo in m.months + ), + columns=("Month", "Production", "Demand", "Capacity"), + ) + .set_index('Month') + .round(2) + ) df.to_excel(writer, sheet_name) print('Conventional Profit', round(value(m.profit))) print('Conventional Revenue', round(value(sum(m.revenue[:])))) @@ -145,26 +185,46 @@ def display_conventional(m, writer, sheet_name): def display_modular(m, writer, sheet_name): - df = pd.DataFrame( - list([ - mo, - m.production[mo].value, - m.market_demand[mo], - m.num_modules[mo].value * 25, - m.num_modules[mo].value, - m.modules_purchased[mo].value, - m.modules_sold[mo].value] for mo in m.months - ), - columns=("Month", "Production", "Demand", "Capacity", - "Num Modules", "Add Modules", "Sold Modules") - ).set_index('Month').round(2) + df = ( + pd.DataFrame( + list( + [ + mo, + m.production[mo].value, + m.market_demand[mo], + m.num_modules[mo].value * 25, + m.num_modules[mo].value, + m.modules_purchased[mo].value, + m.modules_sold[mo].value, + ] + for mo in m.months + ), + columns=( + "Month", + "Production", + "Demand", + "Capacity", + "Num Modules", + "Add Modules", + "Sold Modules", + ), + ) + .set_index('Month') + .round(2) + ) df.to_excel(writer, sheet_name) print('Modular Profit', round(value(m.profit))) print('Modular Revenue', round(value(sum(m.revenue[:])))) - print('Modular Revenue before conventional startup', round(value(sum(m.revenue[mo] for mo in m.months if mo < 12)))) + print( + 'Modular Revenue before conventional startup', + round(value(sum(m.revenue[mo] for mo in m.months if mo < 12))), + ) print('Modular Build Cost', round(value(sum(m.module_buy_cost[:])))) print('Modules Purchased', round(value(sum(m.modules_purchased[:])))) - print('Modular Nondiscount Cost', round(value(m.module_base_cost * sum(m.modules_purchased[:])))) + print( + 'Modular Nondiscount Cost', + round(value(m.module_base_cost * sum(m.modules_purchased[:]))), + ) print('Modular Sale Credit', round(value(sum(m.module_sell_value[:])))) print('Modular Final Salvage Credit', round(value(m.module_final_salvage))) print() diff --git a/gdplib/modprodnet/quarter_distributed.py b/gdplib/modprodnet/quarter_distributed.py index f2bae2e..7746881 100644 --- a/gdplib/modprodnet/quarter_distributed.py +++ b/gdplib/modprodnet/quarter_distributed.py @@ -5,10 +5,23 @@ import matplotlib.pyplot as plt import pandas as pd -from pyomo.environ import (ConcreteModel, Constraint, Expression, Integers, - Objective, Param, RangeSet, Set, SolverFactory, - Suffix, TerminationCondition, TransformationFactory, - Var, sqrt, value) +from pyomo.environ import ( + ConcreteModel, + Constraint, + Expression, + Integers, + Objective, + Param, + RangeSet, + Set, + SolverFactory, + Suffix, + TerminationCondition, + TransformationFactory, + Var, + sqrt, + value, +) def build_model(): @@ -20,7 +33,9 @@ def build_model(): xls_data = pd.read_excel( os.path.join(os.path.dirname(__file__), "quarter_multiple_market_size.xlsx"), - sheet_name=["demand", "locations"], index_col=0) + sheet_name=["demand", "locations"], + index_col=0, + ) @m.Param(m.markets, m.quarters) def market_demand(m, mkt, qtr): @@ -32,20 +47,22 @@ def transport_cost(m, qtr): m.route_fixed_cost = Param( initialize=100, - doc="Cost of establishing a route from a modular site to a market") + doc="Cost of establishing a route from a modular site to a market", + ) m.conv_x = Var(bounds=(0, 300), doc="x-coordinate of centralized plant.") m.conv_y = Var(bounds=(0, 300), doc="y-coordinate of centralized plant.") - m.conv_size = Var(bounds=(10, 700), initialize=10, - doc="Size of conventional plant.") + m.conv_size = Var( + bounds=(10, 700), initialize=10, doc="Size of conventional plant." + ) m.conv_cost = Var() m.conv_base_cost = Param(initialize=1000, doc="Cost for size 60") m.conv_exponent = Param(initialize=0.6) m.cost_calc = Constraint( - expr=m.conv_cost == ( - m.conv_base_cost * (m.conv_size / 60) ** m.conv_exponent)) + expr=m.conv_cost == (m.conv_base_cost * (m.conv_size / 60) ** m.conv_exponent) + ) @m.Param(m.markets) def mkt_x(m, mkt): @@ -59,17 +76,23 @@ def mkt_y(m, mkt): @m.Constraint(m.markets) def distance_calculation(m, mkt): - return m.dist_to_mkt[mkt] == sqrt( - (m.conv_x / 150 - m.mkt_x[mkt] / 150)**2 + - (m.conv_y / 150 - m.mkt_y[mkt] / 150)**2) * 150 + return ( + m.dist_to_mkt[mkt] + == sqrt( + (m.conv_x / 150 - m.mkt_x[mkt] / 150) ** 2 + + (m.conv_y / 150 - m.mkt_y[mkt] / 150) ** 2 + ) + * 150 + ) m.shipments_to_mkt = Var(m.markets, m.quarters, bounds=(0, 400)) m.production = Var(m.quarters, bounds=(0, 1500)) @m.Constraint(m.quarters) def production_satisfaction(m, qtr): - return m.production[qtr] == sum(m.shipments_to_mkt[mkt, qtr] - for mkt in m.markets) + return m.production[qtr] == sum( + m.shipments_to_mkt[mkt, qtr] for mkt in m.markets + ) @m.Constraint(m.quarters) def size_requirement(m, qtr): @@ -84,12 +107,15 @@ def demand_satisfaction(m, mkt, qtr): m.variable_shipment_cost = Expression( expr=sum( - m.shipments_to_mkt[mkt, qtr] * m.dist_to_mkt[mkt] * - m.transport_cost[qtr] - for mkt in m.markets for qtr in m.quarters)) + m.shipments_to_mkt[mkt, qtr] * m.dist_to_mkt[mkt] * m.transport_cost[qtr] + for mkt in m.markets + for qtr in m.quarters + ) + ) m.total_cost = Objective( - expr=m.variable_shipment_cost + 5 * m.route_fixed_cost + m.conv_cost) + expr=m.variable_shipment_cost + 5 * m.route_fixed_cost + m.conv_cost + ) return m @@ -104,14 +130,15 @@ def build_modular_model(): m.markets = RangeSet(5) m.modular_sites = RangeSet(3) m.site_pairs = Set( - initialize=m.modular_sites * m.modular_sites, - filter=lambda _, x, y: not x == y) - m.unique_site_pairs = Set( - initialize=m.site_pairs, filter=lambda _, x, y: x < y) + initialize=m.modular_sites * m.modular_sites, filter=lambda _, x, y: not x == y + ) + m.unique_site_pairs = Set(initialize=m.site_pairs, filter=lambda _, x, y: x < y) xls_data = pd.read_excel( os.path.join(os.path.dirname(__file__), "quarter_multiple_market_size.xlsx"), - sheet_name=["demand", "locations"], index_col=0) + sheet_name=["demand", "locations"], + index_col=0, + ) @m.Param(m.markets, m.quarters) def market_demand(m, mkt, qtr): @@ -123,11 +150,13 @@ def transport_cost(m, qtr): m.route_fixed_cost = Param( initialize=100, - doc="Cost of establishing a route from a modular site to a market") + doc="Cost of establishing a route from a modular site to a market", + ) @m.Param(m.quarters, doc="Cost of transporting a module one mile") def modular_transport_cost(m, qtr): return 1 * (1 + m.discount_rate / 4) ** (-qtr / 4) + m.modular_base_cost = Param(initialize=1000, doc="Cost for size 60") @m.Param(m.quarters) @@ -142,32 +171,27 @@ def mkt_x(m, mkt): def mkt_y(m, mkt): return float(xls_data["locations"]["y"]["market%s" % mkt]) - m.site_x = Var( - m.modular_sites, bounds=(0, 300), initialize={ - 1: 50, 2: 225, 3: 250}) - m.site_y = Var( - m.modular_sites, bounds=(0, 300), initialize={ - 1: 300, 2: 275, 3: 50}) + m.site_x = Var(m.modular_sites, bounds=(0, 300), initialize={1: 50, 2: 225, 3: 250}) + m.site_y = Var(m.modular_sites, bounds=(0, 300), initialize={1: 300, 2: 275, 3: 50}) - m.num_modules = Var( - m.modular_sites, m.quarters, domain=Integers, bounds=(0, 12)) + m.num_modules = Var(m.modular_sites, m.quarters, domain=Integers, bounds=(0, 12)) m.modules_transferred = Var( - m.site_pairs, m.quarters, - domain=Integers, bounds=(0, 12), - doc="Number of modules moved from one site to another in a quarter.") + m.site_pairs, + m.quarters, + domain=Integers, + bounds=(0, 12), + doc="Number of modules moved from one site to another in a quarter.", + ) # m.modules_transferred[...].fix(0) - m.modules_added = Var( - m.modular_sites, m.quarters, domain=Integers, bounds=(0, 12)) + m.modules_added = Var(m.modular_sites, m.quarters, domain=Integers, bounds=(0, 12)) - m.dist_to_mkt = Var( - m.modular_sites, m.markets, bounds=(0, sqrt(300**2 + 300**2))) + m.dist_to_mkt = Var(m.modular_sites, m.markets, bounds=(0, sqrt(300**2 + 300**2))) m.sqr_scaled_dist_to_mkt = Var( - m.modular_sites, m.markets, bounds=(0.001, 8), initialize=0.001) + m.modular_sites, m.markets, bounds=(0.001, 8), initialize=0.001 + ) m.dist_to_site = Var(m.site_pairs, bounds=(0, sqrt(300**2 + 300**2))) - m.sqr_scaled_dist_to_site = Var( - m.site_pairs, bounds=(0.001, 8), initialize=0.001) - m.shipments_to_mkt = Var( - m.modular_sites, m.markets, m.quarters, bounds=(0, 300)) + m.sqr_scaled_dist_to_site = Var(m.site_pairs, bounds=(0.001, 8), initialize=0.001) + m.shipments_to_mkt = Var(m.modular_sites, m.markets, m.quarters, bounds=(0, 300)) m.production = Var(m.modular_sites, m.quarters, bounds=(0, 1500)) @m.Disjunct(m.modular_sites) @@ -175,7 +199,8 @@ def site_active(disj, site): @disj.Constraint(m.quarters) def production_satisfaction(site_disj, qtr): return m.production[site, qtr] == sum( - m.shipments_to_mkt[site, mkt, qtr] for mkt in m.markets) + m.shipments_to_mkt[site, mkt, qtr] for mkt in m.markets + ) @disj.Constraint(m.quarters) def production_limit(site_disj, qtr): @@ -184,34 +209,49 @@ def production_limit(site_disj, qtr): @disj.Constraint(m.quarters) def module_balance(site_disj, qtr): existing_modules = m.num_modules[site, qtr - 1] if qtr >= 1 else 0 - new_modules = (m.modules_added[site, qtr - m.modular_setup_time] - if qtr > m.modular_setup_time else 0) + new_modules = ( + m.modules_added[site, qtr - m.modular_setup_time] + if qtr > m.modular_setup_time + else 0 + ) xfrd_in_modules = sum( - m.modules_transferred[from_site, - site, qtr - m.modular_move_time] + m.modules_transferred[from_site, site, qtr - m.modular_move_time] for from_site in m.modular_sites - if (not from_site == site) and qtr > m.modular_move_time) + if (not from_site == site) and qtr > m.modular_move_time + ) xfrd_out_modules = sum( m.modules_transferred[site, to_site, qtr] - for to_site in m.modular_sites if not to_site == site) + for to_site in m.modular_sites + if not to_site == site + ) return m.num_modules[site, qtr] == ( - new_modules + xfrd_in_modules - xfrd_out_modules + - existing_modules) + new_modules + xfrd_in_modules - xfrd_out_modules + existing_modules + ) @m.Disjunct(m.modular_sites) def site_inactive(disj, site): disj.no_modules = Constraint( - expr=sum(m.num_modules[site, qtr] for qtr in m.quarters) == 0) + expr=sum(m.num_modules[site, qtr] for qtr in m.quarters) == 0 + ) disj.no_module_transfer = Constraint( - expr=sum(m.modules_transferred[site1, site2, qtr] - for site1, site2 in m.site_pairs - for qtr in m.quarters - if site1 == site or site2 == site) == 0) + expr=sum( + m.modules_transferred[site1, site2, qtr] + for site1, site2 in m.site_pairs + for qtr in m.quarters + if site1 == site or site2 == site + ) + == 0 + ) disj.no_shipments = Constraint( - expr=sum(m.shipments_to_mkt[site, mkt, qtr] - for mkt in m.markets for qtr in m.quarters) == 0) + expr=sum( + m.shipments_to_mkt[site, mkt, qtr] + for mkt in m.markets + for qtr in m.quarters + ) + == 0 + ) @m.Disjunction(m.modular_sites) def site_active_or_not(m, site): @@ -220,40 +260,53 @@ def site_active_or_not(m, site): @m.Constraint(m.modular_sites, doc="Symmetry breaking for site activation") def site_active_ordering(m, site): if site + 1 <= max(m.modular_sites): - return (m.site_active[site].binary_indicator_var >= - m.site_active[site + 1].binary_indicator_var) + return ( + m.site_active[site].binary_indicator_var + >= m.site_active[site + 1].binary_indicator_var + ) else: return Constraint.NoConstraint @m.Disjunct(m.unique_site_pairs) def pair_active(disj, site1, site2): disj.site1_active = Constraint( - expr=m.site_active[site1].binary_indicator_var == 1) + expr=m.site_active[site1].binary_indicator_var == 1 + ) disj.site2_active = Constraint( - expr=m.site_active[site2].binary_indicator_var == 1) + expr=m.site_active[site2].binary_indicator_var == 1 + ) disj.site_distance_calc = Constraint( - expr=m.dist_to_site[site1, site2] == sqrt( - m.sqr_scaled_dist_to_site[site1, site2]) * 150) + expr=m.dist_to_site[site1, site2] + == sqrt(m.sqr_scaled_dist_to_site[site1, site2]) * 150 + ) disj.site_distance_symmetry = Constraint( - expr=m.dist_to_site[site1, site2] == m.dist_to_site[site2, site1]) + expr=m.dist_to_site[site1, site2] == m.dist_to_site[site2, site1] + ) disj.site_sqr_distance_calc = Constraint( - expr=m.sqr_scaled_dist_to_site[site1, site2] == ( - (m.site_x[site1] / 150 - m.site_x[site2] / 150)**2 + - (m.site_y[site1] / 150 - m.site_y[site2] / 150)**2)) + expr=m.sqr_scaled_dist_to_site[site1, site2] + == ( + (m.site_x[site1] / 150 - m.site_x[site2] / 150) ** 2 + + (m.site_y[site1] / 150 - m.site_y[site2] / 150) ** 2 + ) + ) disj.site_sqr_distance_symmetry = Constraint( - expr=m.sqr_scaled_dist_to_site[site1, site2] == - m.sqr_scaled_dist_to_site[site2, site1]) + expr=m.sqr_scaled_dist_to_site[site1, site2] + == m.sqr_scaled_dist_to_site[site2, site1] + ) @m.Disjunct(m.unique_site_pairs) def pair_inactive(disj, site1, site2): disj.site1_inactive = Constraint( - expr=m.site_active[site1].binary_indicator_var == 0) + expr=m.site_active[site1].binary_indicator_var == 0 + ) disj.site2_inactive = Constraint( - expr=m.site_active[site2].binary_indicator_var == 0) + expr=m.site_active[site2].binary_indicator_var == 0 + ) disj.no_module_transfer = Constraint( - expr=sum(m.modules_transferred[site1, site2, qtr] - for qtr in m.quarters) == 0) + expr=sum(m.modules_transferred[site1, site2, qtr] for qtr in m.quarters) + == 0 + ) @m.Disjunction(m.unique_site_pairs) def site_pair_active_or_not(m, site1, site2): @@ -262,61 +315,82 @@ def site_pair_active_or_not(m, site1, site2): @m.Constraint(m.markets, m.quarters) def demand_satisfaction(m, mkt, qtr): return m.market_demand[mkt, qtr] <= sum( - m.shipments_to_mkt[site, mkt, qtr] for site in m.modular_sites) + m.shipments_to_mkt[site, mkt, qtr] for site in m.modular_sites + ) @m.Disjunct(m.modular_sites, m.markets) def product_route_active(disj, site, mkt): disj.site_active = Constraint( - expr=m.site_active[site].binary_indicator_var == 1) + expr=m.site_active[site].binary_indicator_var == 1 + ) @disj.Constraint() def market_distance_calculation(site_disj): - return m.dist_to_mkt[site, mkt] == sqrt( - m.sqr_scaled_dist_to_mkt[site, mkt]) * 150 + return ( + m.dist_to_mkt[site, mkt] + == sqrt(m.sqr_scaled_dist_to_mkt[site, mkt]) * 150 + ) @disj.Constraint() def market_sqr_distance_calc(site_disj): return m.sqr_scaled_dist_to_mkt[site, mkt] == ( - (m.site_x[site] / 150 - m.mkt_x[mkt] / 150)**2 + - (m.site_y[site] / 150 - m.mkt_y[mkt] / 150)**2) + (m.site_x[site] / 150 - m.mkt_x[mkt] / 150) ** 2 + + (m.site_y[site] / 150 - m.mkt_y[mkt] / 150) ** 2 + ) @m.Disjunct(m.modular_sites, m.markets) def product_route_inactive(disj, site, mkt): disj.no_shipments = Constraint( - expr=sum(m.shipments_to_mkt[site, mkt, qtr] - for qtr in m.quarters) == 0) + expr=sum(m.shipments_to_mkt[site, mkt, qtr] for qtr in m.quarters) == 0 + ) @m.Disjunction(m.modular_sites, m.markets) def product_route_active_or_not(m, site, mkt): - return [m.product_route_active[site, mkt], - m.product_route_inactive[site, mkt]] + return [m.product_route_active[site, mkt], m.product_route_inactive[site, mkt]] m.variable_shipment_cost = Expression( expr=sum( - m.shipments_to_mkt[site, mkt, qtr] * m.dist_to_mkt[site, mkt] * - m.transport_cost[qtr] - for site in m.modular_sites for mkt in m.markets - for qtr in m.quarters)) + m.shipments_to_mkt[site, mkt, qtr] + * m.dist_to_mkt[site, mkt] + * m.transport_cost[qtr] + for site in m.modular_sites + for mkt in m.markets + for qtr in m.quarters + ) + ) m.fixed_shipment_cost = Expression( - expr=sum(m.product_route_active[site, mkt].binary_indicator_var * - m.route_fixed_cost - for site in m.modular_sites for mkt in m.markets)) + expr=sum( + m.product_route_active[site, mkt].binary_indicator_var * m.route_fixed_cost + for site in m.modular_sites + for mkt in m.markets + ) + ) m.module_purchase_cost = Expression( - expr=sum(m.modules_added[site, qtr] * m.modular_unit_cost[qtr] - for site in m.modular_sites for qtr in m.quarters)) + expr=sum( + m.modules_added[site, qtr] * m.modular_unit_cost[qtr] + for site in m.modular_sites + for qtr in m.quarters + ) + ) - m.module_transfer_cost = Expression(expr=sum( - m.modules_transferred[site1, site2, qtr] - * m.dist_to_site[site1, site2] * m.modular_transport_cost[qtr] - for site1, site2 in m.site_pairs for qtr in m.quarters)) + m.module_transfer_cost = Expression( + expr=sum( + m.modules_transferred[site1, site2, qtr] + * m.dist_to_site[site1, site2] + * m.modular_transport_cost[qtr] + for site1, site2 in m.site_pairs + for qtr in m.quarters + ) + ) m.total_cost = Objective( expr=m.variable_shipment_cost + m.fixed_shipment_cost + m.module_purchase_cost - + m.module_transfer_cost) + + m.module_transfer_cost + ) return m @@ -347,10 +421,10 @@ def product_route_active_or_not(m, site, mkt): # res = SolverFactory('gams').solve(m, tee=True, io_options={ # 'add_options': ['option reslim = 300;']}) res = SolverFactory('gams').solve( - m, tee=True, - io_options={ - 'solver': 'baron', - 'add_options': ['option reslim = 600;']}) + m, + tee=True, + io_options={'solver': 'baron', 'add_options': ['option reslim = 600;']}, + ) # from pyomo.util.infeasible import log_infeasible_constraints # log_infeasible_constraints(m) @@ -358,17 +432,18 @@ def record_generator(): for qtr in m.quarters: yield ( (qtr,) - + tuple(m.num_modules[site, qtr].value - for site in m.modular_sites) - + tuple(m.production[site, qtr].value - for site in m.modular_sites) - + tuple(m.shipments_to_mkt[site, mkt, qtr].value - for site in m.modular_sites - for mkt in m.markets) - + tuple(m.modules_added[site, qtr].value - for site in m.modular_sites) - + tuple(m.modules_transferred[site1, site2, qtr].value - for site1, site2 in m.site_pairs) + + tuple(m.num_modules[site, qtr].value for site in m.modular_sites) + + tuple(m.production[site, qtr].value for site in m.modular_sites) + + tuple( + m.shipments_to_mkt[site, mkt, qtr].value + for site in m.modular_sites + for mkt in m.markets + ) + + tuple(m.modules_added[site, qtr].value for site in m.modular_sites) + + tuple( + m.modules_transferred[site1, site2, qtr].value + for site1, site2 in m.site_pairs + ) ) df = pd.DataFrame.from_records( @@ -376,11 +451,14 @@ def record_generator(): columns=("Qtr",) + tuple("Num Site%s" % site for site in m.modular_sites) + tuple("Prod Site%s" % site for site in m.modular_sites) - + tuple("ShipSite%stoMkt%s" % (site, mkt) - for site in m.modular_sites for mkt in m.markets) + + tuple( + "ShipSite%stoMkt%s" % (site, mkt) + for site in m.modular_sites + for mkt in m.markets + ) + tuple("Add Site%s" % site for site in m.modular_sites) - + tuple("Xfer Site%s to %s" % (site1, site2) - for site1, site2 in m.site_pairs)) + + tuple("Xfer Site%s to %s" % (site1, site2) for site1, site2 in m.site_pairs), + ) df.to_excel("quarter_multiple_modular_config.xlsx") print("Total cost: %s" % value(m.total_cost.expr)) print(" Variable ship cost: %s" % value(m.variable_shipment_cost)) @@ -388,39 +466,72 @@ def record_generator(): print(" Module buy cost: %s" % value(m.module_purchase_cost)) print(" Module xfer cost: %s" % value(m.module_transfer_cost)) for site in m.modular_sites: - print("Site {:1.0f} at ({:3.0f}, {:3.0f})".format( - site, m.site_x[site].value, m.site_y[site].value)) - print(" Supplies markets {}".format(tuple( - mkt for mkt in m.markets - if m.product_route_active[site, - mkt].binary_indicator_var.value == 1))) + print( + "Site {:1.0f} at ({:3.0f}, {:3.0f})".format( + site, m.site_x[site].value, m.site_y[site].value + ) + ) + print( + " Supplies markets {}".format( + tuple( + mkt + for mkt in m.markets + if m.product_route_active[site, mkt].binary_indicator_var.value == 1 + ) + ) + ) if res.solver.termination_condition is not TerminationCondition.optimal: exit() - plt.plot([x.value for x in m.site_x.values()], - [y.value for y in m.site_y.values()], 'k.', markersize=12) - plt.plot([x for x in m.mkt_x.values()], - [y for y in m.mkt_y.values()], 'bo', markersize=12) + plt.plot( + [x.value for x in m.site_x.values()], + [y.value for y in m.site_y.values()], + 'k.', + markersize=12, + ) + plt.plot( + [x for x in m.mkt_x.values()], + [y for y in m.mkt_y.values()], + 'bo', + markersize=12, + ) for mkt in m.markets: - plt.annotate('mkt%s' % mkt, (m.mkt_x[mkt], m.mkt_y[mkt]), - (m.mkt_x[mkt] + 2, m.mkt_y[mkt] + 0)) + plt.annotate( + 'mkt%s' % mkt, + (m.mkt_x[mkt], m.mkt_y[mkt]), + (m.mkt_x[mkt] + 2, m.mkt_y[mkt] + 0), + ) for site in m.modular_sites: plt.annotate( - 'site%s' % site, (m.site_x[site].value, m.site_y[site].value), - (m.site_x[site].value + 2, m.site_y[site].value + 0)) + 'site%s' % site, + (m.site_x[site].value, m.site_y[site].value), + (m.site_x[site].value + 2, m.site_y[site].value + 0), + ) for site, mkt in m.modular_sites * m.markets: if m.product_route_active[site, mkt].binary_indicator_var.value == 1: - plt.arrow(m.site_x[site].value, m.site_y[site].value, - m.mkt_x[mkt] - m.site_x[site].value, - m.mkt_y[mkt] - m.site_y[site].value, - width=0.8, length_includes_head=True, color='r') + plt.arrow( + m.site_x[site].value, + m.site_y[site].value, + m.mkt_x[mkt] - m.site_x[site].value, + m.mkt_y[mkt] - m.site_y[site].value, + width=0.8, + length_includes_head=True, + color='r', + ) for site1, site2 in m.site_pairs: - if sum(m.modules_transferred[site1, site2, qtr].value - for qtr in m.quarters) > 1E-6: - plt.arrow(m.site_x[site1].value, m.site_y[site1].value, - m.site_x[site2].value - m.site_x[site1].value, - m.site_y[site2].value - m.site_y[site1].value, - width=0.9, length_includes_head=True, - linestyle='dotted', color='k') + if ( + sum(m.modules_transferred[site1, site2, qtr].value for qtr in m.quarters) + > 1e-6 + ): + plt.arrow( + m.site_x[site1].value, + m.site_y[site1].value, + m.site_x[site2].value - m.site_x[site1].value, + m.site_y[site2].value - m.site_y[site1].value, + width=0.9, + length_includes_head=True, + linestyle='dotted', + color='k', + ) plt.show() diff --git a/gdplib/pyomo_examples/README.md b/gdplib/pyomo_examples/README.md deleted file mode 100644 index a00148f..0000000 --- a/gdplib/pyomo_examples/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# Models taken from Pyomo.GDP examples directory - -These models are among the oldest in the library. -They were primarily developed by Emma Savannah Johnson (esjohn@sandia.gov), and are simply reproduced here in importable form. diff --git a/gdplib/pyomo_examples/__init__.py b/gdplib/pyomo_examples/__init__.py deleted file mode 100644 index be668e9..0000000 --- a/gdplib/pyomo_examples/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -from .batch_processing import build_model as build_batch_processing_model -from .disease_model import build_model as build_disease_model -from .jobshop import build_small_concrete as build_jobshop_model -from .med_term_purchasing import build_concrete as build_med_term_purchasing_model - -__all__ = [ - 'build_batch_processing_model', - 'build_disease_model', - 'build_jobshop_model', - 'build_med_term_purchasing_model' -] diff --git a/gdplib/pyomo_examples/batch_processing.py b/gdplib/pyomo_examples/batch_processing.py deleted file mode 100644 index 3695e19..0000000 --- a/gdplib/pyomo_examples/batch_processing.py +++ /dev/null @@ -1,236 +0,0 @@ -from os.path import join - -from pyomo.common.fileutils import this_file_dir -from pyomo.environ import * -from pyomo.gdp import * - -'''Problem from http://www.minlp.org/library/problem/index.php?i=172&lib=GDP -We are minimizing the cost of a design of a plant with parallel processing units and storage tanks -in between. We decide the number and volume of units, and the volume and location of the storage -tanks. The problem is convexified and has a nonlinear objective and global constraints - -NOTE: When I refer to 'gams' in the comments, that is Batch101006_BM.gms for now. It's confusing -because the _opt file is different (It has hard-coded bigM parameters so that each constraint -has the "optimal" bigM).''' - - -def build_model(): - - model = AbstractModel() - - # TODO: it looks like they set a bigM for each j. Which I need to look up how to do... - model.BigM = Suffix(direction=Suffix.LOCAL) - model.BigM[None] = 1000 - - - ## Constants from GAMS - StorageTankSizeFactor = 2*5 # btw, I know 2*5 is 10... I don't know why it's written this way in GAMS? - StorageTankSizeFactorByProd = 3 - MinFlow = -log(10000) - VolumeLB = log(300) - VolumeUB = log(3500) - StorageTankSizeLB = log(100) - StorageTankSizeUB = log(15000) - UnitsInPhaseUB = log(6) - UnitsOutOfPhaseUB = log(6) - # TODO: YOU ARE HERE. YOU HAVEN'T ACTUALLY MADE THESE THE BOUNDS YET, NOR HAVE YOU FIGURED OUT WHOSE - # BOUNDS THEY ARE. AND THERE ARE MORE IN GAMS. - - - ########## - # Sets - ########## - - model.PRODUCTS = Set() - model.STAGES = Set(ordered=True) - model.PARALLELUNITS = Set(ordered=True) - - # TODO: this seems like an over-complicated way to accomplish this task... - def filter_out_last(model, j): - return j != model.STAGES.last() - model.STAGESExceptLast = Set(initialize=model.STAGES, filter=filter_out_last) - - - # TODO: these aren't in the formulation?? - #model.STORAGE_TANKS = Set() - - - ############### - # Parameters - ############### - - model.HorizonTime = Param() - model.Alpha1 = Param() - model.Alpha2 = Param() - model.Beta1 = Param() - model.Beta2 = Param() - - model.ProductionAmount = Param(model.PRODUCTS) - model.ProductSizeFactor = Param(model.PRODUCTS, model.STAGES) - model.ProcessingTime = Param(model.PRODUCTS, model.STAGES) - - # These are hard-coded in the GAMS file, hence the defaults - model.StorageTankSizeFactor = Param(model.STAGES, default=StorageTankSizeFactor) - model.StorageTankSizeFactorByProd = Param(model.PRODUCTS, model.STAGES, - default=StorageTankSizeFactorByProd) - - # TODO: bonmin wasn't happy and I think it might have something to do with this? - # or maybe issues with convexity or a lack thereof... I don't know yet. - # I made PRODUCTS ordered so I could do this... Is that bad? And it does index - # from 1, right? - def get_log_coeffs(model, k): - return log(model.PARALLELUNITS.ord(k)) - - model.LogCoeffs = Param(model.PARALLELUNITS, initialize=get_log_coeffs) - - # bounds - model.volumeLB = Param(model.STAGES, default=VolumeLB) - model.volumeUB = Param(model.STAGES, default=VolumeUB) - model.storageTankSizeLB = Param(model.STAGES, default=StorageTankSizeLB) - model.storageTankSizeUB = Param(model.STAGES, default=StorageTankSizeUB) - model.unitsInPhaseUB = Param(model.STAGES, default=UnitsInPhaseUB) - model.unitsOutOfPhaseUB = Param(model.STAGES, default=UnitsOutOfPhaseUB) - - - ################ - # Variables - ################ - - # TODO: right now these match the formulation. There are more in GAMS... - - # unit size of stage j - # model.volume = Var(model.STAGES) - # # TODO: GAMS has a batch size indexed just by products that isn't in the formulation... I'm going - # # to try to avoid it for the moment... - # # batch size of product i at stage j - # model.batchSize = Var(model.PRODUCTS, model.STAGES) - # # TODO: this is different in GAMS... They index by stages too? - # # cycle time of product i divided by batch size of product i - # model.cycleTime = Var(model.PRODUCTS) - # # number of units in parallel out-of-phase (or in phase) at stage j - # model.unitsOutOfPhase = Var(model.STAGES) - # model.unitsInPhase = Var(model.STAGES) - # # TODO: what are we going to do as a boundary condition here? For that last stage? - # # size of intermediate storage tank between stage j and j+1 - # model.storageTankSize = Var(model.STAGES) - - # variables for convexified problem - # TODO: I am beginning to think these are my only variables actually. - # GAMS never un-logs them, I don't think. And I think the GAMs ones - # must be the log ones. - def get_volume_bounds(model, j): - return (model.volumeLB[j], model.volumeUB[j]) - model.volume_log = Var(model.STAGES, bounds=get_volume_bounds) - model.batchSize_log = Var(model.PRODUCTS, model.STAGES) - model.cycleTime_log = Var(model.PRODUCTS) - def get_unitsOutOfPhase_bounds(model, j): - return (0, model.unitsOutOfPhaseUB[j]) - model.unitsOutOfPhase_log = Var(model.STAGES, bounds=get_unitsOutOfPhase_bounds) - def get_unitsInPhase_bounds(model, j): - return (0, model.unitsInPhaseUB[j]) - model.unitsInPhase_log = Var(model.STAGES, bounds=get_unitsInPhase_bounds) - def get_storageTankSize_bounds(model, j): - return (model.storageTankSizeLB[j], model.storageTankSizeUB[j]) - # TODO: these bounds make it infeasible... - model.storageTankSize_log = Var(model.STAGES, bounds=get_storageTankSize_bounds) - - # binary variables for deciding number of parallel units in and out of phase - model.outOfPhase = Var(model.STAGES, model.PARALLELUNITS, within=Binary) - model.inPhase = Var(model.STAGES, model.PARALLELUNITS, within=Binary) - - ############### - # Objective - ############### - - def get_cost_rule(model): - return model.Alpha1 * sum(exp(model.unitsInPhase_log[j] + model.unitsOutOfPhase_log[j] + \ - model.Beta1 * model.volume_log[j]) for j in model.STAGES) +\ - model.Alpha2 * sum(exp(model.Beta2 * model.storageTankSize_log[j]) for j in model.STAGESExceptLast) - model.min_cost = Objective(rule=get_cost_rule) - - - ############## - # Constraints - ############## - - def processing_capacity_rule(model, j, i): - return model.volume_log[j] >= log(model.ProductSizeFactor[i, j]) + model.batchSize_log[i, j] - \ - model.unitsInPhase_log[j] - model.processing_capacity = Constraint(model.STAGES, model.PRODUCTS, rule=processing_capacity_rule) - - def processing_time_rule(model, j, i): - return model.cycleTime_log[i] >= log(model.ProcessingTime[i, j]) - model.batchSize_log[i, j] - \ - model.unitsOutOfPhase_log[j] - model.processing_time = Constraint(model.STAGES, model.PRODUCTS, rule=processing_time_rule) - - def finish_in_time_rule(model): - return model.HorizonTime >= sum(model.ProductionAmount[i]*exp(model.cycleTime_log[i]) \ - for i in model.PRODUCTS) - model.finish_in_time = Constraint(rule=finish_in_time_rule) - - - ############### - # Disjunctions - ############### - - def storage_tank_selection_disjunct_rule(disjunct, selectStorageTank, j): - model = disjunct.model() - def volume_stage_j_rule(disjunct, i): - return model.storageTankSize_log[j] >= log(model.StorageTankSizeFactor[j]) + \ - model.batchSize_log[i, j] - def volume_stage_jPlus1_rule(disjunct, i): - return model.storageTankSize_log[j] >= log(model.StorageTankSizeFactor[j]) + \ - model.batchSize_log[i, j+1] - def batch_size_rule(disjunct, i): - return inequality(-log(model.StorageTankSizeFactorByProd[i,j]), - model.batchSize_log[i,j] - model.batchSize_log[i, j+1], - log(model.StorageTankSizeFactorByProd[i,j])) - def no_batch_rule(disjunct, i): - return model.batchSize_log[i,j] - model.batchSize_log[i,j+1] == 0 - - if selectStorageTank: - disjunct.volume_stage_j = Constraint(model.PRODUCTS, rule=volume_stage_j_rule) - disjunct.volume_stage_jPlus1 = Constraint(model.PRODUCTS, - rule=volume_stage_jPlus1_rule) - disjunct.batch_size = Constraint(model.PRODUCTS, rule=batch_size_rule) - else: - # The formulation says 0, but GAMS has this constant. - # 04/04: Francisco says volume should be free: - # disjunct.no_volume = Constraint(expr=model.storageTankSize_log[j] == MinFlow) - disjunct.no_batch = Constraint(model.PRODUCTS, rule=no_batch_rule) - model.storage_tank_selection_disjunct = Disjunct([0,1], model.STAGESExceptLast, - rule=storage_tank_selection_disjunct_rule) - - def select_storage_tanks_rule(model, j): - return [model.storage_tank_selection_disjunct[selectTank, j] for selectTank in [0,1]] - model.select_storage_tanks = Disjunction(model.STAGESExceptLast, rule=select_storage_tanks_rule) - - # though this is a disjunction in the GAMs model, it is more efficiently formulated this way: - # TODO: what on earth is k? - def units_out_of_phase_rule(model, j): - return model.unitsOutOfPhase_log[j] == sum(model.LogCoeffs[k] * model.outOfPhase[j,k] \ - for k in model.PARALLELUNITS) - model.units_out_of_phase = Constraint(model.STAGES, rule=units_out_of_phase_rule) - - def units_in_phase_rule(model, j): - return model.unitsInPhase_log[j] == sum(model.LogCoeffs[k] * model.inPhase[j,k] \ - for k in model.PARALLELUNITS) - model.units_in_phase = Constraint(model.STAGES, rule=units_in_phase_rule) - - # and since I didn't do the disjunction as a disjunction, we need the XORs: - def units_out_of_phase_xor_rule(model, j): - return sum(model.outOfPhase[j,k] for k in model.PARALLELUNITS) == 1 - model.units_out_of_phase_xor = Constraint(model.STAGES, rule=units_out_of_phase_xor_rule) - - def units_in_phase_xor_rule(model, j): - return sum(model.inPhase[j,k] for k in model.PARALLELUNITS) == 1 - model.units_in_phase_xor = Constraint(model.STAGES, rule=units_in_phase_xor_rule) - - return model.create_instance(join(this_file_dir(), 'batch_processing.dat')) - - -if __name__ == "__main__": - m = build_model() - TransformationFactory('gdp.bigm').apply_to(m) - SolverFactory('gams').solve(m, solver='baron', tee=True, add_options=['option optcr=1e-6;']) - m.min_cost.display() diff --git a/gdplib/pyomo_examples/disease_model.py b/gdplib/pyomo_examples/disease_model.py deleted file mode 100644 index 47b7b81..0000000 --- a/gdplib/pyomo_examples/disease_model.py +++ /dev/null @@ -1,413 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright 2017 National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -# ============================================ -# SIR disease model using a low/high transmission parameter -# This is formulated as a disjunctive program -# -# Daniel Word, November 1, 2010 -# ============================================ - -# import packages -from pyomo.environ import * -from pyomo.gdp import * -import math - - -def build_model(): - """ - Builds the model for the SIR disease model using a low/high transmission parameter. - - The model simulates the spread of an infectious disease over a series of bi-weekly periods, using a disjunctive programming approach to account for variations in disease transmission rates. - - Parameters - ---------- - None - - Returns - ------- - model : Pyomo.ConcreteModel - Pyomo model object that represent the SIR disease model using a low/high transmission parameter. - """ - # import data - # Population Data - - pop = [ 15.881351, 15.881339, 15.881320, 15.881294, 15.881261, 15.881223, 15.881180, 15.881132, 15.881079, 15.881022, 15.880961, 15.880898, 15.880832, 15.880764, 15.880695, 15.880624, 15.880553, 15.880480, 15.880409, 15.880340, 15.880270, 15.880203, 15.880138, 15.880076, 15.880016, 15.879960, 15.879907, 15.879852, 15.879799, 15.879746, 15.879693, 15.879638, 15.879585, 15.879531, 15.879477, 15.879423, 15.879370, 15.879315, 15.879262, 15.879209, 15.879155, 15.879101, 15.879048, 15.878994, 15.878940, 15.878886, 15.878833, 15.878778, 15.878725, 15.878672, 15.878618, 15.878564, 15.878510, 15.878457, 15.878402, 15.878349, 15.878295, 15.878242, 15.878187, 15.878134, 15.878081, 15.878026, 15.877973, 15.877919, 15.877864, 15.877811, 15.877758, 15.877704, 15.877650, 15.877596, 15.877543, 15.877488, 15.877435, 15.877381, 15.877326, 15.877273, 15.877220, 15.877166, 15.877111, 15.877058, 15.877005, 15.876950, 15.876896, 15.876843, 15.876789, 15.876735, 15.876681, 15.876628, 15.876573, 15.876520, 15.876466, 15.876411, 15.876358, 15.876304, 15.876251, 15.876196, 15.876143, 15.876089, 15.876034, 15.875981, 15.875927, 15.875872, 15.875819, 15.875765, 15.875712, 15.875657, 15.875604, 15.875550, 15.875495, 15.875442, 15.875388, 15.875335, 15.875280, 15.875226, 15.875173, 15.875118, 15.875064, 15.875011, 15.874956, 15.874902, 15.874849, 15.874795, 15.874740, 15.874687, 15.874633, 15.874578, 15.874525, 15.874471, 15.874416, 15.874363, 15.874309, 15.874256, 15.874201, 15.874147, 15.874094, 15.874039, 15.873985, 15.873931, 15.873878, 15.873823, 15.873769, 15.873716, 15.873661, 15.873607, 15.873554, 15.873499, 15.873445, 15.873391, 15.873338, 15.873283, 15.873229, 15.873175, 15.873121, 15.873067, 15.873013, 15.872960, 15.872905, 15.872851, 15.872797, 15.872742, 15.872689, 15.872635, 15.872580, 15.872526, 15.872473, 15.872419, 15.872364, 15.872310, 15.872256, 15.872202, 15.872148, 15.872094, 15.872039, 15.871985, 15.871932, 15.871878, 15.871823, 15.871769, 15.871715, 15.871660, 15.871607, 15.871553, 15.871499, 15.871444, 15.871390, 15.871337, 15.871282, 15.871228, 15.871174, 15.871119, 15.871065, 15.871012, 15.870958, 15.870903, 15.870849, 15.870795, 15.870740, 15.870686, 15.870633, 15.870577, 15.870524, 15.870470, 15.870416, 15.870361, 15.870307, 15.870253, 15.870198, 15.870144, 15.870091, 15.870037, 15.869982, 15.869928, 15.869874, 15.869819, 15.869765, 15.869711, 15.869656, 15.869602, 15.869548, 15.869495, 15.869439, 15.869386, 15.869332, 15.869277, 15.869223, 15.869169, 15.869114, 15.869060, 15.869006, 15.868952, 15.868897, 15.868843, 15.868789, 15.868734, 15.868679, 15.868618, 15.868556, 15.868489, 15.868421, 15.868351, 15.868280, 15.868208, 15.868134, 15.868063, 15.867991, 15.867921, 15.867852, 15.867785, 15.867721, 15.867659, 15.867601, 15.867549, 15.867499, 15.867455, 15.867416, 15.867383, 15.867357, 15.867338, 15.867327, 15.867321, 15.867327, 15.867338, 15.867359, 15.867386, 15.867419, 15.867459, 15.867505, 15.867555, 15.867610, 15.867671, 15.867734, 15.867801, 15.867869, 15.867941, 15.868012, 15.868087, 15.868161, 15.868236, 15.868310, 15.868384, 15.868457, 15.868527, 15.868595, 15.868661, 15.868722, 15.868780, 15.868837, 15.868892, 15.868948, 15.869005, 15.869061, 15.869116, 15.869173, 15.869229, 15.869284, 15.869341, 15.869397, 15.869452, 15.869509, 15.869565, 15.869620, 15.869677, 15.869733, 15.869788, 15.869845, 15.869901, 15.869956, 15.870012, 15.870069, 15.870124, 15.870180, 15.870237, 15.870292, 15.870348, 15.870405, 15.870461, 15.870516, 15.870572, 15.870629, 15.870684, 15.870740, 15.870796, 15.870851, 15.870908, 15.870964, 15.871019, 15.871076, 15.871132, 15.871187, 15.871243, 15.871300, 15.871355, 15.871411, 15.871467, 15.871522, 15.871579, 15.871635, 15.871691, 15.871746, 15.871802, 15.871859, 15.871914, 15.871970, 15.872026, 15.872081, 15.872138, 15.872194, 15.872249, 15.872305, 15.872361, 15.872416, 15.872473, 15.872529, 15.872584, 15.872640, 15.872696, 15.872751, 15.872807, 15.872864, 15.872919, 15.872975, 15.873031, 15.873087, 15.873142, 15.873198, 15.873255, 15.873310, 15.873366, 15.873422, 15.873477, 15.873533, 15.873589, 15.873644, 15.873700, 15.873757, 15.873811, 15.873868, 15.873924, 15.873979, 15.874035, 15.874091, 15.874146, 15.874202, 15.874258, 15.874313, 15.874369, 15.874425, 15.874481, 15.874536, 15.874592] - - logIstar = [7.943245, 8.269994, 8.517212, 8.814208, 9.151740, 9.478472, 9.559847, 9.664087, 9.735378, 9.852583, 9.692265, 9.498807, 9.097634, 8.388878, 7.870516, 7.012956, 6.484941, 5.825368, 5.346815, 5.548361, 5.706732, 5.712617, 5.709714, 5.696888, 5.530087, 5.826563, 6.643563, 7.004292, 7.044663, 7.190259, 7.335926, 7.516861, 7.831779, 8.188895, 8.450204, 8.801436, 8.818379, 8.787658, 8.601685, 8.258338, 7.943364, 7.425585, 7.062834, 6.658307, 6.339600, 6.526984, 6.679178, 6.988758, 7.367331, 7.746694, 8.260558, 8.676522, 9.235582, 9.607778, 9.841917, 10.081571, 10.216090, 10.350366, 10.289668, 10.248842, 10.039504, 9.846343, 9.510392, 9.190923, 8.662465, 7.743221, 7.128458, 5.967898, 5.373883, 5.097497, 4.836570, 5.203345, 5.544798, 5.443047, 5.181152, 5.508669, 6.144130, 6.413744, 6.610423, 6.748885, 6.729511, 6.789841, 6.941034, 7.093516, 7.307039, 7.541077, 7.644803, 7.769145, 7.760187, 7.708017, 7.656795, 7.664983, 7.483828, 6.887324, 6.551093, 6.457449, 6.346064, 6.486300, 6.612378, 6.778753, 6.909477, 7.360570, 8.150303, 8.549044, 8.897572, 9.239323, 9.538751, 9.876531, 10.260911, 10.613536, 10.621510, 10.661115, 10.392899, 10.065536, 9.920090, 9.933097, 9.561691, 8.807713, 8.263463, 7.252184, 6.669083, 5.877763, 5.331878, 5.356563, 5.328469, 5.631146, 6.027497, 6.250717, 6.453919, 6.718444, 7.071636, 7.348905, 7.531528, 7.798226, 8.197941, 8.578809, 8.722964, 8.901152, 8.904370, 8.889865, 8.881902, 8.958903, 8.721281, 8.211509, 7.810624, 7.164607, 6.733688, 6.268503, 5.905983, 5.900432, 5.846547, 6.245427, 6.786271, 7.088480, 7.474295, 7.650063, 7.636703, 7.830990, 8.231516, 8.584816, 8.886908, 9.225216, 9.472778, 9.765505, 9.928623, 10.153033, 10.048574, 9.892620, 9.538818, 8.896100, 8.437584, 7.819738, 7.362598, 6.505880, 5.914972, 6.264584, 6.555019, 6.589319, 6.552029, 6.809771, 7.187616, 7.513918, 8.017712, 8.224957, 8.084474, 8.079148, 8.180991, 8.274269, 8.413748, 8.559599, 8.756090, 9.017927, 9.032720, 9.047983, 8.826873, 8.366489, 8.011876, 7.500830, 7.140406, 6.812626, 6.538719, 6.552218, 6.540129, 6.659927, 6.728530, 7.179692, 7.989210, 8.399173, 8.781128, 9.122303, 9.396378, 9.698512, 9.990104, 10.276543, 10.357284, 10.465869, 10.253833, 10.018503, 9.738407, 9.484367, 9.087025, 8.526409, 8.041126, 7.147168, 6.626706, 6.209446, 5.867231, 5.697439, 5.536769, 5.421413, 5.238297, 5.470136, 5.863007, 6.183083, 6.603569, 6.906278, 7.092324, 7.326612, 7.576052, 7.823430, 7.922775, 8.041677, 8.063403, 8.073229, 8.099726, 8.168522, 8.099041, 8.011404, 7.753147, 6.945211, 6.524244, 6.557723, 6.497742, 6.256247, 5.988794, 6.268093, 6.583316, 7.106842, 8.053929, 8.508237, 8.938915, 9.311863, 9.619753, 9.931745, 10.182361, 10.420978, 10.390829, 10.389230, 10.079342, 9.741479, 9.444561, 9.237448, 8.777687, 7.976436, 7.451502, 6.742856, 6.271545, 5.782289, 5.403089, 5.341954, 5.243509, 5.522993, 5.897001, 6.047042, 6.100738, 6.361727, 6.849562, 7.112544, 7.185346, 7.309412, 7.423746, 7.532142, 7.510318, 7.480175, 7.726362, 8.061117, 8.127072, 8.206166, 8.029634, 7.592953, 7.304869, 7.005394, 6.750019, 6.461377, 6.226432, 6.287047, 6.306452, 6.783694, 7.450957, 7.861692, 8.441530, 8.739626, 8.921994, 9.168961, 9.428077, 9.711664, 10.032714, 10.349937, 10.483985, 10.647475, 10.574038, 10.522431, 10.192246, 9.756246, 9.342511, 8.872072, 8.414189, 7.606582, 7.084701, 6.149903, 5.517257, 5.839429, 6.098090, 6.268935, 6.475965, 6.560543, 6.598942, 6.693938, 6.802531, 6.934345, 7.078370, 7.267736, 7.569640, 7.872204, 8.083603, 8.331226, 8.527144, 8.773523, 8.836599, 8.894303, 8.808326, 8.641717, 8.397901, 7.849034, 7.482899, 7.050252, 6.714103, 6.900603, 7.050765, 7.322905, 7.637986, 8.024340, 8.614505, 8.933591, 9.244008, 9.427410, 9.401385, 9.457744, 9.585068, 9.699673, 9.785478, 9.884559, 9.769732, 9.655075, 9.423071, 9.210198, 8.786654, 8.061787, 7.560976, 6.855829, 6.390707, 5.904006, 5.526631, 5.712303, 5.867027, 5.768367, 5.523352, 5.909118, 6.745543, 6.859218 ] - - deltaS = [ 9916.490263 ,12014.263380 ,13019.275755 ,12296.373612 ,8870.995603 ,1797.354574 ,-6392.880771 ,-16150.825387 ,-27083.245106 ,-40130.421462 ,-50377.169958 ,-57787.717468 ,-60797.223427 ,-59274.041897 ,-55970.213230 ,-51154.650927 ,-45877.841034 ,-40278.553775 ,-34543.967175 ,-28849.633641 ,-23192.776605 ,-17531.130740 ,-11862.021829 ,-6182.456792 ,-450.481090 ,5201.184400 ,10450.773882 ,15373.018272 ,20255.699431 ,24964.431669 ,29470.745887 ,33678.079947 ,37209.808930 ,39664.432393 ,41046.735479 ,40462.982011 ,39765.070209 ,39270.815830 ,39888.077002 ,42087.276604 ,45332.012929 ,49719.128772 ,54622.190928 ,59919.718626 ,65436.341097 ,70842.911460 ,76143.747430 ,81162.358574 ,85688.102884 ,89488.917734 ,91740.108470 ,91998.787916 ,87875.986012 ,79123.877908 ,66435.611045 ,48639.250610 ,27380.282817 ,2166.538464 ,-21236.428084 ,-43490.803535 ,-60436.624080 ,-73378.401966 ,-80946.278268 ,-84831.969493 ,-84696.627286 ,-81085.365407 ,-76410.847049 ,-70874.415387 ,-65156.276464 ,-59379.086883 ,-53557.267619 ,-47784.164830 ,-42078.001172 ,-36340.061427 ,-30541.788202 ,-24805.281435 ,-19280.817165 ,-13893.690606 ,-8444.172221 ,-3098.160839 ,2270.908649 ,7594.679295 ,12780.079247 ,17801.722109 ,22543.091206 ,26897.369814 ,31051.285734 ,34933.809557 ,38842.402859 ,42875.230152 ,47024.395356 ,51161.516122 ,55657.298307 ,60958.155424 ,66545.635029 ,72202.930397 ,77934.761905 ,83588.207792 ,89160.874522 ,94606.115027 ,99935.754968 ,104701.404975 ,107581.670606 ,108768.440311 ,107905.700480 ,104062.148863 ,96620.281684 ,83588.443029 ,61415.088182 ,27124.031692 ,-7537.285321 ,-43900.451653 ,-70274.062783 ,-87573.481475 ,-101712.148408 ,-116135.719087 ,-124187.225446 ,-124725.278371 ,-122458.145590 ,-117719.918256 ,-112352.138605 ,-106546.806030 ,-100583.803012 ,-94618.253238 ,-88639.090897 ,-82725.009842 ,-76938.910669 ,-71248.957807 ,-65668.352795 ,-60272.761991 ,-55179.538428 ,-50456.021161 ,-46037.728058 ,-42183.912670 ,-39522.184006 ,-38541.255303 ,-38383.665728 ,-39423.998130 ,-40489.466130 ,-41450.406768 ,-42355.156592 ,-43837.562085 ,-43677.262972 ,-41067.896944 ,-37238.628465 ,-32230.392026 ,-26762.766062 ,-20975.163308 ,-15019.218554 ,-9053.105545 ,-3059.663132 ,2772.399618 ,8242.538397 ,13407.752291 ,18016.047539 ,22292.125752 ,26616.583347 ,30502.564253 ,33153.890890 ,34216.684448 ,33394.220786 ,29657.417791 ,23064.375405 ,12040.831532 ,-2084.921068 ,-21390.235970 ,-38176.615985 ,-51647.714482 ,-59242.564959 ,-60263.150854 ,-58599.245165 ,-54804.972560 ,-50092.112608 ,-44465.812552 ,-38533.096297 ,-32747.104307 ,-27130.082610 ,-21529.632955 ,-15894.611939 ,-10457.566933 ,-5429.042583 ,-903.757828 ,2481.947589 ,5173.789976 ,8358.768202 ,11565.584635 ,14431.147931 ,16951.619820 ,18888.807708 ,20120.884465 ,20222.141242 ,18423.168124 ,16498.668271 ,14442.624242 ,14070.038273 ,16211.370808 ,19639.815904 ,24280.360465 ,29475.380079 ,35030.793540 ,40812.325095 ,46593.082382 ,52390.906885 ,58109.310860 ,63780.896094 ,68984.456561 ,72559.442320 ,74645.487900 ,74695.219755 ,72098.143876 ,66609.929889 ,56864.971296 ,41589.295266 ,19057.032104 ,-5951.329863 ,-34608.796853 ,-56603.801584 ,-72678.838057 ,-83297.070856 ,-90127.593511 ,-92656.040614 ,-91394.995510 ,-88192.056842 ,-83148.833075 ,-77582.587173 ,-71750.440823 ,-65765.369857 ,-59716.101820 ,-53613.430067 ,-47473.832358 ,-41287.031890 ,-35139.919259 ,-29097.671507 ,-23178.836760 ,-17486.807388 ,-12046.775779 ,-6802.483422 ,-1867.556171 ,2644.380534 ,6615.829501 ,10332.557518 ,13706.737038 ,17017.991307 ,20303.136670 ,23507.386461 ,26482.194102 ,29698.585356 ,33196.305757 ,37385.914179 ,42872.996212 ,48725.617879 ,54564.488527 ,60453.841604 ,66495.146265 ,72668.620416 ,78723.644870 ,84593.136677 ,89974.936239 ,93439.798630 ,95101.207834 ,94028.126381 ,89507.925620 ,80989.846001 ,66944.274744 ,47016.422041 ,19932.783790 ,-6198.433172 ,-32320.379400 ,-49822.852084 ,-60517.553414 ,-66860.548269 ,-70849.714105 ,-71058.721556 ,-67691.947812 ,-63130.703822 ,-57687.607311 ,-51916.952488 ,-45932.054982 ,-39834.909941 ,-33714.535713 ,-27564.443333 ,-21465.186188 ,-15469.326408 ,-9522.358787 ,-3588.742161 ,2221.802073 ,7758.244339 ,13020.269708 ,18198.562827 ,23211.338588 ,28051.699645 ,32708.577247 ,37413.795242 ,42181.401920 ,46462.499633 ,49849.582315 ,53026.578940 ,55930.600705 ,59432.642178 ,64027.356857 ,69126.843653 ,74620.328837 ,80372.056070 ,86348.152766 ,92468.907239 ,98568.998246 ,104669.511588 ,110445.790143 ,115394.348973 ,119477.553152 ,121528.574511 ,121973.674087 ,121048.017786 ,118021.473181 ,112151.993711 ,102195.999157 ,85972.731130 ,61224.719621 ,31949.279603 ,-3726.022971 ,-36485.298619 ,-67336.469799 ,-87799.366129 ,-98865.713558 ,-104103.651120 ,-105068.402300 ,-103415.820781 ,-99261.356633 ,-94281.850081 ,-88568.701325 ,-82625.711921 ,-76766.776770 ,-70998.803524 ,-65303.404499 ,-59719.198305 ,-54182.230439 ,-48662.904657 ,-43206.731668 ,-37732.701095 ,-32375.478519 ,-27167.508567 ,-22197.211891 ,-17722.869502 ,-13925.135219 ,-10737.893027 ,-8455.327914 ,-7067.008358 ,-7086.991191 ,-7527.693561 ,-8378.025732 ,-8629.383998 ,-7854.586079 ,-5853.040657 ,-1973.225485 ,2699.850783 ,8006.098287 ,13651.734934 ,19139.318072 ,24476.645420 ,29463.480336 ,33899.078820 ,37364.528796 ,38380.214949 ,37326.585649 ,33428.470616 ,27441.000494 ,21761.126583 ,15368.408081 ,7224.234078 ,-2702.217396 ,-14109.682505 ,-27390.915614 ,-38569.562393 ,-47875.155339 ,-53969.121872 ,-57703.473001 ,-57993.198171 ,-54908.391840 ,-50568.410328 ,-45247.622563 ,-39563.224328 ,-33637.786521 ,-27585.345413 ,-21572.074797 ,-15597.363909 ,-9577.429076 ,-3475.770622 ,2520.378408 ,8046.881775 ,13482.345595 ] - - beta_set = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26] - - - # from new_data_set import * # Uncomment this line to use new data set - - # declare model name - model = ConcreteModel() - - # declare constants - bpy = 26 # biweeks per year - years = 15 # years of data - bigM = 50.0 # big M for disjunction constraints - - # declare sets - model.S_meas = RangeSet(1, bpy * years) - model.S_meas_small = RangeSet(1, bpy * years - 1) - model.S_beta = RangeSet(1, bpy) - - # define variable bounds - def _gt_zero(m, i): - """ - Defines boundary constraints ensuring variables remain greater than zero. - - Parameters - ---------- - m : Pyomo.ConcreteModel - SIR disease model using a low/high transmission parameter - i : int - index of biweekly periods in the data set - - Returns - ------- - tuple - A tuple representing the lower and upper bounds for the variable, ensuring it remains positive. - """ - return (0.0, 1e7) - - def _beta_bounds(m): - """ - Sets the bounds for the transmission parameter beta within the model. - - Parameters - ---------- - m : Pyomo.ConcreteModel - SIR disease model using a low/high transmission parameter - - Returns - ------- - tuple - A tuple representing the lower and upper bounds for the beta variable. - """ - return (None, 5.0) - - # Define variables - # Log of estimated cases; All the variables are represented as common logarithm which the log base is 10. - # The original code inside build_model employs a disjunctive approach with integrated constraints. - # On the other hand, the commented code uses separate constraints for each scenario, applying the Big-M Reformulation. - # Binary variables (model.y) are defined inside on the build_model() function. The disjuncts for the Big-M Reformulation is written outside of the code. - - # model.logI = Var(model.S_meas, bounds=_gt_zero, doc='log of estimated cases') - model.logI = Var(model.S_meas, bounds=(0.001, 1e7), doc='log of estimated cases') - # log of transmission parameter beta - # model.logbeta = Var(model.S_beta, bounds=_gt_zero, doc='log of transmission parameter beta') - model.logbeta = Var( - model.S_beta, bounds=(0.0001, 5), doc='log of transmission parameter beta' - ) - # binary variable y over all betas - # model.y = Var(model.S_beta, within=Binary, doc='binary variable y over all betas') - # low value of beta - # model.logbeta_low = Var(bounds=_beta_bounds, doc='low value of beta') - model.logbeta_low = Var(bounds=(0.0001, 5)) - # high value of beta - # model.logbeta_high = Var(bounds=_beta_bounds, doc='high value of beta') - model.logbeta_high = Var(bounds=(0.0001, 5), doc='high value of beta') - # dummy variables - model.p = Var(model.S_meas, bounds=_gt_zero, doc='dummy variable p') - model.n = Var(model.S_meas, bounds=_gt_zero, doc='dummy variable n') - - # define indexed constants - - # log of measured cases after adjusting for under-reporting - logIstar = logIstar - # changes in susceptible population profile from susceptible reconstruction - deltaS = deltaS - # mean susceptibles (Number of Population) - # meanS = 1.04e6 - meanS = 8.65e5 # Number of Population(people) - logN = pop # log of measured population (Number of Population(log scale with log base 10)) - # define index for beta over all measurements () - beta_set = beta_set - - # define objective - def _obj_rule(m): - """ - Objective function for the SIR disease model, aiming to minimize the total discrepancy between estimated and observed infectious cases. - - Parameters - ---------- - m : Pyomo.ConcreteModel - SIR disease model using a low/high transmission parameter - - Returns - ------- - Pyomo.Expression - The expression for the objective function, which is the sum of overestimation and underestimation errors across all time periods. - - Notes - ----- - These variables ('p' and 'n') are likely to represent the overestimation and underestimation errors, respectively, - in the model's estimation of infectious cases compared to the observed data. - By minimizing their sum, the model seeks to closely align its estimations with the actual observed data. - """ - expr = sum(m.p[i] + m.n[i] for i in m.S_meas) - return expr - - model.obj = Objective(rule=_obj_rule, sense=minimize, doc='objective function') - - # define constraints - def _logSIR(m, i): - """ - SIR model constraint capturing the dynamics of infectious disease spread using a logarithmic formulation. - - Parameters - ---------- - m : Pyomo.ConcreteModel - SIR disease model using a low/high transmission parameter - i : int - index of biweekly periods in the data set - - Returns - ------- - tuple - A tuple containing the constraint expression for the SIR dynamics at the i-th bi-weekly period. - - Notes - ----- - This constraint is based on the differential equations of the SIR model, discretized and transformed into a logarithmic scale. - The 0.0 in (0.0, expr) enforces expr to be zero, defining an equality constraint essential for the SIR model to accurately capture the exact dynamics of disease transmission between time steps. - """ - expr = m.logI[i + 1] - ( - m.logbeta[beta_set[i - 1]] - + m.logI[i] - + math.log(deltaS[i - 1] + meanS) - - logN[i - 1] - ) - return (0.0, expr) - - model.logSIR = Constraint( - model.S_meas_small, rule=_logSIR, doc='log of SIR disease model' - ) - - # objective function constraint - def _p_n_const(m, i): - """ - Defines a constraint relating the model's estimated infectious cases to observed data, adjusted for overestimation and underestimation. - - It includes the variables 'p' and 'n', which represent the overestimation and underestimation errors, respectively. - - Parameters - ---------- - m : Pyomo.ConcreteModel - SIR disease model using a low/high transmission parameter - i : int - index of biweekly periods in the data set - - Returns - ------- - tuple - A tuple containing the constraint expression for adjusting the model's estimated cases at the i-th bi-weekly period. - - Notes - ----- - The constraint is formulated to account for the difference between the logarithm of observed infectious cases (logIstar) and the model's logarithm of estimated infectious cases (m.logI). - The 'p' and 'n' variables in the model represent overestimation and underestimation errors, respectively, and this constraint helps in aligning the model's estimates with actual observed data. - """ - expr = logIstar[i - 1] - m.logI[i] - m.p[i] + m.n[i] - return (0.0, expr) - - model.p_n_const = Constraint( - model.S_meas, rule=_p_n_const, doc='constraint for p and n' - ) - - # disjuncts - - model.BigM = Suffix() - model.y = RangeSet(0, 1) - - def _high_low(disjunct, i, y): - """ - Disjunct function for setting high and low beta values based on the binary variable. - - Parameters - ---------- - disjunct : Pyomo.Disjunction - The disjunct block being defined. - i : int - Index of biweekly periods in the data set. - y : int - Binary variable indicating whether the high or low beta value is used. - - Returns - ------- - None - Modifies the disjunct block to include the appropriate constraint based on the value of y. - - Notes - ----- - This function contributes to the disjunctive formulation of the model, allowing for the selection between high and low transmission rates for the disease. - """ - model = disjunct.model() - if y: - disjunct.c = Constraint(expr=model.logbeta_high - model.logbeta[i] == 0.0) - else: - disjunct.c = Constraint(expr=model.logbeta[i] - model.logbeta_low == 0.0) - model.BigM[disjunct.c] = bigM - - model.high_low = Disjunct( - model.S_beta, - model.y, - rule=_high_low, - doc='disjunct for high and low beta values', - ) - - # disjunctions - def _disj(model, i): - """ - Defines a disjunction for each beta value to choose between high and low transmission rates. - - Parameters - ---------- - model : Pyomo.Disjunction - disjunction for the high and low beta values - i : int - Index of biweekly periods in the data set - - Returns - ------- - list - A list of disjuncts for the i-th biweekly period, enabling the model to choose between - high and low beta values. - Each disjunct represents a set of constraints that are activated - based on the binary decision variables. - - Notes - ----- - This list is used by Pyomo to create a disjunction for each biweekly period, allowing the model to choose between the high or low beta value constraints based on the optimization process. - The defined disjunctions are integral to the model, enabling it to adaptively select the appropriate beta value for each time period, reflecting changes in disease transmission dynamics. - """ - return [model.high_low[i, j] for j in model.y] - - model.disj = Disjunction( - model.S_beta, rule=_disj, doc='disjunction for high and low beta values' - ) - - return model - - -# disjuncts -# The commented code sets up explicit high and low beta constraints for the SIR model using a big-M reformulation, bypassing Pyomo's built-in disjunctive programming tools. -# high beta disjuncts -# def highbeta_L(m,i): -# """ -# Defines the lower bound constraint for the high transmission parameter beta in the SIR model. - -# Parameters -# ---------- -# m : Pyomo.ConcreteModel -# SIR disease model using a low/high transmission parameter -# i : int -# index of biweekly periods in the data set - -# Returns -# ------- -# tuple -# A tuple (0.0, expr, None) where expr is the Pyomo expression for the lower bound of the high beta disjunct at the i-th biweekly period. -# This represents the lower bound of the high beta disjunct at the i-th biweekly period. - -# Notes -# ----- -# The given function is given as the expression that the disjunctions are converted by big-M reformulation. -# The binary variable m.y[i] is commented inside the model function. -# """ -# expr = m.logbeta[i] - m.logbeta_high + bigM*(1-m.y[i]) -# return (0.0, expr, None) -# model.highbeta_L = Constraint(model.S_beta, rule=highbeta_L) - -# def highbeta_U(m,i): -# """ -# Defines the upper bound constraint for the high transmission parameter beta in the SIR model. - -# Parameters -# ---------- -# m : Pyomo.ConcreteModel -# SIR disease model using a low/high transmission parameter -# i : int -# Index of biweekly periods in the data set. - -# Returns -# ------- -# tuple -# A tuple (None, expr, 0.0) where expr is the Pyomo expression for the upper bound of the high beta disjunct at the i-th biweekly period. -# This represents the upper bound of the high beta disjunct at the i-th biweekly period. - -# Notes -# The given function is given as the expression that the disjunctions are converted by big-M reformulation. -# The binary variable m.y[i] is commented inside the model function. -# """ -# expr = m.logbeta[i] - m.logbeta_high -# return (None, expr, 0.0) -# model.highbeta_U = Constraint(model.S_beta, rule=highbeta_U) - -# # low beta disjuncts -# def lowbeta_U(m,i): -# """ -# Defines the upper bound constraint for the low transmission parameter beta in the SIR model. - -# Parameters -# ---------- -# m (Pyomo.ConcreteModel): SIR disease model using a low/high transmission parameter -# i (int): index of biweekly periods in the data set - -# Returns: -# A tuple (None, expr, 0.0) where expr is the Pyomo expression for the upper bound of the low beta disjunct at the i-th biweekly period. -# This represents the upper bound of the low beta disjunct at the i-th biweekly period. - -# Notes -# ----- -# The given function is given as the expression that the disjunctions are converted by big-M reformulation. -# The binary variable m.y[i] is commented inside the model function. -# """ -# expr = m.logbeta[i] - m.logbeta_low - bigM*(m.y[i]) -# return (None, expr, 0.0) -# model.lowbeta_U = Constraint(model.S_beta, rule=lowbeta_U) - -# def lowbeta_L(m,i): -# """ -# Defines the lower bound constraint for the low transmission parameter beta in the SIR model. - -# Parameters -# ---------- -# m (Pyomo.ConcreteModel): SIR disease model using a low/high transmission parameter -# i (int): index of biweekly periods in the data set - -# Returns -# ------- -# tuple -# A tuple (0.0, expr, None) where expr is the Pyomo expression for the lower bound of the low beta disjunct at the i-th biweekly period. -# This represents the lower bound of the low beta disjunct at the i-th biweekly period. - -# Notes -# ----- -# This lower bound constraint is part of the model's disjunctive framework, allowing for the differentiation between low and high transmission rates. -# The big-M method integrates this constraint into the model based on the state of the binary decision variable `m.y[i]`. -# """ -# expr = m.logbeta[i] - m.logbeta_low -# return (0.0, expr, None) -# model.lowbeta_L = Constraint(model.S_beta, rule=lowbeta_L) - - -if __name__ == "__main__": - m = build_model() - TransformationFactory('gdp.bigm').apply_to(m) - SolverFactory('gams').solve( - m, solver='baron', tee=True, add_options=['option optcr=1e-6;'] - ) - m.obj.display() diff --git a/gdplib/pyomo_examples/jobshop.py b/gdplib/pyomo_examples/jobshop.py deleted file mode 100644 index 84bb159..0000000 --- a/gdplib/pyomo_examples/jobshop.py +++ /dev/null @@ -1,94 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright 2017 National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.environ import * -from pyomo.gdp import * -from pyomo.common.fileutils import this_file_dir -from os.path import join - -# -# Jobshop example from http://www.gams.com/modlib/libhtml/logmip4.htm -# -# This model solves a jobshop scheduling, which has a set of jobs -# which must be processed in sequence of stages but not all jobs -# require all stages. A zero wait transfer policy is assumed between -# stages. To obtain a feasible solution it is necessary to eliminate -# all clashes between jobs. It requires that no two jobs be performed -# at any stage at any time. The objective is to minimize the makespan, -# the time to complete all jobs. -# -# References: -# -# Raman & Grossmann, Computers and Chemical Engineering 18, 7, p.563-578, 1994. -# -# Aldo Vecchietti, LogMIP User's Manual, http://www.logmip.ceride.gov.ar/, 2007 -# - -def build_model(): - model = AbstractModel() - - model.JOBS = Set(ordered=True) - model.STAGES = Set(ordered=True) - model.I_BEFORE_K = RangeSet(0,1) - - # Task durations - model.tau = Param(model.JOBS, model.STAGES, default=0) - - # Total Makespan (this will be the objective) - model.ms = Var() - # Start time of each job - def t_bounds(model, I): - return (0, sum(value(model.tau[idx]) for idx in model.tau)) - model.t = Var( model.JOBS, within=NonNegativeReals, bounds=t_bounds ) - - # Auto-generate the L set (potential collisions between 2 jobs at any stage. - def _L_filter(model, I, K, J): - return I < K and model.tau[I,J] and model.tau[K,J] - model.L = Set( initialize=model.JOBS * model.JOBS * model.STAGES, - dimen=3, filter=_L_filter) - - # Makespan is greater than the start time of every job + that job's - # total duration - def _feas(model, I): - return model.ms >= model.t[I] + sum(model.tau[I,M] for M in model.STAGES) - model.Feas = Constraint(model.JOBS, rule=_feas) - - # Disjunctions to prevent clashes at a stage: This creates a set of - # disjunct pairs: one if job I occurs before job K and the other if job - # K occurs before job I. - def _NoClash(disjunct, I, K, J, IthenK): - model = disjunct.model() - lhs = model.t[I] + sum([M= model.DemandLB[j,t] - model.demand_LB = Constraint(model.Products, model.TimePeriods, rule=demand_LB_rule) - - - # FIXED PRICE CONTRACT - - # Disjunction for Fixed Price contract buying options - def FP_contract_disjunct_rule(disjunct, j, t, buy): - model = disjunct.model() - if buy: - disjunct.c = Constraint(expr=model.AmountPurchased_FP[j,t] <= MAX_AMOUNT_FP) - else: - disjunct.c = Constraint(expr=model.AmountPurchased_FP[j,t] == 0) - model.FP_contract_disjunct = Disjunct(model.RawMaterials, model.TimePeriods, - model.BuyFPContract, rule=FP_contract_disjunct_rule) - - # Fixed price disjunction - def FP_contract_rule(model, j, t): - return [model.FP_contract_disjunct[j,t,buy] for buy in model.BuyFPContract] - model.FP_disjunction = Disjunction(model.RawMaterials, model.TimePeriods, - rule=FP_contract_rule) - - # cost constraint for fixed price contract (independent contraint) - def FP_contract_cost_rule(model, j, t): - return model.Cost_FP[j,t] == model.AmountPurchased_FP[j,t] * \ - model.Prices[j,t] - model.FP_contract_cost = Constraint(model.RawMaterials, model.TimePeriods, - rule=FP_contract_cost_rule) - - - # DISCOUNT CONTRACT - - # Disjunction for Discount contract - def discount_contract_disjunct_rule(disjunct, j, t, buy): - model = disjunct.model() - if buy == 'BelowMin': - disjunct.belowMin = Constraint( - expr=model.AmountPurchasedBelowMin_Discount[j,t] <= \ - model.MinAmount_Discount[j,t]) - disjunct.aboveMin = Constraint( - expr=model.AmountPurchasedAboveMin_Discount[j,t] == 0) - elif buy == 'AboveMin': - disjunct.belowMin = Constraint( - expr=model.AmountPurchasedBelowMin_Discount[j,t] == \ - model.MinAmount_Discount[j,t]) - disjunct.aboveMin = Constraint( - expr=model.AmountPurchasedAboveMin_Discount[j,t] >= 0) - elif buy == 'NotSelected': - disjunct.belowMin = Constraint( - expr=model.AmountPurchasedBelowMin_Discount[j,t] == 0) - disjunct.aboveMin = Constraint( - expr=model.AmountPurchasedAboveMin_Discount[j,t] == 0) - else: - raise RuntimeError("Unrecognized choice for discount contract: %s" % buy) - model.discount_contract_disjunct = Disjunct(model.RawMaterials, model.TimePeriods, - model.BuyDiscountContract, rule=discount_contract_disjunct_rule) - - # Discount contract disjunction - def discount_contract_rule(model, j, t): - return [model.discount_contract_disjunct[j,t,buy] \ - for buy in model.BuyDiscountContract] - model.discount_contract = Disjunction(model.RawMaterials, model.TimePeriods, - rule=discount_contract_rule) - - # cost constraint for discount contract (independent constraint) - def discount_cost_rule(model, j, t): - return model.Cost_Discount[j,t] == model.RegPrice_Discount[j,t] * \ - model.AmountPurchasedBelowMin_Discount[j,t] + \ - model.DiscountPrice_Discount[j,t] * model.AmountPurchasedAboveMin_Discount[j,t] - model.discount_cost = Constraint(model.RawMaterials, model.TimePeriods, - rule=discount_cost_rule) - - - # BULK CONTRACT - - # Bulk contract buying options disjunct - def bulk_contract_disjunct_rule(disjunct, j, t, buy): - model = disjunct.model() - if buy == 'BelowMin': - disjunct.amount = Constraint( - expr=model.AmountPurchased_Bulk[j,t] <= model.MinAmount_Bulk[j,t]) - disjunct.price = Constraint( - expr=model.Cost_Bulk[j,t] == model.RegPrice_Bulk[j,t] * \ - model.AmountPurchased_Bulk[j,t]) - elif buy == 'AboveMin': - disjunct.amount = Constraint( - expr=model.AmountPurchased_Bulk[j,t] >= model.MinAmount_Bulk[j,t]) - disjunct.price = Constraint( - expr=model.Cost_Bulk[j,t] == model.DiscountPrice_Bulk[j,t] * \ - model.AmountPurchased_Bulk[j,t]) - elif buy == 'NotSelected': - disjunct.amount = Constraint(expr=model.AmountPurchased_Bulk[j,t] == 0) - disjunct.price = Constraint(expr=model.Cost_Bulk[j,t] == 0) - else: - raise RuntimeError("Unrecognized choice for bulk contract: %s" % buy) - model.bulk_contract_disjunct = Disjunct(model.RawMaterials, model.TimePeriods, - model.BuyBulkContract, rule=bulk_contract_disjunct_rule) - - # Bulk contract disjunction - def bulk_contract_rule(model, j, t): - return [model.bulk_contract_disjunct[j,t,buy] for buy in model.BuyBulkContract] - model.bulk_contract = Disjunction(model.RawMaterials, model.TimePeriods, - rule=bulk_contract_rule) - - - # FIXED DURATION CONTRACT - - def FD_1mo_contract(disjunct, j, t): - model = disjunct.model() - disjunct.amount1 = Constraint(expr=model.AmountPurchased_FD[j,t] >= \ - MIN_AMOUNT_FD_1MONTH) - disjunct.price1 = Constraint(expr=model.Cost_FD[j,t] == \ - model.Prices_Length[j,1,t] * model.AmountPurchased_FD[j,t]) - model.FD_1mo_contract = Disjunct( - model.RawMaterials, model.TimePeriods, rule=FD_1mo_contract) - - def FD_2mo_contract(disjunct, j, t): - model = disjunct.model() - disjunct.amount1 = Constraint(expr=model.AmountPurchased_FD[j,t] >= \ - model.MinAmount_Length[j,2]) - disjunct.price1 = Constraint(expr=model.Cost_FD[j,t] == \ - model.Prices_Length[j,2,t] * model.AmountPurchased_FD[j,t]) - # only enforce these if we aren't in the last time period - if t < model.TimePeriods[-1]: - disjunct.amount2 = Constraint(expr=model.AmountPurchased_FD[j, t+1] >= \ - model.MinAmount_Length[j,2]) - disjunct.price2 = Constraint(expr=model.Cost_FD[j,t+1] == \ - model.Prices_Length[j,2,t] * model.AmountPurchased_FD[j, t+1]) - model.FD_2mo_contract = Disjunct( - model.RawMaterials, model.TimePeriods, rule=FD_2mo_contract) - - def FD_3mo_contract(disjunct, j, t): - model = disjunct.model() - # NOTE: I think there is a mistake in the GAMS file in line 327. - # they use the bulk minamount rather than the length one. - #I am doing the same here for validation purposes. - disjunct.amount1 = Constraint(expr=model.AmountPurchased_FD[j,t] >= \ - model.MinAmount_Bulk[j,3]) - disjunct.cost1 = Constraint(expr=model.Cost_FD[j,t] == \ - model.Prices_Length[j,3,t] * model.AmountPurchased_FD[j,t]) - # check we aren't in one of the last two time periods - if t < model.TimePeriods[-1]: - disjunct.amount2 = Constraint(expr=model.AmountPurchased_FD[j,t+1] >= \ - model.MinAmount_Length[j,3]) - disjunct.cost2 = Constraint(expr=model.Cost_FD[j,t+1] == \ - model.Prices_Length[j,3,t] * model.AmountPurchased_FD[j,t+1]) - if t < model.TimePeriods[-2]: - disjunct.amount3 = Constraint(expr=model.AmountPurchased_FD[j,t+2] >= \ - model.MinAmount_Length[j,3]) - disjunct.cost3 = Constraint(expr=model.Cost_FD[j,t+2] == \ - model.Prices_Length[j,3,t] * model.AmountPurchased_FD[j,t+2]) - model.FD_3mo_contract = Disjunct( - model.RawMaterials, model.TimePeriods, rule=FD_3mo_contract) - - def FD_no_contract(disjunct, j, t): - model = disjunct.model() - disjunct.amount1 = Constraint(expr=model.AmountPurchased_FD[j,t] == 0) - disjunct.cost1 = Constraint(expr=model.Cost_FD[j,t] == 0) - if t < model.TimePeriods[-1]: - disjunct.amount2 = Constraint(expr=model.AmountPurchased_FD[j,t+1] == 0) - disjunct.cost2 = Constraint(expr=model.Cost_FD[j,t+1] == 0) - if t < model.TimePeriods[-2]: - disjunct.amount3 = Constraint(expr=model.AmountPurchased_FD[j,t+2] == 0) - disjunct.cost3 = Constraint(expr=model.Cost_FD[j,t+2] == 0) - model.FD_no_contract = Disjunct( - model.RawMaterials, model.TimePeriods, rule=FD_no_contract) - - def FD_contract(model, j, t): - return [ model.FD_1mo_contract[j,t], model.FD_2mo_contract[j,t], - model.FD_3mo_contract[j,t], model.FD_no_contract[j,t], ] - model.FD_contract = Disjunction(model.RawMaterials, model.TimePeriods, - rule=FD_contract) - - return model - - -def build_concrete(): - return build_model().create_instance(join(this_file_dir(), 'med_term_purchasing.dat')) - - -if __name__ == "__main__": - m = build_concrete() - TransformationFactory('gdp.bigm').apply_to(m) - SolverFactory('gams').solve(m, solver='baron', tee=True, add_options=['option optcr=1e-6;']) - m.profit.display() diff --git a/gdplib/stranded_gas/model.py b/gdplib/stranded_gas/model.py index e563078..adc6720 100644 --- a/gdplib/stranded_gas/model.py +++ b/gdplib/stranded_gas/model.py @@ -1,116 +1,383 @@ -from __future__ import division +""" +model.py +Pyomo ConcreteModel for optimizing a modular stranded gas processing network. + +The model is designed to convert stranded gas into gasoline using a modular and intensified GTL process. +It incorporates the economic dynamics of module investments, gas processing, and product transportation. +Constraints manage the balance of gas supply and consumption, module availability and movement, and production capacities at potential sites. +Disjunctions delineate operational scenarios, such as the existence or absence of pipelines and the activation status of sites, enabling dynamic and flexible system configuration. +The objective function aims to maximize the network's net profit by optimizing revenue from gasoline sales while minimizing operational and capital expenditures across the network. + +References: + [1] Chen, Q., & Grossmann, I. E. (2019). Economies of numbers for a modular stranded gas processing network: Modeling and optimization. In Computer Aided Chemical Engineering (Vol. 47, pp. 257-262). Elsevier. DOI: 10.1016/B978-0-444-64241-7.50100-3 +""" import os import pandas as pd from pyomo.environ import ( - ConcreteModel, Constraint, Integers, NonNegativeReals, Objective, Param, - RangeSet, Set, SolverFactory, Suffix, TransformationFactory, Var, exp, log, - sqrt, summation, value + ConcreteModel, + Constraint, + Integers, + NonNegativeReals, + Objective, + Param, + RangeSet, + Set, + SolverFactory, + Suffix, + TransformationFactory, + Var, + exp, + log, + sqrt, + summation, + value, ) from gdplib.stranded_gas.util import alphanum_sorted from pyomo.environ import TerminationCondition as tc def build_model(): - m = ConcreteModel() + """ + Constructs a Pyomo ConcreteModel for optimizing a modular stranded gas processing network. The model is designed to convert stranded gas into gasoline using a modular and intensified GTL process. It incorporates the economic dynamics of module investments, gas processing, and product transportation. + + Returns + ------- + Pyomo.ConcreteModel + A Pyomo ConcreteModel that represents the multi-period optimization problem of a GTL (Gas To Liquid) network. The model includes variables for the number of modules, their type and placement, as well as for production and transportation of gasoline. It aims to maximize the net present value (NPV) of the processing network. + + References + ---------- + [1] Chen, Q., & Grossmann, I. E. (2019). Economies of numbers for a modular stranded gas processing network: Modeling and optimization. In Computer Aided Chemical Engineering (Vol. 47, pp. 257-262). Elsevier. DOI: 10.1016/B978-0-444-64241-7.50100-3 + """ + m = ConcreteModel('Stranded gas production') m.BigM = Suffix(direction=Suffix.LOCAL) m.periods_per_year = Param(initialize=4, doc="Quarters per year") m.project_life = Param(initialize=15, doc="Years") - m.time = RangeSet(0, m.periods_per_year * - m.project_life - 1, doc="Time periods") + m.time = RangeSet(0, m.periods_per_year * m.project_life - 1, doc="Time periods") m.discount_rate = Param(initialize=0.08, doc="8%") - m.learning_rate = Param(initialize=0.1, doc="Fraction discount for doubling of quantity") + m.learning_rate = Param( + initialize=0.1, doc="Fraction discount for doubling of quantity" + ) - m.module_setup_time = Param( - initialize=1, doc="1 quarter for module transfer") + m.module_setup_time = Param(initialize=1, doc="1 quarter for module transfer") @m.Param(m.time) def discount_factor(m, t): + """ + Calculates the discount factor for a given time period in the model. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo ConcreteModel that represents the multi-period optimization problem of a GTL network. + t : int + Set of time periods quarterly period within the 15 year project life. + + Returns + ------- + Pyomo.Parameter + A float representing the discount factor for the given time period. + """ return (1 + m.discount_rate / m.periods_per_year) ** (-t / m.periods_per_year) - xlsx_data = pd.read_excel(os.path.join(os.path.dirname(__file__), "data.xlsx"), sheet_name=None) + xlsx_data = pd.read_excel( + os.path.join(os.path.dirname(__file__), "data.xlsx"), sheet_name=None + ) module_sheet = xlsx_data['modules'].set_index('Type') - m.module_types = Set(initialize=module_sheet.columns.tolist(),) + m.module_types = Set(initialize=module_sheet.columns.tolist(), doc="Module types") @m.Param(m.module_types) def module_base_cost(m, mtype): + """ + Calculates the base cost of a module of a given type. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo ConcreteModel that represents the multi-period optimization problem of a GTL network. + mtype : str + Index of the module type (A500, R500, S500, U500, A1000, R1000, S1000, U1000, A2000, R2000, S2000, U2000, A5000, R5000). + + Returns + ------- + Pyomo.Parameter + A float representing the base cost of a module of the given type. + """ return float(module_sheet[mtype]['Capital Cost [MM$]']) - @m.Param(m.module_types, doc="Natural gas consumption per module of this type [MMSCF/d]") + @m.Param( + m.module_types, doc="Natural gas consumption per module of this type [MMSCF/d]" + ) def unit_gas_consumption(m, mtype): + """ + Calculates the natural gas consumption per module of a given type. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo ConcreteModel that represents the multi-period optimization problem of a GTL network. + mtype : str + Index of the module type. + + Returns + ------- + Pyomo.Parameter + A float representing the natural gas consumption per module of the given type. + """ return float(module_sheet[mtype]['Nat Gas [MMSCF/d]']) @m.Param(m.module_types, doc="Gasoline production per module of this type [kBD]") def gasoline_production(m, mtype): + """ + Calculates the gasoline production per module of a given type. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo ConcreteModel that represents the multi-period optimization problem of a GTL network. + mtype : str + Index of the module type. + + Returns + ------- + Pyomo.Parameter + A float representing the gasoline production per module of the given type. + """ return float(module_sheet[mtype]['Gasoline [kBD]']) - @m.Param(m.module_types, doc="Overall conversion of natural gas into gasoline per module of this type [kB/MMSCF]") + @m.Param( + m.module_types, + doc="Overall conversion of natural gas into gasoline per module of this type [kB/MMSCF]", + ) def module_conversion(m, mtype): + """ + Calculates the overall conversion of natural gas into gasoline per module of a given type. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo ConcreteModel that represents the multi-period optimization problem of a GTL network. + mtype : str + Index of the module type. + + Returns + ------- + Pyomo.Parameter + A float representing the overall conversion of natural gas into gasoline per module of the given type. + """ return float(module_sheet[mtype]['Conversion [kB/MMSCF]']) site_sheet = xlsx_data['sites'].set_index('Potential site') - m.potential_sites = Set(initialize=site_sheet.index.tolist()) + m.potential_sites = Set(initialize=site_sheet.index.tolist(), doc="Potential sites") m.site_pairs = Set( doc="Pairs of potential sites", initialize=m.potential_sites * m.potential_sites, - filter=lambda _, x, y: not x == y) + filter=lambda _, x, y: not x == y, + ) @m.Param(m.potential_sites) def site_x(m, site): + """ + Calculates the x-coordinate of a potential site. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo ConcreteModel that represents the multi-period optimization problem of a GTL network. + site : str + The index for the potential site. + + Returns + ------- + Pyomo.Parameter + An integer representing the x-coordinate of the potential site. + """ return float(site_sheet['x'][site]) @m.Param(m.potential_sites) def site_y(m, site): + """ + Calculates the y-coordinate of a potential site. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo ConcreteModel that represents the multi-period optimization problem of a GTL network. + site : str + The index for the potential site. + + Returns + ------- + Pyomo.Parameter + An integer representing the y-coordinate of the potential site. + """ return float(site_sheet['y'][site]) well_sheet = xlsx_data['wells'].set_index('Well') - m.well_clusters = Set(initialize=well_sheet.index.tolist()) + m.well_clusters = Set(initialize=well_sheet.index.tolist(), doc="Well clusters") @m.Param(m.well_clusters) def well_x(m, well): + """ + Calculates the x-coordinate of a well cluster. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo ConcreteModel that represents the multi-period optimization problem of a GTL network. + well : str + The index for the well cluster. It starts from w1 and goes up to w12. + + Returns + ------- + Pyomo.Parameter + An integer representing the x-coordinate of the well cluster. + """ return float(well_sheet['x'][well]) @m.Param(m.well_clusters) def well_y(m, well): + """ + Calculates the y-coordinate of a well cluster. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo ConcreteModel that represents the multi-period optimization problem of a GTL network. + well : str + The index for the well cluster. + + Returns + ------- + Pyomo.Parameter + An integer representing the y-coordinate of the well cluster. + """ return float(well_sheet['y'][well]) sched_sheet = xlsx_data['well-schedule'] - decay_curve = [1] + [3.69 * exp(-1.31 * (t + 1) ** 0.292) for t in range(m.project_life * 12)] + decay_curve = [1] + [ + 3.69 * exp(-1.31 * (t + 1) ** 0.292) for t in range(m.project_life * 12) + ] well_profiles = {well: [0 for _ in decay_curve] for well in m.well_clusters} for _, well_info in sched_sheet.iterrows(): start_time = int(well_info['Month']) - prod = [0] * start_time + decay_curve[:len(decay_curve) - start_time] + prod = [0] * start_time + decay_curve[: len(decay_curve) - start_time] prod = [x * float(well_info['max prod [MMSCF/d]']) for x in prod] current_profile = well_profiles[well_info['well-cluster']] - well_profiles[well_info['well-cluster']] = [val + prod[i] for i, val in enumerate(current_profile)] + well_profiles[well_info['well-cluster']] = [ + val + prod[i] for i, val in enumerate(current_profile) + ] @m.Param(m.well_clusters, m.time, doc="Supply of gas from well cluster [MMSCF/day]") def gas_supply(m, well, t): - return sum(well_profiles[well][t * 3:t * 3 + 2]) / 3 + """ + Calculates the supply of gas from a well cluster in a given time period. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo ConcreteModel that represents the multi-period optimization problem of a GTL network. + well : str + The index for the well cluster. + t : int + Set of time periods quarterly period within the 15 year project life. + + Returns + ------- + Pyomo.Parameter + A float representing the supply of gas from the well cluster in the given time period. + """ + return sum(well_profiles[well][t * 3 : t * 3 + 2]) / 3 mkt_sheet = xlsx_data['markets'].set_index('Market') - m.markets = Set(initialize=mkt_sheet.index.tolist()) + m.markets = Set(initialize=mkt_sheet.index.tolist(), doc="Markets") @m.Param(m.markets) def mkt_x(m, mkt): + """ + Calculates the x-coordinate of a market. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo ConcreteModel that represents the multi-period optimization problem of a GTL network. + mkt : str + The index for the market. (m1, m2, m3) + + Returns + ------- + Pyomo.Parameter + An integer representing the x-coordinate of the market. + """ return float(mkt_sheet['x'][mkt]) @m.Param(m.markets) def mkt_y(m, mkt): + """ + Calculates the y-coordinate of a market. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo ConcreteModel that represents the multi-period optimization problem of a GTL network. + mkt : str + The index for the market. + + Returns + ------- + Pyomo.Parameter + An integer representing the y-coordinate of the market. + """ return float(mkt_sheet['y'][mkt]) @m.Param(m.markets, doc="Gasoline demand [kBD]") def mkt_demand(m, mkt): + """ + Calculates the demand for gasoline in a market in a given time period. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo ConcreteModel that represents the multi-period optimization problem of a GTL network. + mkt : str + The index for the market. + + Returns + ------- + Pyomo.Parameter + A float representing the demand for gasoline in the market in the given time period. + """ return float(mkt_sheet['demand [kBD]'][mkt]) - m.sources = Set(initialize=m.well_clusters | m.potential_sites) - m.destinations = Set(initialize=m.potential_sites | m.markets) + m.sources = Set(initialize=m.well_clusters | m.potential_sites, doc="Sources") + m.destinations = Set(initialize=m.potential_sites | m.markets, doc="Destinations") @m.Param(m.sources, m.destinations, doc="Distance [mi]") def distance(m, src, dest): + """ + Calculates the Euclidean distance between a source and a destination within the gas processing network. + Assuming `src_x`, `src_y` for a source and `dest_x`, `dest_y` for a destination are defined within the model, the distance is calculated as follows: + + distance = sqrt((src_x - dest_x) ** 2 + (src_y - dest_y) ** 2) + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo ConcreteModel that represents the multi-period optimization problem of a GTL network. + src : str + The identifier for the source, which could be a well cluster or a potential site. + dest : str + The identifier for the destination, which could be a potential site or a market. + + Returns + ------- + Pyomo.Parameter + A parameter representing the Euclidean distance between the source and destination. + """ if src in m.well_clusters: src_x = m.well_x[src] src_y = m.well_y[src] @@ -126,38 +393,128 @@ def distance(m, src, dest): return sqrt((src_x - dest_x) ** 2 + (src_y - dest_y) ** 2) m.num_modules = Var( - m.module_types, m.potential_sites, m.time, + m.module_types, + m.potential_sites, + m.time, doc="Number of active modules of each type at a site in a period", - domain=Integers, bounds=(0, 50), initialize=1) + domain=Integers, + bounds=(0, 50), + initialize=1, + ) m.modules_transferred = Var( - m.module_types, m.site_pairs, m.time, + m.module_types, + m.site_pairs, + m.time, doc="Number of module transfers initiated from one site to another in a period.", - domain=Integers, bounds=(0, 15), initialize=0) + domain=Integers, + bounds=(0, 15), + initialize=0, + ) m.modules_purchased = Var( - m.module_types, m.potential_sites, m.time, + m.module_types, + m.potential_sites, + m.time, doc="Number of modules of each type purchased for a site in a period", - domain=Integers, bounds=(0, 30), initialize=1) + domain=Integers, + bounds=(0, 30), + initialize=1, + ) m.pipeline_unit_cost = Param(doc="MM$/mile", initialize=2) @m.Param(m.time, doc="Module transport cost per mile [M$/100 miles]") def module_transport_distance_cost(m, t): + """ + Calculates the module transport cost per mile in a given time period. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo ConcreteModel that represents the multi-period optimization problem of a GTL network. + t : int + Set of time periods quarterly period within the 15 year project life. + + Returns + ------- + Pyomo.Parameter + A float representing the module transport cost per mile in the given time period. + """ return 50 * m.discount_factor[t] @m.Param(m.time, doc="Module transport cost per unit [MM$/module]") def module_transport_unit_cost(m, t): + """ + Calculates the module transport cost per unit in a given time period. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo ConcreteModel that represents the multi-period optimization problem of a GTL network. + t : int + Set of time periods quarterly period within the 15 year project life. + + Returns + ------- + Pyomo.Parameter + A float representing the module transport cost per unit in the given time period. + """ return 3 * m.discount_factor[t] @m.Param(m.time, doc="Stranded gas price [$/MSCF]") def nat_gas_price(m, t): + """ + Calculates the price of stranded gas in a given time period. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo ConcreteModel that represents the multi-period optimization problem of a GTL network. + t : int + Set of time periods quarterly period within the 15 year project life. + + Returns + ------- + Pyomo.Parameter + A float representing the price of stranded gas in the given time period. + """ return 5 * m.discount_factor[t] @m.Param(m.time, doc="Gasoline price [$/gal]") def gasoline_price(m, t): + """ + Calculates the price of gasoline in a given time period. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo ConcreteModel that represents the multi-period optimization problem of a GTL network. + t : int + Set of time periods quarterly period within the 15 year project life. + + Returns + ------- + Pyomo.Parameter + A float representing the price of gasoline in the given time period. + """ return 2.5 * m.discount_factor[t] @m.Param(m.time, doc="Gasoline transport cost [$/gal/100 miles]") - def gasoline_tranport_cost(m, t): + def gasoline_transport_cost(m, t): + """ + Calculates the gasoline transport cost in a given time period. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo ConcreteModel that represents the multi-period optimization problem of a GTL network. + t : int + Set of time periods quarterly period within the 15 year project life. + + Returns + ------- + Pyomo.Parameter + A float representing the gasoline transport cost in the given time period. + """ return 0.045 * m.discount_factor[t] m.gal_per_bbl = Param(initialize=42, doc="Gallons per barrel") @@ -166,88 +523,311 @@ def gasoline_tranport_cost(m, t): m.learning_factor = Var( m.module_types, doc="Fraction of cost due to economies of mass production", - domain=NonNegativeReals, bounds=(0, 1), initialize=1) + domain=NonNegativeReals, + bounds=(0, 1), + initialize=1, + ) @m.Disjunct(m.module_types) def mtype_exists(disj, mtype): + """ + Represents the scenario where a specific module type exists within the GTL network. + + Parameters + ---------- + disj : Pyomo.Disjunct + A Pyomo Disjunct that represents the existence of a module type. + mtype : str + Index of the module type. + + Constraints + ------------ + learning_factor_calc : Pyomo Constraint + Captures the learning curve effect by adjusting the learning factor based on the total quantity of modules purchased. + require_module_purchases : Pyomo Constraint + Ensures that at least one module of this type is purchased, activating this disjunct. + """ disj.learning_factor_calc = Constraint( - expr=m.learning_factor[mtype] == (1 - m.learning_rate) ** ( - log(sum(m.modules_purchased[mtype, :, :])) / log(2))) + expr=m.learning_factor[mtype] + == (1 - m.learning_rate) + ** (log(sum(m.modules_purchased[mtype, :, :])) / log(2)), + doc="Learning factor calculation", + ) m.BigM[disj.learning_factor_calc] = 1 disj.require_module_purchases = Constraint( - expr=sum(m.modules_purchased[mtype, :, :]) >= 1) + expr=sum(m.modules_purchased[mtype, :, :]) >= 1, + doc="At least one module purchase", + ) @m.Disjunct(m.module_types) def mtype_absent(disj, mtype): + """ + Represents the scenario where a specific module type does not exist within the GTL network. + + Parameters + ---------- + disj : Pyomo.Disjunct + A Pyomo Disjunct that represents the absence of a module type. + mtype : str + Index of the module type. + """ disj.constant_learning_factor = Constraint( - expr=m.learning_factor[mtype] == 1) + expr=m.learning_factor[mtype] == 1, doc="Constant learning factor" + ) @m.Disjunction(m.module_types) def mtype_existence(m, mtype): + """ + A disjunction that determines whether a module type exists or is absent within the GTL network. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo ConcreteModel that represents the multi-period optimization problem of a GTL network. + mtype : str + Index of the module type. + + Returns + ------- + list of Pyomo.Disjunct + A list containing two disjuncts, one for the scenario where the module type exists and one for where it is absent. + """ return [m.mtype_exists[mtype], m.mtype_absent[mtype]] @m.Expression(m.module_types, m.time, doc="Module unit cost [MM$/module]") def module_unit_cost(m, mtype, t): - return m.module_base_cost[mtype] * m.learning_factor[mtype] * m.discount_factor[t] + """ + Computes the unit cost of a module type at a specific time period, considering the base cost, the learning factor due to economies of numbers, and the time-based discount factor. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo ConcreteModel that represents the multi-period optimization problem of a GTL network. + mtype : str + Index of the module type. + t : int + Set of time periods quarterly period within the 15 year project life. + + Returns + ------- + Pyomo.Expression + A Pyomo Expression that calculates the total unit cost of a module for a given type and time period. + """ + return ( + m.module_base_cost[mtype] * m.learning_factor[mtype] * m.discount_factor[t] + ) m.production = Var( - m.potential_sites, m.time, + m.potential_sites, + m.time, doc="Production of gasoline in a time period [kBD]", - domain=NonNegativeReals, bounds=(0, 30), initialize=10) + domain=NonNegativeReals, + bounds=(0, 30), + initialize=10, + ) m.gas_consumption = Var( - m.potential_sites, m.module_types, m.time, + m.potential_sites, + m.module_types, + m.time, doc="Consumption of natural gas by each module type " "at each site in a time period [MMSCF/d]", - domain=NonNegativeReals, bounds=(0, 250), initialize=50) + domain=NonNegativeReals, + bounds=(0, 250), + initialize=50, + ) m.gas_flows = Var( - m.well_clusters, m.potential_sites, m.time, + m.well_clusters, + m.potential_sites, + m.time, doc="Flow of gas from a well cluster to a site [MMSCF/d]", - domain=NonNegativeReals, bounds=(0, 200), initialize=15) + domain=NonNegativeReals, + bounds=(0, 200), + initialize=15, + ) m.product_flows = Var( - m.potential_sites, m.markets, m.time, + m.potential_sites, + m.markets, + m.time, doc="Product shipments from a site to a market in a period [kBD]", - domain=NonNegativeReals, bounds=(0, 30), initialize=10) + domain=NonNegativeReals, + bounds=(0, 30), + initialize=10, + ) @m.Constraint(m.potential_sites, m.module_types, m.time) def consumption_capacity(m, site, mtype, t): + """ + Ensures that the natural gas consumption at any site for any module type does not exceed the production capacity of the modules present. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo ConcreteModel that represents the multi-period optimization problem of a GTL network. + site : str + The index for the potential site. + mtype : str + Index of the module type. + t : int + Set of time periods quarterly period within the 15 year project life. + + Returns + ------- + Pyomo.Constraint + A constraint that limits the gas consumption per module type at each site, ensuring it does not exceed the capacity provided by the number of active modules of that type at the site during the time period. + """ return m.gas_consumption[site, mtype, t] <= ( - m.num_modules[mtype, site, t] * m.unit_gas_consumption[mtype]) + m.num_modules[mtype, site, t] * m.unit_gas_consumption[mtype] + ) @m.Constraint(m.potential_sites, m.time) def production_limit(m, site, t): + """ + Limits the production of gasoline at each site to the maximum possible based on the gas consumed and the conversion efficiency of the modules. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo ConcreteModel that represents the multi-period optimization problem of a GTL network. + site : str + The index for the potential site. + t : int + Set of time periods quarterly period within the 15 year project life. + + Returns + ------- + Pyomo.Constraint + A constraint that ensures the production of gasoline at each site does not exceed the sum of the product of gas consumption and conversion rates for all module types at that site. + """ return m.production[site, t] <= sum( m.gas_consumption[site, mtype, t] * m.module_conversion[mtype] - for mtype in m.module_types) + for mtype in m.module_types + ) @m.Expression(m.potential_sites, m.time) def capacity(m, site, t): + """ + Calculates the total potential gasoline production capacity at each site for a given time period, based on the number of active modules, their gas consumption, and conversion efficiency. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo ConcreteModel that represents the multi-period optimization problem of a GTL network. + site : str + The index for the potential site. + t : int + Set of time periods quarterly period within the 15 year project life. + + Returns + ------- + Pyomo.Expression + An expression that sums up the potential production capacity at a site, calculated as the product of the number of modules, their individual gas consumption rates, and their conversion efficiency. + """ return sum( - m.num_modules[mtype, site, t] * m.unit_gas_consumption[mtype] - * m.module_conversion[mtype] for mtype in m.module_types) + m.num_modules[mtype, site, t] + * m.unit_gas_consumption[mtype] + * m.module_conversion[mtype] + for mtype in m.module_types + ) @m.Constraint(m.potential_sites, m.time) def gas_supply_meets_consumption(m, site, t): + """ + Ensures that the total gas consumed at a site is exactly matched by the gas supplied to it, reflecting a balance between supply and demand at any given time period. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo ConcreteModel that represents the multi-period optimization problem of a GTL network. + site : str + The index for the potential site. + t : int + Set of time periods quarterly period within the 15 year project life. + + Returns + ------- + Pyomo.Constraint + A constraint that balances the gas supply with the gas consumption at each site, ensuring that the total gas flow to the site equals the total consumption. + """ return sum(m.gas_consumption[site, :, t]) == sum(m.gas_flows[:, site, t]) @m.Constraint(m.well_clusters, m.time) def gas_supply_limit(m, well, t): - return sum(m.gas_flows[well, site, t] - for site in m.potential_sites) <= m.gas_supply[well, t] + """ + Ensures that the total gas supplied from a well cluster does not exceed the available gas supply for that cluster during any given time period. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo ConcreteModel that represents the multi-period optimization problem of a GTL network. + well : str + The index for the well cluster. + t : int + Set of time periods quarterly period within the 15 year project life. + + Returns + ------- + Pyomo.Constraint + A constraint that limits the total gas flow from a well cluster to various sites to not exceed the gas supply available at that well cluster for the given time period. + """ + return ( + sum(m.gas_flows[well, site, t] for site in m.potential_sites) + <= m.gas_supply[well, t] + ) @m.Constraint(m.potential_sites, m.time) def gasoline_production_requirement(m, site, t): - return sum(m.product_flows[site, mkt, t] - for mkt in m.markets) == m.production[site, t] + """ + Ensures that the total amount of gasoline shipped from a site matches the production at that site for each time period. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo ConcreteModel that represents the multi-period optimization problem of a GTL network. + site : str + The index for the potential site. + t : int + Set of time periods quarterly period within the 15 year project life. + + Returns + ------- + Pyomo.Constraint + A constraint that the sum of product flows (gasoline) from a site to various markets equals the total production at that site for the given period. + """ + return ( + sum(m.product_flows[site, mkt, t] for mkt in m.markets) + == m.production[site, t] + ) @m.Constraint(m.potential_sites, m.module_types, m.time) def module_balance(m, site, mtype, t): + """ + Balances the number of modules at a site across time periods by accounting for modules added, transferred, and previously existing. This ensures a consistent and accurate count of modules that reflects all transactions and changes over time. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo ConcreteModel that represents the multi-period optimization problem of a GTL network. + site : str + The index for the potential site. + mtype : str + Index of the module type. + t : int + Set of time periods quarterly period within the 15 year project life. + + Returns + ------- + Pyomo.Constraint + A constraint that maintains an accurate balance of module counts at each site, considering new purchases, transfers in, existing inventory, and transfers out. + """ if t >= m.module_setup_time: - modules_added = m.modules_purchased[ - mtype, site, t - m.module_setup_time] + modules_added = m.modules_purchased[mtype, site, t - m.module_setup_time] modules_transferred_in = sum( m.modules_transferred[ - mtype, from_site, to_site, t - m.module_setup_time] - for from_site, to_site in m.site_pairs if to_site == site) + mtype, from_site, to_site, t - m.module_setup_time + ] + for from_site, to_site in m.site_pairs + if to_site == site + ) else: modules_added = 0 modules_transferred_in = 0 @@ -257,116 +837,331 @@ def module_balance(m, site, mtype, t): existing_modules = 0 modules_transferred_out = sum( m.modules_transferred[mtype, from_site, to_site, t] - for from_site, to_site in m.site_pairs if from_site == site) + for from_site, to_site in m.site_pairs + if from_site == site + ) return m.num_modules[mtype, site, t] == ( - existing_modules + modules_added - + modules_transferred_in - modules_transferred_out) + existing_modules + + modules_added + + modules_transferred_in + - modules_transferred_out + ) @m.Disjunct(m.potential_sites) def site_active(disj, site): + """ + Represents the active state of a potential site within the GTL network. + + Parameters + ---------- + disj : Pyomo.Disjunct + A Pyomo Disjunct that represents the active state of a potential site. + site : str + The index for the potential site. + """ pass @m.Disjunct(m.potential_sites) def site_inactive(disj, site): - disj.no_production = Constraint( - expr=sum(m.production[site, :]) == 0) + """ + Represents the inactive state of a potential site within the GTL network. + + Parameters + ---------- + disj : Pyomo.Disjunct + A Pyomo Disjunct that represents the inactive state of a potential site. + site : str + The index for the potential site. + """ + disj.no_production = Constraint(expr=sum(m.production[site, :]) == 0) disj.no_gas_consumption = Constraint( - expr=sum(m.gas_consumption[site, :, :]) == 0) - disj.no_gas_flows = Constraint( - expr=sum(m.gas_flows[:, site, :]) == 0) - disj.no_product_flows = Constraint( - expr=sum(m.product_flows[site, :, :]) == 0) - disj.no_modules = Constraint( - expr=sum(m.num_modules[:, site, :]) == 0) + expr=sum(m.gas_consumption[site, :, :]) == 0 + ) + disj.no_gas_flows = Constraint(expr=sum(m.gas_flows[:, site, :]) == 0) + disj.no_product_flows = Constraint(expr=sum(m.product_flows[site, :, :]) == 0) + disj.no_modules = Constraint(expr=sum(m.num_modules[:, site, :]) == 0) disj.no_modules_transferred = Constraint( expr=sum( m.modules_transferred[mtypes, from_site, to_site, t] for mtypes in m.module_types for from_site, to_site in m.site_pairs for t in m.time - if from_site == site or to_site == site) == 0) + if from_site == site or to_site == site + ) + == 0, + doc="No modules transferred", + ) disj.no_modules_purchased = Constraint( expr=sum( m.modules_purchased[mtype, site, t] - for mtype in m.module_types for t in m.time) == 0) + for mtype in m.module_types + for t in m.time + ) + == 0, + doc="No modules purchased", + ) @m.Disjunction(m.potential_sites) def site_active_or_not(m, site): + """ + A disjunction that determines whether a potential site is active or inactive within the GTL network. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo ConcreteModel that represents the multi-period optimization problem of a GTL network. + site : str + The index for the potential site. + + Returns + ------- + list of Pyomo.Disjunct + A list containing two disjuncts, one for the scenario where the site is active and one for where it is inactive. + """ return [m.site_active[site], m.site_inactive[site]] @m.Disjunct(m.well_clusters, m.potential_sites) def pipeline_exists(disj, well, site): + """ + Represents the scenario where a pipeline exists between a well cluster and a potential site. + + Parameters + ---------- + disj : Pyomo.Disjunct + _description_ + well : str + The index for the well cluster. + site : str + The index for the potential site. + """ pass @m.Disjunct(m.well_clusters, m.potential_sites) def pipeline_absent(disj, well, site): + """ + Represents the scenario where a pipeline does not exist between a well cluster and a potential site. + + Parameters + ---------- + disj : Pyomo.Disjunct + _description_ + well : str + The index for the well cluster. + site : str + The index for the potential site. + """ disj.no_natural_gas_flow = Constraint( - expr=sum(m.gas_flows[well, site, t] for t in m.time) == 0) + expr=sum(m.gas_flows[well, site, t] for t in m.time) == 0, + doc="No natural gas flow", + ) @m.Disjunction(m.well_clusters, m.potential_sites) def pipeline_existence(m, well, site): + """ + A disjunction that determines whether a pipeline exists or is absent between a well cluster and a potential site. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo ConcreteModel that represents the multi-period optimization problem of a GTL network. + well : str + The index for the well cluster. + site : str + The index for the potential site. + + Returns + ------- + list of Pyomo.Disjunct + A list containing two disjuncts, one for the scenario where a pipeline exists and one for where it is absent. + """ return [m.pipeline_exists[well, site], m.pipeline_absent[well, site]] # Objective Function Construnction @m.Expression(m.potential_sites, doc="MM$") def product_revenue(m, site): + """ + Calculates the total revenue generated from the sale of gasoline produced at each site. This expression multiplies the volume of gasoline sold by the price per gallon, adjusted to millions of dollars for the entire production period. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo ConcreteModel that represents the multi-period optimization problem of a GTL network. + site : str + The index for the potential site. + + Returns + ------- + Pyomo.Expression + An expression representing the total revenue in million dollars from selling the gasoline produced at the site. + """ return sum( m.product_flows[site, mkt, t] # kBD * 1000 # bbl/kB - / 1E6 # $ to MM$ + / 1e6 # $ to MM$ * m.days_per_period - * m.gasoline_price[t] * m.gal_per_bbl + * m.gasoline_price[t] + * m.gal_per_bbl for mkt in m.markets - for t in m.time) + for t in m.time + ) @m.Expression(m.potential_sites, doc="MM$") def raw_material_cost(m, site): + """ + Calculates the total cost of natural gas consumed as a raw material at each site, converted to millions of dollars. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo ConcreteModel that represents the multi-period optimization problem of a GTL network. + site : str + The index for the potential site. + + Returns + ------- + Pyomo.Expression + An expression calculating the total cost of natural gas used, taking into account the gas price and the conversion factor from MSCF to MMSCF. + """ return sum( - m.gas_consumption[site, mtype, t] * m.days_per_period - / 1E6 # $ to MM$ + m.gas_consumption[site, mtype, t] + * m.days_per_period + / 1e6 # $ to MM$ * m.nat_gas_price[t] * 1000 # MMSCF to MSCF - for mtype in m.module_types for t in m.time) + for mtype in m.module_types + for t in m.time + ) @m.Expression( - m.potential_sites, m.markets, - doc="Aggregate cost to transport gasoline from a site to market [MM$]") + m.potential_sites, + m.markets, + doc="Aggregate cost to transport gasoline from a site to market [MM$]", + ) def product_transport_cost(m, site, mkt): + """ + Computes the cost of transporting gasoline from each production site to different markets, expressed in million dollars. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo ConcreteModel that represents the multi-period optimization problem of a GTL network. + site : str + The index for the potential site. + mkt : str + The index for the market. + + Returns + ------- + Pyomo.Expression + The total transportation cost for shipping gasoline from a site to a market, adjusted for the distance and transportation rate. + """ return sum( - m.product_flows[site, mkt, t] * m.gal_per_bbl + m.product_flows[site, mkt, t] + * m.gal_per_bbl * 1000 # bbl/kB - / 1E6 # $ to MM$ - * m.distance[site, mkt] / 100 * m.gasoline_tranport_cost[t] - for t in m.time) + / 1e6 # $ to MM$ + * m.distance[site, mkt] + / 100 + * m.gasoline_transport_cost[t] + for t in m.time + ) @m.Expression(m.well_clusters, m.potential_sites, doc="MM$") def pipeline_construction_cost(m, well, site): - return (m.pipeline_unit_cost * m.distance[well, site] - * m.pipeline_exists[well, site].binary_indicator_var) + """ + Calculates the cost of constructing pipelines from well clusters to potential sites, with costs dependent on the existence of a pipeline, distance, and unit cost per mile. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo ConcreteModel that represents the multi-period optimization problem of a GTL network. + well : str + The index for the well cluster. + site : str + The index for the potential site. + + Returns + ------- + Pyomo.Expression + The cost of pipeline construction, in million dollars, if a pipeline is established between the well cluster and the site. + """ + return ( + m.pipeline_unit_cost + * m.distance[well, site] + * m.pipeline_exists[well, site].binary_indicator_var + ) # Module transport cost @m.Expression(m.site_pairs, doc="MM$") def module_relocation_cost(m, from_site, to_site): + """ + Calculates the cost of relocating modules from one site to another, considering the distance, transport cost per mile, and unit cost per module. This cost includes the transportation costs based on distance and per-unit transport costs, accounting for all modules transferred between specified sites over the entire project duration. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo ConcreteModel that represents the multi-period optimization problem of a GTL network. + from_site : str + Index for the originating site of the module transfer. + to_site : str + Index for the destination site of the module transfer. + + Returns + ------- + Pyomo.Expression + An expression calculating the total relocation cost for modules moved between the two sites, factoring in the distance and both per-mile and per-unit costs, scaled to million dollars. + """ return sum( m.modules_transferred[mtype, from_site, to_site, t] - * m.distance[from_site, to_site] / 100 + * m.distance[from_site, to_site] + / 100 * m.module_transport_distance_cost[t] - / 1E3 # M$ to MM$ + / 1e3 # M$ to MM$ + m.modules_transferred[mtype, from_site, to_site, t] * m.module_transport_unit_cost[t] for mtype in m.module_types - for t in m.time) + for t in m.time + ) @m.Expression(m.potential_sites, doc="MM$") def module_purchase_cost(m, site): + """ + Computes the total cost of purchasing new modules for a specific site, considering the unit costs of modules, which may vary over time due to discounts and other factors. This expression aggregates the costs for all modules purchased across the project's timeframe. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo ConcreteModel that represents the multi-period optimization problem of a GTL network. + site : str + The index for the potential site. + + Returns + ------- + Pyomo.Expression + An expression representing the total cost of module purchases at the specified site, converted to million dollars. + """ return sum( m.module_unit_cost[mtype, t] * m.modules_purchased[mtype, site, t] for mtype in m.module_types - for t in m.time) + for t in m.time + ) @m.Expression(doc="MM$") def profit(m): + """ + Calculates the overall profit for the GTL network by subtracting all relevant costs from the total revenue. This is used as the objective function to be maximized (or minimize the negative profit). + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo ConcreteModel that represents the multi-period optimization problem of a GTL network. + + Returns + ------- + Pyomo.Expression + The net profit expression, computed as the difference between total revenue and all accumulated costs across the network. + """ return ( summation(m.product_revenue) - summation(m.raw_material_cost) @@ -376,15 +1171,47 @@ def profit(m): - summation(m.module_purchase_cost) ) - m.neg_profit = Objective(expr=-m.profit) + m.neg_profit = Objective( + expr=-m.profit, doc="Objective Function: Minimize Negative Profit" + ) # Tightening constraints @m.Constraint(doc="Limit total module purchases over project span.") def restrict_module_purchases(m): + """ + Enforces a limit on the total number of module purchases across all sites and module types throughout the entire project span. This constraint is crucial for controlling capital expenditure and ensuring that the module acquisition does not exceed a specified threshold, helping to maintain budget constraints. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo ConcreteModel that represents the multi-period optimization problem of a GTL network. + + Returns + ------- + Pyomo.Constraint + A global constraint that limits the aggregate number of modules purchased across all sites to 5, ensuring that the total investment in module purchases remains within predefined limits. + """ return sum(m.modules_purchased[...]) <= 5 @m.Constraint(m.site_pairs, doc="Limit transfers between any two sites") def restrict_module_transfers(m, from_site, to_site): + """ + Imposes a limit on the number of modules that can be transferred between any two sites during the entire project timeline. This constraint helps manage logistics and ensures that module reallocation does not become overly frequent or excessive, which could lead to operational inefficiencies and increased costs. + + Parameters + ---------- + m : Pyomo.ConcreteModel + A Pyomo ConcreteModel that represents the multi-period optimization problem of a GTL network. + from_site : str + Index for the origin site from which modules are being transferred. + to_site : str + Index for the destination site to which modules are being transferred. + + Returns + ------- + Pyomo.Constraint + A constraint limiting the total number of modules transferred from one site to another to 5, providing a control mechanism on the frequency and volume of inter-site module movements. + """ return sum(m.modules_transferred[:, from_site, to_site, :]) <= 5 return m diff --git a/gdplib/stranded_gas/util.py b/gdplib/stranded_gas/util.py index b2822a7..e61a60b 100644 --- a/gdplib/stranded_gas/util.py +++ b/gdplib/stranded_gas/util.py @@ -3,6 +3,19 @@ # Alphanumeric sort, taken from https://nedbatchelder.com/blog/200712/human_sorting.html def tryint(s): + """ + Attempts to convert a string to an integer. If conversion fails, returns the original string. + + Parameters + ---------- + s : str + The string to convert to an integer. + + Returns + ------- + int or str + An integer if `s` can be converted, otherwise the original string. + """ try: return int(s) except ValueError: @@ -10,13 +23,35 @@ def tryint(s): def alphanum_key(s): - """ Turn a string into a list of string and number chunks. + """ + Turn a string into a list of string and number chunks. "z23a" -> ["z", 23, "a"] + + Parameters + ---------- + s : str + The string to split into chunks of strings and integers. + + Returns + ------- + list + A list of strings and integers extracted from the input string. """ return [tryint(c) for c in re.split('([0-9]+)', s)] def alphanum_sorted(l): - """ Sort the given list in the way that humans expect. + """ + Sort the given list in the way that humans expect. + + Parameters + ---------- + l : list + The list of strings to sort. + + Returns + ------- + list + The list sorted in human-like alphanumeric order. """ return sorted(l, key=alphanum_key) diff --git a/gdplib/syngas/syngas_adapted.py b/gdplib/syngas/syngas_adapted.py index e656ca5..634e1c8 100644 --- a/gdplib/syngas/syngas_adapted.py +++ b/gdplib/syngas/syngas_adapted.py @@ -3,7 +3,9 @@ import pandas as pd from pyomo.core.expr.logical_expr import * -from pyomo.core.plugins.transform.logical_to_linear import update_boolean_vars_from_binary +from pyomo.core.plugins.transform.logical_to_linear import ( + update_boolean_vars_from_binary, +) from pyomo.environ import * from pyomo.environ import TerminationCondition as tc import os @@ -16,39 +18,47 @@ def build_model(): m.syngas_techs = Set( doc="syngas process technologies", - initialize=['SMR', 'POX', 'ATR', 'CR', 'DMR', 'BR', 'TR'] + initialize=['SMR', 'POX', 'ATR', 'CR', 'DMR', 'BR', 'TR'], ) m.species = Set( - doc="chemical species", - initialize=['CH4', 'H2O', 'O2', 'H2', 'CO', 'CO2'] + doc="chemical species", initialize=['CH4', 'H2O', 'O2', 'H2', 'CO', 'CO2'] ) m.syngas_tech_units = Set( doc="process units involved in syngas process technologies", - initialize=['compressor', 'exchanger', 'reformer'] + initialize=['compressor', 'exchanger', 'reformer'], ) m.utilities = Set( - doc="process utilities", - initialize=['power', 'coolingwater', 'naturalgas'] + doc="process utilities", initialize=['power', 'coolingwater', 'naturalgas'] ) m.aux_equipment = Set( doc="auxiliary process equipment to condition syngas", - initialize=['absorber1', 'bypass1', 'WGS', 'flash', 'PSA', 'absorber2', 'bypass3', 'compressor', 'bypass4'] + initialize=[ + 'absorber1', + 'bypass1', + 'WGS', + 'flash', + 'PSA', + 'absorber2', + 'bypass3', + 'compressor', + 'bypass4', + ], ) m.process_options = Set( doc="process superstructure options", - initialize=m.syngas_techs | m.aux_equipment + initialize=m.syngas_techs | m.aux_equipment, ) m.extra_process_nodes = Set( doc="extra process nodes for mixers and splitters", - initialize=['in', 'ms1', 'm1', 's1', 'ms2', 'ms3', 'm2', 'ms4', 's2'] + initialize=['in', 'ms1', 'm1', 's1', 'ms2', 'ms3', 'm2', 'ms4', 's2'], ) m.all_unit_types = Set( doc="all process unit types", - initialize=m.process_options | m.syngas_tech_units | m.extra_process_nodes + initialize=m.process_options | m.syngas_tech_units | m.extra_process_nodes, ) m.superstructure_nodes = Set( doc="nodes in the superstructure", - initialize=m.process_options | m.extra_process_nodes + initialize=m.process_options | m.extra_process_nodes, ) group1 = {'absorber1', 'bypass1', 'WGS'} group2 = {'compressor', 'bypass3'} @@ -64,7 +74,7 @@ def build_model(): + [(option, 'ms3') for option in group2] + [('ms3', option) for option in group3] + [(option, 'm2') for option in group3] - + [('PSA', 'ms4'), ('ms4', 'ms3'), ('ms4', 's2'), ('s2', 'm1'), ('s2', 'ms1')] + + [('PSA', 'ms4'), ('ms4', 'ms3'), ('ms4', 's2'), ('s2', 'm1'), ('s2', 'ms1')], ) """ @@ -72,12 +82,21 @@ def build_model(): """ m.flow_limit = Param(initialize=5) - m.min_flow_division = Param(initialize=0.1, doc="minimum flow fraction into active unit") + m.min_flow_division = Param( + initialize=0.1, doc="minimum flow fraction into active unit" + ) m.raw_material_cost = Param( m.species, doc="raw material cost [dollar·kmol-1]", - initialize={'CH4': 2.6826, 'H2O': 0.18, 'O2': 0.7432, 'H2': 8, 'CO': 0, 'CO2': 1.8946} + initialize={ + 'CH4': 2.6826, + 'H2O': 0.18, + 'O2': 0.7432, + 'H2': 8, + 'CO': 0, + 'CO2': 1.8946, + }, ) feed_ratios = { @@ -95,137 +114,280 @@ def build_model(): ('TR', 'O2'): 0.47, } - data_dict = pd.read_csv('%s/syngas_conversion_data.txt' % syngas_dir, delimiter=r'\s+').fillna(0).stack().to_dict() + data_dict = ( + pd.read_csv('%s/syngas_conversion_data.txt' % syngas_dir, delimiter=r'\s+') + .fillna(0) + .stack() + .to_dict() + ) m.syngas_conversion_factor = Param(m.species, m.syngas_techs, initialize=data_dict) - m.co2_ratio = Param(doc="CO2-CO ratio of total carbon in final syngas", initialize=0.05) + m.co2_ratio = Param( + doc="CO2-CO ratio of total carbon in final syngas", initialize=0.05 + ) # Capital costs --> cap = (p1*x + p2)*(B1 + B2*Fmf*Fp) m.p1 = Param( m.all_unit_types, doc="capital cost variable parameter", initialize={ - 'compressor': 172.4, 'exchanger': 59.99, 'reformer': 67.64, 'absorber1': 314.1, 'bypass1': 0, - 'WGS': 314.1, 'flash': 314.1, 'PSA': 3.1863e+04, 'absorber2': 314.1, 'bypass3': 0}, - default=0 + 'compressor': 172.4, + 'exchanger': 59.99, + 'reformer': 67.64, + 'absorber1': 314.1, + 'bypass1': 0, + 'WGS': 314.1, + 'flash': 314.1, + 'PSA': 3.1863e04, + 'absorber2': 314.1, + 'bypass3': 0, + }, + default=0, ) m.p2 = Param( m.all_unit_types, doc="capital cost fixed parameter", initialize={ - 'compressor': 104300, 'exchanger': 187100, 'reformer': 480100, 'absorber1': 1.531e+04, 'bypass1': 0, - 'WGS': 1.531e+04, 'flash': 1.531e+04, 'PSA': 666.34e+03, 'absorber2': 1.531e+04, 'bypass3': 0}, - default=0 + 'compressor': 104300, + 'exchanger': 187100, + 'reformer': 480100, + 'absorber1': 1.531e04, + 'bypass1': 0, + 'WGS': 1.531e04, + 'flash': 1.531e04, + 'PSA': 666.34e03, + 'absorber2': 1.531e04, + 'bypass3': 0, + }, + default=0, ) m.material_factor = Param( m.all_unit_types, doc="capital cost material factor", initialize={ - 'compressor': 3.5, 'exchanger': 1.7, 'reformer': 4, 'absorber1': 1, 'bypass1': 0, - 'WGS': 1, 'flash': 1, 'PSA': 3.5, 'absorber2': 1, 'bypass3': 0}, - default=0 + 'compressor': 3.5, + 'exchanger': 1.7, + 'reformer': 4, + 'absorber1': 1, + 'bypass1': 0, + 'WGS': 1, + 'flash': 1, + 'PSA': 3.5, + 'absorber2': 1, + 'bypass3': 0, + }, + default=0, ) m.B1 = Param( m.all_unit_types, doc="bare module parameter 1", initialize={ - 'compressor': 0, 'exchanger': 1.63, 'reformer': 0, 'absorber1': 2.25, 'bypass1': 0, - 'WGS': 1.49, 'flash': 2.25, 'PSA': 0, 'absorber2': 2.25, 'bypass3': 0}, - default=0 + 'compressor': 0, + 'exchanger': 1.63, + 'reformer': 0, + 'absorber1': 2.25, + 'bypass1': 0, + 'WGS': 1.49, + 'flash': 2.25, + 'PSA': 0, + 'absorber2': 2.25, + 'bypass3': 0, + }, + default=0, ) m.B2 = Param( m.all_unit_types, doc="bare module parameter 2", initialize={ - 'compressor': 1, 'exchanger': 1.66, 'reformer': 1, 'absorber1': 1.82, 'bypass1': 0, - 'WGS': 1.52, 'flash': 1.82, 'PSA': 1, 'absorber2': 1.82, 'bypass3': 0}, - default=0 + 'compressor': 1, + 'exchanger': 1.66, + 'reformer': 1, + 'absorber1': 1.82, + 'bypass1': 0, + 'WGS': 1.52, + 'flash': 1.82, + 'PSA': 1, + 'absorber2': 1.82, + 'bypass3': 0, + }, + default=0, + ) + data_dict = ( + pd.read_csv('%s/syngas_pressure_factor_data.txt' % syngas_dir, delimiter=r'\s+') + .stack() + .to_dict() + ) + m.syngas_pressure_factor = Param( + m.syngas_tech_units, m.syngas_techs, initialize=data_dict ) - data_dict = pd.read_csv('%s/syngas_pressure_factor_data.txt' % syngas_dir, delimiter=r'\s+').stack().to_dict() - m.syngas_pressure_factor = Param(m.syngas_tech_units, m.syngas_techs, initialize=data_dict) m.utility_cost = Param( m.utilities, doc="dollars per kWh of utility u [dollar·KWh-1]", - initialize={'power': 0.127, 'coolingwater': 0.01, 'naturalgas': 0.036} + initialize={'power': 0.127, 'coolingwater': 0.01, 'naturalgas': 0.036}, ) m.utility_emission = Param( m.utilities, doc="CO2 emitted per kW [kgCO2·kWh-1]", - initialize={'power': 0.44732, 'coolingwater': 0, 'naturalgas': 0.2674} + initialize={'power': 0.44732, 'coolingwater': 0, 'naturalgas': 0.2674}, ) m.raw_material_emission = Param( m.species, doc="kg CO2 emitted per kmol of raw material [kgCO2·kmol-1]", - initialize={'CH4': 11.7749, 'H2O': 0, 'O2': 10.9525, 'H2': 0, 'CO': 0, 'CO2': 0} + initialize={ + 'CH4': 11.7749, + 'H2O': 0, + 'O2': 10.9525, + 'H2': 0, + 'CO': 0, + 'CO2': 0, + }, ) m.interest_rate = Param(initialize=0.1, doc="annualization index") m.project_years = Param(initialize=8, doc="annualization years") m.annualization_factor = Expression( - expr=m.interest_rate * (1 + m.interest_rate)**m.project_years / ((1 + m.interest_rate)**m.project_years - 1)) + expr=m.interest_rate + * (1 + m.interest_rate) ** m.project_years + / ((1 + m.interest_rate) ** m.project_years - 1) + ) m.CEPCI2015 = Param(initialize=560, doc="equipment cost index 2015") m.CEPCI2001 = Param(initialize=397, doc="equipment cost index 2001") - m.cost_index_ratio = Param(initialize=m.CEPCI2015 / m.CEPCI2001, doc="cost index ratio") + m.cost_index_ratio = Param( + initialize=m.CEPCI2015 / m.CEPCI2001, doc="cost index ratio" + ) - data_dict = pd.read_csv('%s/syngas_utility_data.txt' % syngas_dir, delimiter=r'\s+').stack().to_dict() + data_dict = ( + pd.read_csv('%s/syngas_utility_data.txt' % syngas_dir, delimiter=r'\s+') + .stack() + .to_dict() + ) m.syngas_tech_utility_rate = Param( - m.utilities, m.syngas_techs, initialize=data_dict, - doc="kWh of utility u per kmol of methane fed in syngas process i [kWh·kmol methane -1]") + m.utilities, + m.syngas_techs, + initialize=data_dict, + doc="kWh of utility u per kmol of methane fed in syngas process i [kWh·kmol methane -1]", + ) - data_dict = pd.read_csv('%s/syngas_num_units_data.txt' % syngas_dir, delimiter=r'\s+').stack().to_dict() + data_dict = ( + pd.read_csv('%s/syngas_num_units_data.txt' % syngas_dir, delimiter=r'\s+') + .stack() + .to_dict() + ) m.syngas_tech_num_units = Param( - m.syngas_tech_units, m.syngas_techs, initialize=data_dict, - doc="number of units h in syngas process i") + m.syngas_tech_units, + m.syngas_techs, + initialize=data_dict, + doc="number of units h in syngas process i", + ) m.syngas_tech_exchanger_area = Param( m.syngas_techs, doc="total exchanger area in process i per kmol·h-1 methane fed [m2·h·kmol methane -1]", - initialize={'SMR': 0.885917, 'POX': 0.153036, 'ATR': 0.260322, 'CR': 0.726294, - 'DMR': 0.116814, 'BR': 0.825808, 'TR': 0.10539}) + initialize={ + 'SMR': 0.885917, + 'POX': 0.153036, + 'ATR': 0.260322, + 'CR': 0.726294, + 'DMR': 0.116814, + 'BR': 0.825808, + 'TR': 0.10539, + }, + ) m.syngas_tech_reformer_duty = Param( m.syngas_techs, doc="reformer duties per kmol methane fed [kWh·kmol methane-1]", - initialize={'SMR': 54.654, 'POX': 39.0104, 'ATR': 44.4600, 'CR': 68.2382, - 'DMR': 68.412, 'BR': 61.442, 'TR': 6.592}) + initialize={ + 'SMR': 54.654, + 'POX': 39.0104, + 'ATR': 44.4600, + 'CR': 68.2382, + 'DMR': 68.412, + 'BR': 61.442, + 'TR': 6.592, + }, + ) m.process_tech_pressure = Param( m.syngas_techs, doc="process i operating pressure [bar]", - initialize={'SMR': 20, 'POX': 30, 'ATR': 25, 'CR': 25, 'DMR': 1, 'BR': 7, 'TR': 20} + initialize={ + 'SMR': 20, + 'POX': 30, + 'ATR': 25, + 'CR': 25, + 'DMR': 1, + 'BR': 7, + 'TR': 20, + }, ) m.final_syngas_pressure = Param(doc="final syngas pressure [bar]", initialize=1) - m.psa_hydrogen_recovery = Param(initialize=0.9, doc="percentage of hydrogen separated from the inlet syngas stream") + m.psa_hydrogen_recovery = Param( + initialize=0.9, + doc="percentage of hydrogen separated from the inlet syngas stream", + ) m.psa_separation_hydrogen_purity = Param(initialize=0.999) - m.Keqw = Param(initialize=83.3429, doc="equilibrium constant for WGS reaction at 250ºC") + m.Keqw = Param( + initialize=83.3429, doc="equilibrium constant for WGS reaction at 250ºC" + ) - m.stoichiometric_number = Param(doc="stoichiometric number of product syngas", initialize=1) + m.stoichiometric_number = Param( + doc="stoichiometric number of product syngas", initialize=1 + ) m.max_impurity = Param(initialize=0.1, doc="maximum allowed of impurities") - m.max_syngas_techs = Param(initialize=1, doc="Number of syngas technologies that can be selected") + m.max_syngas_techs = Param( + initialize=1, doc="Number of syngas technologies that can be selected" + ) """ Variables """ m.flow = Var(m.streams, m.species, bounds=(0, m.flow_limit)) - m.wgs_steam = Var(doc="steam molar flow provided in the WGS reactor [kmol·s-1]", bounds=(0, None)) - m.oxygen_flow = Var(doc="O2 molar flow provided in the selective oxidation reactor [kmol·s-1]", bounds=(0, None)) - m.Fabs1 = Var(bounds=(0, None), doc="molar flow of CO2 absorbed in absorber1 [kmol·s-1]") - m.Fabs2 = Var(bounds=(0, None), doc="molar flow of CO2 absorbed in absorber2 [kmol·s-1]") + m.wgs_steam = Var( + doc="steam molar flow provided in the WGS reactor [kmol·s-1]", bounds=(0, None) + ) + m.oxygen_flow = Var( + doc="O2 molar flow provided in the selective oxidation reactor [kmol·s-1]", + bounds=(0, None), + ) + m.Fabs1 = Var( + bounds=(0, None), doc="molar flow of CO2 absorbed in absorber1 [kmol·s-1]" + ) + m.Fabs2 = Var( + bounds=(0, None), doc="molar flow of CO2 absorbed in absorber2 [kmol·s-1]" + ) m.flash_water = Var(bounds=(0, None), doc="water removed in flash [kmol·s-1]") - m.co2_inject = Var(bounds=(0, None), doc="molar flow of CO2 used to adjust syngas composition [kmol·s-1]") - m.psa_recovered = Var(m.species, bounds=(0, None), doc="pure hydrogen stream retained in the PSA [kmol·s-1]") - m.purge_flow = Var(m.species, bounds=(0, None), doc="purged molar flow from the PSA [kmol·s-1]") - m.final_syngas_flow = Var(m.species, bounds=(0, m.flow_limit), doc="final adjusted syngas molar flow [kmol·s-1]") + m.co2_inject = Var( + bounds=(0, None), + doc="molar flow of CO2 used to adjust syngas composition [kmol·s-1]", + ) + m.psa_recovered = Var( + m.species, + bounds=(0, None), + doc="pure hydrogen stream retained in the PSA [kmol·s-1]", + ) + m.purge_flow = Var( + m.species, bounds=(0, None), doc="purged molar flow from the PSA [kmol·s-1]" + ) + m.final_syngas_flow = Var( + m.species, + bounds=(0, m.flow_limit), + doc="final adjusted syngas molar flow [kmol·s-1]", + ) @m.Expression(m.superstructure_nodes, m.species) def flow_into(m, option, species): - return sum(m.flow[src, sink, species] for src, sink in m.streams if sink == option) + return sum( + m.flow[src, sink, species] for src, sink in m.streams if sink == option + ) @m.Expression(m.superstructure_nodes, m.species) def flow_out_from(m, option, species): - return sum(m.flow[src, sink, species] for src, sink in m.streams if src == option) + return sum( + m.flow[src, sink, species] for src, sink in m.streams if src == option + ) @m.Expression(m.superstructure_nodes) def total_flow_into(m, option): @@ -235,63 +397,115 @@ def total_flow_into(m, option): def total_flow_from(m, option): return sum(m.flow_out_from[option, species] for species in m.species) - m.base_tech_capital_cost = Var(m.syngas_techs, m.syngas_tech_units, bounds=(0, None)) + m.base_tech_capital_cost = Var( + m.syngas_techs, m.syngas_tech_units, bounds=(0, None) + ) m.base_tech_operating_cost = Var(m.syngas_techs, m.utilities, bounds=(0, None)) - m.raw_material_total_cost = Var(bounds=(0, None), doc="total cost of raw materials [$·s-1]") + m.raw_material_total_cost = Var( + bounds=(0, None), doc="total cost of raw materials [$·s-1]" + ) @m.Expression(m.species) def raw_material_flow(m, species): return sum(m.flow['in', tech, species] for tech in m.syngas_techs) - m.syngas_tech_cost = Var(bounds=(0, None), doc="total cost of sygas process [$·y-1]") - m.syngas_tech_emissions = Var(bounds=(0, None), doc="CO2 emission of syngas processes") + m.syngas_tech_cost = Var( + bounds=(0, None), doc="total cost of sygas process [$·y-1]" + ) + m.syngas_tech_emissions = Var( + bounds=(0, None), doc="CO2 emission of syngas processes" + ) @m.Expression(m.syngas_techs, m.syngas_tech_units) def module_factors(m, tech, equip): - return m.B1[equip] + m.B2[equip] * m.material_factor[equip] * m.syngas_pressure_factor[equip, tech] + return ( + m.B1[equip] + + m.B2[equip] + * m.material_factor[equip] + * m.syngas_pressure_factor[equip, tech] + ) @m.Expression(m.syngas_techs, m.syngas_tech_units) def variable_utilization(m, tech, equip): - variable_rate_term = {'compressor': m.syngas_tech_utility_rate['power', tech], - 'exchanger': m.syngas_tech_exchanger_area[tech], - 'reformer': m.syngas_tech_reformer_duty[tech]} + variable_rate_term = { + 'compressor': m.syngas_tech_utility_rate['power', tech], + 'exchanger': m.syngas_tech_exchanger_area[tech], + 'reformer': m.syngas_tech_reformer_duty[tech], + } return variable_rate_term[equip] * m.flow['in', tech, 'CH4'] * 3600 - m.aux_unit_capital_cost = Var(m.aux_equipment, bounds=(0, None), doc="auxiliary unit capital cost [$·h-1]") + m.aux_unit_capital_cost = Var( + m.aux_equipment, bounds=(0, None), doc="auxiliary unit capital cost [$·h-1]" + ) - m.first_stage_outlet_pressure = Var(doc="final pressure in the mixer before WGS and absorber", bounds=(0, None)) - m.syngas_tech_outlet_pressure = Var(m.syngas_techs, doc="final pressure after compression after syngas synthesis [bar]", bounds=(0, None)) - m.syngas_tech_compressor_power = Var(m.syngas_techs, doc="utility of compressor i after syngas synthesis", bounds=(0, None)) - m.syngas_tech_compressor_cost = Var(doc="capital cost of compressors after syngas synthesis", bounds=(0, None)) + m.first_stage_outlet_pressure = Var( + doc="final pressure in the mixer before WGS and absorber", bounds=(0, None) + ) + m.syngas_tech_outlet_pressure = Var( + m.syngas_techs, + doc="final pressure after compression after syngas synthesis [bar]", + bounds=(0, None), + ) + m.syngas_tech_compressor_power = Var( + m.syngas_techs, + doc="utility of compressor i after syngas synthesis", + bounds=(0, None), + ) + m.syngas_tech_compressor_cost = Var( + doc="capital cost of compressors after syngas synthesis", bounds=(0, None) + ) m.Xw = Var(bounds=(0, None), doc="Moles per second reacted in WGS reactor") - m.wgs_inlet_temperature = Var(bounds=(0, None), doc="WGS reactor temperature before adjustment [ºC]") - m.wgs_heater = Var(bounds=(0, None), doc="duty required to preheat the syngas molar flow entering the WGS reactor [kW]") + m.wgs_inlet_temperature = Var( + bounds=(0, None), doc="WGS reactor temperature before adjustment [ºC]" + ) + m.wgs_heater = Var( + bounds=(0, None), + doc="duty required to preheat the syngas molar flow entering the WGS reactor [kW]", + ) m.psa_power = Var(bounds=(0, None), doc="power consumed by the PSA [kWh]") - m.syngas_power = Var(bounds=(0, None), doc="power needed to compress syngas from Pinlet to 30.01 bar") + m.syngas_power = Var( + bounds=(0, None), doc="power needed to compress syngas from Pinlet to 30.01 bar" + ) - m.syngas_total_flow = Expression(expr=sum(m.final_syngas_flow[species] for species in {'H2', 'CO2', 'CO', 'CH4'})) + m.syngas_total_flow = Expression( + expr=sum(m.final_syngas_flow[species] for species in {'H2', 'CO2', 'CO', 'CH4'}) + ) @m.Expression(m.aux_equipment) def aux_module_factors(m, equip): return m.B1[equip] + m.B2[equip] * m.material_factor[equip] m.final_total_emissions = Expression( - expr=m.syngas_tech_emissions*3600 - + m.Fabs1*3600*44 + m.Fabs2*3600*44 - - m.co2_inject*3600*44 + m.oxygen_flow*m.raw_material_emission['O2']*3600 - + m.wgs_steam*m.raw_material_emission['H2O']*3600 - + m.purge_flow['CO2']*3600*44 - + (m.psa_power + m.syngas_power + sum(m.syngas_tech_compressor_power[tech] for tech in m.syngas_techs) - )*m.utility_emission['power'] - + m.wgs_heater*0.094316389565193) + expr=m.syngas_tech_emissions * 3600 + + m.Fabs1 * 3600 * 44 + + m.Fabs2 * 3600 * 44 + - m.co2_inject * 3600 * 44 + + m.oxygen_flow * m.raw_material_emission['O2'] * 3600 + + m.wgs_steam * m.raw_material_emission['H2O'] * 3600 + + m.purge_flow['CO2'] * 3600 * 44 + + ( + m.psa_power + + m.syngas_power + + sum(m.syngas_tech_compressor_power[tech] for tech in m.syngas_techs) + ) + * m.utility_emission['power'] + + m.wgs_heater * 0.094316389565193 + ) m.final_total_cost = Expression( - expr=m.syngas_tech_cost*3600 + m.syngas_tech_compressor_cost - + sum(m.aux_unit_capital_cost[option] for option in m.aux_equipment) * m.annualization_factor - + (m.psa_power + m.syngas_power + sum(m.syngas_tech_compressor_power[tech] for tech in m.syngas_techs) - )*m.utility_cost['power'] - + m.wgs_heater*0.064) + expr=m.syngas_tech_cost * 3600 + + m.syngas_tech_compressor_cost + + sum(m.aux_unit_capital_cost[option] for option in m.aux_equipment) + * m.annualization_factor + + ( + m.psa_power + + m.syngas_power + + sum(m.syngas_tech_compressor_power[tech] for tech in m.syngas_techs) + ) + * m.utility_cost['power'] + + m.wgs_heater * 0.064 + ) """ Constraints @@ -301,16 +515,29 @@ def aux_module_factors(m, equip): def syngas_process_feed_species_ratio(m, tech, species): if species == 'CH4': return Constraint.Skip - return m.flow['in', tech, species] == feed_ratios.get((tech, species), 0) * m.flow['in', tech, 'CH4'] + return ( + m.flow['in', tech, species] + == feed_ratios.get((tech, species), 0) * m.flow['in', tech, 'CH4'] + ) @m.Constraint(m.syngas_techs, m.species) def syngas_conversion_calc(m, tech, species): - return m.flow[tech, 'ms1', species] == m.flow['in', tech, 'CH4'] * m.syngas_conversion_factor[species, tech] + return ( + m.flow[tech, 'ms1', species] + == m.flow['in', tech, 'CH4'] * m.syngas_conversion_factor[species, tech] + ) - m.raw_material_cost_calc = Constraint(expr=m.raw_material_total_cost == ( - sum(m.raw_material_flow[species] * m.raw_material_cost[species] for species in m.species) - + m.wgs_steam * m.raw_material_cost['H2O'] + m.oxygen_flow * m.raw_material_cost['O2'] - )) + m.raw_material_cost_calc = Constraint( + expr=m.raw_material_total_cost + == ( + sum( + m.raw_material_flow[species] * m.raw_material_cost[species] + for species in m.species + ) + + m.wgs_steam * m.raw_material_cost['H2O'] + + m.oxygen_flow * m.raw_material_cost['O2'] + ) + ) @m.Disjunct(m.process_options) def unit_exists(disj, option): @@ -330,7 +557,9 @@ def no_flow_out(disj, species): def unit_exists_or_not(disj, option): return [m.unit_exists[option], m.unit_absent[option]] - m.Yunit = BooleanVar(m.process_options, doc="Boolean variable for existence of a process unit") + m.Yunit = BooleanVar( + m.process_options, doc="Boolean variable for existence of a process unit" + ) for option in m.process_options: m.Yunit[option].associate_binary_var(m.unit_exists[option].binary_indicator_var) @@ -341,44 +570,93 @@ def unit_exists_or_not(disj, option): @tech_selected.Constraint(m.syngas_tech_units) def base_tech_capital_cost_calc(disj, equip): return m.base_tech_capital_cost[tech, equip] == ( - (m.p1[equip] * m.variable_utilization[tech, equip] - + m.p2[equip] * m.syngas_tech_num_units[equip, tech] * m.module_factors[tech, equip]) - * m.cost_index_ratio / 8000 + ( + m.p1[equip] * m.variable_utilization[tech, equip] + + m.p2[equip] + * m.syngas_tech_num_units[equip, tech] + * m.module_factors[tech, equip] + ) + * m.cost_index_ratio + / 8000 ) @tech_selected.Constraint(m.utilities) def base_tech_operating_cost_calc(disj, util): return m.base_tech_operating_cost[tech, util] == ( - m.syngas_tech_utility_rate[util, tech] * m.flow['in', tech, 'CH4'] * 3600 * m.utility_cost[util] + m.syngas_tech_utility_rate[util, tech] + * m.flow['in', tech, 'CH4'] + * 3600 + * m.utility_cost[util] ) - m.syngas_process_cost_calc = Constraint(expr=m.syngas_tech_cost == ( - sum(sum(m.base_tech_capital_cost[tech, equip] for equip in m.syngas_tech_units) * m.annualization_factor - + sum(m.base_tech_operating_cost[tech, util] for util in m.utilities) - for tech in m.syngas_techs) + m.raw_material_total_cost * 3600) / 3600) + m.syngas_process_cost_calc = Constraint( + expr=m.syngas_tech_cost + == ( + sum( + sum( + m.base_tech_capital_cost[tech, equip] + for equip in m.syngas_tech_units + ) + * m.annualization_factor + + sum(m.base_tech_operating_cost[tech, util] for util in m.utilities) + for tech in m.syngas_techs + ) + + m.raw_material_total_cost * 3600 + ) + / 3600 + ) - m.syngas_emissions_calc = Constraint(expr=m.syngas_tech_emissions == ( - # Emissions from utilities - sum(m.base_tech_operating_cost[tech, util] / m.utility_cost[util] * m.utility_emission[util] - for tech in m.syngas_techs for util in m.utilities) - # CO2 consumed by syngas processes - - sum(m.flow['in', tech, 'CO2'] for tech in m.syngas_techs) * 3600 * 44 - # Emissions from raw materials - + sum(m.raw_material_flow[species] * m.raw_material_emission[species] * 3600 for species in m.species)) / 3600) + m.syngas_emissions_calc = Constraint( + expr=m.syngas_tech_emissions + == ( + # Emissions from utilities + sum( + m.base_tech_operating_cost[tech, util] + / m.utility_cost[util] + * m.utility_emission[util] + for tech in m.syngas_techs + for util in m.utilities + ) + # CO2 consumed by syngas processes + - sum(m.flow['in', tech, 'CO2'] for tech in m.syngas_techs) * 3600 * 44 + # Emissions from raw materials + + sum( + m.raw_material_flow[species] * m.raw_material_emission[species] * 3600 + for species in m.species + ) + ) + / 3600 + ) # Syngas process pressure adjustment @m.Disjunct(m.syngas_techs) def stage_one_compressor(disj, tech): - disj.compressor_power_calc = Constraint(expr=m.syngas_tech_compressor_power[tech] == ( - (1.5 / (1.5 - 1)) / 0.8 * (40 + 273) * 8.314 * sum(m.flow[tech, 'ms1', species] for species in m.species) - * ((m.syngas_tech_outlet_pressure[tech] / m.process_tech_pressure[tech]) ** (1.5 - 1 / 1.5) - 1) - )) + disj.compressor_power_calc = Constraint( + expr=m.syngas_tech_compressor_power[tech] + == ( + (1.5 / (1.5 - 1)) + / 0.8 + * (40 + 273) + * 8.314 + * sum(m.flow[tech, 'ms1', species] for species in m.species) + * ( + ( + m.syngas_tech_outlet_pressure[tech] + / m.process_tech_pressure[tech] + ) + ** (1.5 - 1 / 1.5) + - 1 + ) + ) + ) pass @m.Disjunct(m.syngas_techs) def stage_one_bypass(bypass, tech): - bypass.no_pressure_increase = Constraint(expr=m.syngas_tech_outlet_pressure[tech] == m.process_tech_pressure[tech]) + bypass.no_pressure_increase = Constraint( + expr=m.syngas_tech_outlet_pressure[tech] == m.process_tech_pressure[tech] + ) pass @m.Disjunction(m.syngas_techs) @@ -387,21 +665,33 @@ def stage_one_compressor_or_bypass(m, tech): m.Ycomp = BooleanVar(m.syngas_techs) for tech in m.syngas_techs: - m.Ycomp[tech].associate_binary_var(m.stage_one_compressor[tech].binary_indicator_var) + m.Ycomp[tech].associate_binary_var( + m.stage_one_compressor[tech].binary_indicator_var + ) @m.LogicalConstraint(m.syngas_techs) def compressor_implies_tech(m, tech): return m.Ycomp[tech].implies(m.Yunit[tech]) - m.syngas_tech_compressor_cost_calc = Constraint(expr=m.syngas_tech_compressor_cost == ( - ((3.553 * 10 ** 5) * sum(m.Ycomp[tech].get_associated_binary() for tech in m.syngas_techs) - + 586 * sum(m.syngas_tech_compressor_power[tech] for tech in m.syngas_techs)) - * m.annualization_factor / 8000 - )) + m.syngas_tech_compressor_cost_calc = Constraint( + expr=m.syngas_tech_compressor_cost + == ( + ( + (3.553 * 10**5) + * sum(m.Ycomp[tech].get_associated_binary() for tech in m.syngas_techs) + + 586 + * sum(m.syngas_tech_compressor_power[tech] for tech in m.syngas_techs) + ) + * m.annualization_factor + / 8000 + ) + ) for tech in m.syngas_techs: tech_selected = m.unit_exists[tech] - tech_selected.pressure_balance = Constraint(expr=m.first_stage_outlet_pressure <= m.syngas_tech_outlet_pressure[tech]) + tech_selected.pressure_balance = Constraint( + expr=m.first_stage_outlet_pressure <= m.syngas_tech_outlet_pressure[tech] + ) # ms1 balances @m.Constraint(m.species) @@ -414,36 +704,66 @@ def ms1_mass_balance(m, species): @unit_exists.Constraint(m.species) def unit_inlet_composition_balance(disj, species): total_flow = sum(m.flow_out_from['ms1', jj] for jj in m.species) - total_flow_to_this_unit = sum(m.flow_into[this_unit, jj] for jj in m.species) + total_flow_to_this_unit = sum( + m.flow_into[this_unit, jj] for jj in m.species + ) total_flow_species = m.flow_out_from['ms1', species] - return total_flow * m.flow['ms1', this_unit, species] == total_flow_to_this_unit * total_flow_species + return ( + total_flow * m.flow['ms1', this_unit, species] + == total_flow_to_this_unit * total_flow_species + ) # WGS Reactor CO + H2O <-> CO2 + H2 wgs_exists = m.unit_exists['WGS'] - wgs_exists.CH4_balance = Constraint(expr=m.flow_out_from['WGS', 'CH4'] == m.flow_into['WGS', 'CH4']) - wgs_exists.CO_balance = Constraint(expr=m.flow_out_from['WGS', 'CO'] == m.flow_into['WGS', 'CO'] - m.Xw) - wgs_exists.CO2_balance = Constraint(expr=m.flow_out_from['WGS', 'CO2'] == m.flow_into['WGS', 'CO2'] + m.Xw) - wgs_exists.H2_balance = Constraint(expr=m.flow_out_from['WGS', 'H2'] == m.flow_into['WGS', 'H2'] + m.Xw) - wgs_exists.H2O_balance = Constraint(expr=m.flow_out_from['WGS', 'H2O'] == m.flow_into['WGS', 'H2O'] + m.wgs_steam - m.Xw) + wgs_exists.CH4_balance = Constraint( + expr=m.flow_out_from['WGS', 'CH4'] == m.flow_into['WGS', 'CH4'] + ) + wgs_exists.CO_balance = Constraint( + expr=m.flow_out_from['WGS', 'CO'] == m.flow_into['WGS', 'CO'] - m.Xw + ) + wgs_exists.CO2_balance = Constraint( + expr=m.flow_out_from['WGS', 'CO2'] == m.flow_into['WGS', 'CO2'] + m.Xw + ) + wgs_exists.H2_balance = Constraint( + expr=m.flow_out_from['WGS', 'H2'] == m.flow_into['WGS', 'H2'] + m.Xw + ) + wgs_exists.H2O_balance = Constraint( + expr=m.flow_out_from['WGS', 'H2O'] + == m.flow_into['WGS', 'H2O'] + m.wgs_steam - m.Xw + ) wgs_exists.max_molar_reaction = Constraint(expr=m.Xw <= m.flow_into['WGS', 'CO']) wgs_exists.rxn_equilibrium = Constraint( - expr=m.Keqw * m.flow_out_from['WGS', 'CO'] * m.flow_out_from['WGS', 'H2O'] == ( - m.flow_out_from['WGS', 'CO2'] * m.flow_out_from['WGS', 'H2'] - )) + expr=m.Keqw * m.flow_out_from['WGS', 'CO'] * m.flow_out_from['WGS', 'H2O'] + == (m.flow_out_from['WGS', 'CO2'] * m.flow_out_from['WGS', 'H2']) + ) - wgs_exists.capital_cost = Constraint(expr=m.aux_unit_capital_cost['WGS'] == ( - (m.p1['WGS'] * 100 * m.flow_out_from['WGS', 'H2'] + m.p2['WGS']) - * m.aux_module_factors['WGS'] / 8000 * m.cost_index_ratio - )) + wgs_exists.capital_cost = Constraint( + expr=m.aux_unit_capital_cost['WGS'] + == ( + (m.p1['WGS'] * 100 * m.flow_out_from['WGS', 'H2'] + m.p2['WGS']) + * m.aux_module_factors['WGS'] + / 8000 + * m.cost_index_ratio + ) + ) wgs_exists.temperature_balance = Constraint( - expr=m.wgs_inlet_temperature * m.total_flow_into['WGS'] == ( - sum(m.flow[tech, 'ms1', species] for tech in m.syngas_techs for species in m.species) * 250 + expr=m.wgs_inlet_temperature * m.total_flow_into['WGS'] + == ( + sum( + m.flow[tech, 'ms1', species] + for tech in m.syngas_techs + for species in m.species + ) + * 250 + sum(m.flow['s2', 'ms1', species] for species in m.species) * 40 - )) + ) + ) wgs_exists.heater_duty = Constraint( - expr=m.wgs_heater == m.total_flow_into['WGS'] * 46 * (250 - m.wgs_inlet_temperature)) + expr=m.wgs_heater + == m.total_flow_into['WGS'] * 46 * (250 - m.wgs_inlet_temperature) + ) # Bypass 1 bypass1_exists = m.unit_exists['bypass1'] @@ -457,20 +777,31 @@ def bypass1_mass_balance(disj, species): @absorber_exists.Constraint(m.species) def absorber_mass_balance(disj, species): - return m.flow_out_from['absorber1', species] == m.flow_into['absorber1', species] - ( - m.Fabs1 if species == 'CO2' else 0) + return m.flow_out_from['absorber1', species] == m.flow_into[ + 'absorber1', species + ] - (m.Fabs1 if species == 'CO2' else 0) - absorber_exists.co2_absorption = Constraint(expr=m.Fabs1 == 0.96 * m.flow_into['absorber1', 'CO2']) - absorber_exists.cost = Constraint(expr=m.aux_unit_capital_cost['absorber1'] == ( - (m.p1['absorber1'] * 100 * m.Fabs1 + m.p2['absorber1']) - * m.aux_module_factors['absorber1'] / 8000 * m.cost_index_ratio - )) + absorber_exists.co2_absorption = Constraint( + expr=m.Fabs1 == 0.96 * m.flow_into['absorber1', 'CO2'] + ) + absorber_exists.cost = Constraint( + expr=m.aux_unit_capital_cost['absorber1'] + == ( + (m.p1['absorber1'] * 100 * m.Fabs1 + m.p2['absorber1']) + * m.aux_module_factors['absorber1'] + / 8000 + * m.cost_index_ratio + ) + ) m.unit_absent['absorber1'].no_absorption = Constraint(expr=m.Fabs1 == 0) for this_unit in group1: unit_exists = m.unit_exists[this_unit] - unit_exists.minimum_flow = Constraint(expr=m.total_flow_into[this_unit] >= m.total_flow_from['ms1'] * m.min_flow_division) + unit_exists.minimum_flow = Constraint( + expr=m.total_flow_into[this_unit] + >= m.total_flow_from['ms1'] * m.min_flow_division + ) # Flash @m.Constraint(m.species) @@ -481,18 +812,30 @@ def m1_mass_balance(m, species): @flash_exists.Constraint(m.species) def flash_mass_balance(disj, species): - return m.flow_out_from['flash', species] == (m.flow_into['flash', species] if not species == 'H2O' else 0) + return m.flow_out_from['flash', species] == ( + m.flow_into['flash', species] if not species == 'H2O' else 0 + ) - flash_exists.water_sep = Constraint(expr=m.flash_water == m.flow_into['flash', 'H2O']) + flash_exists.water_sep = Constraint( + expr=m.flash_water == m.flow_into['flash', 'H2O'] + ) @m.Constraint(m.species) def post_flash_split_outlet(m, species): - return m.flow_out_from['flash', species] == m.flow_into['PSA', species] + m.flow_into['ms2', species] + return ( + m.flow_out_from['flash', species] + == m.flow_into['PSA', species] + m.flow_into['ms2', species] + ) - flash_exists.cost = Constraint(expr=m.aux_unit_capital_cost['flash'] == ( - (m.p1['flash'] * 100 * m.flash_water + m.p2['flash']) - * m.aux_module_factors['flash'] / 8000 * m.cost_index_ratio - )) + flash_exists.cost = Constraint( + expr=m.aux_unit_capital_cost['flash'] + == ( + (m.p1['flash'] * 100 * m.flash_water + m.p2['flash']) + * m.aux_module_factors['flash'] + / 8000 + * m.cost_index_ratio + ) + ) # PSA psa_exists = m.unit_exists['PSA'] @@ -502,32 +845,60 @@ def psa_inlet_composition_balance(disj, species): total_flow = sum(m.flow_out_from['s1', jj] for jj in m.species) total_flow_to_this_unit = sum(m.flow_into['PSA', jj] for jj in m.species) total_flow_species = m.flow_out_from['s1', species] - return total_flow * m.flow['s1', 'PSA', species] == total_flow_to_this_unit * total_flow_species + return ( + total_flow * m.flow['s1', 'PSA', species] + == total_flow_to_this_unit * total_flow_species + ) @m.Constraint(m.species) def ms2_inlet_composition_balance(disj, species): total_flow = sum(m.flow_out_from['s1', jj] for jj in m.species) total_flow_to_this_unit = sum(m.flow_into['ms2', jj] for jj in m.species) total_flow_species = m.flow_out_from['s1', species] - return total_flow * m.flow['s1', 'ms2', species] == total_flow_to_this_unit * total_flow_species + return ( + total_flow * m.flow['s1', 'ms2', species] + == total_flow_to_this_unit * total_flow_species + ) @psa_exists.Constraint(m.species) def psa_mass_balance(disj, species): - return m.flow_out_from['PSA', species] + m.psa_recovered[species] == m.flow_into['PSA', species] + return ( + m.flow_out_from['PSA', species] + m.psa_recovered[species] + == m.flow_into['PSA', species] + ) @psa_exists.Constraint(m.species) def psa_recovery(disj, species): return m.flow_out_from['PSA', species] == m.flow_into['PSA', species] * ( - (1 - m.psa_hydrogen_recovery if species == 'H2' else m.psa_separation_hydrogen_purity) + ( + 1 - m.psa_hydrogen_recovery + if species == 'H2' + else m.psa_separation_hydrogen_purity + ) ) - psa_exists.cost = Constraint(expr=m.aux_unit_capital_cost['PSA'] == ( - (m.p1['PSA'] * m.flow_into['PSA', 'H2'] + m.p2['PSA']) - * m.aux_module_factors['PSA'] / 8000 - )) - psa_exists.psa_utility = Constraint(expr=m.psa_power*m.first_stage_outlet_pressure**(1.5-1/1.5) == ( - (1.5/(1.5-1))/0.8*(40+273) * 8.314 * m.total_flow_into['PSA'] - * ((30+1e-6)**(1.5-1/1.5) - m.first_stage_outlet_pressure**(1.5-1/1.5)))) + psa_exists.cost = Constraint( + expr=m.aux_unit_capital_cost['PSA'] + == ( + (m.p1['PSA'] * m.flow_into['PSA', 'H2'] + m.p2['PSA']) + * m.aux_module_factors['PSA'] + / 8000 + ) + ) + psa_exists.psa_utility = Constraint( + expr=m.psa_power * m.first_stage_outlet_pressure ** (1.5 - 1 / 1.5) + == ( + (1.5 / (1.5 - 1)) + / 0.8 + * (40 + 273) + * 8.314 + * m.total_flow_into['PSA'] + * ( + (30 + 1e-6) ** (1.5 - 1 / 1.5) + - m.first_stage_outlet_pressure ** (1.5 - 1 / 1.5) + ) + ) + ) psa_absent = m.unit_absent['PSA'] @@ -546,7 +917,10 @@ def ms4_inlet_mass_balance(m, species): @m.Constraint(m.species) def ms4_outlet_mass_balance(m, species): - return m.flow_out_from['ms4', species] + m.purge_flow[species] == m.flow_into['ms4', species] + return ( + m.flow_out_from['ms4', species] + m.purge_flow[species] + == m.flow_into['ms4', species] + ) @m.Constraint(m.species) def purge_flow_limit(m, species): @@ -557,14 +931,20 @@ def s2_inlet_composition(m, species): total_flow = sum(m.flow_into['ms4', jj] for jj in m.species) total_flow_to_this_unit = sum(m.flow_into['s2', jj] for jj in m.species) total_flow_species = m.flow_into['ms4', species] - return total_flow * m.flow['ms4', 's2', species] == total_flow_to_this_unit * total_flow_species + return ( + total_flow * m.flow['ms4', 's2', species] + == total_flow_to_this_unit * total_flow_species + ) @m.Constraint(m.species) def ms4_to_ms3_composition(m, species): total_flow = sum(m.flow_into['ms4', jj] for jj in m.species) total_flow_to_this_unit = sum(m.flow['ms4', 's2', jj] for jj in m.species) total_flow_species = m.flow_into['ms4', species] - return total_flow * m.flow['ms4', 's2', species] == total_flow_to_this_unit * total_flow_species + return ( + total_flow * m.flow['ms4', 's2', species] + == total_flow_to_this_unit * total_flow_species + ) # s2 @m.Constraint(m.species) @@ -579,7 +959,8 @@ def no_flow_s2_to_m1(m, species): @m.Constraint(m.species) def ms2_mass_balance(m, species): return m.flow_out_from['ms2', species] == ( - m.flow_into['ms2', species] + (m.co2_inject if species == 'CO2' else 0)) + m.flow_into['ms2', species] + (m.co2_inject if species == 'CO2' else 0) + ) # bypass3 bypass3_exists = m.unit_exists['bypass3'] @@ -597,27 +978,47 @@ def compressor_inlet_mass_balance(disj, species): @compressor_exists.Constraint(m.species) def compressor_mass_balance(disj, species): - return m.flow_out_from['compressor', species] == m.flow_into['compressor', species] + return ( + m.flow_out_from['compressor', species] == m.flow_into['compressor', species] + ) - compressor_exists.cost = Constraint(expr=m.aux_unit_capital_cost['compressor'] == ( - ((3.553 * 10 ** 5) + 586 * m.syngas_power) / 8000 * m.cost_index_ratio - )) + compressor_exists.cost = Constraint( + expr=m.aux_unit_capital_cost['compressor'] + == (((3.553 * 10**5) + 586 * m.syngas_power) / 8000 * m.cost_index_ratio) + ) - compressor_exists.work = Constraint(expr=m.syngas_power * m.first_stage_outlet_pressure**(1.5-1/1.5) == ( - (1.5/(1.5-1))/0.8*(40+273)*8.314*m.total_flow_into['compressor'] - * (m.final_syngas_pressure**(1.5-1/1.5) - m.first_stage_outlet_pressure**(1.5-1/1.5)))) + compressor_exists.work = Constraint( + expr=m.syngas_power * m.first_stage_outlet_pressure ** (1.5 - 1 / 1.5) + == ( + (1.5 / (1.5 - 1)) + / 0.8 + * (40 + 273) + * 8.314 + * m.total_flow_into['compressor'] + * ( + m.final_syngas_pressure ** (1.5 - 1 / 1.5) + - m.first_stage_outlet_pressure ** (1.5 - 1 / 1.5) + ) + ) + ) no_compressor = m.unit_absent['compressor'] - no_compressor.final_pressure = Constraint(expr=m.first_stage_outlet_pressure >= m.final_syngas_pressure) + no_compressor.final_pressure = Constraint( + expr=m.first_stage_outlet_pressure >= m.final_syngas_pressure + ) - compressor_exists.compressor_minimum_flow = Constraint(expr=m.total_flow_into['compressor'] >= ( - m.total_flow_from['flash'] * m.min_flow_division - )) - psa_exists.psa_minimum_flow = Constraint(expr=m.total_flow_into['PSA'] >= ( - m.total_flow_from['flash'] * m.min_flow_division - )) + compressor_exists.compressor_minimum_flow = Constraint( + expr=m.total_flow_into['compressor'] + >= (m.total_flow_from['flash'] * m.min_flow_division) + ) + psa_exists.psa_minimum_flow = Constraint( + expr=m.total_flow_into['PSA'] + >= (m.total_flow_from['flash'] * m.min_flow_division) + ) - m.compressor_or_bypass = LogicalConstraint(expr=exactly(1, m.Yunit['bypass3'], m.Yunit['compressor'])) + m.compressor_or_bypass = LogicalConstraint( + expr=exactly(1, m.Yunit['bypass3'], m.Yunit['compressor']) + ) # ms3 @m.Constraint(m.species) @@ -633,37 +1034,53 @@ def bypass4_mass_balance(disj, species): # absorber 2 absorber2_exists = m.unit_exists['absorber2'] - + @absorber2_exists.Constraint(m.species) def absorber2_mass_balance(disj, species): return m.flow_out_from['absorber2', species] == ( - m.flow_into['absorber2', species] - (m.Fabs2 if species == 'CO2' else 0)) - - absorber2_exists.co2_absorption = Constraint(expr=m.Fabs2 == 0.96 * m.flow_into['absorber2', 'CO2']) - absorber2_exists.cost = Constraint(expr=m.aux_unit_capital_cost['absorber2'] == ( - (m.p1['absorber2'] * 100 * m.Fabs2 + m.p2['absorber2']) - * m.aux_module_factors['absorber2'] / 8000 * m.cost_index_ratio - )) + m.flow_into['absorber2', species] - (m.Fabs2 if species == 'CO2' else 0) + ) + + absorber2_exists.co2_absorption = Constraint( + expr=m.Fabs2 == 0.96 * m.flow_into['absorber2', 'CO2'] + ) + absorber2_exists.cost = Constraint( + expr=m.aux_unit_capital_cost['absorber2'] + == ( + (m.p1['absorber2'] * 100 * m.Fabs2 + m.p2['absorber2']) + * m.aux_module_factors['absorber2'] + / 8000 + * m.cost_index_ratio + ) + ) m.unit_absent['absorber2'].no_absorption = Constraint(expr=m.Fabs2 == 0) - - m.only_one_absorber = LogicalConstraint(expr=atmost(1, m.Yunit['absorber1'], m.Yunit['absorber2'])) + + m.only_one_absorber = LogicalConstraint( + expr=atmost(1, m.Yunit['absorber1'], m.Yunit['absorber2']) + ) @m.Constraint(m.species) def final_mass_balance(m, species): return m.final_syngas_flow[species] == m.flow_into['m2', species] m.syngas_stoich_number = Constraint( - expr=m.stoichiometric_number * (m.final_syngas_flow['CO'] + m.final_syngas_flow['CO2']) == ( - m.final_syngas_flow['H2'] - m.final_syngas_flow['CO2'] - )) + expr=m.stoichiometric_number + * (m.final_syngas_flow['CO'] + m.final_syngas_flow['CO2']) + == (m.final_syngas_flow['H2'] - m.final_syngas_flow['CO2']) + ) m.impurity_limit = Constraint( - expr=m.max_impurity * sum(m.final_syngas_flow[species] for species in {'CO', 'H2', 'CH4', 'CO2', 'H2O'}) + expr=m.max_impurity + * sum( + m.final_syngas_flow[species] + for species in {'CO', 'H2', 'CH4', 'CO2', 'H2O'} + ) >= sum(m.final_syngas_flow[species] for species in {'CH4', 'H2O'}) ) m.syngas_process_limits = LogicalConstraint( - expr=atmost(m.max_syngas_techs, [m.Yunit[tech] for tech in m.syngas_techs])) + expr=atmost(m.max_syngas_techs, [m.Yunit[tech] for tech in m.syngas_techs]) + ) # Bounds m.wgs_heater.setub(10000) @@ -717,7 +1134,7 @@ def final_mass_balance(m, species): m.syngas_maximum_demand = Constraint(expr=m.syngas_total_flow <= 5) m.final_syngas_flow['CO'].fix(0.3) - m.final_syngas_flow['CO2'].fix(0.3*m.co2_ratio) + m.final_syngas_flow['CO2'].fix(0.3 * m.co2_ratio) m.obj = Objective(expr=m.final_total_cost) @@ -742,15 +1159,26 @@ def final_mass_balance(m, species): def display_nonzeros(var): if var.is_indexed(): + def nonzero_rows(): for k, v in var.items(): if v.value == 0: continue yield k, [value(v.lb), v.value, value(v.ub), v.fixed, v.stale, v.domain] + _attr, _, _header, _ = var._pprint() var._pprint_base_impl( - None, False, "", var.local_name, var.doc, - var.is_constructed(), _attr, nonzero_rows(), _header, lambda k, v: v) + None, + False, + "", + var.local_name, + var.doc, + var.is_constructed(), + _attr, + nonzero_rows(), + _header, + lambda k, v: v, + ) else: var.display() @@ -765,9 +1193,14 @@ def nonzero_rows(): # # solver="cplex", add_options=['GAMS_MODEL.optfile=1;', '$onecho > cplex.opt', 'iis=1', '$offecho'], # ) result = SolverFactory('gdpopt').solve( - m, strategy='LOA', tee=True, mip_solver='gams', - nlp_solver='gams', nlp_solver_args=dict(solver='scip', add_options=['option optcr=0;']), - minlp_solver='gams', minlp_solver_args=dict(solver='baron', add_options=['option optcr=0;']) + m, + strategy='LOA', + tee=True, + mip_solver='gams', + nlp_solver='gams', + nlp_solver_args=dict(solver='scip', add_options=['option optcr=0;']), + minlp_solver='gams', + minlp_solver_args=dict(solver='baron', add_options=['option optcr=0;']), ) if not result.solver.termination_condition == tc.optimal: print("Termination Condition: ", result.solver.termination_condition) diff --git a/generate_model_size_report.py b/generate_model_size_report.py new file mode 100644 index 0000000..96ddac0 --- /dev/null +++ b/generate_model_size_report.py @@ -0,0 +1,36 @@ +from datetime import datetime +from importlib import import_module +from pyomo.util.model_size import build_model_size_report +import pandas as pd + + +if __name__ == "__main__": + instance_list = [ + # "batch_processing", + # "biofuel", + # "disease_model", + # "gdp_col", + # "hda", + "jobshop", + # "kaibel", + # "logical", + # "med_term_purchasing", + # "methanol", + # "mod_hens", + # "modprodnet", + # "stranded_gas", + # "syngas", + ] + current_time = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + timelimit = 600 + + for instance in instance_list: + print("Generating model size report: " + instance) + + model = import_module("gdplib." + instance).build_model() + report = build_model_size_report(model) + report_df = pd.DataFrame(report.overall, index=[0]).T + report_df.index.name = "Component" + report_df.columns = ["Number"] + # Generate the model size report (Markdown) + report_df.to_markdown("gdplib/" + instance + "/" + "model_size_report.md") diff --git a/setup.py b/setup.py index a338303..bd40d61 100644 --- a/setup.py +++ b/setup.py @@ -33,7 +33,7 @@ "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", - "Topic :: Software Development :: Libraries :: Python Modules" + "Topic :: Software Development :: Libraries :: Python Modules", ], ) @@ -41,7 +41,9 @@ setup(setup_requires=['setuptools_scm'], use_scm_version=True, **kwargs) except (ImportError, LookupError): default_version = '1.0.0' - warning('Cannot use .git version: package setuptools_scm not installed ' - 'or .git directory not present.') + warning( + 'Cannot use .git version: package setuptools_scm not installed ' + 'or .git directory not present.' + ) print('Defaulting to version: {}'.format(default_version)) setup(**kwargs)