def build_model(): m = ConcreteModel() 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.discount_rate = Param(initialize=0.08, doc="8%") 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.Param(m.time) def discount_factor(m, t): 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) module_sheet = xlsx_data['modules'].set_index('Type') m.module_types = Set(initialize=module_sheet.columns.tolist(), ) @m.Param(m.module_types) def module_base_cost(m, mtype): return float(module_sheet[mtype]['Capital Cost [MM$]']) @m.Param(m.module_types, doc="Natural gas consumption per module of this type [MMSCF/d]") def unit_gas_consumption(m, mtype): 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): 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]" ) def module_conversion(m, mtype): 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.site_pairs = Set(doc="Pairs of potential sites", initialize=m.potential_sites * m.potential_sites, filter=lambda _, x, y: not x == y) @m.Param(m.potential_sites) def site_x(m, site): return float(site_sheet['x'][site]) @m.Param(m.potential_sites) def site_y(m, 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.Param(m.well_clusters) def well_x(m, well): return float(well_sheet['x'][well]) @m.Param(m.well_clusters) def well_y(m, well): 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) ] 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 = [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) ] @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 mkt_sheet = xlsx_data['markets'].set_index('Market') m.markets = Set(initialize=mkt_sheet.index.tolist()) @m.Param(m.markets) def mkt_x(m, mkt): return float(mkt_sheet['x'][mkt]) @m.Param(m.markets) def mkt_y(m, mkt): return float(mkt_sheet['y'][mkt]) @m.Param(m.markets, doc="Gasoline demand [kBD]") def mkt_demand(m, mkt): 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.Param(m.sources, m.destinations, doc="Distance [mi]") def distance(m, src, dest): if src in m.well_clusters: src_x = m.well_x[src] src_y = m.well_y[src] else: src_x = m.site_x[src] src_y = m.site_y[src] if dest in m.markets: dest_x = m.mkt_x[dest] dest_y = m.mkt_y[dest] else: dest_x = m.site_x[dest] dest_y = m.site_y[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, doc="Number of active modules of each type at a site in a period", domain=Integers, bounds=(0, 50), initialize=1) m.modules_transferred = Var( 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) m.modules_purchased = Var( 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) 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): 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): return 3 * m.discount_factor[t] @m.Param(m.time, doc="Stranded gas price [$/MSCF]") def nat_gas_price(m, t): return 5 * m.discount_factor[t] @m.Param(m.time, doc="Gasoline price [$/gal]") def gasoline_price(m, t): return 2.5 * m.discount_factor[t] @m.Param(m.time, doc="Gasoline transport cost [$/gal/100 miles]") def gasoline_tranport_cost(m, t): return 0.045 * m.discount_factor[t] m.gal_per_bbl = Param(initialize=42, doc="Gallons per barrel") m.days_per_period = Param(initialize=90, doc="Days in a production period") m.learning_factor = Var( m.module_types, doc="Fraction of cost due to economies of mass production", domain=NonNegativeReals, bounds=(0, 1), initialize=1) @m.Disjunct(m.module_types) def mtype_exists(disj, mtype): disj.learning_factor_calc = Constraint( expr=m.learning_factor[mtype] == (1 - m.learning_rate)**( log(sum(m.modules_purchased[mtype, :, :])) / log(2))) m.BigM[disj.learning_factor_calc] = 1 disj.require_module_purchases = Constraint( expr=sum(m.modules_purchased[mtype, :, :]) >= 1) @m.Disjunct(m.module_types) def mtype_absent(disj, mtype): disj.constant_learning_factor = Constraint( expr=m.learning_factor[mtype] == 1) @m.Disjunction(m.module_types) def mtype_existence(m, mtype): 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] m.production = Var(m.potential_sites, m.time, doc="Production of gasoline in a time period [kBD]", domain=NonNegativeReals, bounds=(0, 30), initialize=10) m.gas_consumption = Var( 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) m.gas_flows = Var( 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) m.product_flows = Var( 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) @m.Constraint(m.potential_sites, m.module_types, m.time) def consumption_capacity(m, site, mtype, t): return m.gas_consumption[site, mtype, t] <= ( m.num_modules[mtype, site, t] * m.unit_gas_consumption[mtype]) @m.Constraint(m.potential_sites, m.time) def production_limit(m, site, t): return m.production[site, t] <= sum( m.gas_consumption[site, mtype, t] * m.module_conversion[mtype] for mtype in m.module_types) @m.Expression(m.potential_sites, m.time) def capacity(m, site, t): return sum(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): 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] @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] @m.Constraint(m.potential_sites, m.module_types, m.time) def module_balance(m, site, mtype, t): if 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) else: modules_added = 0 modules_transferred_in = 0 if t >= 1: existing_modules = m.num_modules[mtype, site, t - 1] else: 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) return m.num_modules[mtype, site, t] == (existing_modules + modules_added + modules_transferred_in - modules_transferred_out) @m.Disjunct(m.potential_sites) def site_active(disj, site): pass @m.Disjunct(m.potential_sites) def site_inactive(disj, 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) 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) 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) @m.Disjunction(m.potential_sites) def site_active_or_not(m, site): return [m.site_active[site], m.site_inactive[site]] @m.Disjunct(m.well_clusters, m.potential_sites) def pipeline_exists(disj, well, site): pass @m.Disjunct(m.well_clusters, m.potential_sites) def pipeline_absent(disj, well, site): disj.no_natural_gas_flow = Constraint(expr=sum(m.gas_flows[well, site, t] for t in m.time) == 0) @m.Disjunction(m.well_clusters, m.potential_sites) def pipeline_existence(m, well, site): 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): return sum(m.product_flows[site, mkt, t] # kBD * 1000 # bbl/kB / 1E6 # $ to MM$ * m.days_per_period * m.gasoline_price[t] * m.gal_per_bbl for mkt in m.markets for t in m.time) @m.Expression(m.potential_sites, doc="MM$") def raw_material_cost(m, site): return sum(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) @m.Expression( m.potential_sites, m.markets, doc="Aggregate cost to transport gasoline from a site to market [MM$]") def product_transport_cost(m, site, mkt): return sum(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) @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].indicator_var) # Module transport cost @m.Expression(m.site_pairs, doc="MM$") def module_relocation_cost(m, from_site, to_site): return sum(m.modules_transferred[mtype, from_site, to_site, t] * m.distance[from_site, to_site] / 100 * m.module_transport_distance_cost[t] / 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) @m.Expression(m.potential_sites, doc="MM$") def module_purchase_cost(m, site): 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) @m.Expression(doc="MM$") def profit(m): return (summation(m.product_revenue) - summation(m.raw_material_cost) - summation(m.product_transport_cost) - summation(m.pipeline_construction_cost) - summation(m.module_relocation_cost) - summation(m.module_purchase_cost)) m.neg_profit = Objective(expr=-m.profit) # Tightening constraints @m.Constraint(doc="Limit total module purchases over project span.") def restrict_module_purchases(m): 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): return sum(m.modules_transferred[:, from_site, to_site, :]) <= 5 return m
def build_model(use_cafaro_approximation, num_stages): """Build the model.""" 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_streams = Set( initialize=m.hot_process_streams | m.hot_utility_streams) m.cold_streams = Set( initialize=m.cold_process_streams | m.cold_utility_streams) m.utility_streams = Set( initialize=m.hot_utility_streams | m.cold_utility_streams) m.streams = Set( initialize=m.process_streams | m.utility_streams) m.valid_matches = Set( 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.") # 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.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.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.heat_exchanged = Var( m.valid_matches, m.stages, domain=NonNegativeReals, doc="Heat exchanged from hot stream to cold stream in stage [kW]", 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}) m.utility_usage = Var( m.utility_streams, doc="Hot or cold utility used [kW]", domain=NonNegativeReals, initialize=1, bounds=(0, 5000)) m.stage_entry_T = Var( m.streams, m.stages, doc="Temperature of stream at stage entry.", initialize=350, 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.", initialize=350, 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]))) 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]) for strm in m.hot_process_streams: _fix_and_bound(m.stage_entry_T[strm, 1], m.T_in[strm]) _fix_and_bound(m.stage_exit_T[strm, num_stages], m.T_out[strm]) for strm in m.cold_process_streams: _fix_and_bound(m.stage_exit_T[strm, 1], m.T_out[strm]) _fix_and_bound(m.stage_entry_T[strm, num_stages], m.T_in[strm]) m.BigM = Suffix(direction=Suffix.LOCAL) m.utility_unit_cost = Param( m.utility_streams, doc="Annual unit cost of utilities [$/kW]", 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.exchanger_fixed_unit_cost = Param( m.valid_matches, default=2000) 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) if use_cafaro_approximation: 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.") def module_area_cost_factor(m, hot, cold, area): 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.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) m.U = Param( m.valid_matches, default=0.8, 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.") m.exchanger_hot_side_approach_T = Var( 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 ) m.exchanger_cold_side_approach_T = Var( 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 ) m.LMTD = Var( m.valid_matches, m.stages, doc="Log mean temperature difference across the exchanger.", 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) ) 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, doc="Fixed cost for an exchanger between a hot and cold stream.", domain=NonNegativeReals, bounds=(0, 1E5), initialize=0) m.exchanger_area = Var( m.stages, m.valid_matches, doc="Area for an exchanger between a hot and cold stream.", domain=NonNegativeReals, bounds=(0, 500), initialize=5) m.exchanger_area_cost = Var( m.stages, m.valid_matches, doc="Capital cost contribution from exchanger area.", domain=NonNegativeReals, bounds=(0, 1E5), initialize=1000) @m.Constraint(m.hot_process_streams) def overall_hot_stream_heat_balance(m, strm): 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)) @m.Constraint(m.cold_process_streams) def overall_cold_stream_heat_balance(m, strm): 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)) @m.Constraint(m.utility_streams) def overall_utility_stream_usage(m, strm): return m.utility_usage[strm] == ( 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 ) @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.") 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) @m.Constraint(m.stages, m.hot_process_streams) def hot_stream_monotonic_T_decrease(m, stg, strm): 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): 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): return ( 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): return ( 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): 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)) def _exchanger_exists(disj, hot, cold, stg): disj.indicator_var.value = 1 # 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) ) m.BigM[disj.LMTD_calc] = 160 # disj.MTD_calc = Constraint( # doc="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]) / 2 # ) # 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]) 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]) 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]) 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]) 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]) 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]) def _exchanger_absent(disj, hot, cold, stg): disj.indicator_var.value = 0 disj.no_match_exchanger_cost = Constraint( expr=m.exchanger_area_cost[stg, hot, cold] == 0) disj.no_match_exchanger_area = Constraint( expr=m.exchanger_area[stg, hot, cold] == 0) disj.no_match_exchanger_fixed_cost = Constraint( expr=m.exchanger_fixed_cost[stg, hot, cold] == 0) disj.no_heat_exchange = Constraint( expr=m.heat_exchanged[hot, cold, stg] == 0) m.exchanger_exists = Disjunct( 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) m.exchanger_absent = Disjunct( 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) def _exchanger_exists_or_absent(m, hot, cold, stg): return [m.exchanger_exists[hot, cold, stg], m.exchanger_absent[hot, cold, stg]] m.exchanger_exists_or_absent = Disjunction( 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) # Only hot utility matches in first stage and cold utility matches in last # stage for hot, cold in m.valid_matches: if hot not in m.utility_streams: m.exchanger_exists[hot, cold, 1].deactivate() m.exchanger_absent[hot, cold, 1].indicator_var.fix(1) if cold not in m.utility_streams: m.exchanger_exists[hot, cold, num_stages].deactivate() m.exchanger_absent[hot, cold, num_stages].indicator_var.fix(1) # Exclude utility-stream matches in middle stages for hot, cold, stg in m.valid_matches * (m.stages - [1, num_stages]): if hot in m.utility_streams or cold in m.utility_streams: m.exchanger_exists[hot, cold, stg].deactivate() m.exchanger_absent[hot, cold, stg].indicator_var.fix(1) @m.Expression(m.utility_streams) def utility_cost(m, strm): 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 ) return m