def Total_Storage_Energy_Install_Costs_Annual_rule(m, p): # Handle the degenerate case of no storage projects. if len(m.STORAGE_PROJECTS) == 0: return 0 # Construct and cache a set for summation as needed if not hasattr(m, 'Storage_Build_Summation_dict'): m.Storage_Build_Summation_dict = {} for (proj, p2) in m.STORAGE_PROJECTS * m.PERIODS: m.Storage_Build_Summation_dict[p2] = set( (proj, bld_yr) for bld_yr in m.G_NEW_BUILD_YEARS[m.proj_gen_tech[proj]] if bld_yr <= p2 <= m.proj_end_year[proj, bld_yr]) # Use pop to free memory relevant_proj_build_years = m.Storage_Build_Summation_dict.pop(p) return sum(m.BuildStorageEnergyMWh[proj, bld_yr] * m.proj_storage_energy_overnight_cost[proj, bld_yr] * crf(m.interest_rate, m.g_max_age[m.proj_gen_tech[proj]]) for (proj, bld_yr) in relevant_proj_build_years)
def define_components(mod): """ Adds components to a Pyomo abstract model object to describe generation and storage projects. Unless otherwise stated, all power capacity is specified in units of MW and all sets and parameters are mandatory. PROJECTS is the set of generation and storage projects that have been built or could potentially be built. A project is a combination of generation technology, load zone and location. A particular build-out of a project should also include the year in which construction was complete and additional capacity came online. Members of this set are abbreviated as proj or prj in parameter names and indexes. Use of p instead of prj is discouraged because p is reserved for period. proj_dbid[prj] is an external database id for each project. This is an optional parameter than defaults to the project index. proj_gen_tech[prj] describes what kind of generation technology a projects is using. proj_load_zone[prj] is the load zone this project is built in. VARIABLE_PROJECTS is a subset of PROJECTS that only includes variable generators such as wind or solar that have exogenous constraints on their energy production. BASELOAD_PROJECTS is a subset of PROJECTS that only includes baseload generators such as coal or geothermal. LZ_PROJECTS[lz in LOAD_ZONES] is an indexed set that lists all projects within each load zone. PROJECTS_CAP_LIMITED is the subset of PROJECTS that are capacity limited. Most of these will be generator types that are resource limited like wind, solar or geothermal, but this can be specified for any project. Some existing or proposed projects may have upper bounds on increasing capacity or replacing capacity as it is retired based on permits or local air quality regulations. proj_capacity_limit_mw[proj] is defined for generation technologies that are resource limited and do not compete for land area. This describes the maximum possible capacity of a project in units of megawatts. -- CONSTRUCTION -- PROJECT_BUILDYEARS is a two-dimensional set of projects and the years in which construction or expansion occured or can occur. You can think of a project as a physical site that can be built out over time. BuildYear is the year in which construction is completed and new capacity comes online, not the year when constrution begins. BuildYear will be in the past for existing projects and will be the first year of an investment period for new projects. Investment decisions are made for each project/invest period combination. This set is derived from other parameters for all new construction. This set also includes entries for existing projects that have already been built; information for legacy projects come from other files and their build years will usually not correspond to the set of investment periods. There are two recommended options for abbreviating this set for denoting indexes: typically this should be written out as (proj, build_year) for clarity, but when brevity is more important (prj, b) is acceptable. NEW_PROJ_BUILDYEARS is a subset of PROJECT_BUILDYEARS that only includes projects that have not yet been constructed. This is derived by joining the set of PROJECTS with the set of NEW_GENERATION_BUILDYEARS using generation technology. EXISTING_PROJ_BUILDYEARS is a subset of PROJECT_BUILDYEARS that only includes existing projects. proj_existing_cap[(proj, build_year) in EXISTING_PROJ_BUILDYEARS] is a parameter that describes how much capacity was built in the past for existing projects. BuildProj[proj, build_year] is a decision variable that describes how much capacity of a project to install in a given period. This also stores the amount of capacity that was installed in existing projects that are still online. ProjCapacity[proj, period] is an expression that returns the total capacity online in a given period. This is the sum of installed capacity minus all retirements. Max_Build_Potential[proj] is a constraint defined for each project that enforces maximum capacity limits for resource-limited projects. ProjCapacity <= proj_capacity_limit_mw proj_end_year[(proj, build_year) in PROJECT_BUILDYEARS] is the last investment period in the simulation that a given project build will be operated. It can either indicate retirement or the end of the simulation. This is derived from g_max_age. --- OPERATIONS --- PROJECT_BUILDS_OPERATIONAL_PERIODS[proj, build_year] is an indexed set that describes which periods a given project build will be operational. PROJECT_PERIOD_ONLINE_BUILD_YRS[proj, period] is a complementary indexed set that identify which build years will still be online for the given project in the given period. For some project-period combinations, this will be an empty set. PROJECT_OPERATIONAL_PERIODS describes periods in which projects could be operational. Unlike the related sets above, it is not indexed. Instead it is specified as a set of (proj, period) combinations useful for indexing other model components. --- COSTS --- proj_connect_cost_per_mw[prj] is the cost of grid upgrades to support a new project, in dollars per peak MW. These costs include new transmission lines to a substation, substation upgrades and any other grid upgrades that are needed to deliver power from the interconnect point to the load center or from the load center to the broader transmission network. The following cost components are defined for each project and build year. These parameters will always be available, but will typically be populated by the generic costs specified in generator costs inputs file and the load zone cost adjustment multipliers from load_zones inputs file. proj_overnight_cost[proj, build_year] is the overnight capital cost per MW of capacity for building a project in the given period. By "installed in the given period", I mean that it comes online at the beginning of the given period and construction starts before that. proj_fixed_om[proj, build_year] is the annual fixed Operations and Maintenance costs (O&M) per MW of capacity for given project that was installed in the given period. -- Derived cost parameters -- proj_capital_cost_annual[proj, build_year] is the annualized loan payments for a project's capital and connection costs in units of $/MW per year. This is specified in non-discounted real dollars in a future period, not real dollars in net present value. Proj_Fixed_Costs_Annual[proj, period] is the total annual fixed costs (capital as well as fixed operations & maintenance) incurred by a project in a period. This reflects all of the builds are operational in the given period. This is an expression that reflect decision variables. Total_Proj_Fixed_Costs_Annual[period] is the sum of Proj_Fixed_Costs_Annual[proj, period] for all projects that could be online in the target period. This aggregation is performed for the benefit of the objective function. --- DELAYED IMPLEMENATION --- The following components are not implemented at this time. proj_energy_capacity_overnight_cost[proj, period] defaults to the generic costs of the energy component of a storage technology. It can be overridden if different projects have different cost components. For new CAES projects, this could easily be overridden based on whether an empty gas well was nearby that could be reused, whether the local geological conditions made it easy or difficult to drill and construct underground storage, or whether an above-ground pressurized vessel would be needed. For new battery projects, a generic cost would be completely sufficient. proj_replacement_id[prj] is defined for projects that could replace existing generators. LOCATIONS_WITH_COMPETITION is the set of locations that have limited land area where multiple projects can compete for space. Members of this set are abbreviated as either loc or a lowercase L "l" in parameter names and indexes. loc_area_km2[l] describes the land area available for development at a particular location in units of square kilometers. proj_location[prj] is only defined for projects that compete with each other for limited land space at a given location. It refers to a member of the set LOCATIONS_WITH_COMPETITION. For example, if solar thermal and solar PV projects were competing for the same parcel of land, they would need the same location id. proj_land_footprint_mw_km2[prj] describes the land footprint of a project in units of megawatts per square kilometer. Max_Build_Location[location] is a constraint defined for each project that enforces maximum capacity limits for resource-limited locations. sum(BuildProj/proj_land_footprint_mw_km2) <= loc_area_km2 ccs_pipeline_cost_per_mw[proj, build_year] is the normalize cost of a ccs pipeline sized relative to a project's emissions intensity. ProjCommitToMinBuild[proj, build_year] is a binary decision variable that is only defined for generation technologies that have minimum build requirements as specified by g_min_build_capacity[g]. Enforce_Min_Build[proj, build_year] is a constraint that forces project build-outs to meet the minimum build requirements for generation technologies that have those requirements. This is defined as a pair of constraints that force BuildProj to be 0 when ProjCommitToMinBuild is 0, and force BuildProj to be greater than g_min_build_capacity when ProjCommitToMinBuild is 1. The value used for max_reasonable_build_capacity can be set to something like three times the sum of the peak demand of all load zones in a given period, or just to the maximum possible floating point value. When ProjCommitToMinBuild is 1, the upper constraint should be non-binding. ProjCommitToMinBuild * g_min_build_capacity <= BuildProj ... <= ProjCommitToMinBuild * max_reasonable_build_capacity Decommission[proj, build_year, period] is a decision variable that allows early retirement of portions of projects. Any portion of a project that is decomisssioned early will not incur fixed O&M costs and cannot be brought back into service in later periods. NameplateCapacity[proj, build_year, period] is an expression that describes the amount of capacity available from a particular project build in a given period. This takes into account any decomissioning that occured. NameplateCapacity = BuildProj - sum(Decommission) """ mod.PROJECTS = Set() mod.proj_dbid = Param(mod.PROJECTS, default=lambda m, proj: proj) mod.proj_gen_tech = Param(mod.PROJECTS, within=mod.GENERATION_TECHNOLOGIES) mod.proj_load_zone = Param(mod.PROJECTS, within=mod.LOAD_ZONES) mod.min_data_check('PROJECTS', 'proj_gen_tech', 'proj_load_zone') mod.VARIABLE_PROJECTS = Set(initialize=mod.PROJECTS, filter=lambda m, proj: (m.g_is_variable[m.proj_gen_tech[proj]])) mod.BASELOAD_PROJECTS = Set(initialize=mod.PROJECTS, filter=lambda m, proj: (m.g_is_baseload[m.proj_gen_tech[proj]])) mod.LZ_PROJECTS = Set( mod.LOAD_ZONES, initialize=lambda m, lz: set(p for p in m.PROJECTS if m.proj_load_zone[p] == lz)) mod.PROJECTS_CAP_LIMITED = Set(within=mod.PROJECTS) mod.proj_capacity_limit_mw = Param(mod.PROJECTS_CAP_LIMITED, within=PositiveReals) # Add PROJECTS_LOCATION_LIMITED & associated stuff later mod.FUEL_BASED_PROJECTS = Set( initialize=mod.PROJECTS, filter=lambda m, pr: m.g_uses_fuel[m.proj_gen_tech[pr]]) mod.NON_FUEL_BASED_PROJECTS = Set( initialize=mod.PROJECTS, filter=lambda m, pr: not m.g_uses_fuel[m.proj_gen_tech[pr]]) def init_proj_buildyears(m): project_buildyears = set() for proj in m.PROJECTS: g = m.proj_gen_tech[proj] for b in m.G_NEW_BUILD_YEARS[g]: project_buildyears.add((proj, b)) return project_buildyears mod.NEW_PROJ_BUILDYEARS = Set(dimen=2, initialize=init_proj_buildyears) mod.EXISTING_PROJ_BUILDYEARS = Set(dimen=2) mod.proj_existing_cap = Param(mod.EXISTING_PROJ_BUILDYEARS, within=PositiveReals) mod.min_data_check('proj_existing_cap') mod.PROJECT_BUILDYEARS = Set( dimen=2, initialize=lambda m: set(m.EXISTING_PROJ_BUILDYEARS | m. NEW_PROJ_BUILDYEARS)) def init_proj_end_year(m, proj, build_year): g = m.proj_gen_tech[proj] max_age = m.g_max_age[g] earliest_study_year = m.period_start[m.PERIODS.first()] if build_year + max_age < earliest_study_year: return build_year + max_age for p in m.PERIODS: if build_year + max_age < m.period_end[p]: break return p mod.proj_end_year = Param(mod.PROJECT_BUILDYEARS, initialize=init_proj_end_year) mod.min_data_check('proj_end_year') mod.PROJECT_BUILDS_OPERATIONAL_PERIODS = Set( mod.PROJECT_BUILDYEARS, within=mod.PERIODS, ordered=True, initialize=lambda m, proj, bld_yr: set(p for p in m.PERIODS if bld_yr <= p <= m. proj_end_year[proj, bld_yr])) # The set of build years that could be online in the given period # for the given project. mod.PROJECT_PERIOD_ONLINE_BUILD_YRS = Set( mod.PROJECTS, mod.PERIODS, initialize=lambda m, proj, p: set( bld_yr for (prj, bld_yr) in m.PROJECT_BUILDYEARS if prj == proj and bld_yr <= p <= m.proj_end_year[proj, bld_yr])) def bounds_BuildProj(model, proj, bld_yr): if ((proj, bld_yr) in model.EXISTING_PROJ_BUILDYEARS): return (model.proj_existing_cap[proj, bld_yr], model.proj_existing_cap[proj, bld_yr]) elif (proj in model.PROJECTS_CAP_LIMITED): # This does not replace Max_Build_Potential because # Max_Build_Potential applies across all build years. return (0, model.proj_capacity_limit_mw[proj]) else: return (0, None) mod.BuildProj = Var(mod.PROJECT_BUILDYEARS, within=NonNegativeReals, bounds=bounds_BuildProj) # Some projects are retired before the first study period, so they # don't appear in the objective function or any constraints. # In this case, pyomo may leave the variable value undefined even # after a solve, instead of assigning a value within the allowed # range. This causes errors in the Progressive Hedging code, which # expects every variable to have a value after the solve. So as a # starting point we assign an appropriate value to all the existing # projects here. def BuildProj_assign_default_value(m, proj, bld_yr): m.BuildProj[proj, bld_yr] = m.proj_existing_cap[proj, bld_yr] mod.BuildProj_assign_default_value = BuildAction( mod.EXISTING_PROJ_BUILDYEARS, rule=BuildProj_assign_default_value) # To Do: Subtract retirements after I write support for that. mod.ProjCapacity = Expression( mod.PROJECTS, mod.PERIODS, rule=lambda m, proj, period: sum( m.BuildProj[proj, bld_yr] for bld_yr in m.PROJECT_PERIOD_ONLINE_BUILD_YRS[proj, period])) mod.Max_Build_Potential = Constraint( mod.PROJECTS_CAP_LIMITED, mod.PERIODS, rule=lambda m, proj, p: (m.proj_capacity_limit_mw[proj] >= m.ProjCapacity[proj, p])) # Costs mod.proj_connect_cost_per_mw = Param(mod.PROJECTS, within=NonNegativeReals) mod.min_data_check('proj_connect_cost_per_mw') # The next few parameters need values, but those can come from # their parent technology or this specific project. If neither # data source was provided, throw an error. def proj_overnight_cost_default_rule(m, proj, bld_yr): g = m.proj_gen_tech[proj] if (g, bld_yr) in m.g_overnight_cost: return (m.g_overnight_cost[g, bld_yr] * m.lz_cost_multipliers[m.proj_load_zone[proj]]) else: raise ValueError( ("No overnight costs were provided for project {} " + "or its generation technology {}.").format(proj, g)) mod.proj_overnight_cost = Param(mod.PROJECT_BUILDYEARS, within=NonNegativeReals, default=proj_overnight_cost_default_rule) def proj_fixed_om_default_rule(m, proj, bld_yr): g = m.proj_gen_tech[proj] if (g, bld_yr) in m.g_fixed_o_m: return (m.g_fixed_o_m[g, bld_yr] * m.lz_cost_multipliers[m.proj_load_zone[proj]]) else: raise ValueError( ("No fixed O & M costs were provided for project {} " + "or its generation technology {}.").format(proj, g)) mod.proj_fixed_om = Param(mod.PROJECT_BUILDYEARS, within=NonNegativeReals, default=proj_fixed_om_default_rule) # Derived annual costs mod.proj_capital_cost_annual = Param( mod.PROJECT_BUILDYEARS, initialize=lambda m, proj, bld_yr: ((m.proj_overnight_cost[proj, bld_yr] + m.proj_connect_cost_per_mw[ proj]) * crf(m.interest_rate, m.g_max_age[m.proj_gen_tech[proj]]))) mod.PROJECT_OPERATIONAL_PERIODS = Set( dimen=2, initialize=lambda m: set( (proj, p) for (proj, bld_yr) in m.PROJECT_BUILDYEARS for p in m.PROJECT_BUILDS_OPERATIONAL_PERIODS[proj, bld_yr])) mod.Proj_Fixed_Costs_Annual = Expression( mod.PROJECT_OPERATIONAL_PERIODS, rule=lambda m, proj, p: sum(m.BuildProj[proj, bld_yr] * (m.proj_capital_cost_annual[proj, bld_yr] + m.proj_fixed_om[proj, bld_yr]) for (prj, bld_yr) in m.PROJECT_BUILDYEARS if (p in m.PROJECT_BUILDS_OPERATIONAL_PERIODS[ prj, bld_yr] and proj == prj))) # Summarize costs for the objective function. Units should be total # annual future costs in $base_year real dollars. The objective # function will convert these to base_year Net Present Value in # $base_year real dollars. mod.Total_Proj_Fixed_Costs_Annual = Expression( mod.PERIODS, rule=lambda m, p: sum(m.Proj_Fixed_Costs_Annual[proj, p] for (proj, period) in m.PROJECT_OPERATIONAL_PERIODS if p == period)) mod.cost_components_annual.append('Total_Proj_Fixed_Costs_Annual')
def define_components(m): m.PH_PROJECTS = Set() m.ph_load_zone = Param(m.PH_PROJECTS) m.ph_capital_cost_per_mw = Param(m.PH_PROJECTS, within=NonNegativeReals) m.ph_project_life = Param(m.PH_PROJECTS, within=NonNegativeReals) # annual O&M cost for pumped hydro project, percent of capital cost m.ph_fixed_om_percent = Param(m.PH_PROJECTS, within=NonNegativeReals) # total annual cost m.ph_fixed_cost_per_mw_per_year = Param(m.PH_PROJECTS, initialize=lambda m, p: m.ph_capital_cost_per_mw[p] * (crf(m.interest_rate, m.ph_project_life[p]) + m.ph_fixed_om_percent[p]) ) # round-trip efficiency of the pumped hydro facility m.ph_efficiency = Param(m.PH_PROJECTS) # average energy available from water inflow each day # (system must balance energy net of this each day) m.ph_inflow_mw = Param(m.PH_PROJECTS) # maximum size of pumped hydro project m.ph_max_capacity_mw = Param(m.PH_PROJECTS) # How much pumped hydro to build m.BuildPumpedHydroMW = Var(m.PH_PROJECTS, m.PERIODS, within=NonNegativeReals) m.Pumped_Hydro_Proj_Capacity_MW = Expression(m.PH_PROJECTS, m.PERIODS, rule=lambda m, pr, pe: sum(m.BuildPumpedHydroMW[pr, pp] for pp in m.CURRENT_AND_PRIOR_PERIODS[pe]) ) # flag indicating whether any capacity is added to each project each year m.BuildAnyPumpedHydro = Var(m.PH_PROJECTS, m.PERIODS, within=Binary) # How to run pumped hydro m.PumpedHydroProjGenerateMW = Var(m.PH_PROJECTS, m.TIMEPOINTS, within=NonNegativeReals) m.PumpedHydroProjStoreMW = Var(m.PH_PROJECTS, m.TIMEPOINTS, within=NonNegativeReals) # constraints on construction of pumped hydro # don't build more than the max allowed capacity m.Pumped_Hydro_Max_Build = Constraint(m.PH_PROJECTS, m.PERIODS, rule=lambda m, pr, pe: m.Pumped_Hydro_Proj_Capacity_MW[pr, pe] <= m.ph_max_capacity_mw[pr] ) # force the build flag on for the year(s) when pumped hydro is built m.Pumped_Hydro_Set_Build_Flag = Constraint(m.PH_PROJECTS, m.PERIODS, rule=lambda m, pr, pe: m.BuildPumpedHydroMW[pr, pe] <= m.BuildAnyPumpedHydro[pr, pe] * m.ph_max_capacity_mw[pr] ) # only build in one year (can be deactivated to allow incremental construction) m.Pumped_Hydro_Build_Once = Constraint(m.PH_PROJECTS, rule=lambda m, pr: sum(m.BuildAnyPumpedHydro[pr, pe] for pe in m.PERIODS) <= 1) # only build full project size (deactivated by default, to allow smaller projects) m.Pumped_Hydro_Build_All_Or_None = Constraint(m.PH_PROJECTS, m.PERIODS, rule=lambda m, pr, pe: m.BuildPumpedHydroMW[pr, pe] == m.BuildAnyPumpedHydro[pr, pe] * m.ph_max_capacity_mw[pr] ) m.Deactivate_Pumped_Hydro_Build_All_Or_None = BuildAction(rule=lambda m: m.Pumped_Hydro_Build_All_Or_None.deactivate() ) # limits on pumping and generation m.Pumped_Hydro_Max_Generate_Rate = Constraint(m.PH_PROJECTS, m.TIMEPOINTS, rule=lambda m, pr, t: m.PumpedHydroProjGenerateMW[pr, t] <= m.Pumped_Hydro_Proj_Capacity_MW[pr, m.tp_period[t]] ) m.Pumped_Hydro_Max_Store_Rate = Constraint(m.PH_PROJECTS, m.TIMEPOINTS, rule=lambda m, pr, t: m.PumpedHydroProjStoreMW[pr, t] <= m.Pumped_Hydro_Proj_Capacity_MW[pr, m.tp_period[t]] ) # return reservoir to at least the starting level every day, net of any inflow # it can also go higher than starting level, which indicates spilling surplus water m.Pumped_Hydro_Daily_Balance = Constraint(m.PH_PROJECTS, m.TIMESERIES, rule=lambda m, pr, ts: sum( m.PumpedHydroProjStoreMW[pr, tp] * m.ph_efficiency[pr] + m.ph_inflow_mw[pr] - m.PumpedHydroProjGenerateMW[pr, tp] for tp in m.TS_TPS[ts] ) >= 0 ) m.GeneratePumpedHydro = Expression(m.LOAD_ZONES, m.TIMEPOINTS, rule=lambda m, z, t: sum(m.PumpedHydroProjGenerateMW[pr, t] for pr in m.PH_PROJECTS if m.ph_load_zone[pr]==z) ) m.StorePumpedHydro = Expression(m.LOAD_ZONES, m.TIMEPOINTS, rule=lambda m, z, t: sum(m.PumpedHydroProjStoreMW[pr, t] for pr in m.PH_PROJECTS if m.ph_load_zone[pr]==z) ) # calculate costs m.Pumped_Hydro_Fixed_Cost_Annual = Expression(m.PERIODS, rule=lambda m, pe: sum(m.ph_fixed_cost_per_mw_per_year[pr] * m.Pumped_Hydro_Proj_Capacity_MW[pr, pe] for pr in m.PH_PROJECTS) ) m.cost_components_annual.append('Pumped_Hydro_Fixed_Cost_Annual') # add the pumped hydro to the model's energy balance m.LZ_Energy_Components_Produce.append('GeneratePumpedHydro') m.LZ_Energy_Components_Consume.append('StorePumpedHydro') # total pumped hydro capacity in each zone each period (for reporting) m.Pumped_Hydro_Capacity_MW = Expression(m.LOAD_ZONES, m.PERIODS, rule=lambda m, z, pe: sum(m.Pumped_Hydro_Proj_Capacity_MW[pr, pe] for pr in m.PH_PROJECTS if m.ph_load_zone[pr]==z) ) # force construction of a fixed amount of pumped hydro if m.options.ph_mw is not None: print "Forcing construction of {m} MW of pumped hydro.".format(m=m.options.ph_mw) m.Build_Pumped_Hydro_MW = Constraint(m.LOAD_ZONES, rule=lambda m, z: m.Pumped_Hydro_Capacity_MW[z, m.PERIODS.last()] == m.options.ph_mw ) # force construction of pumped hydro only in a certain period if m.options.ph_year is not None: print "Allowing construction of pumped hydro only in {p}.".format(p=m.options.ph_year) m.Build_Pumped_Hydro_Year = Constraint( m.PH_PROJECTS, m.PERIODS, rule=lambda m, pr, pe: m.BuildPumpedHydroMW[pr, pe] == 0.0 if pe != m.options.ph_year else Constraint.Skip )
def define_components(m): # make helper set identifying all timeseries in each period if hasattr(m, "PERIOD_TS"): print "DEPRECATION NOTE: PERIOD_TS is defined in hydrogen.py, but it already exists, so this can be removed." else: m.PERIOD_TS = Set(m.PERIODS, ordered=True, within=m.TIMESERIES, initialize=lambda m, p: [ts for ts in m.TIMESERIES if m.ts_period[ts] == p]) # electrolyzer details m.hydrogen_electrolyzer_capital_cost_per_mw = Param() m.hydrogen_electrolyzer_fixed_cost_per_mw_year = Param(default=0.0) m.hydrogen_electrolyzer_variable_cost_per_kg = Param(default=0.0) # assumed to include any refurbishment needed m.hydrogen_electrolyzer_kg_per_mwh = Param() # assumed to deliver H2 at enough pressure for liquifier and daily buffering m.hydrogen_electrolyzer_life_years = Param() m.BuildElectrolyzerMW = Var(m.LOAD_ZONES, m.PERIODS, within=NonNegativeReals) m.ElectrolyzerCapacityMW = Expression(m.LOAD_ZONES, m.PERIODS, rule=lambda m, z, p: sum(m.BuildElectrolyzerMW[z, p_] for p_ in m.CURRENT_AND_PRIOR_PERIODS[p])) m.RunElectrolyzerMW = Var(m.LOAD_ZONES, m.TIMEPOINTS, within=NonNegativeReals) m.ProduceHydrogenKgPerHour = Expression(m.LOAD_ZONES, m.TIMEPOINTS, rule=lambda m, z, t: m.RunElectrolyzerMW[z, t] * m.hydrogen_electrolyzer_kg_per_mwh) # note: we assume there is a gaseous hydrogen storage tank that is big enough to buffer # daily production, storage and withdrawals of hydrogen, but we don't include a cost # for this (because it will be negligible compared to the rest of the costs) # This allows the system to do some intra-day arbitrage without going all the way to liquification # liquifier details m.hydrogen_liquifier_capital_cost_per_kg_per_hour = Param() m.hydrogen_liquifier_fixed_cost_per_kg_hour_year = Param(default=0.0) m.hydrogen_liquifier_variable_cost_per_kg = Param(default=0.0) m.hydrogen_liquifier_mwh_per_kg = Param() m.hydrogen_liquifier_life_years = Param() m.BuildLiquifierKgPerHour = Var(m.LOAD_ZONES, m.PERIODS, within=NonNegativeReals) # capacity to build, measured in kg/hour of throughput m.LiquifierCapacityKgPerHour = Expression(m.LOAD_ZONES, m.PERIODS, rule=lambda m, z, p: sum(m.BuildLiquifierKgPerHour[z, p_] for p_ in m.CURRENT_AND_PRIOR_PERIODS[p])) m.LiquifyHydrogenKgPerHour = Var(m.LOAD_ZONES, m.TIMEPOINTS, within=NonNegativeReals) m.LiquifyHydrogenMW = Expression(m.LOAD_ZONES, m.TIMEPOINTS, rule=lambda m, z, t: m.LiquifyHydrogenKgPerHour[z, t] * m.hydrogen_liquifier_mwh_per_kg ) # storage tank details m.liquid_hydrogen_tank_capital_cost_per_kg = Param() m.liquid_hydrogen_tank_life_years = Param() m.BuildLiquidHydrogenTankKg = Var(m.LOAD_ZONES, m.PERIODS, within=NonNegativeReals) # in kg m.LiquidHydrogenTankCapacityKg = Expression(m.LOAD_ZONES, m.PERIODS, rule=lambda m, z, p: sum(m.BuildLiquidHydrogenTankKg[z, p_] for p_ in m.CURRENT_AND_PRIOR_PERIODS[p])) m.StoreLiquidHydrogenKg = Expression(m.LOAD_ZONES, m.TIMESERIES, rule=lambda m, z, ts: m.ts_duration_of_tp[ts] * sum(m.LiquifyHydrogenKgPerHour[z, tp] for tp in m.TS_TPS[ts]) ) m.WithdrawLiquidHydrogenKg = Var(m.LOAD_ZONES, m.TIMESERIES, within=NonNegativeReals) # note: we assume the system will be large enough to neglect boil-off # fuel cell details m.hydrogen_fuel_cell_capital_cost_per_mw = Param() m.hydrogen_fuel_cell_fixed_cost_per_mw_year = Param(default=0.0) m.hydrogen_fuel_cell_variable_cost_per_mwh = Param(default=0.0) # assumed to include any refurbishment needed m.hydrogen_fuel_cell_mwh_per_kg = Param() m.hydrogen_fuel_cell_life_years = Param() m.BuildFuelCellMW = Var(m.LOAD_ZONES, m.PERIODS, within=NonNegativeReals) m.FuelCellCapacityMW = Expression(m.LOAD_ZONES, m.PERIODS, rule=lambda m, z, p: sum(m.BuildFuelCellMW[z, p_] for p_ in m.CURRENT_AND_PRIOR_PERIODS[p])) m.DispatchFuelCellMW = Var(m.LOAD_ZONES, m.TIMEPOINTS, within=NonNegativeReals) m.ConsumeHydrogenKgPerHour = Expression(m.LOAD_ZONES, m.TIMEPOINTS, rule=lambda m, z, t: m.DispatchFuelCellMW[z, t] / m.hydrogen_fuel_cell_mwh_per_kg ) # hydrogen mass balances # note: this allows for buffering of same-day production and consumption # of hydrogen without ever liquifying it m.Hydrogen_Conservation_of_Mass_Daily = Constraint(m.LOAD_ZONES, m.TIMESERIES, rule=lambda m, z, ts: m.StoreLiquidHydrogenKg[z, ts] - m.WithdrawLiquidHydrogenKg[z, ts] == m.ts_duration_of_tp[ts] * sum( m.ProduceHydrogenKgPerHour[z, tp] - m.ConsumeHydrogenKgPerHour[z, tp] for tp in m.TS_TPS[ts] ) ) m.Hydrogen_Conservation_of_Mass_Annual = Constraint(m.LOAD_ZONES, m.PERIODS, rule=lambda m, z, p: sum( (m.StoreLiquidHydrogenKg[z, ts] - m.WithdrawLiquidHydrogenKg[z, ts]) * m.ts_scale_to_year[ts] for ts in m.PERIOD_TS[p] ) == 0 ) # limits on equipment m.Max_Run_Electrolyzer = Constraint(m.LOAD_ZONES, m.TIMEPOINTS, rule=lambda m, z, t: m.RunElectrolyzerMW[z, t] <= m.ElectrolyzerCapacityMW[z, m.tp_period[t]]) m.Max_Run_Fuel_Cell = Constraint(m.LOAD_ZONES, m.TIMEPOINTS, rule=lambda m, z, t: m.DispatchFuelCellMW[z, t] <= m.FuelCellCapacityMW[z, m.tp_period[t]]) m.Max_Run_Liquifier = Constraint(m.LOAD_ZONES, m.TIMEPOINTS, rule=lambda m, z, t: m.LiquifyHydrogenKgPerHour[z, t] <= m.LiquifierCapacityKgPerHour[z, m.tp_period[t]]) # there must be enough storage to hold _all_ the production each period (net of same-day consumption) # note: this assumes we cycle the system only once per year (store all energy, then release all energy) # alternatives: allow monthly or seasonal cycling, or directly model the whole year with inter-day linkages m.Max_Store_Liquid_Hydrogen = Constraint(m.LOAD_ZONES, m.PERIODS, rule=lambda m, z, p: sum(m.StoreLiquidHydrogenKg[z, ts] * m.ts_scale_to_year[ts] for ts in m.PERIOD_TS[p]) <= m.LiquidHydrogenTankCapacityKg[z, p] ) # add electricity consumption and production to the model m.LZ_Energy_Components_Consume.append('RunElectrolyzerMW') m.LZ_Energy_Components_Consume.append('LiquifyHydrogenMW') m.LZ_Energy_Components_Produce.append('DispatchFuelCellMW') # add costs to the model m.HydrogenVariableCost = Expression(m.TIMEPOINTS, rule=lambda m, t: sum( m.ProduceHydrogenKgPerHour[z, t] * m.hydrogen_electrolyzer_variable_cost_per_kg + m.LiquifyHydrogenKgPerHour[z, t] * m.hydrogen_liquifier_variable_cost_per_kg + m.DispatchFuelCellMW[z, t] * m.hydrogen_fuel_cell_variable_cost_per_mwh for z in m.LOAD_ZONES ) ) m.HydrogenFixedCostAnnual = Expression(m.PERIODS, rule=lambda m, p: sum( m.ElectrolyzerCapacityMW[z, p] * ( m.hydrogen_electrolyzer_capital_cost_per_mw * crf(m.interest_rate, m.hydrogen_electrolyzer_life_years) + m.hydrogen_electrolyzer_fixed_cost_per_mw_year) + m.LiquifierCapacityKgPerHour[z, p] * ( m.hydrogen_liquifier_capital_cost_per_kg_per_hour * crf(m.interest_rate, m.hydrogen_liquifier_life_years) + m.hydrogen_liquifier_fixed_cost_per_kg_hour_year) + m.LiquidHydrogenTankCapacityKg[z, p] * ( m.liquid_hydrogen_tank_capital_cost_per_kg * crf(m.interest_rate, m.liquid_hydrogen_tank_life_years)) + m.FuelCellCapacityMW[z, p] * ( m.hydrogen_fuel_cell_capital_cost_per_mw * crf(m.interest_rate, m.hydrogen_fuel_cell_life_years) + m.hydrogen_fuel_cell_fixed_cost_per_mw_year) for z in m.LOAD_ZONES ) ) m.cost_components_tp.append('HydrogenVariableCost') m.cost_components_annual.append('HydrogenFixedCostAnnual')
def define_components(mod): """ Adds components to a Pyomo abstract model object to describe generation and storage projects. Unless otherwise stated, all power capacity is specified in units of MW and all sets and parameters are mandatory. PROJECTS is the set of generation and storage projects that have been built or could potentially be built. A project is a combination of generation technology, load zone and location. A particular build-out of a project should also include the year in which construction was complete and additional capacity came online. Members of this set are abbreviated as proj or prj in parameter names and indexes. Use of p instead of prj is discouraged because p is reserved for period. proj_dbid[prj] is an external database id for each project. This is an optional parameter than defaults to the project index. proj_gen_tech[prj] describes what kind of generation technology a projects is using. proj_load_zone[prj] is the load zone this project is built in. VARIABLE_PROJECTS is a subset of PROJECTS that only includes variable generators such as wind or solar that have exogenous constraints on their energy production. BASELOAD_PROJECTS is a subset of PROJECTS that only includes baseload generators such as coal or geothermal. LZ_PROJECTS[lz in LOAD_ZONES] is an indexed set that lists all projects within each load zone. PROJECTS_CAP_LIMITED is the subset of PROJECTS that are capacity limited. Most of these will be generator types that are resource limited like wind, solar or geothermal, but this can be specified for any project. Some existing or proposed projects may have upper bounds on increasing capacity or replacing capacity as it is retired based on permits or local air quality regulations. proj_capacity_limit_mw[proj] is defined for generation technologies that are resource limited and do not compete for land area. This describes the maximum possible capacity of a project in units of megawatts. -- CONSTRUCTION -- PROJECT_BUILDYEARS is a two-dimensional set of projects and the years in which construction or expansion occured or can occur. You can think of a project as a physical site that can be built out over time. BuildYear is the year in which construction is completed and new capacity comes online, not the year when constrution begins. BuildYear will be in the past for existing projects and will be the first year of an investment period for new projects. Investment decisions are made for each project/invest period combination. This set is derived from other parameters for all new construction. This set also includes entries for existing projects that have already been built; information for legacy projects come from other files and their build years will usually not correspond to the set of investment periods. There are two recommended options for abbreviating this set for denoting indexes: typically this should be written out as (proj, build_year) for clarity, but when brevity is more important (prj, b) is acceptable. NEW_PROJ_BUILDYEARS is a subset of PROJECT_BUILDYEARS that only includes projects that have not yet been constructed. This is derived by joining the set of PROJECTS with the set of NEW_GENERATION_BUILDYEARS using generation technology. EXISTING_PROJ_BUILDYEARS is a subset of PROJECT_BUILDYEARS that only includes existing projects. proj_existing_cap[(proj, build_year) in EXISTING_PROJ_BUILDYEARS] is a parameter that describes how much capacity was built in the past for existing projects. BuildProj[proj, build_year] is a decision variable that describes how much capacity of a project to install in a given period. This also stores the amount of capacity that was installed in existing projects that are still online. ProjCapacity[proj, period] is an expression that returns the total capacity online in a given period. This is the sum of installed capacity minus all retirements. Max_Build_Potential[proj] is a constraint defined for each project that enforces maximum capacity limits for resource-limited projects. ProjCapacity <= proj_capacity_limit_mw proj_end_year[(proj, build_year) in PROJECT_BUILDYEARS] is the last investment period in the simulation that a given project build will be operated. It can either indicate retirement or the end of the simulation. This is derived from g_max_age. --- OPERATIONS --- PROJECT_BUILDS_OPERATIONAL_PERIODS[proj, build_year] is an indexed set that describes which periods a given project build will be operational. PROJECT_PERIOD_ONLINE_BUILD_YRS[proj, period] is a complementary indexed set that identify which build years will still be online for the given project in the given period. For some project-period combinations, this will be an empty set. PROJECT_OPERATIONAL_PERIODS describes periods in which projects could be operational. Unlike the related sets above, it is not indexed. Instead it is specified as a set of (proj, period) combinations useful for indexing other model components. --- COSTS --- proj_connect_cost_per_mw[prj] is the cost of grid upgrades to support a new project, in dollars per peak MW. These costs include new transmission lines to a substation, substation upgrades and any other grid upgrades that are needed to deliver power from the interconnect point to the load center or from the load center to the broader transmission network. The following cost components are defined for each project and build year. These parameters will always be available, but will typically be populated by the generic costs specified in generator costs inputs file and the load zone cost adjustment multipliers from load_zones inputs file. proj_overnight_cost[proj, build_year] is the overnight capital cost per MW of capacity for building a project in the given period. By "installed in the given period", I mean that it comes online at the beginning of the given period and construction starts before that. proj_fixed_om[proj, build_year] is the annual fixed Operations and Maintenance costs (O&M) per MW of capacity for given project that was installed in the given period. -- Derived cost parameters -- proj_capital_cost_annual[proj, build_year] is the annualized loan payments for a project's capital and connection costs in units of $/MW per year. This is specified in non-discounted real dollars in a future period, not real dollars in net present value. Proj_Fixed_Costs_Annual[proj, period] is the total annual fixed costs (capital as well as fixed operations & maintenance) incurred by a project in a period. This reflects all of the builds are operational in the given period. This is an expression that reflect decision variables. Total_Proj_Fixed_Costs_Annual[period] is the sum of Proj_Fixed_Costs_Annual[proj, period] for all projects that could be online in the target period. This aggregation is performed for the benefit of the objective function. --- DELAYED IMPLEMENATION --- The following components are not implemented at this time. proj_energy_capacity_overnight_cost[proj, period] defaults to the generic costs of the energy component of a storage technology. It can be overridden if different projects have different cost components. For new CAES projects, this could easily be overridden based on whether an empty gas well was nearby that could be reused, whether the local geological conditions made it easy or difficult to drill and construct underground storage, or whether an above-ground pressurized vessel would be needed. For new battery projects, a generic cost would be completely sufficient. proj_replacement_id[prj] is defined for projects that could replace existing generators. LOCATIONS_WITH_COMPETITION is the set of locations that have limited land area where multiple projects can compete for space. Members of this set are abbreviated as either loc or a lowercase L "l" in parameter names and indexes. loc_area_km2[l] describes the land area available for development at a particular location in units of square kilometers. proj_location[prj] is only defined for projects that compete with each other for limited land space at a given location. It refers to a member of the set LOCATIONS_WITH_COMPETITION. For example, if solar thermal and solar PV projects were competing for the same parcel of land, they would need the same location id. proj_land_footprint_mw_km2[prj] describes the land footprint of a project in units of megawatts per square kilometer. Max_Build_Location[location] is a constraint defined for each project that enforces maximum capacity limits for resource-limited locations. sum(BuildProj/proj_land_footprint_mw_km2) <= loc_area_km2 ccs_pipeline_cost_per_mw[proj, build_year] is the normalize cost of a ccs pipeline sized relative to a project's emissions intensity. ProjCommitToMinBuild[proj, build_year] is a binary decision variable that is only defined for generation technologies that have minimum build requirements as specified by g_min_build_capacity[g]. Enforce_Min_Build[proj, build_year] is a constraint that forces project build-outs to meet the minimum build requirements for generation technologies that have those requirements. This is defined as a pair of constraints that force BuildProj to be 0 when ProjCommitToMinBuild is 0, and force BuildProj to be greater than g_min_build_capacity when ProjCommitToMinBuild is 1. The value used for max_reasonable_build_capacity can be set to something like three times the sum of the peak demand of all load zones in a given period, or just to the maximum possible floating point value. When ProjCommitToMinBuild is 1, the upper constraint should be non-binding. ProjCommitToMinBuild * g_min_build_capacity <= BuildProj ... <= ProjCommitToMinBuild * max_reasonable_build_capacity Decommission[proj, build_year, period] is a decision variable that allows early retirement of portions of projects. Any portion of a project that is decomisssioned early will not incur fixed O&M costs and cannot be brought back into service in later periods. NameplateCapacity[proj, build_year, period] is an expression that describes the amount of capacity available from a particular project build in a given period. This takes into account any decomissioning that occured. NameplateCapacity = BuildProj - sum(Decommission) """ mod.PROJECTS = Set() mod.proj_dbid = Param(mod.PROJECTS, default=lambda m, proj: proj) mod.proj_gen_tech = Param(mod.PROJECTS, within=mod.GENERATION_TECHNOLOGIES) mod.proj_load_zone = Param(mod.PROJECTS, within=mod.LOAD_ZONES) mod.min_data_check('PROJECTS', 'proj_gen_tech', 'proj_load_zone') mod.VARIABLE_PROJECTS = Set( initialize=mod.PROJECTS, filter=lambda m, proj: ( m.g_is_variable[m.proj_gen_tech[proj]])) mod.BASELOAD_PROJECTS = Set( initialize=mod.PROJECTS, filter=lambda m, proj: ( m.g_is_baseload[m.proj_gen_tech[proj]])) mod.LZ_PROJECTS = Set( mod.LOAD_ZONES, initialize=lambda m, lz: set( p for p in m.PROJECTS if m.proj_load_zone[p] == lz)) mod.PROJECTS_CAP_LIMITED = Set(within=mod.PROJECTS) mod.proj_capacity_limit_mw = Param( mod.PROJECTS_CAP_LIMITED, within=PositiveReals) # Add PROJECTS_LOCATION_LIMITED & associated stuff later mod.FUEL_BASED_PROJECTS = Set( initialize=mod.PROJECTS, filter=lambda m, pr: m.g_uses_fuel[m.proj_gen_tech[pr]]) mod.NON_FUEL_BASED_PROJECTS = Set( initialize=mod.PROJECTS, filter=lambda m, pr: not m.g_uses_fuel[m.proj_gen_tech[pr]]) def init_proj_buildyears(m): project_buildyears = set() for proj in m.PROJECTS: g = m.proj_gen_tech[proj] for b in m.G_NEW_BUILD_YEARS[g]: project_buildyears.add((proj, b)) return project_buildyears mod.NEW_PROJ_BUILDYEARS = Set( dimen=2, initialize=init_proj_buildyears) mod.EXISTING_PROJ_BUILDYEARS = Set( dimen=2) mod.proj_existing_cap = Param( mod.EXISTING_PROJ_BUILDYEARS, within=PositiveReals) mod.min_data_check('proj_existing_cap') mod.PROJECT_BUILDYEARS = Set( dimen=2, initialize=lambda m: set( m.EXISTING_PROJ_BUILDYEARS | m.NEW_PROJ_BUILDYEARS)) def init_proj_end_year(m, proj, build_year): g = m.proj_gen_tech[proj] max_age = m.g_max_age[g] earliest_study_year = m.period_start[m.PERIODS.first()] if build_year + max_age < earliest_study_year: return build_year + max_age for p in m.PERIODS: if build_year + max_age < m.period_end[p]: break return p mod.proj_end_year = Param( mod.PROJECT_BUILDYEARS, initialize=init_proj_end_year) mod.min_data_check('proj_end_year') mod.PROJECT_BUILDS_OPERATIONAL_PERIODS = Set( mod.PROJECT_BUILDYEARS, within=mod.PERIODS, ordered=True, initialize=lambda m, proj, bld_yr: set( p for p in m.PERIODS if bld_yr <= p <= m.proj_end_year[proj, bld_yr])) # The set of build years that could be online in the given period # for the given project. mod.PROJECT_PERIOD_ONLINE_BUILD_YRS = Set( mod.PROJECTS, mod.PERIODS, initialize=lambda m, proj, p: set( bld_yr for (prj, bld_yr) in m.PROJECT_BUILDYEARS if prj == proj and bld_yr <= p <= m.proj_end_year[proj, bld_yr])) def bounds_BuildProj(model, proj, bld_yr): if((proj, bld_yr) in model.EXISTING_PROJ_BUILDYEARS): return (model.proj_existing_cap[proj, bld_yr], model.proj_existing_cap[proj, bld_yr]) elif(proj in model.PROJECTS_CAP_LIMITED): # This does not replace Max_Build_Potential because # Max_Build_Potential applies across all build years. return (0, model.proj_capacity_limit_mw[proj]) else: return (0, None) mod.BuildProj = Var( mod.PROJECT_BUILDYEARS, within=NonNegativeReals, bounds=bounds_BuildProj) # Some projects are retired before the first study period, so they # don't appear in the objective function or any constraints. # In this case, pyomo may leave the variable value undefined even # after a solve, instead of assigning a value within the allowed # range. This causes errors in the Progressive Hedging code, which # expects every variable to have a value after the solve. So as a # starting point we assign an appropriate value to all the existing # projects here. def BuildProj_assign_default_value(m, proj, bld_yr): m.BuildProj[proj, bld_yr] = m.proj_existing_cap[proj, bld_yr] mod.BuildProj_assign_default_value = BuildAction( mod.EXISTING_PROJ_BUILDYEARS, rule=BuildProj_assign_default_value ) # To Do: Subtract retirements after I write support for that. mod.ProjCapacity = Expression( mod.PROJECTS, mod.PERIODS, rule=lambda m, proj, period: sum( m.BuildProj[proj, bld_yr] for bld_yr in m.PROJECT_PERIOD_ONLINE_BUILD_YRS[proj, period])) mod.Max_Build_Potential = Constraint( mod.PROJECTS_CAP_LIMITED, mod.PERIODS, rule=lambda m, proj, p: ( m.proj_capacity_limit_mw[proj] >= m.ProjCapacity[proj, p])) # Costs mod.proj_connect_cost_per_mw = Param(mod.PROJECTS, within=NonNegativeReals) mod.min_data_check('proj_connect_cost_per_mw') # The next few parameters need values, but those can come from # their parent technology or this specific project. If neither # data source was provided, throw an error. def proj_overnight_cost_default_rule(m, proj, bld_yr): g = m.proj_gen_tech[proj] if (g, bld_yr) in m.g_overnight_cost: return(m.g_overnight_cost[g, bld_yr] * m.lz_cost_multipliers[m.proj_load_zone[proj]]) else: raise ValueError( ("No overnight costs were provided for project {} " + "or its generation technology {}.").format(proj, g)) mod.proj_overnight_cost = Param( mod.PROJECT_BUILDYEARS, within=NonNegativeReals, default=proj_overnight_cost_default_rule) def proj_fixed_om_default_rule(m, proj, bld_yr): g = m.proj_gen_tech[proj] if (g, bld_yr) in m.g_fixed_o_m: return(m.g_fixed_o_m[g, bld_yr] * m.lz_cost_multipliers[m.proj_load_zone[proj]]) else: raise ValueError( ("No fixed O & M costs were provided for project {} " + "or its generation technology {}.").format(pr, g)) mod.proj_fixed_om = Param( mod.PROJECT_BUILDYEARS, within=NonNegativeReals, default=proj_fixed_om_default_rule) # Derived annual costs mod.proj_capital_cost_annual = Param( mod.PROJECT_BUILDYEARS, initialize=lambda m, proj, bld_yr: ( (m.proj_overnight_cost[proj, bld_yr] + m.proj_connect_cost_per_mw[proj]) * crf(m.interest_rate, m.g_max_age[m.proj_gen_tech[proj]]))) mod.PROJECT_OPERATIONAL_PERIODS = Set( dimen=2, initialize=lambda m: set( (proj, p) for (proj, bld_yr) in m.PROJECT_BUILDYEARS for p in m.PROJECT_BUILDS_OPERATIONAL_PERIODS[proj, bld_yr])) mod.Proj_Fixed_Costs_Annual = Expression( mod.PROJECT_OPERATIONAL_PERIODS, rule=lambda m, proj, p: sum( m.BuildProj[proj, bld_yr] * (m.proj_capital_cost_annual[proj, bld_yr] + m.proj_fixed_om[proj, bld_yr]) for (prj, bld_yr) in m.PROJECT_BUILDYEARS if (p in m.PROJECT_BUILDS_OPERATIONAL_PERIODS[prj, bld_yr] and proj == prj))) # Summarize costs for the objective function. Units should be total # annual future costs in $base_year real dollars. The objective # function will convert these to base_year Net Present Value in # $base_year real dollars. mod.Total_Proj_Fixed_Costs_Annual = Expression( mod.PERIODS, rule=lambda m, p: sum( m.Proj_Fixed_Costs_Annual[proj, p] for (proj, period) in m.PROJECT_OPERATIONAL_PERIODS if p == period)) mod.cost_components_annual.append('Total_Proj_Fixed_Costs_Annual')
def define_components(mod): """ Adds components to a Pyomo abstract model object to describe bulk transmission of an electric grid. This includes parameters, build decisions and constraints. Unless otherwise stated, all power capacity is specified in units of MW and all sets and parameters are mandatory. TRANSMISSION_LINES is the complete set of transmission pathways connecting load zones. Each member of this set is a one dimensional identifier such as "A-B". This set has no regard for directionality of transmisison lines and will generate an error if you specify two lines that move in opposite directions such as (A to B) and (B to A). Another derived set - TRANS_LINES_DIRECTIONAL - stores directional information. Transmission may be abbreviated as trans or tx in parameter names or indexes. trans_lz1[tx] and trans_lz2[tx] specify the load zones at either end of a transmission line. The order of 1 and 2 is unimportant, but you are encouraged to be consistent to simplify merging information back into external databases. trans_dbid[tx in TRANSMISSION_LINES] is an external database identifier for each transmission line. This is an optional parameter than defaults to the identifier of the transmission line. trans_length_km[tx in TRANSMISSION_LINES] is the length of each transmission line in kilometers. trans_efficiency[tx in TRANSMISSION_LINES] is the proportion of energy sent down a line that is delivered. If 2 percent of energy sent down a line is lost, this value would be set to 0.98. trans_new_build_allowed[tx in TRANSMISSION_LINES] is a binary value indicating whether new transmission build-outs are allowed along a transmission line. This optional parameter defaults to True. TRANS_BUILD_YEARS is the set of transmission lines and years in which they have been or could be built. This set includes past and potential future builds. All future builds must come online in the first year of an investment period. This set is composed of two elements with members: (tx, build_year). For existing transmission where the build years are not known, build_year is set to 'Legacy'. EXISTING_TRANS_BLD_YRS is a subset of TRANS_BUILD_YEARS that lists builds that happened before the first investment period. For most datasets the build year is unknown, so is it always set to 'Legacy'. existing_trans_cap[tx in TRANSMISSION_LINES] is a parameter that describes how many MW of capacity has been installed before the start of the study. NEW_TRANS_BLD_YRS is a subset of TRANS_BUILD_YEARS that describes potential builds. BuildTrans[(tx, bld_yr) in TRANS_BUILD_YEARS] is a decision variable that describes the transfer capacity in MW installed on a cooridor in a given build year. For existing builds, this variable is locked to the existing capacity. TransCapacity[(tx, bld_yr) in TRANS_BUILD_YEARS] is an expression that returns the total nameplate transfer capacity of a transmission line in a given period. This is the sum of existing and newly-build capacity. trans_derating_factor[tx in TRANSMISSION_LINES] is an overall derating factor for each transmission line that can reflect forced outage rates, stability or contingency limitations. This parameter is optional and defaults to 1. This parameter should be in the range of 0 to 1, being 0 a value that disables the line completely. TransCapacityAvailable[(tx, bld_yr) in TRANS_BUILD_YEARS] is an expression that returns the available transfer capacity of a transmission line in a given period, taking into account the nameplate capacity and derating factor. trans_terrain_multiplier[tx in TRANSMISSION_LINES] is a cost adjuster applied to each transmission line that reflects the additional costs that may be incurred for traversing that specific terrain. Crossing mountains or cities will be more expensive than crossing plains. This parameter is optional and defaults to 1. This parameter should be in the range of 0.5 to 3. trans_capital_cost_per_mw_km describes the generic costs of building new transmission in units of $BASE_YEAR per MW transfer capacity per km. This is optional and defaults to 1000. trans_lifetime_yrs is the number of years in which a capital construction loan for a new transmission line is repaid. This optional parameter defaults to 20 years based on 2009 WREZ transmission model transmission data. At the end of this time, we assume transmission lines will be rebuilt at the same cost. trans_fixed_o_m_fraction is describes the fixed Operations and Maintenance costs as a fraction of capital costs. This optional parameter defaults to 0.03 based on 2009 WREZ transmission model transmission data costs for existing transmission maintenance. trans_cost_hourly[tx TRANSMISSION_LINES] is the cost of building transmission lines in units of $BASE_YEAR / MW- transfer-capacity / hour. This derived parameter is based on the total annualized capital and fixed O&M costs, then divides that by hours per year to determine the portion of costs incurred hourly. TRANS_DIRECTIONAL is a derived set of directional paths that electricity can flow along transmission lines. Each element of this set is a two-dimensional entry that describes the origin and destination of the flow: (load_zone_from, load_zone_to). Every transmission line will generate two entries in this set. Members of this set are abbreviated as trans_d where possible, but may be abbreviated as tx in situations where brevity is important and it is unlikely to be confused with the overall transmission line. trans_d_line[trans_d] is the transmission line associated with this directional path. PERIOD_RELEVANT_TRANS_BUILDS[p in PERIODS] is an indexed set that describes which transmission builds will be operational in a given period. Currently, transmission lines are kept online indefinitely, with parts being replaced as they wear out. PERIOD_RELEVANT_TRANS_BUILDS[p] will return a subset of (tx, bld_yr) in TRANS_BUILD_YEARS. --- Delayed implementation --- is_dc_line ... Do I even need to implement this? --- NOTES --- The cost stream over time for transmission lines differs from the SWITCH-WECC model. The SWITCH-WECC model assumed new transmission had a financial lifetime of 20 years, which was the length of the loan term. During this time, fixed operations & maintenance costs were also incurred annually and these were estimated to be 3 percent of the initial capital costs. These fixed O&M costs were obtained from the 2009 WREZ transmission model transmission data costs for existing transmission maintenance .. most of those lines were old and their capital loans had been paid off, so the O&M were the costs of keeping them operational. SWITCH-WECC basically assumed the lines could be kept online indefinitely with that O&M budget, with components of the lines being replaced as needed. This payment schedule and lifetimes was assumed to hold for both existing and new lines. This made the annual costs change over time, which could create edge effects near the end of the study period. SWITCH-WECC had different cost assumptions for local T&D; capital expenses and fixed O&M expenses were rolled in together, and those were assumed to continue indefinitely. This basically assumed that local T&D would be replaced at the end of its financial lifetime. SWITCH-Pyomo treats all transmission and distribution (long- distance or local) the same. Any capacity that is built will be kept online indefinitely. At the end of its financial lifetime, existing capacity will be retired and rebuilt, so the annual cost of a line upgrade will remain constant in every future year. """ mod.TRANSMISSION_LINES = Set() mod.trans_lz1 = Param(mod.TRANSMISSION_LINES, within=mod.LOAD_ZONES) mod.trans_lz2 = Param(mod.TRANSMISSION_LINES, within=mod.LOAD_ZONES) mod.min_data_check('TRANSMISSION_LINES', 'trans_lz1', 'trans_lz2') mod.trans_dbid = Param(mod.TRANSMISSION_LINES, default=lambda m, tx: tx) mod.trans_length_km = Param(mod.TRANSMISSION_LINES, within=PositiveReals) mod.trans_efficiency = Param( mod.TRANSMISSION_LINES, within=PositiveReals, validate=lambda m, val, tx: val <= 1) mod.EXISTING_TRANS_BLD_YRS = Set( dimen=2, initialize=lambda m: set( (tx, 'Legacy') for tx in m.TRANSMISSION_LINES)) mod.existing_trans_cap = Param( mod.TRANSMISSION_LINES, within=NonNegativeReals) mod.min_data_check( 'trans_length_km', 'trans_efficiency', 'EXISTING_TRANS_BLD_YRS', 'existing_trans_cap') mod.trans_new_build_allowed = Param( mod.TRANSMISSION_LINES, within=Boolean, default=True) mod.NEW_TRANS_BLD_YRS = Set( dimen=2, initialize=lambda m: m.TRANSMISSION_LINES * m.PERIODS, filter=lambda m, tx, p: m.trans_new_build_allowed[tx]) mod.TRANS_BUILD_YEARS = Set( dimen=2, initialize=lambda m: m.EXISTING_TRANS_BLD_YRS | m.NEW_TRANS_BLD_YRS) mod.PERIOD_RELEVANT_TRANS_BUILDS = Set( mod.PERIODS, within=mod.TRANS_BUILD_YEARS, initialize=lambda m, p: set( (tx, bld_yr) for (tx, bld_yr) in m.TRANS_BUILD_YEARS if bld_yr <= p)) def bounds_BuildTrans(model, tx, bld_yr): if((tx, bld_yr) in model.EXISTING_TRANS_BLD_YRS): return (model.existing_trans_cap[tx], model.existing_trans_cap[tx]) else: return (0, None) mod.BuildTrans = Var( mod.TRANS_BUILD_YEARS, within=NonNegativeReals, bounds=bounds_BuildTrans) mod.TransCapacity = Expression( mod.TRANSMISSION_LINES, mod.PERIODS, rule=lambda m, tx, period: sum( m.BuildTrans[tx, bld_yr] for (tx2, bld_yr) in m.TRANS_BUILD_YEARS if tx2 == tx and (bld_yr == 'Legacy' or bld_yr <= period))) mod.trans_derating_factor = Param( mod.TRANSMISSION_LINES, within=NonNegativeReals, default=1, validate=lambda m, val, tx: val <= 1) mod.TransCapacityAvailable = Expression( mod.TRANSMISSION_LINES, mod.PERIODS, rule=lambda m, tx, period: ( m.TransCapacity[tx, period] * m.trans_derating_factor[tx])) mod.trans_terrain_multiplier = Param( mod.TRANSMISSION_LINES, within=Reals, default=1, validate=lambda m, val, tx: val >= 0.5 and val <= 3) mod.trans_capital_cost_per_mw_km = Param( within=PositiveReals, default=1000) mod.trans_lifetime_yrs = Param( within=PositiveReals, default=20) mod.trans_fixed_o_m_fraction = Param( within=PositiveReals, default=0.03) # Total annual fixed costs for building new transmission lines... # Multiply capital costs by capital recover factor to get annual # payments. Add annual fixed O&M that are expressed as a fraction of # overnight costs. mod.trans_cost_annual = Param( mod.TRANSMISSION_LINES, within=PositiveReals, initialize=lambda m, tx: ( m.trans_capital_cost_per_mw_km * m.trans_terrain_multiplier[tx] * m.trans_length_km[tx] * (crf(m.interest_rate, m.trans_lifetime_yrs) + m.trans_fixed_o_m_fraction))) # An expression to summarize annual costs for the objective # function. Units should be total annual future costs in $base_year # real dollars. The objective function will convert these to # base_year Net Present Value in $base_year real dollars. mod.Trans_Fixed_Costs_Annual = Expression( mod.PERIODS, rule=lambda m, p: sum( m.BuildTrans[tx, bld_yr] * m.trans_cost_annual[tx] for (tx, bld_yr) in m.PERIOD_RELEVANT_TRANS_BUILDS[p])) mod.cost_components_annual.append('Trans_Fixed_Costs_Annual') def init_TRANS_DIRECTIONAL(model): tx_dir = set() for tx in model.TRANSMISSION_LINES: tx_dir.add((model.trans_lz1[tx], model.trans_lz2[tx])) tx_dir.add((model.trans_lz2[tx], model.trans_lz1[tx])) return tx_dir mod.TRANS_DIRECTIONAL = Set( dimen=2, initialize=init_TRANS_DIRECTIONAL) mod.CONNECTED_LOAD_ZONES = Set( mod.LOAD_ZONES, initialize=lambda m, lz: set( z for z in m.LOAD_ZONES if (lz,z) in m.TRANS_DIRECTIONAL)) def init_trans_d_line(m, lz_from, lz_to): for tx in m.TRANSMISSION_LINES: if((m.trans_lz1[tx] == lz_from and m.trans_lz2[tx] == lz_to) or (m.trans_lz2[tx] == lz_from and m.trans_lz1[tx] == lz_to)): return tx mod.trans_d_line = Param( mod.TRANS_DIRECTIONAL, within=mod.TRANSMISSION_LINES, initialize=init_trans_d_line)
def define_components(mod): """ STORAGE_TECH is a subset of GENERATION_TECHNOLOGIES that can store electricity for later discharge. Members of this set can be abbreviated as storage or s. g_storage_efficiency[STORAGE_TECH] describes the round trip efficiency of a storage technology. A storage technology that is 75 percent efficient would have a storage_efficiency of .75. If 1 MWh was stored in such a storage project, 750 kWh would be available for extraction later. Internal leakage or energy dissipation of storage technologies is assumed to be neglible, which is consistent with short-duration storage technologies currently on the market which tend to consume stored power within 1 day. If a given storage technology has significant internal discharge when it stores power for extended time perios, then those behaviors will need to be modeled in more detail. g_store_to_release_ratio[STORAGE_TECH] describes the maximum rate that energy can be stored, expressed as a ratio of discharge power capacity. This is an optional parameter and will default to 1. If a storage project has 1 MW of dischage capacity and a max_store_rate of 1.2, then it can consume up to 1.2 MW of power while charging. g_storage_energy_overnight_cost[storage, period] is the overnight capital cost per MWh of energy capacity for building the given storage technology installed in the given investment period. This is only defined for storage technologies. Note that this describes the energy component and the overnight_cost describes the power component. The previous three parameters provide default values for projects that implement that technology. These parameters may be overridden on a project-specific basis via proj_storage_efficiency[STORAGE_PROJECTS], proj_store_to_release_ratio[STORAGE_PROJECTS], and proj_storage_energy_overnight_cost[(proj, bld_yr) in STORAGE_PROJECT_BUILDYEARS] STORAGE_PROJECTS is the set of projects that use storage technologies. STORAGE_PROJECT_BUILDYEARS is the subset of PROJECT_BUILDYEARS, restricted to storage projects. BuildStorageEnergyMWh[(proj, bld_yr) in STORAGE_PROJECT_BUILDYEARS] is a decision of how much energy capacity to build onto a storage project. This is analogous to BuildProj, but for energy rather than power. Total_Storage_Energy_Install_Costs_Annual[PERIODS] is an expression of the annual costs incurred by the BuildStorageEnergyMWh decision. StorageEnergyCapacity[proj, period] is an expression describing the cumulative available energy capacity of BuildStorageEnergyMWh. This is analogous to ProjCapacity. STORAGE_PROJ_DISPATCH_POINTS is the subset of PROJ_DISPATCH_POINTS, restricted to storage projects. ChargeStorage[(proj, t) in STORAGE_PROJ_DISPATCH_POINTS] is a dispatch decision of how much to charge a storage project in each timepoint. LZ_NetCharge[LOAD_ZONE, TIMEPOINT] is an expression describing the aggregate impact of ChargeStorage in each load zone and timepoint. Charge_Storage_Upper_Limit[(proj, t) in STORAGE_PROJ_DISPATCH_POINTS] constrains ChargeStorage to available power capacity (accounting for proj_store_to_release_ratio) StateOfChargeMWh[(proj, t) in STORAGE_PROJ_DISPATCH_POINTS] is a variable for tracking state of charge. This value stores the state of charge at the end of each timepoint for each storage project. Track_State_Of_Charge[(proj, t) in STORAGE_PROJ_DISPATCH_POINTS] constrains StateOfChargeMWh based on the StateOfChargeMWh in the previous timepoint, ChargeStorage and DispatchProj. State_Of_Charge_Upper_Limit[(proj, t) in STORAGE_PROJ_DISPATCH_POINTS] constrains StateOfChargeMWh based on installed energy capacity. """ mod.STORAGE_PROJECTS = Set(within=mod.PROJECTS) mod.proj_storage_efficiency = Param( mod.STORAGE_PROJECTS, within=PercentFraction) mod.proj_store_to_release_ratio = Param( mod.STORAGE_PROJECTS, within=PositiveReals, default=1.0) mod.STORAGE_PROJECT_BUILDYEARS = Set( dimen=2, initialize=mod.PROJECT_BUILDYEARS, filter=lambda m, proj, bld_yr: proj in m.STORAGE_PROJECTS) mod.proj_storage_energy_overnight_cost = Param( mod.STORAGE_PROJECT_BUILDYEARS, within=NonNegativeReals) mod.min_data_check('proj_storage_energy_overnight_cost') mod.BuildStorageEnergyMWh = Var( mod.STORAGE_PROJECT_BUILDYEARS, within=NonNegativeReals) # Summarize capital costs of energy storage for the objective function. mod.Total_Storage_Energy_Install_Costs_Annual = Expression( mod.PERIODS, rule=lambda m, p: sum(m.BuildStorageEnergyMWh[proj, bld_yr] * m.proj_storage_energy_overnight_cost[proj, bld_yr] * crf(m.interest_rate, m.proj_max_age[proj]) for (proj, bld_yr) in m.STORAGE_PROJECT_BUILDYEARS)) mod.cost_components_annual.append( 'Total_Storage_Energy_Install_Costs_Annual') mod.StorageEnergyCapacity = Expression( mod.STORAGE_PROJECTS, mod.PERIODS, rule=lambda m, proj, period: sum( m.BuildStorageEnergyMWh[proj, bld_yr] for bld_yr in m.PROJECT_PERIOD_ONLINE_BUILD_YRS[proj, period])) mod.STORAGE_PROJ_DISPATCH_POINTS = Set( initialize=mod.PROJ_DISPATCH_POINTS, filter=lambda m, proj, t: proj in m.STORAGE_PROJECTS) mod.ChargeStorage = Var( mod.STORAGE_PROJ_DISPATCH_POINTS, within=NonNegativeReals) # Summarize storage charging for the energy balance equations def LZ_NetCharge_rule(m, lz, t): # Construct and cache a set for summation as needed if not hasattr(m, 'Storage_Charge_Summation_dict'): m.Storage_Charge_Summation_dict = {} for (lz2, t2) in m.LOAD_ZONES * m.TIMEPOINTS: m.Storage_Charge_Summation_dict[lz2, t2] = set() for proj in m.PROJECTS_ACTIVE_IN_TIMEPOINT[t]: if (proj not in m.STORAGE_PROJECTS or m.proj_load_zone[proj] != lz2): continue m.Storage_Charge_Summation_dict[lz2, t2].add(proj) # Use pop to free memory relevant_projects = m.Storage_Charge_Summation_dict.pop((lz, t)) return sum(m.ChargeStorage[proj, t] for proj in relevant_projects) mod.LZ_NetCharge = Expression( mod.LOAD_ZONES, mod.TIMEPOINTS, rule=LZ_NetCharge_rule) # Register net dispatch as contributing to a load zone's energy mod.LZ_Energy_Components_Consume.append('LZ_NetCharge') def Charge_Storage_Upper_Limit_rule(m, proj, t): return m.ChargeStorage[proj,t] <= \ m.DispatchUpperLimit[proj, t] * m.proj_store_to_release_ratio[proj] mod.Charge_Storage_Upper_Limit = Constraint( mod.STORAGE_PROJ_DISPATCH_POINTS, rule=Charge_Storage_Upper_Limit_rule) mod.StateOfChargeMWh = Var( mod.STORAGE_PROJ_DISPATCH_POINTS, within=NonNegativeReals) def Track_State_Of_Charge_rule(m, proj, t): return m.StateOfChargeMWh[proj, t] == \ m.StateOfChargeMWh[proj, m.tp_previous[t]] + \ (m.ChargeStorage[proj, t] * m.proj_storage_efficiency[proj] - m.DispatchProj[proj, t]) * m.tp_duration_hrs[t] mod.Track_State_Of_Charge = Constraint( mod.STORAGE_PROJ_DISPATCH_POINTS, rule=Track_State_Of_Charge_rule) def State_Of_Charge_Upper_Limit_rule(m, proj, t): return m.StateOfChargeMWh[proj, t] <= \ m.StorageEnergyCapacity[proj, m.tp_period[t]] mod.State_Of_Charge_Upper_Limit = Constraint( mod.STORAGE_PROJ_DISPATCH_POINTS, rule=State_Of_Charge_Upper_Limit_rule)
def define_components(m): m.pumped_hydro_capital_cost_per_mw = Param() m.pumped_hydro_project_life = Param() # annual O&M cost for pumped hydro project, percent of capital cost m.pumped_hydro_fixed_om_percent = Param() # total annual cost m.pumped_hydro_fixed_cost_per_mw_per_year = Param(initialize=lambda m: m.pumped_hydro_capital_cost_per_mw * (crf(m.interest_rate, m.pumped_hydro_project_life) + m.pumped_hydro_fixed_om_percent) ) # round-trip efficiency of the pumped hydro facility m.pumped_hydro_efficiency = Param() # average energy available from water inflow each day # (system must balance energy net of this each day) m.pumped_hydro_inflow_mw = Param() # maximum size of pumped hydro project m.pumped_hydro_max_capacity_mw = Param(default=1000) # How much pumped hydro to build m.BuildPumpedHydroMW = Var(m.LOAD_ZONES, m.PERIODS, within=NonNegativeReals) m.Pumped_Hydro_Capacity_MW = Expression(m.LOAD_ZONES, m.PERIODS, rule=lambda m, z, p: sum(m.BuildPumpedHydroMW[z, pp] for pp in m.CURRENT_AND_PRIOR_PERIODS[p]) ) # constraints on construction of pumped hydro m.BuildAnyPumpedHydro = Var(m.LOAD_ZONES, m.PERIODS, bounds=(0, 1)) # within=Binary) # force the build flag on for the year(s) when pumped hydro is built, # and cap the build at the max allowed capacity m.Pumped_Hydro_Max_Build = Constraint(m.LOAD_ZONES, m.PERIODS, rule=lambda m, z, p: m.BuildPumpedHydroMW[z, p] <= m.BuildAnyPumpedHydro[z, p] * m.pumped_hydro_max_capacity_mw ) # only build pumped hydro in one period (can't add incrementally) m.Pumped_Hydro_Only_Build_Once = Constraint(m.LOAD_ZONES, rule=lambda m, z: sum(m.BuildAnyPumpedHydro[z, p] for p in m.PERIODS) <= 1 ) # How to run pumped hydro m.GeneratePumpedHydro = Var(m.LOAD_ZONES, m.TIMEPOINTS, within=NonNegativeReals) m.StorePumpedHydro = Var(m.LOAD_ZONES, m.TIMEPOINTS, within=NonNegativeReals) # calculate costs m.Pumped_Hydro_Fixed_Cost_Annual = Expression(m.PERIODS, rule=lambda m, p: sum(m.pumped_hydro_fixed_cost_per_mw_per_year * m.Pumped_Hydro_Capacity_MW[z, p] for z in m.LOAD_ZONES) ) m.cost_components_annual.append('Pumped_Hydro_Fixed_Cost_Annual') # add the pumped hydro to the model's energy balance m.LZ_Energy_Components_Produce.append('GeneratePumpedHydro') m.LZ_Energy_Components_Consume.append('StorePumpedHydro') # limits on pumping and generation m.Pumped_Hydro_Max_Generate_Rate = Constraint(m.LOAD_ZONES, m.TIMEPOINTS, rule=lambda m, z, t: m.GeneratePumpedHydro[z, t] <= m.Pumped_Hydro_Capacity_MW[z, m.tp_period[t]] ) m.Pumped_Hydro_Max_Store_Rate = Constraint(m.LOAD_ZONES, m.TIMEPOINTS, rule=lambda m, z, t: m.StorePumpedHydro[z, t] <= m.Pumped_Hydro_Capacity_MW[z, m.tp_period[t]] ) # return reservoir to the starting level every day, net of any inflow # it can also go higher than starting level, which indicates spilling surplus water m.Pumped_Hydro_Daily_Balance = Constraint(m.LOAD_ZONES, m.TIMESERIES, rule=lambda m, z, ts: sum( m.StorePumpedHydro[z, tp] * m.pumped_hydro_efficiency + m.pumped_hydro_inflow_mw - m.GeneratePumpedHydro[z, tp] for tp in m.TS_TPS[ts] ) >= 0 )
def define_components(m): m.PH_PROJECTS = Set() m.ph_load_zone = Param(m.PH_PROJECTS) m.ph_capital_cost_per_mw = Param(m.PH_PROJECTS, within=NonNegativeReals) m.ph_project_life = Param(m.PH_PROJECTS, within=NonNegativeReals) # annual O&M cost for pumped hydro project, percent of capital cost m.ph_fixed_om_percent = Param(m.PH_PROJECTS, within=NonNegativeReals) # total annual cost m.ph_fixed_cost_per_mw_per_year = Param( m.PH_PROJECTS, initialize=lambda m, p: m.ph_capital_cost_per_mw[p] * (crf( m.interest_rate, m.ph_project_life[p]) + m.ph_fixed_om_percent[p])) # round-trip efficiency of the pumped hydro facility m.ph_efficiency = Param(m.PH_PROJECTS) # average energy available from water inflow each day # (system must balance energy net of this each day) m.ph_inflow_mw = Param(m.PH_PROJECTS) # maximum size of pumped hydro project m.ph_max_capacity_mw = Param(m.PH_PROJECTS) # How much pumped hydro to build m.BuildPumpedHydroMW = Var(m.PH_PROJECTS, m.PERIODS, within=NonNegativeReals) m.Pumped_Hydro_Proj_Capacity_MW = Expression( m.PH_PROJECTS, m.PERIODS, rule=lambda m, pr, pe: sum(m.BuildPumpedHydroMW[pr, pp] for pp in m.CURRENT_AND_PRIOR_PERIODS[pe])) # flag indicating whether any capacity is added to each project each year m.BuildAnyPumpedHydro = Var(m.PH_PROJECTS, m.PERIODS, within=Binary) # How to run pumped hydro m.PumpedHydroProjGenerateMW = Var(m.PH_PROJECTS, m.TIMEPOINTS, within=NonNegativeReals) m.PumpedHydroProjStoreMW = Var(m.PH_PROJECTS, m.TIMEPOINTS, within=NonNegativeReals) # constraints on construction of pumped hydro # don't build more than the max allowed capacity m.Pumped_Hydro_Max_Build = Constraint( m.PH_PROJECTS, m.PERIODS, rule=lambda m, pr, pe: m.Pumped_Hydro_Proj_Capacity_MW[ pr, pe] <= m.ph_max_capacity_mw[pr]) # force the build flag on for the year(s) when pumped hydro is built m.Pumped_Hydro_Set_Build_Flag = Constraint( m.PH_PROJECTS, m.PERIODS, rule=lambda m, pr, pe: m.BuildPumpedHydroMW[pr, pe] <= m. BuildAnyPumpedHydro[pr, pe] * m.ph_max_capacity_mw[pr]) # only build in one year (can be deactivated to allow incremental construction) m.Pumped_Hydro_Build_Once = Constraint( m.PH_PROJECTS, rule=lambda m, pr: sum(m.BuildAnyPumpedHydro[pr, pe] for pe in m.PERIODS) <= 1) # only build full project size (deactivated by default, to allow smaller projects) m.Pumped_Hydro_Build_All_Or_None = Constraint( m.PH_PROJECTS, m.PERIODS, rule=lambda m, pr, pe: m.BuildPumpedHydroMW[pr, pe] == m. BuildAnyPumpedHydro[pr, pe] * m.ph_max_capacity_mw[pr]) m.Deactivate_Pumped_Hydro_Build_All_Or_None = BuildAction( rule=lambda m: m.Pumped_Hydro_Build_All_Or_None.deactivate()) # limits on pumping and generation m.Pumped_Hydro_Max_Generate_Rate = Constraint( m.PH_PROJECTS, m.TIMEPOINTS, rule=lambda m, pr, t: m.PumpedHydroProjGenerateMW[ pr, t] <= m.Pumped_Hydro_Proj_Capacity_MW[pr, m.tp_period[t]]) m.Pumped_Hydro_Max_Store_Rate = Constraint( m.PH_PROJECTS, m.TIMEPOINTS, rule=lambda m, pr, t: m.PumpedHydroProjStoreMW[ pr, t] <= m.Pumped_Hydro_Proj_Capacity_MW[pr, m.tp_period[t]]) # return reservoir to at least the starting level every day, net of any inflow # it can also go higher than starting level, which indicates spilling surplus water m.Pumped_Hydro_Daily_Balance = Constraint( m.PH_PROJECTS, m.TIMESERIES, rule=lambda m, pr, ts: sum(m.PumpedHydroProjStoreMW[ pr, tp] * m.ph_efficiency[pr] + m.ph_inflow_mw[pr] - m. PumpedHydroProjGenerateMW[pr, tp] for tp in m.TS_TPS[ts]) >= 0) m.GeneratePumpedHydro = Expression( m.LOAD_ZONES, m.TIMEPOINTS, rule=lambda m, z, t: sum(m.PumpedHydroProjGenerateMW[pr, t] for pr in m.PH_PROJECTS if m.ph_load_zone[pr] == z)) m.StorePumpedHydro = Expression( m.LOAD_ZONES, m.TIMEPOINTS, rule=lambda m, z, t: sum(m.PumpedHydroProjStoreMW[pr, t] for pr in m.PH_PROJECTS if m.ph_load_zone[pr] == z)) # calculate costs m.Pumped_Hydro_Fixed_Cost_Annual = Expression( m.PERIODS, rule=lambda m, pe: sum(m.ph_fixed_cost_per_mw_per_year[pr] * m. Pumped_Hydro_Proj_Capacity_MW[pr, pe] for pr in m.PH_PROJECTS)) m.cost_components_annual.append('Pumped_Hydro_Fixed_Cost_Annual') # add the pumped hydro to the model's energy balance m.LZ_Energy_Components_Produce.append('GeneratePumpedHydro') m.LZ_Energy_Components_Consume.append('StorePumpedHydro') # total pumped hydro capacity in each zone each period (for reporting) m.Pumped_Hydro_Capacity_MW = Expression( m.LOAD_ZONES, m.PERIODS, rule=lambda m, z, pe: sum(m.Pumped_Hydro_Proj_Capacity_MW[pr, pe] for pr in m.PH_PROJECTS if m.ph_load_zone[pr] == z)) # force construction of a fixed amount of pumped hydro if m.options.ph_mw is not None: print "Forcing construction of {m} MW of pumped hydro.".format( m=m.options.ph_mw) m.Build_Pumped_Hydro_MW = Constraint( m.LOAD_ZONES, rule=lambda m, z: m.Pumped_Hydro_Capacity_MW[ z, m.PERIODS.last()] == m.options.ph_mw) # force construction of pumped hydro only in a certain period if m.options.ph_year is not None: print "Allowing construction of pumped hydro only in {p}.".format( p=m.options.ph_year) m.Build_Pumped_Hydro_Year = Constraint( m.PH_PROJECTS, m.PERIODS, rule=lambda m, pr, pe: m.BuildPumpedHydroMW[pr, pe] == 0.0 if pe != m.options.ph_year else Constraint.Skip)
def define_components(mod): """ Adds components to a Pyomo abstract model object to describe generation and storage projects. Unless otherwise stated, all power capacity is specified in units of MW and all sets and parameters are mandatory. GENERATION_PROJECTS is the set of generation and storage projects that have been built or could potentially be built. A project is a combination of generation technology, load zone and location. A particular build-out of a project should also include the year in which construction was complete and additional capacity came online. Members of this set are abbreviated as gen in parameter names and g in indexes. Use of p instead of g is discouraged because p is reserved for period. gen_dbid[g] is an external database id for each generation project. This is an optional parameter than defaults to the project index. gen_tech[g] describes what kind of technology a generation project is using. gen_load_zone[g] is the load zone this generation project is built in. VARIABLE_GENS is a subset of GENERATION_PROJECTS that only includes variable generators such as wind or solar that have exogenous constraints on their energy production. BASELOAD_GENS is a subset of GENERATION_PROJECTS that only includes baseload generators such as coal or geothermal. GENS_IN_ZONE[z in LOAD_ZONES] is an indexed set that lists all generation projects within each load zone. CAPACITY_LIMITED_GENS is the subset of GENERATION_PROJECTS that are capacity limited. Most of these will be generator types that are resource limited like wind, solar or geothermal, but this can be specified for any generation project. Some existing or proposed generation projects may have upper bounds on increasing capacity or replacing capacity as it is retired based on permits or local air quality regulations. gen_capacity_limit_mw[g] is defined for generation technologies that are resource limited and do not compete for land area. This describes the maximum possible capacity of a generation project in units of megawatts. -- CONSTRUCTION -- GEN_BLD_YRS is a two-dimensional set of generation projects and the years in which construction or expansion occured or can occur. You can think of a project as a physical site that can be built out over time. BuildYear is the year in which construction is completed and new capacity comes online, not the year when constrution begins. BuildYear will be in the past for existing projects and will be the first year of an investment period for new projects. Investment decisions are made for each project/invest period combination. This set is derived from other parameters for all new construction. This set also includes entries for existing projects that have already been built and planned projects whose capacity buildouts have already been decided; information for legacy projects come from other files and their build years will usually not correspond to the set of investment periods. There are two recommended options for abbreviating this set for denoting indexes: typically this should be written out as (g, build_year) for clarity, but when brevity is more important (g, b) is acceptable. NEW_GEN_BLD_YRS is a subset of GEN_BLD_YRS that only includes projects that have not yet been constructed. This is derived by joining the set of GENERATION_PROJECTS with the set of NEW_GENERATION_BUILDYEARS using generation technology. PREDETERMINED_GEN_BLD_YRS is a subset of GEN_BLD_YRS that only includes existing or planned projects that are not subject to optimization. gen_predetermined_cap[(g, build_year) in PREDETERMINED_GEN_BLD_YRS] is a parameter that describes how much capacity was built in the past for existing projects, or is planned to be built for future projects. BuildGen[g, build_year] is a decision variable that describes how much capacity of a project to install in a given period. This also stores the amount of capacity that was installed in existing projects that are still online. GenCapacity[g, period] is an expression that returns the total capacity online in a given period. This is the sum of installed capacity minus all retirements. Max_Build_Potential[g] is a constraint defined for each project that enforces maximum capacity limits for resource-limited projects. GenCapacity <= gen_capacity_limit_mw NEW_GEN_WITH_MIN_BUILD_YEARS is the subset of NEW_GEN_BLD_YRS for which minimum capacity build-out constraints will be enforced. BuildMinGenCap[g, build_year] is a binary variable that indicates whether a project will build capacity in a period or not. If the model is committing to building capacity, then the minimum must be enforced. Enforce_Min_Build_Lower[g, build_year] and Enforce_Min_Build_Upper[g, build_year] are a pair of constraints that force project build-outs to meet the minimum build requirements for generation technologies that have those requirements. They force BuildGen to be 0 when BuildMinGenCap is 0, and to be greater than g_min_build_capacity when BuildMinGenCap is 1. In the latter case, the upper constraint should be non-binding; the upper limit is set to 10 times the peak non-conincident demand of the entire system. --- OPERATIONS --- PERIODS_FOR_GEN_BLD_YR[g, build_year] is an indexed set that describes which periods a given project build will be operational. BLD_YRS_FOR_GEN_PERIOD[g, period] is a complementary indexed set that identify which build years will still be online for the given project in the given period. For some project-period combinations, this will be an empty set. GEN_PERIODS describes periods in which generation projects could be operational. Unlike the related sets above, it is not indexed. Instead it is specified as a set of (g, period) combinations useful for indexing other model components. --- COSTS --- gen_connect_cost_per_mw[g] is the cost of grid upgrades to support a new project, in dollars per peak MW. These costs include new transmission lines to a substation, substation upgrades and any other grid upgrades that are needed to deliver power from the interconnect point to the load center or from the load center to the broader transmission network. The following cost components are defined for each project and build year. These parameters will always be available, but will typically be populated by the generic costs specified in generator costs inputs file and the load zone cost adjustment multipliers from load_zones inputs file. gen_overnight_cost[g, build_year] is the overnight capital cost per MW of capacity for building a project in the given period. By "installed in the given period", I mean that it comes online at the beginning of the given period and construction starts before that. gen_fixed_om[g, build_year] is the annual fixed Operations and Maintenance costs (O&M) per MW of capacity for given project that was installed in the given period. -- Derived cost parameters -- gen_capital_cost_annual[g, build_year] is the annualized loan payments for a project's capital and connection costs in units of $/MW per year. This is specified in non-discounted real dollars in a future period, not real dollars in net present value. Proj_Fixed_Costs_Annual[g, period] is the total annual fixed costs (capital as well as fixed operations & maintenance) incurred by a project in a period. This reflects all of the builds are operational in the given period. This is an expression that reflect decision variables. ProjFixedCosts[period] is the sum of Proj_Fixed_Costs_Annual[g, period] for all projects that could be online in the target period. This aggregation is performed for the benefit of the objective function. TODO: - Allow early capacity retirements with savings on fixed O&M """ mod.GENERATION_PROJECTS = Set() ######## NEW ADDITION ########## mod.NON_PRODUCING_GENERATION_PROJECTS = Set() mod.PRODUCING_GENERATION_PROJECTS = Set( dimen=1, initialize=lambda m: m.GENERATION_PROJECTS - m. NON_PRODUCING_GENERATION_PROJECTS) ############################## mod.gen_dbid = Param(mod.GENERATION_PROJECTS, default=lambda m, g: g) #NOT USED mod.gen_tech = Param(mod.GENERATION_PROJECTS) mod.GENERATION_TECHNOLOGIES = Set( initialize=lambda m: {m.gen_tech[g] for g in m.GENERATION_PROJECTS}) #NOT USED mod.gen_energy_source = Param( mod.GENERATION_PROJECTS, validate=lambda m, val, g: val in m.ENERGY_SOURCES or val == "multiple" ) #NOT USED mod.gen_load_zone = Param(mod.GENERATION_PROJECTS, within=mod.LOAD_ZONES) mod.gen_max_age = Param(mod.GENERATION_PROJECTS, within=PositiveIntegers) mod.gen_is_variable = Param( mod.GENERATION_PROJECTS, within=Boolean) # NOT USED except scenario_data mod.gen_is_baseload = Param( mod.GENERATION_PROJECTS, within=Boolean) # used in gen_min_load_fraction, init_gen_availability mod.gen_is_cogen = Param( mod.GENERATION_PROJECTS, within=Boolean, default=False ) # prevent non-cogen plants from burning pure LSFO after 2017 due to MATS emission restrictions mod.gen_is_distributed = Param(mod.GENERATION_PROJECTS, within=Boolean, default=False) #in dispatch.py mod.gen_scheduled_outage_rate = Param(mod.GENERATION_PROJECTS, within=PercentFraction, default=0) mod.gen_forced_outage_rate = Param(mod.GENERATION_PROJECTS, within=PercentFraction, default=0) mod.min_data_check('GENERATION_PROJECTS', 'gen_tech', 'gen_energy_source', 'gen_load_zone', 'gen_max_age', 'gen_is_variable', 'gen_is_baseload') mod.GENS_IN_ZONE = Set( mod.LOAD_ZONES, initialize=lambda m, z: set(g for g in m.GENERATION_PROJECTS if m.gen_load_zone[g] == z)) mod.VARIABLE_GENS = Set(initialize=mod.GENERATION_PROJECTS, filter=lambda m, g: m.gen_is_variable[g]) mod.BASELOAD_GENS = Set( initialize=mod.GENERATION_PROJECTS, filter=lambda m, g: m.gen_is_baseload[ g]) #VARIABLE_GENS and BASELOAD_GENS don't cover the whole space mod.CAPACITY_LIMITED_GENS = Set(within=mod.GENERATION_PROJECTS) #NOT USED mod.gen_capacity_limit_mw = Param(mod.CAPACITY_LIMITED_GENS, within=PositiveReals) #NOT USED mod.DISCRETELY_SIZED_GENS = Set( within=mod.GENERATION_PROJECTS) #### NOT USED mod.gen_unit_size = Param(mod.DISCRETELY_SIZED_GENS, within=PositiveReals) mod.CCS_EQUIPPED_GENS = Set( within=mod.GENERATION_PROJECTS ) #carbon capture and storage would eliminate the emission >> see dispatch.py mod.gen_ccs_capture_efficiency = Param(mod.CCS_EQUIPPED_GENS, within=PercentFraction) mod.gen_ccs_energy_load = Param(mod.CCS_EQUIPPED_GENS, within=PercentFraction) mod.gen_uses_fuel = Param(mod.GENERATION_PROJECTS, initialize=lambda m, g: (m.gen_energy_source[g] in m.FUELS or m. gen_energy_source[g] == "multiple")) ############ NEW ADDITION ######################################################################## # mod.NON_FUEL_BASED_GENS = Set( # initialize=mod.GENERATION_PROJECTS, # filter=lambda m, g: not m.gen_uses_fuel[g] not m.gen_downstream[g]) mod.NON_FUEL_BASED_GENS = Set(initialize=mod.GENERATION_PROJECTS, filter=lambda m, g: not m.gen_uses_fuel[g]) ################################################################################################## mod.FUEL_BASED_GENS = Set(initialize=mod.GENERATION_PROJECTS, filter=lambda m, g: m.gen_uses_fuel[g]) mod.gen_full_load_heat_rate = Param(mod.FUEL_BASED_GENS, within=PositiveReals) mod.MULTIFUEL_GENS = Set( initialize=mod.GENERATION_PROJECTS, filter=lambda m, g: m.gen_energy_source[g] == "multiple") mod.FUELS_FOR_MULTIFUEL_GEN = Set(mod.MULTIFUEL_GENS, within=mod.FUELS) mod.FUELS_FOR_GEN = Set( mod.FUEL_BASED_GENS, initialize=lambda m, g: (m.FUELS_FOR_MULTIFUEL_GEN[g] if g in m.MULTIFUEL_GENS else [m.gen_energy_source[g]])) mod.PREDETERMINED_GEN_BLD_YRS = Set(dimen=2) mod.PREDETERMINED_GEN_BLD_YRS2 = Set(dimen=2) mod.GEN_BLD_YRS = Set(dimen=2, validate=lambda m, g, bld_yr: ((g, bld_yr) in m.PREDETERMINED_GEN_BLD_YRS or (g, bld_yr) in m.GENERATION_PROJECTS * m.PERIODS)) mod.NEW_GEN_BLD_YRS = Set( dimen=2, initialize=lambda m: m.GEN_BLD_YRS - m.PREDETERMINED_GEN_BLD_YRS) mod.GEN_BLD_YRS2 = Set(dimen=2, validate=lambda m, g, bld_yr: ((g, bld_yr) in m.PREDETERMINED_GEN_BLD_YRS2 or (g, bld_yr) in m.GENERATION_PROJECTS * m.PERIODS)) mod.NEW_GEN_BLD_YRS2 = Set( dimen=2, initialize=lambda m: m.GEN_BLD_YRS2 - m.PREDETERMINED_GEN_BLD_YRS2) mod.gen_predetermined_cap = Param(mod.PREDETERMINED_GEN_BLD_YRS, within=NonNegativeReals) mod.gen_predetermined_cap2 = Param(mod.PREDETERMINED_GEN_BLD_YRS2, within=NonNegativeReals) mod.min_data_check('gen_predetermined_cap') mod.min_data_check('gen_predetermined_cap2') def _gen_build_can_operate_in_period(m, g, build_year, period): if build_year in m.PERIODS: online = m.period_start[build_year] else: online = build_year retirement = online + m.gen_max_age[g] return (online <= m.period_start[period] < retirement) # This is probably more correct, but is a different behavior # mid_period = m.period_start[period] + 0.5 * m.period_length_years[period] # return online <= m.period_start[period] and mid_period <= retirement # The set of periods when a project built in a certain year will be online mod.PERIODS_FOR_GEN_BLD_YR = Set( mod.GEN_BLD_YRS, within=mod.PERIODS, ordered=True, initialize=lambda m, g, bld_yr: set( period for period in m.PERIODS if _gen_build_can_operate_in_period(m, g, bld_yr, period))) # The set of build years that could be online in the given period # for the given project. mod.BLD_YRS_FOR_GEN_PERIOD = Set( mod.GENERATION_PROJECTS, mod.PERIODS, initialize=lambda m, g, period: set( bld_yr for (gen, bld_yr) in m.GEN_BLD_YRS if gen == g and _gen_build_can_operate_in_period( m, g, bld_yr, period))) def bounds_BuildGen(model, g, bld_yr): if ((g, bld_yr) in model.PREDETERMINED_GEN_BLD_YRS): return (model.gen_predetermined_cap[g, bld_yr], model.gen_predetermined_cap[g, bld_yr]) elif (g in model.CAPACITY_LIMITED_GENS): # This does not replace Max_Build_Potential because # Max_Build_Potential applies across all build years. return (0, model.gen_capacity_limit_mw[g]) else: return (0, None) mod.BuildGen = Var(mod.GEN_BLD_YRS, within=NonNegativeReals, bounds=bounds_BuildGen) # Some projects are retired before the first study period, so they # don't appear in the objective function or any constraints. # In this case, pyomo may leave the variable value undefined even # after a solve, instead of assigning a value within the allowed # range. This causes errors in the Progressive Hedging code, which # expects every variable to have a value after the solve. So as a # starting point we assign an appropriate value to all the existing # projects here. def BuildGen_assign_default_value(m, g, bld_yr): m.BuildGen[g, bld_yr] = m.gen_predetermined_cap[g, bld_yr] mod.BuildGen_assign_default_value = BuildAction( mod.PREDETERMINED_GEN_BLD_YRS, rule=BuildGen_assign_default_value) def bounds_BuildGen2(model, g, bld_yr): if ((g, bld_yr) in model.PREDETERMINED_GEN_BLD_YRS2): return (model.gen_predetermined_cap2[g, bld_yr], model.gen_predetermined_cap2[g, bld_yr]) elif (g in model.CAPACITY_LIMITED_GENS): # This does not replace Max_Build_Potential because # Max_Build_Potential applies across all build years. return (0, model.gen_capacity_limit_mw[g]) else: return (0, None) mod.BuildGen2 = Var(mod.GEN_BLD_YRS, within=NonNegativeReals, bounds=bounds_BuildGen2) def BuildGen_assign_default_value2(m, g, bld_yr): m.BuildGen2[g, bld_yr] = m.gen_predetermined_cap2[g, bld_yr] mod.BuildGen_assign_default_value2 = BuildAction( mod.PREDETERMINED_GEN_BLD_YRS2, rule=BuildGen_assign_default_value2) mod.GEN_PERIODS = Set(dimen=2, initialize=mod.GENERATION_PROJECTS * mod.PERIODS) mod.GenCapacity = Expression( mod.GEN_PERIODS, rule=lambda m, g, period: sum(m.BuildGen[g, bld_yr] for bld_yr in m. BLD_YRS_FOR_GEN_PERIOD[g, period])) mod.Max_Build_Potential = Constraint( mod.CAPACITY_LIMITED_GENS, mod.PERIODS, rule=lambda m, g, p: (m.gen_capacity_limit_mw[g] >= m.GenCapacity[g, p])) # The following components enforce minimum capacity build-outs. # Note that this adds binary variables to the model. mod.gen_min_build_capacity = Param(mod.GENERATION_PROJECTS, within=NonNegativeReals, default=0) mod.NEW_GEN_WITH_MIN_BUILD_YEARS = Set(initialize=mod.NEW_GEN_BLD_YRS, filter=lambda m, g, p: (m.gen_min_build_capacity[g] > 0)) mod.BuildMinGenCap = Var(mod.NEW_GEN_WITH_MIN_BUILD_YEARS, within=Binary) mod.Enforce_Min_Build_Lower = Constraint( mod.NEW_GEN_WITH_MIN_BUILD_YEARS, rule=lambda m, g, p: (m.BuildMinGenCap[g, p] * m. gen_min_build_capacity[g] <= m.BuildGen[g, p])) # Define a constant for enforcing binary constraints on project capacity # The value of 100 GW should be larger than any expected build size. For # perspective, the world's largest electric power plant (Three Gorges Dam) # is 22.5 GW. I tried using 1 TW, but CBC had numerical stability problems # with that value and chose a suboptimal solution for the # discrete_and_min_build example which is installing capacity of 3-5 MW. mod._gen_max_cap_for_binary_constraints = 10**5 mod.Enforce_Min_Build_Upper = Constraint( mod.NEW_GEN_WITH_MIN_BUILD_YEARS, rule=lambda m, g, p: (m.BuildGen[g, p] <= m.BuildMinGenCap[g, p] * mod. _gen_max_cap_for_binary_constraints)) # Costs mod.gen_variable_om = Param(mod.GENERATION_PROJECTS, within=NonNegativeReals) mod.gen_connect_cost_per_mw = Param(mod.GENERATION_PROJECTS, within=NonNegativeReals) mod.min_data_check('gen_variable_om', 'gen_connect_cost_per_mw') mod.gen_overnight_cost = Param(mod.GEN_BLD_YRS, within=NonNegativeReals) mod.gen_fixed_om = Param(mod.GEN_BLD_YRS, within=NonNegativeReals) mod.min_data_check('gen_overnight_cost', 'gen_fixed_om') # Derived annual costs mod.gen_capital_cost_annual = Param( mod.GEN_BLD_YRS, initialize=lambda m, g, bld_yr: ( (m.gen_overnight_cost[g, bld_yr] + m.gen_connect_cost_per_mw[g] ) * crf(m.interest_rate, m.gen_max_age[g]))) mod.GenCapitalCosts = Expression( mod.GEN_PERIODS, rule=lambda m, g, p: sum(m.BuildGen[g, bld_yr] * m. gen_capital_cost_annual[g, bld_yr] for bld_yr in m.BLD_YRS_FOR_GEN_PERIOD[g, p])) mod.GenFixedOMCosts = Expression( mod.GEN_PERIODS, rule=lambda m, g, p: sum(m.BuildGen[g, bld_yr] * m.gen_fixed_om[ g, bld_yr] for bld_yr in m.BLD_YRS_FOR_GEN_PERIOD[g, p])) # Summarize costs for the objective function. Units should be total # annual future costs in $base_year real dollars. The objective # function will convert these to base_year Net Present Value in # $base_year real dollars. ####### NEW ADDITION ######################################################################################################################## mod.gen_hawaii_share = Param(mod.GENERATION_PROJECTS, within=NonNegativeReals) # mod.TotalGenFixedCosts = Expression( # mod.PERIODS, # rule=lambda m, p: sum( (m.GenCapitalCosts[g, p] + m.GenFixedOMCosts[g, p]) * m.gen_hawaii_share[g] # for g in m.GENERATION_PROJECTS ) # ) # mod.Cost_Components_Per_Period.append('TotalGenFixedCosts') mod.TotalGenFixedCosts = Expression( mod.PERIODS, rule=lambda m, p: sum(m.GenCapitalCosts[g, p] + m.GenFixedOMCosts[g, p] for g in m.GENERATION_PROJECTS)) mod.Cost_Components_Per_Period.append('TotalGenFixedCosts') mod.GENERATION_PROJECTS_EXTRACTION = Set( dimen=1, initialize=[ "Wells_Shale_Oil", "Wells_Offshore_Oil", "Wells_Shale_Gas", "Wells_Offshore_Gas", "Mines_Coal_Surface" ], filter=lambda m, r: r in mod.GENERATION_PROJECTS) mod.GENERATION_PROJECTS_EXTRACTION_PERIOD = Set( dimen=2, initialize=mod.GENERATION_PROJECTS_EXTRACTION * mod.PERIODS) # mod.GENERATION_PROJECTS_EXTRACTION_FUEL = Set(dimen=2, within=mod.GENERATION_PROJECTS) mod.GENERATION_PROJECTS_EXTRACTION_ALL = Set( dimen=1, initialize=[ "Wells_Shale_Oil", "Wells_Conventional_Oil" "Wells_Offshore_Oil", "Wells_Shale_Gas", "Wells_Offshore_Gas", "Wells_Conventional_Gas", "Mines_Coal_Surface", "Mines_Coal_Underground" ], filter=lambda m, r: r in m.GENERATION_PROJECTS) # mod.EXTRACTION_SET_FOR_GEN_RATIO = Set(mod.GENERATION_PROJECTS_EXTRACTION, within=mod.GENERATION_PROJECTS_EXTRACTION_ALL) # mod.EXTRACTION_SET_FOR_GEN = Set(mod.GENERATION_PROJECTS_EXTRACTION, # initialize=lambda m, g: ( # m.EXTRACTION_SET_FOR_GEN_RATIO[g] # if g in m.GENERATION_PROJECTS_EXTRACTION # else [m.gen_energy_source[g]])) # mod.MULTIFUEL_GENS = Set( # initialize=mod.GENERATION_PROJECTS, # filter=lambda m, g: m.gen_energy_source[g] == "multiple") # mod.FUELS_FOR_MULTIFUEL_GEN = Set(mod.MULTIFUEL_GENS, within=mod.FUELS) # mod.FUELS_FOR_GEN = Set(mod.FUEL_BASED_GENS, # initialize=lambda m, g: ( # m.FUELS_FOR_MULTIFUEL_GEN[g] # if g in m.MULTIFUEL_GENS # else [m.gen_energy_source[g]])) mod.Limited_Fuels = Set(dimen=1, initialize=["Oil", "LNG", "Coal"], filter=lambda m, r: r in m.FUELS) mod.FUELS_PERIOD = Set(dimen=2, initialize=mod.Limited_Fuels * mod.PERIODS)
def define_components(m): # TODO: change this to allow multiple storage technologies. # battery capital cost # TODO: accept a single battery_capital_cost_per_mwh_capacity value or the annual values shown here m.BATTERY_CAPITAL_COST_YEARS = Set() # list of all years for which capital costs are available m.battery_capital_cost_per_mwh_capacity_by_year = Param(m.BATTERY_CAPITAL_COST_YEARS) # TODO: merge this code with batteries.py and auto-select between fixed calendar life and cycle life # based on whether battery_n_years or battery_n_cycles is provided. (Or find some hybrid that can # handle both well?) # number of years the battery can last; we assume there is no limit on cycle life within this period m.battery_n_years = Param() # maximum depth of discharge m.battery_max_discharge = Param() # round-trip efficiency m.battery_efficiency = Param() # fastest time that storage can be emptied (down to max_discharge) m.battery_min_discharge_time = Param() # amount of battery capacity to build and use (in MWh) # TODO: integrate this with other project data, so it can contribute to reserves, etc. m.BuildBattery = Var(m.LOAD_ZONES, m.PERIODS, within=NonNegativeReals) m.Battery_Capacity = Expression(m.LOAD_ZONES, m.PERIODS, rule=lambda m, z, p: sum( m.BuildBattery[z, bld_yr] for bld_yr in m.CURRENT_AND_PRIOR_PERIODS[p] if bld_yr + m.battery_n_years > p ) ) # rate of charging/discharging battery m.ChargeBattery = Var(m.LOAD_ZONES, m.TIMEPOINTS, within=NonNegativeReals) m.DischargeBattery = Var(m.LOAD_ZONES, m.TIMEPOINTS, within=NonNegativeReals) # storage level at start of each timepoint m.BatteryLevel = Var(m.LOAD_ZONES, m.TIMEPOINTS, within=NonNegativeReals) # add the storage to the model's energy balance m.LZ_Energy_Components_Produce.append('DischargeBattery') m.LZ_Energy_Components_Consume.append('ChargeBattery') # add the batteries to the objective function # cost recovery for any battery capacity currently active m.BatteryAnnualCost = Expression( m.PERIODS, rule=lambda m, p: sum( m.BuildBattery[z, bld_yr] * m.battery_capital_cost_per_mwh_capacity_by_year[bld_yr] * crf(m.interest_rate, m.battery_n_years) for bld_yr in m.CURRENT_AND_PRIOR_PERIODS[p] if bld_yr + m.battery_n_years > p for z in m.LOAD_ZONES ) ) m.cost_components_annual.append('BatteryAnnualCost') # Calculate the state of charge based on conservation of energy # NOTE: this is circular for each day # NOTE: the overall level for the day is free, but the levels each timepoint are chained. m.Battery_Level_Calc = Constraint(m.LOAD_ZONES, m.TIMEPOINTS, rule=lambda m, z, t: m.BatteryLevel[z, t] == m.BatteryLevel[z, m.tp_previous[t]] + m.battery_efficiency * m.ChargeBattery[z, m.tp_previous[t]] - m.DischargeBattery[z, m.tp_previous[t]] ) # limits on storage level m.Battery_Min_Level = Constraint(m.LOAD_ZONES, m.TIMEPOINTS, rule=lambda m, z, t: (1.0 - m.battery_max_discharge) * m.Battery_Capacity[z, m.tp_period[t]] <= m.BatteryLevel[z, t] ) m.Battery_Max_Level = Constraint(m.LOAD_ZONES, m.TIMEPOINTS, rule=lambda m, z, t: m.BatteryLevel[z, t] <= m.Battery_Capacity[z, m.tp_period[t]] ) m.Battery_Max_Charge = Constraint(m.LOAD_ZONES, m.TIMEPOINTS, rule=lambda m, z, t: m.ChargeBattery[z, t] <= m.Battery_Capacity[z, m.tp_period[t]] * m.battery_max_discharge / m.battery_min_discharge_time ) m.Battery_Max_Disharge = Constraint(m.LOAD_ZONES, m.TIMEPOINTS, rule=lambda m, z, t: m.DischargeBattery[z, t] <= m.Battery_Capacity[z, m.tp_period[t]] * m.battery_max_discharge / m.battery_min_discharge_time )