예제 #1
0
파일: build.py 프로젝트: boli9301/switch-1
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()
    mod.gen_dbid = Param(mod.GENERATION_PROJECTS, default=lambda m, g: g)
    mod.gen_tech = Param(mod.GENERATION_PROJECTS)
    mod.GENERATION_TECHNOLOGIES = Set(
        initialize=lambda m: {m.gen_tech[g]
                              for g in m.GENERATION_PROJECTS})
    mod.gen_energy_source = Param(mod.GENERATION_PROJECTS,
                                  validate=lambda m, val, g: val in m.
                                  ENERGY_SOURCES or val == "multiple")
    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)
    mod.gen_is_baseload = Param(mod.GENERATION_PROJECTS, within=Boolean)
    mod.gen_is_cogen = Param(mod.GENERATION_PROJECTS,
                             within=Boolean,
                             default=False)
    mod.gen_is_distributed = Param(mod.GENERATION_PROJECTS,
                                   within=Boolean,
                                   default=False)
    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.VARIABLE_GENS_IN_ZONE = Set(
        mod.LOAD_ZONES,
        initialize=lambda m, z:
        [g for g in m.GENS_IN_ZONE[z] if m.gen_is_variable[g]])
    mod.BASELOAD_GENS = Set(initialize=mod.GENERATION_PROJECTS,
                            filter=lambda m, g: m.gen_is_baseload[g])
    # TODO: use a construction dictionary or closure to create all the GENS_BY_...
    # indexed sets more efficiently
    mod.GENS_BY_TECHNOLOGY = Set(
        mod.GENERATION_TECHNOLOGIES,
        initialize=lambda m, t:
        [g for g in m.GENERATION_PROJECTS if m.gen_tech[g] == t])

    mod.CAPACITY_LIMITED_GENS = Set(within=mod.GENERATION_PROJECTS)
    mod.gen_capacity_limit_mw = Param(mod.CAPACITY_LIMITED_GENS,
                                      within=PositiveReals)
    mod.DISCRETELY_SIZED_GENS = Set(within=mod.GENERATION_PROJECTS)
    mod.gen_unit_size = Param(mod.DISCRETELY_SIZED_GENS, within=PositiveReals)
    mod.CCS_EQUIPPED_GENS = Set(within=mod.GENERATION_PROJECTS)
    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"))
    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=NonNegativeReals)
    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.GENS_BY_NON_FUEL_ENERGY_SOURCE = Set(
        mod.NON_FUEL_ENERGY_SOURCES,
        initialize=lambda m, s:
        [g for g in m.NON_FUEL_BASED_GENS if m.gen_energy_source[g] == s])
    mod.GENS_BY_FUEL = Set(
        mod.FUELS,
        initialize=lambda m, f:
        [g for g in m.FUEL_BASED_GENS if f in m.FUELS_FOR_GEN[g]])

    mod.PREDETERMINED_GEN_BLD_YRS = 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_predetermined_cap = Param(mod.PREDETERMINED_GEN_BLD_YRS,
                                      within=NonNegativeReals)
    mod.min_data_check('gen_predetermined_cap')

    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)))
    # The set of periods when a generator is available to run
    mod.PERIODS_FOR_GEN = Set(
        mod.GENERATION_PROJECTS,
        initialize=lambda m, g:
        [p for p in m.PERIODS if len(m.BLD_YRS_FOR_GEN_PERIOD[g, p]) > 0])

    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)

    # note: in pull request 78, commit e7f870d..., GEN_PERIODS
    # was mistakenly redefined as GENERATION_PROJECTS * PERIODS.
    # That didn't directly affect the objective function in the tests
    # because most code uses GEN_TPS, which was defined correctly.
    # But it did have some subtle effects on the main Hawaii model.
    # It would be good to have a test that this set is correct,
    # e.g., assertions that in the 3zone_toy model,
    # ('C-Coal_ST', 2020) in m.GEN_PERIODS and ('C-Coal_ST', 2030) not in m.GEN_PERIODS
    # and 'C-Coal_ST' in m.GENS_IN_PERIOD[2020] and 'C-Coal_ST' not in m.GENS_IN_PERIOD[2030]
    mod.GEN_PERIODS = Set(dimen=2,
                          initialize=lambda m: [(g, p)
                                                for g in m.GENERATION_PROJECTS
                                                for p in m.PERIODS_FOR_GEN[g]])

    mod.GenCapacity = Expression(
        mod.GENERATION_PROJECTS,
        mod.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.GENERATION_PROJECTS,
        mod.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.GENERATION_PROJECTS,
        mod.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.
    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')
예제 #2
0
def define_components(mod):
    """
    
    STORAGE_GENS is the subset of projects that can provide energy storage.

    STORAGE_GEN_BLD_YRS is the subset of GEN_BLD_YRS, restricted
    to storage projects.

    gen_storage_efficiency[STORAGE_GENS] 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.

    gen_store_to_release_ratio[STORAGE_GENS] 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 gen_store_to_release_ratio
    of 1.2, then it can consume up to 1.2 MW of power while charging.

    gen_storage_energy_to_power_ratio[STORAGE_GENS], if specified, restricts
    the storage capacity (in MWh) to be a fixed multiple of the output 
    power (in MW), i.e., specifies a particular number of hours of 
    storage capacity. Omit this column or specify "." to allow Switch 
    to choose the energy/power ratio. (Note: gen_storage_energy_overnight_cost 
    or gen_overnight_cost should often be set to 0 when using this.)

    gen_storage_max_cycles_per_year[STORAGE_GENS], if specified, restricts
    the number of charge/discharge cycles each storage project can perform
    per year; one cycle is defined as discharging an amount of energy
    equal to the storage capacity of the project.

    gen_storage_energy_overnight_cost[(g, bld_yr) in
    STORAGE_GEN_BLD_YRS] 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.
    
    BuildStorageEnergy[(g, bld_yr) in STORAGE_GEN_BLD_YRS]
    is a decision of how much energy capacity to build onto a storage
    project. This is analogous to BuildGen, but for energy rather than power.
    
    StorageEnergyInstallCosts[PERIODS] is an expression of the
    annual costs incurred by the BuildStorageEnergy decision.
    
    StorageEnergyCapacity[g, period] is an expression describing the
    cumulative available energy capacity of BuildStorageEnergy. This is
    analogous to GenCapacity.
    
    STORAGE_GEN_TPS is the subset of GEN_TPS,
    restricted to storage projects.

    ChargeStorage[(g, t) in STORAGE_GEN_TPS] is a dispatch
    decision of how much to charge a storage project in each timepoint.
    
    StorageNetCharge[LOAD_ZONE, TIMEPOINT] is an expression describing the
    aggregate impact of ChargeStorage in each load zone and timepoint.
    
    Charge_Storage_Upper_Limit[(g, t) in STORAGE_GEN_TPS]
    constrains ChargeStorage to available power capacity (accounting for
    gen_store_to_release_ratio)
    
    StateOfCharge[(g, t) in STORAGE_GEN_TPS] 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[(g, t) in STORAGE_GEN_TPS] constrains
    StateOfCharge based on the StateOfCharge in the previous timepoint,
    ChargeStorage and DispatchGen.
    
    State_Of_Charge_Upper_Limit[(g, t) in STORAGE_GEN_TPS]
    constrains StateOfCharge based on installed energy capacity.

    """

    mod.STORAGE_GENS = Set(within=mod.GENERATION_PROJECTS)
    mod.STORAGE_GEN_PERIODS = Set(
        within=mod.GEN_PERIODS, 
        initialize=lambda m: [(g, p) for g in m.STORAGE_GENS for p in m.PERIODS_FOR_GEN[g]]
    )
    mod.gen_storage_efficiency = Param(
        mod.STORAGE_GENS,
        within=PercentFraction)
    # TODO: rename to gen_charge_to_discharge_ratio?
    mod.gen_store_to_release_ratio = Param(
        mod.STORAGE_GENS,
        within=PositiveReals,
        default=1.0)
    mod.gen_storage_energy_to_power_ratio = Param(
        mod.STORAGE_GENS,
        within=NonNegativeReals,
        default=float("inf")) # inf is a flag that no value is specified (nan and None don't work)
    mod.gen_storage_max_cycles_per_year = Param(
        mod.STORAGE_GENS,
        within=NonNegativeReals,
        default=float('inf'))

    # TODO: build this set up instead of filtering down, to improve performance
    mod.STORAGE_GEN_BLD_YRS = Set(
        dimen=2,
        initialize=mod.GEN_BLD_YRS,
        filter=lambda m, g, bld_yr: g in m.STORAGE_GENS)
    mod.gen_storage_energy_overnight_cost = Param(
        mod.STORAGE_GEN_BLD_YRS,
        within=NonNegativeReals)
    mod.min_data_check('gen_storage_energy_overnight_cost')
    mod.BuildStorageEnergy = Var(
        mod.STORAGE_GEN_BLD_YRS,
        within=NonNegativeReals)

    # Summarize capital costs of energy storage for the objective function.
    mod.StorageEnergyInstallCosts = Expression(
        mod.PERIODS,
        rule=lambda m, p: sum(m.BuildStorageEnergy[g, bld_yr] *
                   m.gen_storage_energy_overnight_cost[g, bld_yr] *
                   crf(m.interest_rate, m.gen_max_age[g])
                   for (g, bld_yr) in m.STORAGE_GEN_BLD_YRS))
    mod.Cost_Components_Per_Period.append(
        'StorageEnergyInstallCosts')

    mod.StorageEnergyCapacity = Expression(
        mod.STORAGE_GENS, mod.PERIODS,
        rule=lambda m, g, period: sum(
            m.BuildStorageEnergy[g, bld_yr]
            for bld_yr in m.BLD_YRS_FOR_GEN_PERIOD[g, period]))

    mod.STORAGE_GEN_TPS = Set(
        dimen=2,
        initialize=lambda m: (
            (g, tp) 
                for g in m.STORAGE_GENS
                    for tp in m.TPS_FOR_GEN[g]))

    mod.ChargeStorage = Var(
        mod.STORAGE_GEN_TPS,
        within=NonNegativeReals)
    
    # Summarize storage charging for the energy balance equations
    # TODO: rename this StorageTotalCharging or similar (to indicate it's a 
    # sum for a zone, not a net quantity for a project)
    def rule(m, z, t):
        # Construct and cache a set for summation as needed
        if not hasattr(m, 'Storage_Charge_Summation_dict'):
            m.Storage_Charge_Summation_dict = collections.defaultdict(set)
            for g, t2 in m.STORAGE_GEN_TPS:
                z2 = m.gen_load_zone[g]
                m.Storage_Charge_Summation_dict[z2, t2].add(g)
        # Use pop to free memory
        relevant_projects = m.Storage_Charge_Summation_dict.pop((z, t), {})
        return sum(m.ChargeStorage[g, t] for g in relevant_projects)
    mod.StorageNetCharge = Expression(mod.LOAD_ZONES, mod.TIMEPOINTS, rule=rule)
    # Register net charging with zonal energy balance. Discharging is already
    # covered by DispatchGen.
    mod.Zone_Power_Withdrawals.append('StorageNetCharge')

    # use fixed energy/power ratio (# hours of capacity) when specified
    mod.Enforce_Fixed_Energy_Storage_Ratio = Constraint(
        mod.STORAGE_GEN_BLD_YRS,
        rule=lambda m, g, y: 
            Constraint.Skip if m.gen_storage_energy_to_power_ratio[g] == float("inf") # no value specified
            else
            (m.BuildStorageEnergy[g, y] == m.gen_storage_energy_to_power_ratio[g] * m.BuildGen[g, y])
    )

    def Charge_Storage_Upper_Limit_rule(m, g, t):
        return m.ChargeStorage[g,t] <= \
            m.DispatchUpperLimit[g, t] * m.gen_store_to_release_ratio[g]
    mod.Charge_Storage_Upper_Limit = Constraint(
        mod.STORAGE_GEN_TPS,
        rule=Charge_Storage_Upper_Limit_rule)
                
    mod.StateOfCharge = Var(
        mod.STORAGE_GEN_TPS,
        within=NonNegativeReals)

    def Track_State_Of_Charge_rule(m, g, t):
        return m.StateOfCharge[g, t] == \
            m.StateOfCharge[g, m.tp_previous[t]] + \
            (m.ChargeStorage[g, t] * m.gen_storage_efficiency[g] -
             m.DispatchGen[g, t]) * m.tp_duration_hrs[t]
    mod.Track_State_Of_Charge = Constraint(
        mod.STORAGE_GEN_TPS,
        rule=Track_State_Of_Charge_rule)

    def State_Of_Charge_Upper_Limit_rule(m, g, t):
        return m.StateOfCharge[g, t] <= \
            m.StorageEnergyCapacity[g, m.tp_period[t]]
    mod.State_Of_Charge_Upper_Limit = Constraint(
        mod.STORAGE_GEN_TPS,
        rule=State_Of_Charge_Upper_Limit_rule)
        
    # batteries can only complete the specified number of cycles per year, averaged over each period
    mod.Battery_Cycle_Limit = Constraint(
        mod.STORAGE_GEN_PERIODS, 
        rule=lambda m, g, p:
            # solvers sometimes perform badly with infinite constraint
            Constraint.Skip if m.gen_storage_max_cycles_per_year[g] == float('inf')
            else (
                sum(m.DispatchGen[g, tp] * m.tp_duration_hrs[tp] for tp in m.TPS_IN_PERIOD[p])
                <= 
                m.gen_storage_max_cycles_per_year[g] * m.StorageEnergyCapacity[g, p] * m.period_length_years[p]
            )
    )
예제 #3
0
def post_solve(m, outdir=None):
    """ Calculate detailed costs per generation project per period. """

    if outdir is None:
        outdir = m.options.outputs_dir

    zone_fuel_cost = get_zone_fuel_cost(m)
    has_subsidies = hasattr(m, 'gen_investment_subsidy_fraction')

    gen_data = OrderedDict()
    gen_period_data = OrderedDict()
    gen_vintage_period_data = OrderedDict()
    for g, p in sorted(m.GEN_PERIODS):
        # helper function to calculate annual sums
        def ann(expr):
            try:
                return sum(
                    expr(g, t) * m.tp_weight_in_year[t]
                    for t in m.TPS_IN_PERIOD[p])
            except AttributeError:
                # expression uses a component that doesn't exist
                return None

        # is this a storage gen?
        is_storage = hasattr(m, 'STORAGE_GENS') and g in m.STORAGE_GENS

        BuildGen = m.BuildGen[g, p] if (g, p) in m.GEN_BLD_YRS else 0.0
        # BuildStorageEnergy = (
        #     m.BuildStorageEnergy[g, p]
        #     if is_storage and (g, p) in m.GEN_BLD_YRS
        #     else 0.0
        # )

        gen_data[g] = OrderedDict(gen_tech=m.gen_tech[g],
                                  gen_load_zone=m.gen_load_zone[g],
                                  gen_energy_source=m.gen_energy_source[g],
                                  gen_is_intermittent=int(
                                      m.gen_is_variable[g]))

        # temporary storage of per-generator data to be allocated per-vintage
        # below
        gen_period_data = OrderedDict(
            total_output=0.0
            if is_storage else ann(lambda g, t: m.DispatchGen[g, t]),
            renewable_output=0.0
            if is_storage else ann(lambda g, t: renewable_mw(m, g, t)),
            non_renewable_output=0.0 if is_storage else
            ann(lambda g, t: m.DispatchGen[g, t] - renewable_mw(m, g, t)),
            storage_load=(
                ann(lambda g, t: m.ChargeStorage[g, t] - m.DispatchGen[g, t])
                if is_storage else 0.0),
            fixed_om=m.GenFixedOMCosts[g, p],
            variable_om=ann(
                lambda g, t: m.DispatchGen[g, t] * m.gen_variable_om[g]),
            startup_om=ann(lambda g, t: m.gen_startup_om[g] * m.
                           StartupGenCapacity[g, t] / m.tp_duration_hrs[t]),
            fuel_cost=ann(lambda g, t: sum(
                0.0  # avoid nan fuel prices for unused fuels
                if m.GenFuelUseRate[g, t, f] == 0.0 else
                (m.GenFuelUseRate[g, t, f] * zone_fuel_cost[m.gen_load_zone[
                    g], f, m.tp_period[t]]) for f in m.FUELS_FOR_GEN[g])
                          if g in m.FUEL_BASED_GENS else 0.0))

        for v in m.BLD_YRS_FOR_GEN_PERIOD[g, p]:
            # fill in data for each vintage of generator that is active now
            gen_vintage_period_data[g, v, p] = OrderedDict(
                capacity_in_place=m.BuildGen[g, v],
                capacity_added=m.BuildGen[g, p] if p == v else 0.0,
                capital_outlay=(m.BuildGen[g, p] *
                                (m.gen_overnight_cost[g, p] +
                                 m.gen_connect_cost_per_mw[g]) *
                                ((1.0 - m.gen_investment_subsidy_fraction[g, p]
                                  ) if has_subsidies else 1.0) +
                                ((m.BuildStorageEnergy[g, p] *
                                  m.gen_storage_energy_overnight_cost[g, p])
                                 if is_storage else 0.0)) if p == v else 0.0,
                amortized_cost=m.BuildGen[g, v] *
                m.gen_capital_cost_annual[g, v] +
                ((m.BuildStorageEnergy[g, v] *
                  m.gen_storage_energy_overnight_cost[g, v] *
                  crf(m.interest_rate, m.gen_max_age[g]))
                 if is_storage else 0.0) -
                ((m.gen_investment_subsidy_fraction[g, v] * m.BuildGen[g, v] *
                  m.gen_capital_cost_annual[g, v]) if has_subsidies else 0.0),
            )
            # allocate per-project values among the vintages based on amount
            # of capacity currently online (may not be physically meaningful if
            # gens have discrete commitment, but we assume the gens are run
            # roughly this way)
            vintage_share = ratio(m.BuildGen[g, v], m.GenCapacity[g, p])
            for var, val in gen_period_data.items():
                gen_vintage_period_data[g, v, p][var] = vintage_share * val

    # record capacity retirements
    # (this could be done earlier if we included the variable name
    # in the dictionary key tuple instead of having a data dict for
    # each key)
    for g, v in m.GEN_BLD_YRS:
        retire_year = v + m.gen_max_age[g]
        # find the period when this retires
        for p in m.PERIODS:
            if p >= retire_year:
                gen_vintage_period_data \
                    .setdefault((g, v, p), OrderedDict())['capacity_retired'] \
                    = m.BuildGen[g, v]
                break

    # convert dicts to data frames
    generator_df = (pd.DataFrame(
        evaluate(gen_vintage_period_data)).unstack().to_frame(name='value'))
    generator_df.index.names = [
        'generation_project', 'gen_vintage', 'period', 'variable'
    ]
    for g, d in gen_data.items():
        for k, v in d.items():
            # assign generator general data to all rows with generator==g
            generator_df.loc[g, k] = v
    # convert from float
    generator_df['gen_is_intermittent'] = generator_df[
        'gen_is_intermittent'].astype(int)
    generator_df = generator_df.reset_index().set_index([
        'generation_project', 'gen_vintage', 'gen_tech', 'gen_load_zone',
        'gen_energy_source', 'gen_is_intermittent', 'variable'
    ]).sort_index()
    generator_df.to_csv(os.path.join(outdir, 'generation_project_details.csv'),
                        index=True)

    # dict should be var, gen, period
    # but gens have all-years values too (technology, fuel, etc.)
    # and there are per-year non-gen values

    # report other costs on an undiscounted, annualized basis
    # (custom modules, transmission, etc.)

    # List of comparisons to make later; dict value shows which model
    # components should match which variables in generator_df
    itemized_cost_comparisons = {
        'gen_fixed_cost': ([
            'TotalGenFixedCosts', 'StorageEnergyFixedCost',
            'TotalGenCapitalCostsSubsidy'
        ], ['amortized_cost', 'fixed_om']),
        'fuel_cost': (['FuelCostsPerPeriod',
                       'RFM_Fixed_Costs_Annual'], ['fuel_cost']),
        'variable_om':
        (['GenVariableOMCostsInTP',
          'Total_StartupGenCapacity_OM_Costs'], ['startup_om', 'variable_om'])
    }

    ##### most detailed level of data:
    # owner, tech, generator, fuel (if relevant, otherwise 'all' or specific fuel or 'multiple'?)
    # then aggregate up
    """
    In generic summarize_results.py:
    - lists of summary expressions; each creates a new variable per indexing set
      then those get added to summary tables, which then get aggregated
    gen_fuel_period_exprs
    gen_period_exprs (can incl. owner, added to top of list from outside)
    gen_exprs -> get pushed down into gen_period table? or only when creating by-period summaries?
    period_exprs (get added as quasi-gens)
    fuel_period_exprs

    these create tables like 'summary_per_gen_fuel_period' (including quasi gen
    data from period_exprs and fuel_period_exprs).
    Those get pivoted
    to make 'summary_per_gen_fuel_by_period', with data from 'summary_per_gen_fuel'
    added to the same rows. Maybe there should be a list of summary groups too. ugh.
    """

    # list of costs that should have already been accounted for
    itemized_gen_costs = set(
        component
        for model_costs, df_costs in itemized_cost_comparisons.values()
        for component in model_costs)

    non_gen_costs = OrderedDict()
    for p in m.PERIODS:
        non_gen_costs[p] = {
            cost: getattr(m, cost)[p]
            for cost in m.Cost_Components_Per_Period
            if cost not in itemized_gen_costs
        }
        for cost in m.Cost_Components_Per_TP:
            if cost not in itemized_gen_costs:
                non_gen_costs[p][cost] = sum(
                    getattr(m, cost)[t] * m.tp_weight_in_year[t]
                    for t in m.TPS_IN_PERIOD[p])
        non_gen_costs[p]['co2_emissions'] = m.AnnualEmissions[p]
        non_gen_costs[p]['gross_load'] = ann(
            lambda g, t: sum(m.zone_demand_mw[z, t] for z in m.LOAD_ZONES))
        non_gen_costs[p]['ev_load'] = 0.0
        if hasattr(m, 'ChargeEVs'):
            non_gen_costs[p]['ev_load'] += ann(
                lambda g, t: sum(m.ChargeEVs[z, t] for z in m.LOAD_ZONES))
        if hasattr(m, 'ev_charge_min') and hasattr(m, 'ChargeEVs_min'):
            m.logger.error(
                'ERROR: Need to update {} to handle combined loads from '
                'ev_simple and ev_advanced modules'.format(__name__))
        if hasattr(m, 'StorePumpedHydro'):
            non_gen_costs[p]['Pumped_Hydro_Net_Load'] = ann(lambda g, t: sum(
                m.StorePumpedHydro[z, t] - m.GeneratePumpedHydro[z, t]
                for z in m.LOAD_ZONES))

    non_gen_df = pd.DataFrame(
        evaluate(non_gen_costs)).unstack().to_frame(name='value')
    non_gen_df.index.names = ['period', 'variable']
    non_gen_df.to_csv(
        os.path.join(outdir, 'non_generation_costs_by_period.csv'))

    # check whether reported generator costs match values used in the model
    gen_df_totals = generator_df.groupby(['variable', 'period'])['value'].sum()
    gen_total_costs = defaultdict(float)
    for label, (model_costs, df_costs) in itemized_cost_comparisons.items():
        for p in m.PERIODS:
            for cost in model_costs:
                if cost in m.Cost_Components_Per_Period:
                    cost_val = value(getattr(m, cost)[p])
                elif cost in m.Cost_Components_Per_TP:
                    # aggregate to period
                    cost_val = value(
                        sum(
                            getattr(m, cost)[t] * m.tp_weight_in_year[t]
                            for t in m.TPS_IN_PERIOD[p]))
                else:
                    cost_val = 0.0
                gen_total_costs[label, p, 'model'] += cost_val
            gen_total_costs[label, p,
                            'reported'] = (gen_df_totals.loc[df_costs,
                                                             p].sum())
            mc = gen_total_costs[label, p, 'model']
            rc = gen_total_costs[label, p, 'reported']
            if different(mc, rc):
                m.logger.warning(
                    "WARNING: model and reported values don't match for {} in "
                    "{}: {:,.0f} != {:,.0f}; NPV of difference: {:,.0f}.".
                    format(label, p, mc, rc,
                           m.bring_annual_costs_to_base_year[p] * (mc - rc)))
                raise
            # else:
            #     m.logger.info(
            #         "INFO: model and reported values match for {} in "
            #         "{}: {} == {}.".format(label, p, mc, rc)
            #     )

    # check costs on an aggregated basis too (should be OK if the gen costs are)
    cost_vars = [
        var for model_costs, df_costs in itemized_cost_comparisons.values()
        for var in df_costs
    ]
    total_costs = (
        generator_df.loc[pd.IndexSlice[:, :, :, :, cost_vars], :].
        groupby('period')['value'].sum()) + non_gen_df.unstack(0).drop(
            ['co2_emissions', 'gross_load', 'Pumped_Hydro_Net_Load']).sum()
    npv_cost = value(
        sum(m.bring_annual_costs_to_base_year[p] * v
            for ((_, p), v) in total_costs.iteritems()))
    system_cost = value(m.SystemCost)
    if different(npv_cost, system_cost):
        m.logger.warning(
            "WARNING: NPV of all costs in model doesn't match reported total: "
            "{:,.0f} != {:,.0f}; difference: {:,.0f}.".format(
                npv_cost, system_cost, npv_cost - system_cost))

    print()
    print("TODO: *** check for missing MWh terms in {}.".format(__name__))
    print()

    print("Creating RIST summary; may take several minutes.")
    summarize_for_rist(m, outdir)

    # data for HECO info request 2/14/20
    print("Saving hourly reserve data.")
    report_hourly_reserves(m)
    if hasattr(m, 'Smooth_Free_Variables'):
        # using the smooth_dispatch module; re-report dispatch data
        print("Re-saving dispatch data after smoothing.")
        import switch_model.generators.core.dispatch as dispatch
        dispatch.post_solve(m, m.options.outputs_dir)
    else:
        print(
            "WARNING: the smooth_dispatch module is not being used. Hourly "
            "dispatch may be rough and hourly contingency reserve targets may "
            "inflated.")

    print("Comparing Switch to EIA production data.")
    if True:
        compare_switch_to_eia_production(m)
    else:
        print("(skipped, takes several minutes)")
예제 #4
0
파일: hydrogen.py 프로젝트: nielswxf/switch
def define_components(m):

    # 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_minimum_size_kg = Param(default=0.0)
    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.TPS_IN_TS[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.TPS_IN_TS[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.TS_IN_PERIOD[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]])

    # minimum size for hydrogen tank
    m.BuildAnyLiquidHydrogenTank = Var(m.LOAD_ZONES, m.PERIODS, within=Binary)
    m.Set_BuildAnyLiquidHydrogenTank_Flag = Constraint(m.LOAD_ZONES, m.PERIODS, rule=lambda m, z, p:
        Constraint.Skip if m.liquid_hydrogen_tank_minimum_size_kg == 0.0
        else (
            m.BuildLiquidHydrogenTankKg[z, p] 
            <= 
            1000 * m.BuildAnyLiquidHydrogenTank[z, p] * m.liquid_hydrogen_tank_minimum_size_kg
        )
    )
    m.Build_Minimum_Liquid_Hydrogen_Tank = Constraint(m.LOAD_ZONES, m.PERIODS, rule=lambda m, z, p:
        Constraint.Skip if m.liquid_hydrogen_tank_minimum_size_kg == 0.0
        else (
            m.BuildLiquidHydrogenTankKg[z, p] 
            >= 
            m.BuildAnyLiquidHydrogenTank[z, p] * m.liquid_hydrogen_tank_minimum_size_kg
        )
    )

    # maximum amount that hydrogen fuel cells can contribute to system reserves
    # Note: we assume we can't use fuel cells for reserves unless we've also built at least half 
    # as much electrolyzer capacity and a tank that can provide the reserves for 12 hours
    # (this is pretty arbitrary, but avoids just installing a fuel cell as a "free" source of reserves)
    m.HydrogenFuelCellMaxReservePower = Var(m.LOAD_ZONES, m.TIMEPOINTS)
    m.Hydrogen_FC_Reserve_Capacity_Limit = Constraint(m.LOAD_ZONES, m.TIMEPOINTS, rule=lambda m, z, t:
        m.HydrogenFuelCellMaxReservePower[z, t]
        <=
        m.FuelCellCapacityMW[z, m.tp_period[t]]
    )
    m.Hydrogen_FC_Reserve_Storage_Limit = Constraint(m.LOAD_ZONES, m.TIMEPOINTS, rule=lambda m, z, t:
        m.HydrogenFuelCellMaxReservePower[z, t]
        <=
        m.LiquidHydrogenTankCapacityKg[z, m.tp_period[t]] * m.hydrogen_fuel_cell_mwh_per_kg / 12.0
    )
    m.Hydrogen_FC_Reserve_Electrolyzer_Limit = Constraint(m.LOAD_ZONES, m.TIMEPOINTS, rule=lambda m, z, t:
        m.HydrogenFuelCellMaxReservePower[z, t]
        <=
        2.0 * m.ElectrolyzerCapacityMW[z, m.tp_period[t]]
    )

    # how much extra power could hydrogen equipment produce or absorb on short notice (for reserves)
    m.HydrogenSlackUp = Expression(m.LOAD_ZONES, m.TIMEPOINTS, rule=lambda m, z, t:
        m.RunElectrolyzerMW[z, t] 
        + m.LiquifyHydrogenMW[z, t]
        + m.HydrogenFuelCellMaxReservePower[z, t]
        - m.DispatchFuelCellMW[z, t]
    )
    m.HydrogenSlackDown = Expression(m.LOAD_ZONES, m.TIMEPOINTS, rule=lambda m, z, t:
        m.ElectrolyzerCapacityMW[z, m.tp_period[t]] - m.RunElectrolyzerMW[z, t] 
        # ignore liquifier potential since it's small and this is a low-value reserve product
        + m.DispatchFuelCellMW[z, 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.TS_IN_PERIOD[p])
        <= m.LiquidHydrogenTankCapacityKg[z, p]
    )
    
    # add electricity consumption and production to the zonal energy balance
    m.Zone_Power_Withdrawals.append('RunElectrolyzerMW')
    m.Zone_Power_Withdrawals.append('LiquifyHydrogenMW')
    m.Zone_Power_Injections.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_Per_TP.append('HydrogenVariableCost')
    m.Cost_Components_Per_Period.append('HydrogenFixedCostAnnual')
    
    # Register with spinning reserves if it is available
    if [rt.lower() for rt in m.options.hydrogen_reserve_types] != ['none']:
        # Register with spinning reserves
        if hasattr(m, 'Spinning_Reserve_Up_Provisions'):
            # calculate available slack from hydrogen equipment
            m.HydrogenSlackUpForArea = Expression(
                m.BALANCING_AREA_TIMEPOINTS, 
                rule=lambda m, b, t:
                    sum(m.HydrogenSlackUp[z, t] for z in m.ZONES_IN_BALANCING_AREA[b])
            )
            m.HydrogenSlackDownForArea = Expression(
                m.BALANCING_AREA_TIMEPOINTS, 
                rule=lambda m, b, t: 
                    sum(m.HydrogenSlackDown[z, t] for z in m.ZONES_IN_BALANCING_AREA[b])
            )
            if hasattr(m, 'GEN_SPINNING_RESERVE_TYPES'):
                # using advanced formulation, index by reserve type, balancing area, timepoint
                # define variables for each type of reserves to be provided
                # choose how to allocate the slack between the different reserve products
                m.HYDROGEN_SPINNING_RESERVE_TYPES = Set(
                    initialize=m.options.hydrogen_reserve_types
                )
                m.HydrogenSpinningReserveUp = Var(
                    m.HYDROGEN_SPINNING_RESERVE_TYPES, m.BALANCING_AREA_TIMEPOINTS,
                    within=NonNegativeReals
                )
                m.HydrogenSpinningReserveDown = Var(
                    m.HYDROGEN_SPINNING_RESERVE_TYPES, m.BALANCING_AREA_TIMEPOINTS,
                    within=NonNegativeReals
                )
                # constrain reserve provision within available slack
                m.Limit_HydrogenSpinningReserveUp = Constraint(
                    m.BALANCING_AREA_TIMEPOINTS, 
                    rule=lambda m, ba, tp: 
                        sum(
                            m.HydrogenSpinningReserveUp[rt, ba, tp]
                            for rt in m.HYDROGEN_SPINNING_RESERVE_TYPES
                        ) <= m.HydrogenSlackUpForArea[ba, tp]
                )
                m.Limit_HydrogenSpinningReserveDown = Constraint(
                    m.BALANCING_AREA_TIMEPOINTS, 
                    rule=lambda m, ba, tp: 
                        sum(
                            m.HydrogenSpinningReserveDown[rt, ba, tp]
                            for rt in m.HYDROGEN_SPINNING_RESERVE_TYPES
                        ) <= m.HydrogenSlackDownForArea[ba, tp]
                )
                m.Spinning_Reserve_Up_Provisions.append('HydrogenSpinningReserveUp')
                m.Spinning_Reserve_Down_Provisions.append('HydrogenSpinningReserveDown')
            else:
                # using older formulation, only one type of spinning reserves, indexed by balancing area, timepoint
                if m.options.hydrogen_reserve_types != ['spinning']:
                    raise ValueError(
                        'Unable to use reserve types other than "spinning" with simple spinning reserves module.'
                    )
                m.Spinning_Reserve_Up_Provisions.append('HydrogenSlackUpForArea')
                m.Spinning_Reserve_Down_Provisions.append('HydrogenSlackDownForArea')
예제 #5
0
파일: storage.py 프로젝트: ygz94426/switch
def define_components(mod):
    """

    STORAGE_GENS is the subset of projects that can provide energy storage.

    STORAGE_GEN_BLD_YRS is the subset of GEN_BLD_YRS, restricted
    to storage projects.

    gen_storage_efficiency[STORAGE_GENS] 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.

    gen_store_to_release_ratio[STORAGE_GENS] 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 gen_store_to_release_ratio
    of 1.2, then it can consume up to 1.2 MW of power while charging.

    gen_storage_energy_to_power_ratio[STORAGE_GENS], if specified, restricts
    the storage capacity (in MWh) to be a fixed multiple of the output
    power (in MW), i.e., specifies a particular number of hours of
    storage capacity. Omit this column or specify "." to allow Switch
    to choose the energy/power ratio. (Note: gen_storage_energy_overnight_cost
    or gen_overnight_cost should often be set to 0 when using this.)

    gen_storage_max_cycles_per_year[STORAGE_GENS], if specified, restricts
    the number of charge/discharge cycles each storage project can perform
    per year; one cycle is defined as discharging an amount of energy
    equal to the storage capacity of the project.

    gen_storage_energy_overnight_cost[(g, bld_yr) in
    STORAGE_GEN_BLD_YRS] 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.

    BuildStorageEnergy[(g, bld_yr) in STORAGE_GEN_BLD_YRS]
    is a decision of how much energy capacity to build onto a storage
    project. This is analogous to BuildGen, but for energy rather than power.

    StorageEnergyInstallCosts[PERIODS] is an expression of the
    annual costs incurred by the BuildStorageEnergy decision.

    StorageEnergyCapacity[g, period] is an expression describing the
    cumulative available energy capacity of BuildStorageEnergy. This is
    analogous to GenCapacity.

    STORAGE_GEN_TPS is the subset of GEN_TPS,
    restricted to storage projects.

    ChargeStorage[(g, t) in STORAGE_GEN_TPS] is a dispatch
    decision of how much to charge a storage project in each timepoint.

    StorageNetCharge[LOAD_ZONE, TIMEPOINT] is an expression describing the
    aggregate impact of ChargeStorage in each load zone and timepoint.

    Charge_Storage_Upper_Limit[(g, t) in STORAGE_GEN_TPS]
    constrains ChargeStorage to available power capacity (accounting for
    gen_store_to_release_ratio)

    StateOfCharge[(g, t) in STORAGE_GEN_TPS] 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[(g, t) in STORAGE_GEN_TPS] constrains
    StateOfCharge based on the StateOfCharge in the previous timepoint,
    ChargeStorage and DispatchGen.

    State_Of_Charge_Upper_Limit[(g, t) in STORAGE_GEN_TPS]
    constrains StateOfCharge based on installed energy capacity.

    """

    mod.STORAGE_GENS = Set(within=mod.GENERATION_PROJECTS)
    mod.STORAGE_GEN_PERIODS = Set(
        within=mod.GEN_PERIODS,
        initialize=lambda m: [(g, p) for g in m.STORAGE_GENS for p in m.PERIODS_FOR_GEN[g]]
    )
    mod.gen_storage_efficiency = Param(
        mod.STORAGE_GENS,
        within=PercentFraction)
    # TODO: rename to gen_charge_to_discharge_ratio?
    mod.gen_store_to_release_ratio = Param(
        mod.STORAGE_GENS,
        within=NonNegativeReals,
        default=1.0)
    mod.gen_storage_energy_to_power_ratio = Param(
        mod.STORAGE_GENS,
        within=NonNegativeReals,
        default=float("inf")) # inf is a flag that no value is specified (nan and None don't work)
    mod.gen_storage_max_cycles_per_year = Param(
        mod.STORAGE_GENS,
        within=NonNegativeReals,
        default=float('inf'))

    # TODO: build this set up instead of filtering down, to improve performance
    mod.STORAGE_GEN_BLD_YRS = Set(
        dimen=2,
        initialize=mod.GEN_BLD_YRS,
        filter=lambda m, g, bld_yr: g in m.STORAGE_GENS)
    mod.gen_storage_energy_overnight_cost = Param(
        mod.STORAGE_GEN_BLD_YRS,
        within=NonNegativeReals)
    mod.min_data_check('gen_storage_energy_overnight_cost')
    mod.BuildStorageEnergy = Var(
        mod.STORAGE_GEN_BLD_YRS,
        within=NonNegativeReals)

    # Summarize capital costs of energy storage for the objective function.
    mod.StorageEnergyInstallCosts = Expression(
        mod.PERIODS,
        rule=lambda m, p: sum(m.BuildStorageEnergy[g, bld_yr] *
                   m.gen_storage_energy_overnight_cost[g, bld_yr] *
                   crf(m.interest_rate, m.gen_max_age[g])
                   for (g, bld_yr) in m.STORAGE_GEN_BLD_YRS))
    mod.Cost_Components_Per_Period.append(
        'StorageEnergyInstallCosts')

    mod.StorageEnergyCapacity = Expression(
        mod.STORAGE_GENS, mod.PERIODS,
        rule=lambda m, g, period: sum(
            m.BuildStorageEnergy[g, bld_yr]
            for bld_yr in m.BLD_YRS_FOR_GEN_PERIOD[g, period]))

    mod.STORAGE_GEN_TPS = Set(
        dimen=2,
        initialize=lambda m: (
            (g, tp)
                for g in m.STORAGE_GENS
                    for tp in m.TPS_FOR_GEN[g]))

    mod.ChargeStorage = Var(
        mod.STORAGE_GEN_TPS,
        within=NonNegativeReals)

    # Summarize storage charging for the energy balance equations
    # TODO: rename this StorageTotalCharging or similar (to indicate it's a
    # sum for a zone, not a net quantity for a project)
    def rule(m, z, t):
        # Construct and cache a set for summation as needed
        if not hasattr(m, 'Storage_Charge_Summation_dict'):
            m.Storage_Charge_Summation_dict = collections.defaultdict(set)
            for g, t2 in m.STORAGE_GEN_TPS:
                z2 = m.gen_load_zone[g]
                m.Storage_Charge_Summation_dict[z2, t2].add(g)
        # Use pop to free memory
        relevant_projects = m.Storage_Charge_Summation_dict.pop((z, t), {})
        return sum(m.ChargeStorage[g, t] for g in relevant_projects)
    mod.StorageNetCharge = Expression(mod.LOAD_ZONES, mod.TIMEPOINTS, rule=rule)
    # Register net charging with zonal energy balance. Discharging is already
    # covered by DispatchGen.
    mod.Zone_Power_Withdrawals.append('StorageNetCharge')

    # use fixed energy/power ratio (# hours of capacity) when specified
    mod.Enforce_Fixed_Energy_Storage_Ratio = Constraint(
        mod.STORAGE_GEN_BLD_YRS,
        rule=lambda m, g, y:
            Constraint.Skip if m.gen_storage_energy_to_power_ratio[g] == float("inf") # no value specified
            else
            (m.BuildStorageEnergy[g, y] == m.gen_storage_energy_to_power_ratio[g] * m.BuildGen[g, y])
    )

    def Charge_Storage_Upper_Limit_rule(m, g, t):
        return m.ChargeStorage[g,t] <= \
            m.DispatchUpperLimit[g, t] * m.gen_store_to_release_ratio[g]
    mod.Charge_Storage_Upper_Limit = Constraint(
        mod.STORAGE_GEN_TPS,
        rule=Charge_Storage_Upper_Limit_rule)

    mod.StateOfCharge = Var(
        mod.STORAGE_GEN_TPS,
        within=NonNegativeReals)

    def Track_State_Of_Charge_rule(m, g, t):
        return m.StateOfCharge[g, t] == \
            m.StateOfCharge[g, m.tp_previous[t]] + \
            (m.ChargeStorage[g, t] * m.gen_storage_efficiency[g] -
             m.DispatchGen[g, t]) * m.tp_duration_hrs[t]
    mod.Track_State_Of_Charge = Constraint(
        mod.STORAGE_GEN_TPS,
        rule=Track_State_Of_Charge_rule)

    def State_Of_Charge_Upper_Limit_rule(m, g, t):
        return m.StateOfCharge[g, t] <= \
            m.StorageEnergyCapacity[g, m.tp_period[t]]
    mod.State_Of_Charge_Upper_Limit = Constraint(
        mod.STORAGE_GEN_TPS,
        rule=State_Of_Charge_Upper_Limit_rule)

    # batteries can only complete the specified number of cycles per year, averaged over each period
    mod.Battery_Cycle_Limit = Constraint(
        mod.STORAGE_GEN_PERIODS,
        rule=lambda m, g, p:
            # solvers sometimes perform badly with infinite constraint
            Constraint.Skip if m.gen_storage_max_cycles_per_year[g] == float('inf')
            else (
                sum(m.DispatchGen[g, tp] * m.tp_duration_hrs[tp] for tp in m.TPS_IN_PERIOD[p])
                <=
                m.gen_storage_max_cycles_per_year[g] * m.StorageEnergyCapacity[g, p] * m.period_length_years[p]
            )
    )
예제 #6
0
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()
    mod.gen_dbid = Param(mod.GENERATION_PROJECTS, default=lambda m, g: g)
    mod.gen_tech = Param(mod.GENERATION_PROJECTS)
    mod.GENERATION_TECHNOLOGIES = Set(initialize=lambda m:
        {m.gen_tech[g] for g in m.GENERATION_PROJECTS}
    )
    mod.gen_energy_source = Param(mod.GENERATION_PROJECTS, 
        validate=lambda m,val,g: val in m.ENERGY_SOURCES or val == "multiple")
    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)
    mod.gen_is_baseload = Param(mod.GENERATION_PROJECTS, within=Boolean)
    mod.gen_is_cogen = Param(mod.GENERATION_PROJECTS, within=Boolean, default=False)
    mod.gen_is_distributed = Param(mod.GENERATION_PROJECTS, within=Boolean, default=False)
    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])
    
    mod.CAPACITY_LIMITED_GENS = Set(within=mod.GENERATION_PROJECTS)
    mod.gen_capacity_limit_mw = Param(
        mod.CAPACITY_LIMITED_GENS, within=PositiveReals)
    mod.DISCRETELY_SIZED_GENS = Set(within=mod.GENERATION_PROJECTS)
    mod.gen_unit_size = Param(
        mod.DISCRETELY_SIZED_GENS, within=PositiveReals)
    mod.CCS_EQUIPPED_GENS = Set(within=mod.GENERATION_PROJECTS)
    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"))
    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.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_predetermined_cap = Param(
        mod.PREDETERMINED_GEN_BLD_YRS,
        within=NonNegativeReals)
    mod.min_data_check('gen_predetermined_cap')
    

    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)

    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.
    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')
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_FOR_PERIOD[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 storage dispatch to the zonal energy balance
    m.Zone_Power_Injections.append('DischargeBattery')
    m.Zone_Power_Withdrawals.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_FOR_PERIOD[p]
            if bld_yr + m.battery_n_years > p for z in m.LOAD_ZONES))
    m.Cost_Components_Per_Period.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.tp_duration_hrs[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_Rate = Constraint(
        m.LOAD_ZONES,
        m.TIMEPOINTS,
        rule=lambda m, z, t: m.ChargeBattery[z, t] <=
        # changed 2018-02-20 to allow full discharge in min_discharge_time,
        # (previously pegged to battery_max_discharge)
        m.Battery_Capacity[z, m.tp_period[t]] / m.battery_min_discharge_time)
    m.Battery_Max_Discharge_Rate = 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_min_discharge_time)

    # how much could output/input be increased on short notice (to provide reserves)
    m.BatterySlackUp = Expression(
        m.LOAD_ZONES,
        m.TIMEPOINTS,
        rule=lambda m, z, t: m.Battery_Capacity[z, m.tp_period[
            t]] / m.battery_min_discharge_time - m.DischargeBattery[
                z, t] + m.ChargeBattery[z, t])
    m.BatterySlackDown = Expression(
        m.LOAD_ZONES,
        m.TIMEPOINTS,
        rule=lambda m, z, t: m.Battery_Capacity[z, m.tp_period[
            t]] / m.battery_min_discharge_time - m.ChargeBattery[
                z, t] + m.DischargeBattery[z, t])

    # assume batteries can only complete one full cycle per day, averaged over each period
    # (this was pegged to battery_max_discharge before 2018-02-20)
    m.Battery_Cycle_Limit = Constraint(
        m.LOAD_ZONES,
        m.PERIODS,
        rule=lambda m, z, p: sum(m.DischargeBattery[z, tp] * m.tp_duration_hrs[
            tp] for tp in m.TPS_IN_PERIOD[p]) <= m.Battery_Capacity[
                z, p] * m.period_length_hours[p])

    # Register with spinning reserves if it is available
    if hasattr(m, 'Spinning_Reserve_Up_Provisions'):
        m.BatterySpinningReserveUp = Expression(
            m.BALANCING_AREA_TIMEPOINTS,
            rule=lambda m, b, t: sum(m.BatterySlackUp[z, t]
                                     for z in m.ZONES_IN_BALANCING_AREA[b]))
        m.Spinning_Reserve_Up_Provisions.append('BatterySpinningReserveUp')

        m.BatterySpinningReserveDown = Expression(
            m.BALANCING_AREA_TIMEPOINTS,
            rule=lambda m, b, t: \
                sum(m.BatterySlackDown[z, t]
                    for z in m.ZONES_IN_BALANCING_AREA[b])
        )
        m.Spinning_Reserve_Down_Provisions.append('BatterySpinningReserveDown')
예제 #8
0
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 transmission 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.

    BLD_YRS_FOR_TX 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'.

    BLD_YRS_FOR_EXISTING_TX is a subset of BLD_YRS_FOR_TX 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 BLD_YRS_FOR_TX that describes
    potential builds.

    BuildTx[(tx, bld_yr) in BLD_YRS_FOR_TX] is a decision variable
    that describes the transfer capacity in MW installed on a corridor
    in a given build year. For existing builds, this variable is locked
    to the existing capacity.

    TxCapacityNameplate[(tx, bld_yr) in BLD_YRS_FOR_TX] 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.

    TxCapacityNameplateAvailable[(tx, bld_yr) in BLD_YRS_FOR_TX] 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_om_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.

    DIRECTIONAL_TX 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.

    TX_BUILDS_IN_PERIOD[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.

    TX_BUILDS_IN_PERIOD[p] will return a subset of (tx, bld_yr)
    in BLD_YRS_FOR_TX.

    --- 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)
    # we don't do a min_data_check for TRANSMISSION_LINES, because it may be empty for model
    # configurations that are sometimes run with interzonal transmission and sometimes not
    # (e.g., island interconnect scenarios). However, presence of this column will still be
    # checked by load_data_aug.
    mod.min_data_check('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=NonNegativeReals)
    mod.trans_efficiency = Param(mod.TRANSMISSION_LINES,
                                 within=PercentFraction)
    mod.BLD_YRS_FOR_EXISTING_TX = 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)
    # Note: we don't do a min_data_check for BLD_YRS_FOR_EXISTING_TX, because it may be empty for
    # models that start with no pre-existing transmission (e.g., island interconnect scenarios).
    mod.min_data_check('trans_length_km', 'trans_efficiency',
                       '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=mod.TRANSMISSION_LINES * mod.PERIODS,
        filter=lambda m, tx, p: m.trans_new_build_allowed[tx])
    mod.BLD_YRS_FOR_TX = Set(
        dimen=2,
        initialize=lambda m: m.BLD_YRS_FOR_EXISTING_TX | m.NEW_TRANS_BLD_YRS)
    mod.TX_BUILDS_IN_PERIOD = Set(mod.PERIODS,
                                  within=mod.BLD_YRS_FOR_TX,
                                  initialize=lambda m, p: set(
                                      (tx, bld_yr)
                                      for (tx, bld_yr) in m.BLD_YRS_FOR_TX
                                      if bld_yr == 'Legacy' or bld_yr <= p))

    def bounds_BuildTx(model, tx, bld_yr):
        if ((tx, bld_yr) in model.BLD_YRS_FOR_EXISTING_TX):
            return (model.existing_trans_cap[tx], model.existing_trans_cap[tx])
        else:
            return (0, None)

    mod.BuildTx = Var(mod.BLD_YRS_FOR_TX,
                      within=NonNegativeReals,
                      bounds=bounds_BuildTx)
    mod.TxCapacityNameplate = Expression(
        mod.TRANSMISSION_LINES,
        mod.PERIODS,
        rule=lambda m, tx, period: sum(
            m.BuildTx[tx, bld_yr] for (tx2, bld_yr) in m.BLD_YRS_FOR_TX
            if tx2 == tx and (bld_yr == 'Legacy' or bld_yr <= period)))
    mod.trans_derating_factor = Param(mod.TRANSMISSION_LINES,
                                      within=PercentFraction,
                                      default=1)
    mod.TxCapacityNameplateAvailable = Expression(
        mod.TRANSMISSION_LINES,
        mod.PERIODS,
        rule=lambda m, tx, period:
        (m.TxCapacityNameplate[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=NonNegativeReals,
                                             default=1000)
    mod.trans_lifetime_yrs = Param(within=NonNegativeReals, default=20)
    mod.trans_fixed_om_fraction = Param(within=NonNegativeReals, 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=NonNegativeReals,
        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_om_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.TxFixedCosts = Expression(
        mod.PERIODS,
        rule=lambda m, p: sum(m.BuildTx[tx, bld_yr] * m.trans_cost_annual[tx]
                              for (tx, bld_yr) in m.TX_BUILDS_IN_PERIOD[p]))
    mod.Cost_Components_Per_Period.append('TxFixedCosts')

    def init_DIRECTIONAL_TX(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.DIRECTIONAL_TX = Set(dimen=2, initialize=init_DIRECTIONAL_TX)
    mod.TX_CONNECTIONS_TO_ZONE = Set(
        mod.LOAD_ZONES,
        initialize=lambda m, lz: set(z for z in m.LOAD_ZONES
                                     if (z, lz) in m.DIRECTIONAL_TX))

    def init_trans_d_line(m, zone_from, zone_to):
        for tx in m.TRANSMISSION_LINES:
            if ((m.trans_lz1[tx] == zone_from and m.trans_lz2[tx] == zone_to)
                    or
                (m.trans_lz2[tx] == zone_from and m.trans_lz1[tx] == zone_to)):
                return tx

    mod.trans_d_line = Param(mod.DIRECTIONAL_TX,
                             within=mod.TRANSMISSION_LINES,
                             initialize=init_trans_d_line)
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_FOR_PERIOD[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 storage dispatch to the zonal energy balance
    m.Zone_Power_Injections.append('DischargeBattery')
    m.Zone_Power_Withdrawals.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_FOR_PERIOD[p] if bld_yr + m.battery_n_years > p
                    for z in m.LOAD_ZONES
        )
    )
    m.Cost_Components_Per_Period.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.tp_duration_hrs[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_Rate = Constraint(m.LOAD_ZONES, m.TIMEPOINTS, rule=lambda m, z, t:
        m.ChargeBattery[z, t]
        <=
        # changed 2018-02-20 to allow full discharge in min_discharge_time,
        # (previously pegged to battery_max_discharge)
        m.Battery_Capacity[z, m.tp_period[t]] / m.battery_min_discharge_time
    )
    m.Battery_Max_Discharge_Rate = 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_min_discharge_time
    )

    # how much could output/input be increased on short notice (to provide reserves)
    m.BatterySlackUp = Expression(m.LOAD_ZONES, m.TIMEPOINTS, rule=lambda m, z, t:
        m.Battery_Capacity[z, m.tp_period[t]] / m.battery_min_discharge_time
        - m.DischargeBattery[z, t]
        + m.ChargeBattery[z, t]
    )
    m.BatterySlackDown = Expression(m.LOAD_ZONES, m.TIMEPOINTS, rule=lambda m, z, t:
        m.Battery_Capacity[z, m.tp_period[t]] / m.battery_min_discharge_time
        - m.ChargeBattery[z, t]
        + m.DischargeBattery[z, t]
    )

    # assume batteries can only complete one full cycle per day, averaged over each period
    # (this was pegged to battery_max_discharge before 2018-02-20)
    m.Battery_Cycle_Limit = Constraint(m.LOAD_ZONES, m.PERIODS, rule=lambda m, z, p:
        sum(m.DischargeBattery[z, tp] * m.tp_duration_hrs[tp] for tp in m.TPS_IN_PERIOD[p])
        <=
        m.Battery_Capacity[z, p] * m.period_length_hours[p]
    )

    # Register with spinning reserves if it is available
    if 'Spinning_Reserve_Up_Provisions' in dir(m):
        m.BatterySpinningReserveUp = Expression(
            m.BALANCING_AREA_TIMEPOINTS,
            rule=lambda m, b, t:
                sum(m.BatterySlackUp[z, t]
                    for z in m.ZONES_IN_BALANCING_AREA[b])
        )
        m.Spinning_Reserve_Up_Provisions.append('BatterySpinningReserveUp')

        m.BatterySpinningReserveDown = Expression(
            m.BALANCING_AREA_TIMEPOINTS,
            rule=lambda m, b, t: \
                sum(m.BatterySlackDown[z, t] 
                    for z in m.ZONES_IN_BALANCING_AREA[b])
        )
        m.Spinning_Reserve_Down_Provisions.append('BatterySpinningReserveDown')
예제 #10
0
def define_components(m):

    m.PH_GENS = Set()

    m.ph_load_zone = Param(m.PH_GENS)

    m.ph_capital_cost_per_mw = Param(m.PH_GENS, within=NonNegativeReals)
    m.ph_project_life = Param(m.PH_GENS, within=NonNegativeReals)

    # annual O&M cost for pumped hydro project, percent of capital cost
    m.ph_fixed_om_percent = Param(m.PH_GENS, within=NonNegativeReals)

    # total annual cost
    m.ph_fixed_cost_per_mw_per_year = Param(m.PH_GENS, 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_GENS)

    # average energy available from water inflow each day
    # (system must balance energy net of this each day)
    m.ph_inflow_mw = Param(m.PH_GENS)

    # maximum size of pumped hydro project
    m.ph_max_capacity_mw = Param(m.PH_GENS)

    # How much pumped hydro to build
    m.BuildPumpedHydroMW = Var(m.PH_GENS, m.PERIODS, within=NonNegativeReals)
    m.Pumped_Hydro_Proj_Capacity_MW = Expression(m.PH_GENS, m.PERIODS, rule=lambda m, g, pe:
        sum(m.BuildPumpedHydroMW[g, pp] for pp in m.CURRENT_AND_PRIOR_PERIODS_FOR_PERIOD[pe])
    )

    # flag indicating whether any capacity is added to each project each year
    m.BuildAnyPumpedHydro = Var(m.PH_GENS, m.PERIODS, within=Binary)

    # How to run pumped hydro
    m.PumpedHydroProjGenerateMW = Var(m.PH_GENS, m.TIMEPOINTS, within=NonNegativeReals)
    m.PumpedHydroProjStoreMW = Var(m.PH_GENS, 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_GENS, m.PERIODS, rule=lambda m, g, pe:
        m.Pumped_Hydro_Proj_Capacity_MW[g, pe] <= m.ph_max_capacity_mw[g]
    )

    # force the build flag on for the year(s) when pumped hydro is built
    m.Pumped_Hydro_Set_Build_Flag = Constraint(m.PH_GENS, m.PERIODS, rule=lambda m, g, pe:
        m.BuildPumpedHydroMW[g, pe] <= m.BuildAnyPumpedHydro[g, pe] * m.ph_max_capacity_mw[g]
    )
    # only build in one year (can be deactivated to allow incremental construction)
    m.Pumped_Hydro_Build_Once = Constraint(m.PH_GENS, rule=lambda m, g:
        sum(m.BuildAnyPumpedHydro[g, 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_GENS, m.PERIODS, rule=lambda m, g, pe:
        m.BuildPumpedHydroMW[g, pe] == m.BuildAnyPumpedHydro[g, pe] * m.ph_max_capacity_mw[g]
    )
    # 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_GENS, m.TIMEPOINTS, rule=lambda m, g, t:
        m.PumpedHydroProjGenerateMW[g, t]
        <=
        m.Pumped_Hydro_Proj_Capacity_MW[g, m.tp_period[t]]
    )
    m.Pumped_Hydro_Max_Store_Rate = Constraint(m.PH_GENS, m.TIMEPOINTS, rule=lambda m, g, t:
        m.PumpedHydroProjStoreMW[g, t]
        <=
        m.Pumped_Hydro_Proj_Capacity_MW[g, 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_GENS, m.TIMESERIES, rule=lambda m, g, ts:
        sum(
            m.PumpedHydroProjStoreMW[g, tp] * m.ph_efficiency[g]
            + m.ph_inflow_mw[g]
            - m.PumpedHydroProjGenerateMW[g, tp]
            for tp in m.TPS_IN_TS[ts]
         ) >= 0
    )

    m.GeneratePumpedHydro = Expression(m.LOAD_ZONES, m.TIMEPOINTS, rule=lambda m, z, t:
        sum(m.PumpedHydroProjGenerateMW[g, t] for g in m.PH_GENS if m.ph_load_zone[g]==z)
    )
    m.StorePumpedHydro = Expression(m.LOAD_ZONES, m.TIMEPOINTS, rule=lambda m, z, t:
        sum(m.PumpedHydroProjStoreMW[g, t] for g in m.PH_GENS if m.ph_load_zone[g]==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[g] * m.Pumped_Hydro_Proj_Capacity_MW[g, pe] for g in m.PH_GENS)
    )
    m.Cost_Components_Per_Period.append('Pumped_Hydro_Fixed_Cost_Annual')

    # add pumped hydro to zonal energy balance
    m.Zone_Power_Injections.append('GeneratePumpedHydro')
    m.Zone_Power_Withdrawals.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[g, pe] for g in m.PH_GENS if m.ph_load_zone[g]==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_GENS, m.PERIODS,
            rule=lambda m, g, pe:
                m.BuildPumpedHydroMW[g, pe] == 0.0 if pe != m.options.ph_year else Constraint.Skip
        )
예제 #11
0
파일: build.py 프로젝트: anamileva/switch
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 transmission 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.

    BLD_YRS_FOR_TX 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'.

    BLD_YRS_FOR_EXISTING_TX is a subset of BLD_YRS_FOR_TX 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 BLD_YRS_FOR_TX that describes
    potential builds.

    BuildTx[(tx, bld_yr) in BLD_YRS_FOR_TX] is a decision variable
    that describes the transfer capacity in MW installed on a corridor
    in a given build year. For existing builds, this variable is locked
    to the existing capacity.

    TxCapacityNameplate[(tx, bld_yr) in BLD_YRS_FOR_TX] 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.

    TxCapacityNameplateAvailable[(tx, bld_yr) in BLD_YRS_FOR_TX] 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.

    DIRECTIONAL_TX 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.

    TX_BUILDS_IN_PERIOD[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.
    
    TX_BUILDS_IN_PERIOD[p] will return a subset of (tx, bld_yr)
    in BLD_YRS_FOR_TX.

    --- 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.BLD_YRS_FOR_EXISTING_TX = 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', 'BLD_YRS_FOR_EXISTING_TX',
        '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=mod.TRANSMISSION_LINES * mod.PERIODS,
        filter=lambda m, tx, p: m.trans_new_build_allowed[tx])
    mod.BLD_YRS_FOR_TX = Set(
        dimen=2,
        initialize=lambda m: m.BLD_YRS_FOR_EXISTING_TX | m.NEW_TRANS_BLD_YRS)
    mod.TX_BUILDS_IN_PERIOD = Set(
        mod.PERIODS,
        within=mod.BLD_YRS_FOR_TX,
        initialize=lambda m, p: set(
            (tx, bld_yr) for (tx, bld_yr) in m.BLD_YRS_FOR_TX
            if bld_yr == 'Legacy' or bld_yr <= p))

    def bounds_BuildTx(model, tx, bld_yr):
        if((tx, bld_yr) in model.BLD_YRS_FOR_EXISTING_TX):
            return (model.existing_trans_cap[tx],
                    model.existing_trans_cap[tx])
        else:
            return (0, None)
    mod.BuildTx = Var(
        mod.BLD_YRS_FOR_TX,
        within=NonNegativeReals,
        bounds=bounds_BuildTx)
    mod.TxCapacityNameplate = Expression(
        mod.TRANSMISSION_LINES, mod.PERIODS,
        rule=lambda m, tx, period: sum(
            m.BuildTx[tx, bld_yr]
            for (tx2, bld_yr) in m.BLD_YRS_FOR_TX
            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.TxCapacityNameplateAvailable = Expression(
        mod.TRANSMISSION_LINES, mod.PERIODS,
        rule=lambda m, tx, period: (
            m.TxCapacityNameplate[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.TxFixedCosts = Expression(
        mod.PERIODS,
        rule=lambda m, p: sum(
            m.BuildTx[tx, bld_yr] * m.trans_cost_annual[tx]
            for (tx, bld_yr) in m.TX_BUILDS_IN_PERIOD[p]))
    mod.Cost_Components_Per_Period.append('TxFixedCosts')

    def init_DIRECTIONAL_TX(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.DIRECTIONAL_TX = Set(
        dimen=2,
        initialize=init_DIRECTIONAL_TX)
    mod.TX_CONNECTIONS_TO_ZONE = Set(
        mod.LOAD_ZONES,
        initialize=lambda m, lz: set(
            z for z in m.LOAD_ZONES if (z,lz) in m.DIRECTIONAL_TX))

    def init_trans_d_line(m, zone_from, zone_to):
        for tx in m.TRANSMISSION_LINES:
            if((m.trans_lz1[tx] == zone_from and m.trans_lz2[tx] == zone_to) or
               (m.trans_lz2[tx] == zone_from and m.trans_lz1[tx] == zone_to)):
                return tx
    mod.trans_d_line = Param(
        mod.DIRECTIONAL_TX,
        within=mod.TRANSMISSION_LINES,
        initialize=init_trans_d_line)
예제 #12
0
def define_components(m):
    
    m.PH_GENS = Set()
    
    m.ph_load_zone = Param(m.PH_GENS)
    
    m.ph_capital_cost_per_mw = Param(m.PH_GENS, within=NonNegativeReals)
    m.ph_gect_life = Param(m.PH_GENS, within=NonNegativeReals)
    
    # annual O&M cost for pumped hydro project, percent of capital cost
    m.ph_fixed_om_percent = Param(m.PH_GENS, within=NonNegativeReals)
    
    # total annual cost
    m.ph_fixed_cost_per_mw_per_year = Param(m.PH_GENS, initialize=lambda m, p:
        m.ph_capital_cost_per_mw[p] * 
            (crf(m.interest_rate, m.ph_gect_life[p]) + m.ph_fixed_om_percent[p])
    )
    
    # round-trip efficiency of the pumped hydro facility
    m.ph_efficiency = Param(m.PH_GENS)
    
    # average energy available from water inflow each day
    # (system must balance energy net of this each day)
    m.ph_inflow_mw = Param(m.PH_GENS)
    
    # maximum size of pumped hydro project
    m.ph_max_capacity_mw = Param(m.PH_GENS)
    
    # How much pumped hydro to build
    m.BuildPumpedHydroMW = Var(m.PH_GENS, m.PERIODS, within=NonNegativeReals)
    m.Pumped_Hydro_Proj_Capacity_MW = Expression(m.PH_GENS, m.PERIODS, rule=lambda m, g, pe:
        sum(m.BuildPumpedHydroMW[g, 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_GENS, m.PERIODS, within=Binary)    

    # How to run pumped hydro
    m.PumpedHydroProjGenerateMW = Var(m.PH_GENS, m.TIMEPOINTS, within=NonNegativeReals)
    m.PumpedHydroProjStoreMW = Var(m.PH_GENS, 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_GENS, m.PERIODS, rule=lambda m, g, pe:
        m.Pumped_Hydro_Proj_Capacity_MW[g, pe] <= m.ph_max_capacity_mw[g]
    )
    
    # force the build flag on for the year(s) when pumped hydro is built
    m.Pumped_Hydro_Set_Build_Flag = Constraint(m.PH_GENS, m.PERIODS, rule=lambda m, g, pe:
        m.BuildPumpedHydroMW[g, pe] <= m.BuildAnyPumpedHydro[g, pe] * m.ph_max_capacity_mw[g]
    )
    # only build in one year (can be deactivated to allow incremental construction)
    m.Pumped_Hydro_Build_Once = Constraint(m.PH_GENS, rule=lambda m, g:
        sum(m.BuildAnyPumpedHydro[g, 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_GENS, m.PERIODS, rule=lambda m, g, pe:
        m.BuildPumpedHydroMW[g, pe] == m.BuildAnyPumpedHydro[g, pe] * m.ph_max_capacity_mw[g]
    )
    # 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_GENS, m.TIMEPOINTS, rule=lambda m, g, t:
        m.PumpedHydroProjGenerateMW[g, t]
        <=
        m.Pumped_Hydro_Proj_Capacity_MW[g, m.tp_period[t]]
    )
    m.Pumped_Hydro_Max_Store_Rate = Constraint(m.PH_GENS, m.TIMEPOINTS, rule=lambda m, g, t:
        m.PumpedHydroProjStoreMW[g, t]
        <=
        m.Pumped_Hydro_Proj_Capacity_MW[g, 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_GENS, m.TIMESERIES, rule=lambda m, g, ts:
        sum(
            m.PumpedHydroProjStoreMW[g, tp] * m.ph_efficiency[g]
            + m.ph_inflow_mw[g]
            - m.PumpedHydroProjGenerateMW[g, tp]
            for tp in m.TPS_IN_TS[ts]
         ) >= 0
    )

    m.GeneratePumpedHydro = Expression(m.LOAD_ZONES, m.TIMEPOINTS, rule=lambda m, z, t:
        sum(m.PumpedHydroProjGenerateMW[g, t] for g in m.PH_GENS if m.ph_load_zone[g]==z)
    )
    m.StorePumpedHydro = Expression(m.LOAD_ZONES, m.TIMEPOINTS, rule=lambda m, z, t:
        sum(m.PumpedHydroProjStoreMW[g, t] for g in m.PH_GENS if m.ph_load_zone[g]==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[g] * m.Pumped_Hydro_Proj_Capacity_MW[g, pe] for g in m.PH_GENS)
    )
    m.Cost_Components_Per_Period.append('Pumped_Hydro_Fixed_Cost_Annual')
    
    # add pumped hydro to zonal energy balance
    m.Zone_Power_Injections.append('GeneratePumpedHydro')
    m.Zone_Power_Withdrawals.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[g, pe] for g in m.PH_GENS if m.ph_load_zone[g]==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_GENS, m.PERIODS,
            rule=lambda m, g, pe:
                m.BuildPumpedHydroMW[g, pe] == 0.0 if pe != m.options.ph_year else Constraint.Skip
        )
예제 #13
0
def define_hydrogen_components(m):

    # 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_FOR_PERIOD[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_FOR_PERIOD[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_minimum_size_kg = Param(default=0.0)
    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_FOR_PERIOD[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.TPS_IN_TS[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_FOR_PERIOD[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.TPS_IN_TS[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.TS_IN_PERIOD[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]])

    # minimum size for hydrogen tank
    m.BuildAnyLiquidHydrogenTank = Var(m.LOAD_ZONES, m.PERIODS, within=Binary)
    m.Set_BuildAnyLiquidHydrogenTank_Flag = Constraint(m.LOAD_ZONES, m.PERIODS, rule=lambda m, z, p:
        Constraint.Skip if m.liquid_hydrogen_tank_minimum_size_kg == 0.0
        else (
            m.BuildLiquidHydrogenTankKg[z, p]
            <=
            1000 * m.BuildAnyLiquidHydrogenTank[z, p] * m.liquid_hydrogen_tank_minimum_size_kg
        )
    )
    m.Build_Minimum_Liquid_Hydrogen_Tank = Constraint(m.LOAD_ZONES, m.PERIODS, rule=lambda m, z, p:
        Constraint.Skip if m.liquid_hydrogen_tank_minimum_size_kg == 0.0
        else (
            m.BuildLiquidHydrogenTankKg[z, p]
            >=
            m.BuildAnyLiquidHydrogenTank[z, p] * m.liquid_hydrogen_tank_minimum_size_kg
        )
    )

    # maximum amount that hydrogen fuel cells can contribute to system reserves
    # Note: we assume we can't use fuel cells for reserves unless we've also built at least half
    # as much electrolyzer capacity and a tank that can provide the reserves for 12 hours
    # (this is pretty arbitrary, but avoids just installing a fuel cell as a "free" source of reserves)
    m.HydrogenFuelCellMaxReservePower = Var(m.LOAD_ZONES, m.TIMEPOINTS)
    m.Hydrogen_FC_Reserve_Capacity_Limit = Constraint(m.LOAD_ZONES, m.TIMEPOINTS, rule=lambda m, z, t:
        m.HydrogenFuelCellMaxReservePower[z, t]
        <=
        m.FuelCellCapacityMW[z, m.tp_period[t]]
    )
    m.Hydrogen_FC_Reserve_Storage_Limit = Constraint(m.LOAD_ZONES, m.TIMEPOINTS, rule=lambda m, z, t:
        m.HydrogenFuelCellMaxReservePower[z, t]
        <=
        m.LiquidHydrogenTankCapacityKg[z, m.tp_period[t]] * m.hydrogen_fuel_cell_mwh_per_kg / 12.0
    )
    m.Hydrogen_FC_Reserve_Electrolyzer_Limit = Constraint(m.LOAD_ZONES, m.TIMEPOINTS, rule=lambda m, z, t:
        m.HydrogenFuelCellMaxReservePower[z, t]
        <=
        2.0 * m.ElectrolyzerCapacityMW[z, m.tp_period[t]]
    )

    # how much extra power could hydrogen equipment produce or absorb on short notice (for reserves)
    m.HydrogenSlackUp = Expression(m.LOAD_ZONES, m.TIMEPOINTS, rule=lambda m, z, t:
        m.RunElectrolyzerMW[z, t]
        + m.LiquifyHydrogenMW[z, t]
        + m.HydrogenFuelCellMaxReservePower[z, t]
        - m.DispatchFuelCellMW[z, t]
    )
    m.HydrogenSlackDown = Expression(m.LOAD_ZONES, m.TIMEPOINTS, rule=lambda m, z, t:
        m.ElectrolyzerCapacityMW[z, m.tp_period[t]] - m.RunElectrolyzerMW[z, t]
        # ignore liquifier potential since it's small and this is a low-value reserve product
        + m.DispatchFuelCellMW[z, 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.TS_IN_PERIOD[p])
        <= m.LiquidHydrogenTankCapacityKg[z, p]
    )

    # add electricity consumption and production to the zonal energy balance
    m.Zone_Power_Withdrawals.append('RunElectrolyzerMW')
    m.Zone_Power_Withdrawals.append('LiquifyHydrogenMW')
    m.Zone_Power_Injections.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_Per_TP.append('HydrogenVariableCost')
    m.Cost_Components_Per_Period.append('HydrogenFixedCostAnnual')

    # Register with spinning reserves if it is available
    if [rt.lower() for rt in m.options.hydrogen_reserve_types] != ['none']:
        # Register with spinning reserves
        if hasattr(m, 'Spinning_Reserve_Up_Provisions'):
            # calculate available slack from hydrogen equipment
            m.HydrogenSlackUpForArea = Expression(
                m.BALANCING_AREA_TIMEPOINTS,
                rule=lambda m, b, t:
                    sum(m.HydrogenSlackUp[z, t] for z in m.ZONES_IN_BALANCING_AREA[b])
            )
            m.HydrogenSlackDownForArea = Expression(
                m.BALANCING_AREA_TIMEPOINTS,
                rule=lambda m, b, t:
                    sum(m.HydrogenSlackDown[z, t] for z in m.ZONES_IN_BALANCING_AREA[b])
            )
            if hasattr(m, 'GEN_SPINNING_RESERVE_TYPES'):
                # using advanced formulation, index by reserve type, balancing area, timepoint
                # define variables for each type of reserves to be provided
                # choose how to allocate the slack between the different reserve products
                m.HYDROGEN_SPINNING_RESERVE_TYPES = Set(
                    initialize=m.options.hydrogen_reserve_types
                )
                m.HydrogenSpinningReserveUp = Var(
                    m.HYDROGEN_SPINNING_RESERVE_TYPES, m.BALANCING_AREA_TIMEPOINTS,
                    within=NonNegativeReals
                )
                m.HydrogenSpinningReserveDown = Var(
                    m.HYDROGEN_SPINNING_RESERVE_TYPES, m.BALANCING_AREA_TIMEPOINTS,
                    within=NonNegativeReals
                )
                # constrain reserve provision within available slack
                m.Limit_HydrogenSpinningReserveUp = Constraint(
                    m.BALANCING_AREA_TIMEPOINTS,
                    rule=lambda m, ba, tp:
                        sum(
                            m.HydrogenSpinningReserveUp[rt, ba, tp]
                            for rt in m.HYDROGEN_SPINNING_RESERVE_TYPES
                        ) <= m.HydrogenSlackUpForArea[ba, tp]
                )
                m.Limit_HydrogenSpinningReserveDown = Constraint(
                    m.BALANCING_AREA_TIMEPOINTS,
                    rule=lambda m, ba, tp:
                        sum(
                            m.HydrogenSpinningReserveDown[rt, ba, tp]
                            for rt in m.HYDROGEN_SPINNING_RESERVE_TYPES
                        ) <= m.HydrogenSlackDownForArea[ba, tp]
                )
                m.Spinning_Reserve_Up_Provisions.append('HydrogenSpinningReserveUp')
                m.Spinning_Reserve_Down_Provisions.append('HydrogenSpinningReserveDown')
            else:
                # using older formulation, only one type of spinning reserves, indexed by balancing area, timepoint
                if m.options.hydrogen_reserve_types != ['spinning']:
                    raise ValueError(
                        'Unable to use reserve types other than "spinning" with simple spinning reserves module.'
                    )
                m.Spinning_Reserve_Up_Provisions.append('HydrogenSlackUpForArea')
                m.Spinning_Reserve_Down_Provisions.append('HydrogenSlackDownForArea')
예제 #14
0
def define_components(mod):
    """
    
    STORAGE_GENS is the subset of projects that can provide energy storage.

    STORAGE_GEN_BLD_YRS is the subset of GEN_BLD_YRS, restricted
    to storage projects.

    gen_storage_efficiency[STORAGE_GENS] 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.

    gen_store_to_release_ratio[STORAGE_GENS] 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.

    gen_storage_energy_overnight_cost[(g, bld_yr) in
    STORAGE_GEN_BLD_YRS] 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.
    
    BuildStorageEnergy[(g, bld_yr) in STORAGE_GEN_BLD_YRS]
    is a decision of how much energy capacity to build onto a storage
    project. This is analogous to BuildGen, but for energy rather than power.
    
    StorageEnergyInstallCosts[PERIODS] is an expression of the
    annual costs incurred by the BuildStorageEnergy decision.
    
    StorageEnergyCapacity[g, period] is an expression describing the
    cumulative available energy capacity of BuildStorageEnergy. This is
    analogous to GenCapacity.
    
    STORAGE_GEN_TPS is the subset of GEN_TPS,
    restricted to storage projects.

    ChargeStorage[(g, t) in STORAGE_GEN_TPS] is a dispatch
    decision of how much to charge a storage project in each timepoint.
    
    StorageNetCharge[LOAD_ZONE, TIMEPOINT] is an expression describing the
    aggregate impact of ChargeStorage in each load zone and timepoint.
    
    Charge_Storage_Upper_Limit[(g, t) in STORAGE_GEN_TPS]
    constrains ChargeStorage to available power capacity (accounting for
    gen_store_to_release_ratio)
    
    StateOfCharge[(g, t) in STORAGE_GEN_TPS] 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[(g, t) in STORAGE_GEN_TPS] constrains
    StateOfCharge based on the StateOfCharge in the previous timepoint,
    ChargeStorage and DispatchGen.
    
    State_Of_Charge_Upper_Limit[(g, t) in STORAGE_GEN_TPS]
    constrains StateOfCharge based on installed energy capacity.

    """

    mod.STORAGE_GENS = Set(within=mod.GENERATION_PROJECTS)
    mod.gen_storage_efficiency = Param(mod.STORAGE_GENS,
                                       within=PercentFraction)
    mod.gen_store_to_release_ratio = Param(mod.STORAGE_GENS,
                                           within=PositiveReals,
                                           default=1.0)

    mod.STORAGE_GEN_BLD_YRS = Set(
        dimen=2,
        initialize=mod.GEN_BLD_YRS,
        filter=lambda m, g, bld_yr: g in m.STORAGE_GENS)
    mod.gen_storage_energy_overnight_cost = Param(mod.STORAGE_GEN_BLD_YRS,
                                                  within=NonNegativeReals)
    mod.min_data_check('gen_storage_energy_overnight_cost')
    mod.BuildStorageEnergy = Var(mod.STORAGE_GEN_BLD_YRS,
                                 within=NonNegativeReals)

    # Summarize capital costs of energy storage for the objective function.
    mod.StorageEnergyInstallCosts = Expression(
        mod.PERIODS,
        rule=lambda m, p: sum(m.BuildStorageEnergy[
            g, bld_yr] * m.gen_storage_energy_overnight_cost[g, bld_yr] * crf(
                m.interest_rate, m.gen_max_age[g])
                              for (g, bld_yr) in m.STORAGE_GEN_BLD_YRS))
    mod.Cost_Components_Per_Period.append('StorageEnergyInstallCosts')

    mod.StorageEnergyCapacity = Expression(
        mod.STORAGE_GENS,
        mod.PERIODS,
        rule=lambda m, g, period: sum(m.BuildStorageEnergy[
            g, bld_yr] for bld_yr in m.BLD_YRS_FOR_GEN_PERIOD[g, period]))

    mod.STORAGE_GEN_TPS = Set(dimen=2,
                              initialize=lambda m:
                              ((g, tp) for g in m.STORAGE_GENS
                               for tp in m.TPS_FOR_GEN[g]))

    mod.ChargeStorage = Var(mod.STORAGE_GEN_TPS, within=NonNegativeReals)

    # Summarize storage charging for the energy balance equations
    def StorageNetCharge_rule(m, z, t):
        # Construct and cache a set for summation as needed
        if not hasattr(m, 'Storage_Charge_Summation_dict'):
            m.Storage_Charge_Summation_dict = collections.defaultdict(set)
            for g, t2 in m.STORAGE_GEN_TPS:
                z2 = m.gen_load_zone[g]
                m.Storage_Charge_Summation_dict[z2, t2].add(g)
        # Use pop to free memory
        relevant_projects = m.Storage_Charge_Summation_dict.pop((z, t))
        return sum(m.ChargeStorage[g, t] for g in relevant_projects)

    mod.StorageNetCharge = Expression(mod.LOAD_ZONES,
                                      mod.TIMEPOINTS,
                                      rule=StorageNetCharge_rule)
    # Register net charging with zonal energy balance. Discharging is already
    # covered by DispatchGen.
    mod.Zone_Power_Withdrawals.append('StorageNetCharge')

    def Charge_Storage_Upper_Limit_rule(m, g, t):
        return m.ChargeStorage[g,t] <= \
            m.DispatchUpperLimit[g, t] * m.gen_store_to_release_ratio[g]

    mod.Charge_Storage_Upper_Limit = Constraint(
        mod.STORAGE_GEN_TPS, rule=Charge_Storage_Upper_Limit_rule)

    mod.StateOfCharge = Var(mod.STORAGE_GEN_TPS, within=NonNegativeReals)

    def Track_State_Of_Charge_rule(m, g, t):
        return m.StateOfCharge[g, t] == \
            m.StateOfCharge[g, m.tp_previous[t]] + \
            (m.ChargeStorage[g, t] * m.gen_storage_efficiency[g] -
             m.DispatchGen[g, t]) * m.tp_duration_hrs[t]

    mod.Track_State_Of_Charge = Constraint(mod.STORAGE_GEN_TPS,
                                           rule=Track_State_Of_Charge_rule)

    def State_Of_Charge_Upper_Limit_rule(m, g, t):
        return m.StateOfCharge[g, t] <= \
            m.StorageEnergyCapacity[g, m.tp_period[t]]

    mod.State_Of_Charge_Upper_Limit = Constraint(
        mod.STORAGE_GEN_TPS, rule=State_Of_Charge_Upper_Limit_rule)
예제 #15
0
파일: storage.py 프로젝트: anamileva/switch
def define_components(mod):
    """
    
    STORAGE_GENS is the subset of projects that can provide energy storage.

    STORAGE_GEN_BLD_YRS is the subset of GEN_BLD_YRS, restricted
    to storage projects.

    gen_storage_efficiency[STORAGE_GENS] 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.

    gen_store_to_release_ratio[STORAGE_GENS] 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.

    gen_storage_energy_overnight_cost[(g, bld_yr) in
    STORAGE_GEN_BLD_YRS] 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.
    
    BuildStorageEnergy[(g, bld_yr) in STORAGE_GEN_BLD_YRS]
    is a decision of how much energy capacity to build onto a storage
    project. This is analogous to BuildGen, but for energy rather than power.
    
    StorageEnergyInstallCosts[PERIODS] is an expression of the
    annual costs incurred by the BuildStorageEnergy decision.
    
    StorageEnergyCapacity[g, period] is an expression describing the
    cumulative available energy capacity of BuildStorageEnergy. This is
    analogous to GenCapacity.
    
    STORAGE_GEN_TPS is the subset of GEN_TPS,
    restricted to storage projects.

    ChargeStorage[(g, t) in STORAGE_GEN_TPS] is a dispatch
    decision of how much to charge a storage project in each timepoint.
    
    StorageNetCharge[LOAD_ZONE, TIMEPOINT] is an expression describing the
    aggregate impact of ChargeStorage in each load zone and timepoint.
    
    Charge_Storage_Upper_Limit[(g, t) in STORAGE_GEN_TPS]
    constrains ChargeStorage to available power capacity (accounting for
    gen_store_to_release_ratio)
    
    StateOfCharge[(g, t) in STORAGE_GEN_TPS] 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[(g, t) in STORAGE_GEN_TPS] constrains
    StateOfCharge based on the StateOfCharge in the previous timepoint,
    ChargeStorage and DispatchGen.
    
    State_Of_Charge_Upper_Limit[(g, t) in STORAGE_GEN_TPS]
    constrains StateOfCharge based on installed energy capacity.

    """

    mod.STORAGE_GENS = Set(within=mod.GENERATION_PROJECTS)
    mod.gen_storage_efficiency = Param(
        mod.STORAGE_GENS,
        within=PercentFraction)
    mod.gen_store_to_release_ratio = Param(
        mod.STORAGE_GENS,
        within=PositiveReals,
        default=1.0)

    mod.STORAGE_GEN_BLD_YRS = Set(
        dimen=2,
        initialize=mod.GEN_BLD_YRS,
        filter=lambda m, g, bld_yr: g in m.STORAGE_GENS)
    mod.gen_storage_energy_overnight_cost = Param(
        mod.STORAGE_GEN_BLD_YRS,
        within=NonNegativeReals)
    mod.min_data_check('gen_storage_energy_overnight_cost')
    mod.BuildStorageEnergy = Var(
        mod.STORAGE_GEN_BLD_YRS,
        within=NonNegativeReals)

    # Summarize capital costs of energy storage for the objective function.
    mod.StorageEnergyInstallCosts = Expression(
        mod.PERIODS,
        rule=lambda m, p: sum(m.BuildStorageEnergy[g, bld_yr] *
                   m.gen_storage_energy_overnight_cost[g, bld_yr] *
                   crf(m.interest_rate, m.gen_max_age[g])
                   for (g, bld_yr) in m.STORAGE_GEN_BLD_YRS))
    mod.Cost_Components_Per_Period.append(
        'StorageEnergyInstallCosts')

    mod.StorageEnergyCapacity = Expression(
        mod.STORAGE_GENS, mod.PERIODS,
        rule=lambda m, g, period: sum(
            m.BuildStorageEnergy[g, bld_yr]
            for bld_yr in m.BLD_YRS_FOR_GEN_PERIOD[g, period]))

    mod.STORAGE_GEN_TPS = Set(
        dimen=2,
        initialize=lambda m: (
            (g, tp) 
                for g in m.STORAGE_GENS
                    for tp in m.TPS_FOR_GEN[g]))

    mod.ChargeStorage = Var(
        mod.STORAGE_GEN_TPS,
        within=NonNegativeReals)
    
    # Summarize storage charging for the energy balance equations
    def StorageNetCharge_rule(m, z, t):
        # Construct and cache a set for summation as needed
        if not hasattr(m, 'Storage_Charge_Summation_dict'):
            m.Storage_Charge_Summation_dict = collections.defaultdict(set)
            for g, t2 in m.STORAGE_GEN_TPS:
                z2 = m.gen_load_zone[g]
                m.Storage_Charge_Summation_dict[z2, t2].add(g)
        # Use pop to free memory
        relevant_projects = m.Storage_Charge_Summation_dict.pop((z, t))
        return sum(m.ChargeStorage[g, t] for g in relevant_projects)
    mod.StorageNetCharge = Expression(
        mod.LOAD_ZONES, mod.TIMEPOINTS,
        rule=StorageNetCharge_rule)
    # Register net charging with zonal energy balance. Discharging is already
    # covered by DispatchGen.
    mod.Zone_Power_Withdrawals.append('StorageNetCharge')

    def Charge_Storage_Upper_Limit_rule(m, g, t):
        return m.ChargeStorage[g,t] <= \
            m.DispatchUpperLimit[g, t] * m.gen_store_to_release_ratio[g]
    mod.Charge_Storage_Upper_Limit = Constraint(
        mod.STORAGE_GEN_TPS,
        rule=Charge_Storage_Upper_Limit_rule)
                
    mod.StateOfCharge = Var(
        mod.STORAGE_GEN_TPS,
        within=NonNegativeReals)

    def Track_State_Of_Charge_rule(m, g, t):
        return m.StateOfCharge[g, t] == \
            m.StateOfCharge[g, m.tp_previous[t]] + \
            (m.ChargeStorage[g, t] * m.gen_storage_efficiency[g] -
             m.DispatchGen[g, t]) * m.tp_duration_hrs[t]
    mod.Track_State_Of_Charge = Constraint(
        mod.STORAGE_GEN_TPS,
        rule=Track_State_Of_Charge_rule)

    def State_Of_Charge_Upper_Limit_rule(m, g, t):
        return m.StateOfCharge[g, t] <= \
            m.StorageEnergyCapacity[g, m.tp_period[t]]
    mod.State_Of_Charge_Upper_Limit = Constraint(
        mod.STORAGE_GEN_TPS,
        rule=State_Of_Charge_Upper_Limit_rule)