Exemple #1
0
    def __init__(self):
        # Pre-processed data for model construction
        self.data = ModelData()

        # Solver options
        self.keepfiles = False
        self.solver_options = {}  # 'mip tolerances integrality': 0.01
        self.opt = SolverFactory('cplex', solver_io='lp')
Exemple #2
0
    def __init__(self, first_year=2016, final_year=2040, scenarios_per_year=5):
        self.dual = Dual(first_year, final_year, scenarios_per_year)
        self.data = ModelData()
        self.analysis = AnalyseResults()

        # Common model components. May need to update these values
        self.common = CommonComponents(first_year=first_year,
                                       final_year=final_year,
                                       scenarios_per_year=scenarios_per_year)

        # Model sets
        self.sets = self.get_model_sets()
Exemple #3
0
    def __init__(self, first_year, final_year, scenarios_per_year):
        # Model data
        self.data = ModelData()

        # First year in model horizon (min is 2016)
        self.first_year = first_year

        # Default final year (max is 2050)
        self.final_year = final_year

        # Default number of scenarios per year (max is 10)
        self.scenarios_per_year = scenarios_per_year
Exemple #4
0
def from_gbd_jsons(dm):
    """ Create ModelData object from old DM3 JSON file

    Parameters
    ----------
    dm : str, the JSON data

    Results
    -------
    returns new ModelData object
    """
    # load some ancillary data from the gbd
    import dismod3
    import csv
    dm['countries_for'] = dict(
        [[dismod3.utils.clean(x[0]), x[1:]] for x in csv.reader(open(dismod3.settings.CSV_PATH + 'country_region.csv'))]
        )
    dm['population_by_age'] = dict(
        [[(r['Country Code'], r['Year'], r['Sex']),
          [max(.001,float(r['Age %d Population' % i])) for i in range(dismod3.settings.MAX_AGE)]] 
         for r in csv.DictReader(open(dismod3.settings.CSV_PATH + 'population.csv'))
         if len(r['Country Code']) == 3]
        )


    d = ModelData()

    d.input_data = _input_data_from_gbd_json(dm)
    d.output_template = _output_template_from_gbd_json(dm)
    d.parameters = _parameters_from_gbd_json(dm)
    d.hierarchy, d.nodes_to_fit = _hierarchy_from_gbd_json(dm)

    print 'load completed successfully'

    return d
Exemple #5
0
    def __init__(self, output_dir=os.path.join(os.path.dirname(__name__), os.path.pardir, 'output')):
        self.output_dir = output_dir

        # Object containing model data
        self.data = ModelData()

        # Class used to analyse model results
        self.analysis = AnalyseResults()

        # Solver options
        self.keepfiles = True
        self.solver_options = {}
        self.opt = SolverFactory('cplex', solver_io='lp')
Exemple #6
0
def _parameters_from_gbd_json(dm):
    """ copy expert priors"""
    parameters = ModelData().parameters
    old_name = dict(i='incidence', p='prevalence', rr='relative_risk', r='remission', f='excess_mortality', X='duration', pf='prevalence_x_excess-mortality')
    for t in 'i p r f rr X pf'.split():
        if 'global_priors' in dm['params']:
            parameters[t]['parameter_age_mesh'] = dm['params']['global_priors']['parameter_age_mesh']
            parameters[t]['y_maximum'] = dm['params']['global_priors']['y_maximum']
            for prior in 'smoothness heterogeneity level_value level_bounds increasing decreasing'.split():
                if old_name[t] in dm['params']['global_priors'][prior]:
                    parameters[t][prior] = dm['params']['global_priors'][prior][old_name[t]]

            # make 1000 effectively infinite, because the gui only goes up to 1000
            if 'level_bounds' in parameters[t] and parameters[t]['level_bounds']['upper'] == 1000.:
                parameters[t]['level_bounds']['upper'] = 1e6
        parameters[t]['fixed_effects'] = {}
        parameters[t]['random_effects'] = {}

    if 'global_priors' in dm['params']:
        parameters['ages'] = range(dm['params']['global_priors']['parameter_age_mesh'][0], dm['params']['global_priors']['parameter_age_mesh'][-1]+1)

    for t in 'i p r f'.split():
        key = 'sex_effect_%s' % old_name[t]
        if key in dm['params']:
            prior = dm['params'][key]
            parameters[t]['fixed_effects']['x_sex'] = dict(dist='Normal', mu=pl.log(prior['mean']),
                                                           sigma=(pl.log(prior['upper_ci']) - pl.log(prior['lower_ci']))/(2*1.96))
        key = 'region_effect_%s' % old_name[t]
        if key in dm['params']:
            prior = dm['params'][key]
            for iso3 in dm['countries_for']['world']:
                parameters[t]['random_effects'][iso3] = dict(dist='TruncatedNormal', mu=0., sigma=prior['std'], lower=-2*prior['std'], upper=2*prior['std'])

        # include alternative prior on sigma_alpha based on heterogeneity
        for i in range(5):  # max depth of hierarchy is 5
            effect = 'sigma_alpha_%s_%d'%(t,i)
            #parameters[t]['random_effects'][effect] = dict(dist='TruncatedNormal', mu=.01, sigma=.01, lower=.01, upper=.05)
            #if 'heterogeneity' in parameters[t]:
            #    if parameters[t]['heterogeneity'] == 'Moderately':
            #        parameters[t]['random_effects'][effect] = dict(dist='TruncatedNormal', mu=.05, sigma=.05, lower=.01, upper=1.)
            #    elif parameters[t]['heterogeneity'] == 'Very':
            #        parameters[t]['random_effects'][effect] = dict(dist='TruncatedNormal', mu=.01, sigma=.01, lower=.002, upper=.2)

    return parameters
def validation_model(yname, train_size):
    datafem = FEMData(yname, [70])

    mape = []
    for iter in range(10):
        print("\nIteration: {}".format(iter))

        datamodel = ModelData(yname, train_size, "forward")
        X_train, X_test = datamodel.X, datafem.X
        y_train, y_test = datamodel.y, datafem.y

        data = dde.data.DataSet(X_train=X_train,
                                y_train=y_train,
                                X_test=X_test,
                                y_test=y_test)

        # mape.append(svm(data))
        mape.append(nn(data))

    print(yname, train_size)
    print(np.mean(mape), np.std(mape))
Exemple #8
0
class InvestmentPlan:
    # Pre-processed data for model construction
    data = ModelData()

    # Common model components to investment plan and operating sub-problems (sets)
    components = CommonComponents()

    # Object used to perform common calculations. E.g. discount factors.
    calculations = Calculations()

    def __init__(self):
        # Solver options
        self.keepfiles = False
        self.solver_options = {}  # 'MIPGap': 0.0005
        self.opt = SolverFactory('cplex', solver_io='mps')

    def define_sets(self, m):
        """Define investment plan sets"""
        pass

    def define_parameters(self, m):
        """Investment plan model parameters"""
        def solar_build_limits_rule(_m, z):
            """Solar build limits for each NEM zone"""

            return float(
                self.data.candidate_unit_build_limits_dict[z]['SOLAR'])

        # Maximum solar capacity allowed per zone
        m.SOLAR_BUILD_LIMITS = Param(m.Z, rule=solar_build_limits_rule)

        def wind_build_limits_rule(_m, z):
            """Wind build limits for each NEM zone"""

            return float(self.data.candidate_unit_build_limits_dict[z]['WIND'])

        # Maximum wind capacity allowed per zone
        m.WIND_BUILD_LIMITS = Param(m.Z, rule=wind_build_limits_rule)

        def storage_build_limits_rule(_m, z):
            """Storage build limits for each NEM zone"""

            return float(
                self.data.candidate_unit_build_limits_dict[z]['STORAGE'])

        # Maximum storage capacity allowed per zone
        m.STORAGE_BUILD_LIMITS = Param(m.Z, rule=storage_build_limits_rule)

        def candidate_unit_build_costs_rule(_m, g, y):
            """
            Candidate unit build costs [$/MW]

            Note: build cost in $/MW. May need to scale if numerical conditioning problems.
            """

            if g in m.G_C_STORAGE:
                return float(self.data.battery_build_costs_dict[y][g] * 1000)

            else:
                return float(
                    self.data.candidate_units_dict[('BUILD_COST', y)][g] *
                    1000)

        # Candidate unit build cost
        m.I_C = Param(m.G_C, m.Y, rule=candidate_unit_build_costs_rule)

        def candidate_unit_life_rule(_m, g):
            """Asset life of candidate units [years]"""
            # TODO: Use data from NTNPD. Just making an assumption for now.
            return float(25)

        # Candidate unit life
        m.A = Param(m.G_C, rule=candidate_unit_life_rule)

        def amortisation_rate_rule(_m, g):
            """Amortisation rate for a given investment"""

            # Numerator for amortisation rate expression
            num = self.data.WACC * ((1 + self.data.WACC)**m.A[g])

            # Denominator for amortisation rate expression
            den = ((1 + self.data.WACC)**m.A[g]) - 1

            # Amortisation rate
            amortisation_rate = num / den

            return amortisation_rate

        # Amortisation rate for a given investment
        m.GAMMA = Param(m.G_C, rule=amortisation_rate_rule)

        def discount_factor_rule(_m, y):
            """Discount factor for each year in model horizon"""

            return self.calculations.discount_factor(y, m.Y.first())

        # Discount factor
        m.DISCOUNT_FACTOR = Param(m.Y, rule=discount_factor_rule)

        def fixed_operations_and_maintenance_cost_rule(_m, g):
            """Fixed FOM cost [$/MW/year]

            Note: Data in NTNDP is in terms of $/kW/year. Must multiply by 1000 to convert to $/MW/year
            """

            if g in m.G_E:
                return float(
                    self.data.existing_units_dict[('PARAMETERS', 'FOM')][g] *
                    1000)

            elif g in m.G_C_THERM.union(m.G_C_WIND, m.G_C_SOLAR):
                return float(
                    self.data.candidate_units_dict[('PARAMETERS', 'FOM')][g] *
                    1000)

            elif g in m.G_STORAGE:
                # TODO: Need to find reasonable FOM cost for storage units - setting = MEL-WIND for now
                return float(
                    self.data.candidate_units_dict[('PARAMETERS',
                                                    'FOM')]['MEL-WIND'] * 1000)

            else:
                raise Exception(f'Unexpected generator encountered: {g}')

        # Fixed operations and maintenance cost
        m.C_FOM = Param(m.G, rule=fixed_operations_and_maintenance_cost_rule)

        # Weighted cost of capital - interest rate assumed in discounting + amortisation calculations
        m.WACC = Param(initialize=float(self.data.WACC))

        # Candidate capacity dual variable obtained from sub-problem solution. Updated each iteration.
        m.PSI_FIXED = Param(m.G_C, m.Y, m.S, initialize=0, mutable=True)

        # Lower bound for Benders auxiliary variable
        m.ALPHA_LOWER_BOUND = Param(initialize=float(-10e9))

        return m

    @staticmethod
    def define_variables(m):
        """Define investment plan variables"""

        # Investment in each time period
        m.x_c = Var(m.G_C, m.Y, within=NonNegativeReals, initialize=0)

        # Binary variable to select capacity size
        m.d = Var(m.G_C_THERM,
                  m.Y,
                  m.G_C_THERM_SIZE_OPTIONS,
                  within=Binary,
                  initialize=0)

        # Auxiliary variable - total capacity available at each time period
        m.a = Var(m.G_C, m.Y, initialize=0)

        # Auxiliary variable - gives lower bound for subproblem solution
        m.alpha = Var()

        return m

    def define_expressions(self, m):
        """Define investment plan expressions"""
        def investment_cost_rule(_m, y):
            """Total amortised investment cost for a given year [$]"""
            return sum(m.GAMMA[g] * m.I_C[g, y] * m.x_c[g, y] for g in m.G_C)

        # Investment cost in a given year
        m.INV = Expression(m.Y, rule=investment_cost_rule)

        def fom_cost_rule(_m, y):
            """Total fixed operations and maintenance cost for a given year (not discounted)"""

            # FOM costs for candidate units
            candidate_fom = sum(m.C_FOM[g] * m.a[g, y] for g in m.G_C)

            # FOM costs for existing units - note no FOM cost paid if unit retires
            existing_fom = sum(m.C_FOM[g] * m.P_MAX[g] * (1 - m.F[g, y])
                               for g in m.G_E)

            # Expression for total FOM cost
            total_fom = candidate_fom + existing_fom

            return total_fom

        # Fixed operating cost for candidate existing generators for each year in model horizon
        m.FOM = Expression(m.Y, rule=fom_cost_rule)

        def total_fom_cost_rule(_m):
            """Total discounted FOM cost over all years in model horizon"""

            return sum(m.DISCOUNT_FACTOR[y] * m.FOM[y] for y in m.Y)

        # Total discounted FOM cost over model horizon
        m.FOM_TOTAL = Expression(rule=total_fom_cost_rule)

        def total_fom_end_of_horizon_rule(_m):
            """FOM cost assumed to propagate beyond end of model horizon"""

            # Assumes discounted FOM cost in final year paid in perpetuity
            total_cost = (m.DISCOUNT_FACTOR[m.Y.last()] /
                          m.WACC) * m.FOM[m.Y.last()]

            return total_cost

        # Accounting for FOM beyond end of model horizon (EOH)
        m.FOM_EOH = Expression(rule=total_fom_end_of_horizon_rule)

        def total_investment_cost_rule(_m):
            """Total discounted amortised investment cost"""

            # Total discounted amortised investment cost
            total_cost = sum(
                (m.DISCOUNT_FACTOR[y] / m.WACC) * m.INV[y] for y in m.Y)

            return total_cost

        # Total investment cost
        m.INV_TOTAL = Expression(rule=total_investment_cost_rule)

        # Total discounted investment and FOM cost (taking into account costs beyond end of model horizon)
        m.TOTAL_PLANNING_COST = Expression(expr=m.FOM_TOTAL + m.FOM_EOH +
                                           m.INV_TOTAL)

        return m

    @staticmethod
    def define_constraints(m):
        """Define feasible investment plan constraints"""
        def discrete_thermal_size_rule(_m, g, y):
            """Discrete sizing rule for candidate thermal units"""

            # Discrete size options for candidate thermal units
            size_options = {0: 0, 1: 100, 2: 400, 3: 400}

            return m.x_c[g, y] - sum(m.d[g, y, n] * float(size_options[n])
                                     for n in m.G_C_THERM_SIZE_OPTIONS) == 0

        # Discrete investment size for candidate thermal units
        m.DISCRETE_THERMAL_SIZE = Constraint(m.G_C_THERM,
                                             m.Y,
                                             rule=discrete_thermal_size_rule)

        def single_discrete_selection_rule(_m, g, y):
            """Can only select one size option per investment period"""

            return sum(m.d[g, y, n]
                       for n in m.G_C_THERM_SIZE_OPTIONS) - float(1) == 0

        # Single size selection constraint per investment period
        m.SINGLE_DISCRETE_SELECTION = Constraint(
            m.G_C_THERM, m.Y, rule=single_discrete_selection_rule)

        def total_capacity_rule(_m, g, y):
            """Total installed capacity in a given year"""

            return m.a[g, y] - sum(m.x_c[g, j] for j in m.Y if j <= y) == 0

        # Total installed capacity for each candidate technology type at each point in model horizon
        m.TOTAL_CAPACITY = Constraint(m.G_C, m.Y, rule=total_capacity_rule)

        def solar_build_limits_cons_rule(_m, z, y):
            """Enforce solar build limits in each NEM zone"""

            # Solar generators belonging to zone 'z'
            gens = [g for g in m.G_C_SOLAR if g.split('-')[0] == z]

            if gens:
                return sum(m.a[g, y]
                           for g in gens) - m.SOLAR_BUILD_LIMITS[z] <= 0
            else:
                return Constraint.Skip

        # Storage build limit constraint for each NEM zone
        m.SOLAR_BUILD_LIMIT_CONS = Constraint(
            m.Z, m.Y, rule=solar_build_limits_cons_rule)

        def wind_build_limits_cons_rule(_m, z, y):
            """Enforce wind build limits in each NEM zone"""

            # Wind generators belonging to zone 'z'
            gens = [g for g in m.G_C_WIND if g.split('-')[0] == z]

            if gens:
                return sum(m.a[g, y]
                           for g in gens) - m.WIND_BUILD_LIMITS[z] <= 0
            else:
                return Constraint.Skip

        # Wind build limit constraint for each NEM zone
        m.WIND_BUILD_LIMIT_CONS = Constraint(m.Z,
                                             m.Y,
                                             rule=wind_build_limits_cons_rule)

        def storage_build_limits_cons_rule(_m, z, y):
            """Enforce storage build limits in each NEM zone"""

            # Storage generators belonging to zone 'z'
            gens = [g for g in m.G_C_STORAGE if g.split('-')[0] == z]

            if gens:
                return sum(m.a[g, y]
                           for g in gens) - m.STORAGE_BUILD_LIMITS[z] <= 0
            else:
                return Constraint.Skip

        # Storage build limit constraint for each NEM zone
        m.STORAGE_BUILD_LIMIT_CONS = Constraint(
            m.Z, m.Y, rule=storage_build_limits_cons_rule)

        # Bound on Benders decomposition lower-bound variable
        m.ALPHA_LOWER_BOUND_CONS = Constraint(
            expr=m.alpha >= m.ALPHA_LOWER_BOUND)

        # Container for benders cuts
        m.BENDERS_CUTS = ConstraintList()

        return m

    @staticmethod
    def define_objective(m):
        """Define objective function"""
        def objective_function_rule(_m):
            """Investment plan objective function"""

            # Objective function
            objective_function = m.TOTAL_PLANNING_COST + m.alpha

            return objective_function

        # Investment plan objective function
        m.OBJECTIVE = Objective(rule=objective_function_rule, sense=minimize)

        return m

    def construct_model(self):
        """Construct investment plan model"""

        # Initialise model object
        m = ConcreteModel()

        # Prepare to import dual variables
        m.dual = Suffix(direction=Suffix.IMPORT)

        # Define sets
        m = self.components.define_sets(m)

        # Define parameters common to all sub-problems
        m = self.components.define_parameters(m)

        # Define parameters
        m = self.define_parameters(m)

        # Define variables
        m = self.define_variables(m)

        # Define expressions
        m = self.define_expressions(m)

        # Define constraints
        m = self.define_constraints(m)

        # Define objective
        m = self.define_objective(m)

        return m

    def solve_model(self, m):
        """Solve model instance"""

        # Solve model
        self.opt.solve(m,
                       tee=False,
                       options=self.solver_options,
                       keepfiles=self.keepfiles)

        # Log infeasible constraints if they exist
        log_infeasible_constraints(m)

        return m

    def get_cut_component(self, m, iteration, year, scenario,
                          investment_solution_dir, uc_solution_dir):
        """Construct cut component"""

        # Discount factor
        discount = self.calculations.discount_factor(year, base_year=2016)

        # Duration
        duration = self.calculations.scenario_duration_days(year, scenario)

        with open(
                os.path.join(
                    uc_solution_dir,
                    f'uc-results_{iteration}_{year}_{scenario}.pickle'),
                'rb') as f:
            uc_result = pickle.load(f)

        with open(
                os.path.join(investment_solution_dir,
                             f'investment-results_{iteration}.pickle'),
                'rb') as f:
            inv_result = pickle.load(f)

        # Construct cut component
        cut = discount * duration * sum(
            uc_result['PSI_FIXED'][g] *
            (m.a[g, year] - inv_result['a'][(g, year)]) for g in m.G_C)

        return cut

    def get_benders_cut(self, m, iteration, investment_solution_dir,
                        uc_solution_dir):
        """Construct a Benders optimality cut"""

        # Cost accounting for total operating cost for each scenario over model horizon
        scenario_cost = self.calculations.get_total_discounted_operating_scenario_cost(
            iteration, uc_solution_dir)

        # Cost account for operating costs beyond end of model horizon
        scenario_cost_eoh = self.calculations.get_end_of_horizon_operating_cost(
            m, iteration, uc_solution_dir)

        # Total (fixed) cost to include in Benders cut
        total_cost = scenario_cost + scenario_cost_eoh

        # Cut components containing dual variables
        cut_components = sum(
            self.get_cut_component(m, iteration, y, s, investment_solution_dir,
                                   uc_solution_dir) for y in m.Y for s in m.S)

        # Benders cut
        cut = m.alpha >= total_cost + cut_components

        return cut

    def add_benders_cut(self, m, iteration, investment_solution_dir,
                        uc_solution_dir):
        """Update model parameters"""

        # Construct Benders optimality cut
        cut = self.get_benders_cut(m, iteration, investment_solution_dir,
                                   uc_solution_dir)

        # Add Benders cut to constraint list
        m.BENDERS_CUTS.add(expr=cut)

        return m

    @staticmethod
    def fix_binary_variables(m):
        """Fix all binary variables"""

        for g in m.G_C_THERM:
            for y in m.Y:
                for n in m.G_C_THERM_SIZE_OPTIONS:
                    # Fix binary variables related to discrete sizing decision for candidate thermal units
                    m.d[g, y, n].fix()

        return m

    @staticmethod
    def unfix_binary_variables(m):
        """Unfix all binary variables"""

        for g in m.G_C_THERM:
            for y in m.Y:
                for n in m.G_C_THERM_SIZE_OPTIONS:
                    # Unfix binary variables related to discrete sizing decision for candidate thermal units
                    m.d[g, y, n].unfix()

        return m

    @staticmethod
    def save_solution(m, iteration, investment_solution_dir):
        """Save model solution"""

        # Save investment plan
        output = {
            'a': m.a.get_values(),
            'x_c': m.x_c.get_values(),
            'OBJECTIVE': m.OBJECTIVE.expr()
        }

        # Save investment plan results
        with open(
                os.path.join(investment_solution_dir,
                             f'investment-results_{iteration}.pickle'),
                'wb') as f:
            pickle.dump(output, f)

    @staticmethod
    def initialise_investment_plan(m, iteration, solution_dir):
        """Initial values for investment plan - used in first iteration. Assume candidate capacity = 0"""

        # Feasible investment plan for first iteration
        plan = {
            'a': {(g, y): float(0)
                  for g in m.G_C for y in m.Y},
            'x_c': {(g, y): float(0)
                    for g in m.G_C for y in m.Y},
            'OBJECTIVE': -1e9
        }

        # Save investment plan
        with open(
                os.path.join(solution_dir,
                             f'investment-results_{iteration}.pickle'),
                'wb') as f:
            pickle.dump(plan, f)

        return plan
Exemple #9
0
 def __init__(self):
     self.data = ModelData()
Exemple #10
0
 def __init__(self, root_output_dir=os.path.join(os.path.dirname(__file__), os.path.pardir, '2_model', 'output')):
     self.root_output_dir = root_output_dir
     self.data = ModelData()
Exemple #11
0
class InvestmentPlan:
    # Pre-processed data for model construction
    data = ModelData()

    # Common model components to investment plan and operating sub-problems (sets)
    components = CommonComponents()

    def __init__(self):
        # Solver options
        self.keepfiles = False
        self.solver_options = {}  # 'MIPGap': 0.0005
        self.opt = SolverFactory('gurobi', solver_io='mps')

    def define_parameters(self, m):
        """Investment plan model parameters"""

        def solar_build_limits_rule(_m, z):
            """Solar build limits for each NEM zone"""

            return float(self.data.candidate_unit_build_limits_dict[z]['SOLAR'])

        # Maximum solar capacity allowed per zone
        m.SOLAR_BUILD_LIMITS = Param(m.Z, rule=solar_build_limits_rule)

        def wind_build_limits_rule(_m, z):
            """Wind build limits for each NEM zone"""

            return float(self.data.candidate_unit_build_limits_dict[z]['WIND'])

        # Maximum wind capacity allowed per zone
        m.WIND_BUILD_LIMITS = Param(m.Z, rule=wind_build_limits_rule)

        def storage_build_limits_rule(_m, z):
            """Storage build limits for each NEM zone"""

            return float(self.data.candidate_unit_build_limits_dict[z]['STORAGE'])

        # Maximum storage capacity allowed per zone
        m.STORAGE_BUILD_LIMITS = Param(m.Z, rule=storage_build_limits_rule)

        def candidate_unit_build_costs_rule(_m, g, y):
            """
            Candidate unit build costs [$/MW]

            Note: build cost in $/MW. May need to scale if numerical conditioning problems.
            """

            if g in m.G_C_STORAGE:
                return float(self.data.battery_build_costs_dict[y][g] * 1000)
            else:
                return float(self.data.candidate_units_dict[('BUILD_COST', y)][g] * 1000)

        # Candidate unit build cost
        m.I_C = Param(m.G_C, m.Y, rule=candidate_unit_build_costs_rule)

        def candidate_unit_life_rule(_m, g):
            """Asset life of candidate units [years]"""
            # TODO: Use data from NTNPD. Just making an assumption for now.
            return float(25)

        # Candidate unit life
        m.A = Param(m.G_C, rule=candidate_unit_life_rule)

        def amortisation_rate_rule(_m, g):
            """Amortisation rate for a given investment"""

            # Numerator for amortisation rate expression
            num = self.data.WACC * ((1 + self.data.WACC) ** m.A[g])

            # Denominator for amortisation rate expression
            den = ((1 + self.data.WACC) ** m.A[g]) - 1

            # Amortisation rate
            amortisation_rate = num / den

            return amortisation_rate

        # Amortisation rate for a given investment
        m.GAMMA = Param(m.G_C, rule=amortisation_rate_rule)

        def fixed_operations_and_maintenance_cost_rule(_m, g):
            """Fixed FOM cost [$/MW/year]

            Note: Data in NTNDP is in terms of $/kW/year. Must multiply by 1000 to convert to $/MW/year
            """

            if g in m.G_E:
                return float(self.data.existing_units_dict[('PARAMETERS', 'FOM')][g] * 1000)

            elif g in m.G_C_THERM.union(m.G_C_WIND, m.G_C_SOLAR):
                return float(self.data.candidate_units_dict[('PARAMETERS', 'FOM')][g] * 1000)

            elif g in m.G_STORAGE:
                # TODO: Need to find reasonable FOM cost for storage units - setting = MEL-WIND for now
                return float(self.data.candidate_units_dict[('PARAMETERS', 'FOM')]['MEL-WIND'] * 1000)
            else:
                raise Exception(f'Unexpected generator encountered: {g}')

        # Fixed operations and maintenance cost
        m.C_FOM = Param(m.G, rule=fixed_operations_and_maintenance_cost_rule)

        def discount_factor_rule(_m, y):
            """Discount factor"""

            # Discount factor
            discount_factor = 1 / ((1 + self.data.WACC) ** (y - m.Y.first()))

            return discount_factor

        # Discount factor - used to take into account the time value of money and compute present values
        m.DISCOUNT_FACTOR = Param(m.Y, rule=discount_factor_rule)

        # Total emissions (obtained by computing emissions from all sub-problems). Updated each iteration.
        m.TOTAL_EMISSIONS = Param(initialize=0, mutable=True)

        # Cumulative emissions target
        m.CUMULATIVE_EMISSIONS_TARGET = Param(initialize=100e9, mutable=True)

        # Cost of violating cumulative emissions constraint (assumed) [$/tCO2]
        m.C_E = Param(initialize=10000)

        # Weighted cost of capital - interest rate assumed in discounting + amortisation calculations
        m.WACC = Param(initialize=float(self.data.WACC))

        # Candidate capacity dual variable obtained from sub-problem solution. Updated each iteration.
        m.PSI_FIXED = Param(m.G_C, m.Y, m.S, initialize=0, mutable=True)

        # Candidate capacity variable obtained from sub-problem
        m.CANDIDATE_CAPACITY_FIXED = Param(m.G_C, m.Y, m.S, initialize=0, mutable=True)

        return m

    @staticmethod
    def define_variables(m):
        """Define investment plan variables"""

        # Investment in each time period
        m.x_c = Var(m.G_C, m.Y, within=NonNegativeReals, initialize=0)

        # Binary variable to select capacity size
        m.d = Var(m.G_C_THERM, m.Y, m.G_C_THERM_SIZE_OPTIONS, within=Binary, initialize=0)

        # Auxiliary variable - total capacity available at each time period
        m.a = Var(m.G_C, m.Y, initialize=0)

        # Cumulative emissions constraint violation
        m.f_e = Var(within=NonNegativeReals, initialize=0)

        return m

    @staticmethod
    def define_expressions(m):
        """Define investment plan expressions"""

        def investment_cost_rule(_m, y):
            """Total amortised investment cost for a given year [$]"""
            return sum(m.GAMMA[g] * m.I_C[g, y] * m.x_c[g, y] for g in m.G_C)

        # Investment cost in a given year
        m.INV = Expression(m.Y, rule=investment_cost_rule)

        # Emissions constraint violation penalty
        m.PEN = Expression(expr=m.C_E * m.f_e)

        def fom_cost_rule(_m, y):
            """Total fixed operations and maintenance cost for a given year (not discounted)"""

            # FOM costs for candidate units
            candidate_fom = sum(m.C_FOM[g] * m.a[g, y] for g in m.G_C)

            # FOM costs for existing units - note no FOM cost paid if unit retires
            existing_fom = sum(m.C_FOM[g] * m.P_MAX[g] * (1 - m.F[g, y]) for g in m.G_E)

            # Expression for total FOM cost
            total_fom = candidate_fom + existing_fom

            return total_fom

        # Fixed operating cost for candidate existing generators for each year in model horizon
        m.FOM = Expression(m.Y, rule=fom_cost_rule)

        return m

    @staticmethod
    def define_constraints(m):
        """Define feasible investment plan constraints"""

        def discrete_thermal_size_rule(_m, g, y):
            """Discrete sizing rule for candidate thermal units"""

            # Discrete size options for candidate thermal units
            size_options = {0: 0, 1: 100, 2: 400, 3: 400}

            return m.x_c[g, y] - sum(m.d[g, y, n] * float(size_options[n]) for n in m.G_C_THERM_SIZE_OPTIONS) == 0

        # Discrete investment size for candidate thermal units
        m.DISCRETE_THERMAL_SIZE = Constraint(m.G_C_THERM, m.Y, rule=discrete_thermal_size_rule)

        def single_discrete_selection_rule(_m, g, y):
            """Can only select one size option per investment period"""

            return sum(m.d[g, y, n] for n in m.G_C_THERM_SIZE_OPTIONS) - float(1) == 0

        # Single size selection constraint per investment period
        m.SINGLE_DISCRETE_SELECTION = Constraint(m.G_C_THERM, m.Y, rule=single_discrete_selection_rule)

        def total_capacity_rule(_m, g, y):
            """Total installed capacity in a given year"""

            return m.a[g, y] - sum(m.x_c[g, j] for j in m.Y if j <= y) == 0

        # Total installed capacity for each candidate technology type at each point in model horizon
        m.TOTAL_CAPACITY = Constraint(m.G_C, m.Y, rule=total_capacity_rule)

        def solar_build_limits_cons_rule(_m, z, y):
            """Enforce solar build limits in each NEM zone"""

            # Solar generators belonging to zone 'z'
            gens = [g for g in m.G_C_SOLAR if g.split('-')[0] == z]

            if gens:
                return sum(m.a[g, y] for g in gens) - m.SOLAR_BUILD_LIMITS[z] <= 0
            else:
                return Constraint.Skip

        # Storage build limit constraint for each NEM zone
        m.SOLAR_BUILD_LIMIT_CONS = Constraint(m.Z, m.Y, rule=solar_build_limits_cons_rule)

        def wind_build_limits_cons_rule(_m, z, y):
            """Enforce wind build limits in each NEM zone"""

            # Wind generators belonging to zone 'z'
            gens = [g for g in m.G_C_WIND if g.split('-')[0] == z]

            if gens:
                return sum(m.a[g, y] for g in gens) - m.WIND_BUILD_LIMITS[z] <= 0
            else:
                return Constraint.Skip

        # Wind build limit constraint for each NEM zone
        m.WIND_BUILD_LIMIT_CONS = Constraint(m.Z, m.Y, rule=wind_build_limits_cons_rule)

        def wind_build_limits_cons_rule(_m, z, y):
            """Enforce storage build limits in each NEM zone"""

            # Storage generators belonging to zone 'z'
            gens = [g for g in m.G_C_STORAGE if g.split('-')[0] == z]

            if gens:
                return sum(m.a[g, y] for g in gens) - m.STORAGE_BUILD_LIMITS[z] <= 0
            else:
                return Constraint.Skip

        # Storage build limit constraint for each NEM zone
        m.STORAGE_BUILD_LIMIT_CONS = Constraint(m.Z, m.Y, rule=wind_build_limits_cons_rule)

        # Cumulative emissions target
        m.EMISSIONS_CONSTRAINT = Constraint(expr=-m.TOTAL_EMISSIONS + m.CUMULATIVE_EMISSIONS_TARGET + m.f_e >= 0)

        return m

    @staticmethod
    def define_objective(m):
        """Define objective function"""

        def objective_function_rule(_m):
            """Investment plan objective function"""

            # Total investment cost over model horizon
            investment_cost = sum((m.DISCOUNT_FACTOR[y] / m.WACC) * m.INV[y] for y in m.Y)

            # Fixed operations and maintenance cost over model horizon
            fom_cost = sum(m.DISCOUNT_FACTOR[y] * m.FOM[y] for y in m.Y)

            # End of year operating costs (assumed to be paid in perpetuity to take into account end-of-year effects)
            end_of_year_cost = (m.DISCOUNT_FACTOR[m.Y.last()] / m.WACC) * m.FOM[m.Y.last()]

            # Sub-problem dual information
            dual_cost = sum(m.PSI_FIXED[g, y, s] * (m.CANDIDATE_CAPACITY_FIXED[g, y, s] - m.a[g, y])
                            for g in m.G_C for y in m.Y for s in m.S)

            # Objective function - note: also considers penalty (m.PEN) associated with emissions constraint violation
            objective_function = investment_cost + fom_cost + end_of_year_cost + m.PEN - dual_cost

            return objective_function

        # Investment plan objective function
        m.OBJECTIVE = Objective(rule=objective_function_rule, sense=minimize)

        return m

    def construct_model(self):
        """Construct investment plan model"""

        # Initialise model object
        m = ConcreteModel()

        # Prepare to import dual variables
        m.dual = Suffix(direction=Suffix.IMPORT)

        # Define sets
        m = self.components.define_sets(m)

        # Define parameters common to all sub-problems
        m = self.components.define_parameters(m)

        # Define parameters
        m = self.define_parameters(m)

        # Define variables
        m = self.define_variables(m)

        # Define expressions
        m = self.define_expressions(m)

        # Define constraints
        m = self.define_constraints(m)

        # Define objective
        m = self.define_objective(m)

        return m

    def solve_model(self, m):
        """Solve model instance"""

        # Solve model
        self.opt.solve(m, tee=False, options=self.solver_options, keepfiles=self.keepfiles)

        # Log infeasible constraints if they exist
        log_infeasible_constraints(m)

        return m

    @staticmethod
    def update_parameters(m, uc_solution_dir):
        """Update model parameters"""

        # All results from unit commitment subproblem solution
        result_files = [f for f in os.listdir(uc_solution_dir) if '.pickle' in f]

        # Initialise total emissions
        total_emissions = 0

        # For each unit commitment results file
        for f in result_files:

            # Get year and scenario from filename
            year, scenario = int(f.split('_')[-2]), int(f.split('_')[-1].replace('.pickle', ''))

            # Open scenario solution file
            with open(os.path.join(uc_solution_dir, f), 'rb') as g:

                # Load scenario solution
                scenario_solution = pickle.load(g)

                # Get total emissions
                total_emissions += scenario_solution['SCENARIO_EMISSIONS']

                # Extract dual variables associated with capacity sizing decision for each candidate generator
                for generator, val in scenario_solution['PSI_FIXED'].items():

                    # Compute update amount
                    increment = (val - m.PSI_FIXED[generator, year, scenario].value) / 2

                    # Update dual variables - add previously to previously computed value
                    m.PSI_FIXED[generator, year, scenario] += increment

                # Extract fixed capacity used in subproblems
                for generator, val in scenario_solution['CANDIDATE_CAPACITY_FIXED'].items():

                    # Update fixed candidate capacity
                    m.CANDIDATE_CAPACITY_FIXED[generator, year, scenario] = val

        # Update total emissions from all operating scenarios
        m.TOTAL_EMISSIONS = total_emissions

        return m

    @staticmethod
    def fix_binary_variables(m):
        """Fix all binary variables"""

        for g in m.G_C_THERM:
            for y in m.Y:
                for n in m.G_C_THERM_SIZE_OPTIONS:
                    # Fix binary variables related to discrete sizing decision for candidate thermal units
                    m.d[g, y, n].fix()

        return m

    @staticmethod
    def unfix_binary_variables(m):
        """Unfix all binary variables"""

        for g in m.G_C_THERM:
            for y in m.Y:
                for n in m.G_C_THERM_SIZE_OPTIONS:
                    # Unfix binary variables related to discrete sizing decision for candidate thermal units
                    m.d[g, y, n].unfix()

        return m

    @staticmethod
    def save_solution(m, solution_dir):
        """Save model solution"""

        # Save investment plan
        investment_plan_output = {'CAPACITY_FIXED': m.a.get_values(),
                                  'LAMBDA_FIXED': m.dual[m.EMISSIONS_CONSTRAINT]}

        # Save investment plan results
        with open(os.path.join(solution_dir, 'investment-results.pickle'), 'wb') as f:
            pickle.dump(investment_plan_output, f)
Exemple #12
0
class Subproblem:
    # Pre-processed model data
    data = ModelData()

    def __init__(self):
        # Solver options
        self.keepfiles = False
        self.solver_options = {'Method': 1}  # 'MIPGap': 0.0005
        self.opt = SolverFactory('gurobi', solver_io='lp')

        # Setup logger
        logging.basicConfig(filename='subproblem.log', filemode='a',
                            format='%(asctime)s %(name)s %(levelname)s %(message)s',
                            datefmt='%Y-%m-%d %H:%M:%S',
                            level=logging.DEBUG)

        logging.info("Running subproblem")
        self.logger = logging.getLogger('Subproblem')

    def construct_model(self):
        """Construct subproblem model components"""

        # Used to define sets and parameters common to both master and subproblem
        common_components = CommonComponents()

        # Initialise base model object
        m = ConcreteModel()

        # Define sets - common to both master and subproblem
        m = common_components.define_sets(m)

        # Define parameters - common to both master and subproblem
        m = common_components.define_parameters(m)

        # Define variables - common to both master and subproblem
        m = common_components.define_variables(m)

        # Define expressions - common to both master and subproblem
        m = common_components.define_expressions(m)

        return m

    def _update_marginal_costs(self, m, g, i):
        """Marginal costs for existing and candidate generators

        Note: Marginal costs for existing and candidate thermal plant depend on fuel costs, which are time
        varying. Therefore marginal costs for thermal plant must be define for each year in model horizon.
        """

        if g in m.G_E_THERM:

            # Last year in the dataset for which fuel cost information exists
            max_year = max([year for cat, year in self.data.existing_units_dict.keys() if cat == 'FUEL_COST'])

            # If year in model horizon exceeds max year for which data are available use values for last
            # available year
            if i > max_year:
                # Use final year in dataset as max year
                i = max_year

            marginal_cost = float(self.data.existing_units_dict[('FUEL_COST', i)][g]
                                  * self.data.existing_units_dict[('PARAMETERS', 'HEAT_RATE')][g])
        elif g in m.G_C_THERM:

            # Last year in the dataset for which fuel cost information exists
            max_year = max([year for cat, year in self.data.candidate_units_dict.keys() if cat == 'FUEL_COST'])

            # If year in model horizon exceeds max year for which data are available use values for last
            # available year
            if i > max_year:
                # Use final year in dataset as max year
                i = max_year

            marginal_cost = float(self.data.candidate_units_dict[('FUEL_COST', i)][g]
                                  * self.data.candidate_units_dict[('PARAMETERS', 'HEAT_RATE')][g])

        else:
            raise Exception(f'Unexpected generator or year: {g} {i}')

        return marginal_cost

    def _update_investment_costs(self, m, g, i):
        """Update investment costs"""

        if g in m.G_C_STORAGE:
            # Build costs for batteries
            return float(self.data.battery_build_costs_dict[i][g] * 1000)
        else:
            # Build costs for other candidate units
            return float(self.data.candidate_units_dict[('BUILD_COST', i)][g] * 1000)

    def update_parameters_year(self, m, i):
        """Update model parameters for a given year and operating scenario"""

        for g in m.G_E_THERM.union(m.G_C_THERM):
            # Update marginal costs
            m.C_MC[g] = self._update_marginal_costs(m, g, i)

            # Update investment costs
            if g in m.G_C_THERM:
                m.C_INV[g] = self._update_investment_costs(m, g, i)

        return m
Exemple #13
0
class UnitCommitment:
    def __init__(self):
        # Pre-processed data for model construction
        self.data = ModelData()

        # Solver options
        self.keepfiles = False
        self.solver_options = {}  # 'mip tolerances integrality': 0.01
        self.opt = SolverFactory('cplex', solver_io='lp')

    def define_sets(self, m, overlap):
        """Define sets to be used in model"""

        # NEM regions
        m.R = Set(initialize=self.data.nem_regions)

        # NEM zones
        m.Z = Set(initialize=self.data.nem_zones)

        # Links between NEM zones
        m.L = Set(initialize=self.data.links)

        # Interconnectors for which flow limits are defined
        m.L_I = Set(initialize=self.data.links_constrained)

        # Scheduled generators
        m.G_SCHEDULED = Set(initialize=self.data.scheduled_duids)

        # Semi-scheduled generators (e.g. wind, solar)
        m.G_SEMI_SCHEDULED = Set(initialize=self.data.semi_scheduled_duids)

        # Existing storage units
        m.G_STORAGE = Set(initialize=self.data.storage_duids)

        # Generators considered in analysis - only semi and semi-scheduled generators (and storage units)
        m.G_MODELLED = m.G_SCHEDULED.union(m.G_SEMI_SCHEDULED).union(
            m.G_STORAGE)

        # Thermal units
        m.G_THERM = Set(
            initialize=self.data.get_thermal_unit_duids()).intersection(
                m.G_MODELLED)

        # Wind units
        m.G_WIND = Set(
            initialize=self.data.get_wind_unit_duids()).intersection(
                m.G_MODELLED)

        # Solar units
        m.G_SOLAR = Set(
            initialize=self.data.get_solar_unit_duids()).intersection(
                m.G_MODELLED)

        # Existing hydro units
        m.G_HYDRO = Set(
            initialize=self.data.get_hydro_unit_duids()).intersection(
                m.G_MODELLED)

        # Slow start thermal generators (existing and candidate)
        m.G_THERM_SLOW = Set(
            initialize=self.data.slow_start_duids).intersection(m.G_MODELLED)

        # Quick start thermal generators (existing and candidate)
        m.G_THERM_QUICK = Set(
            initialize=self.data.quick_start_duids).intersection(m.G_MODELLED)

        # All generators (storage units excluded)
        m.G = m.G_THERM.union(m.G_WIND).union(m.G_SOLAR).union(m.G_HYDRO)

        # Operating scenario hour
        m.T = RangeSet(1, 24 + overlap, ordered=True)

        return m

    def define_parameters(self, m):
        """Define unit commitment problem parameters"""
        def emissions_intensity_rule(_m, g):
            """Emissions intensity (tCO2/MWh)"""

            return float(self.data.generators.loc[g, 'EMISSIONS'])

        # Emissions intensities for all generators
        m.EMISSIONS_RATE = Param(m.G,
                                 rule=emissions_intensity_rule,
                                 mutable=True)

        def minimum_region_up_reserve_rule(_m, r):
            """Minimum upward reserve rule"""

            return float(self.data.minimum_reserve_levels[r])

        # Minimum up reserve
        m.RESERVE_UP = Param(m.R, rule=minimum_region_up_reserve_rule)

        def ramp_rate_startup_rule(_m, g):
            """Startup ramp-rate (MW)"""

            return float(self.data.generators.loc[g, 'RR_STARTUP'])

        # Startup ramp-rate for existing and candidate thermal generators
        m.RR_SU = Param(m.G_THERM, rule=ramp_rate_startup_rule)

        def ramp_rate_shutdown_rule(_m, g):
            """Shutdown ramp-rate (MW)"""

            return float(self.data.generators.loc[g, 'RR_SHUTDOWN'])

        # Shutdown ramp-rate for existing and candidate thermal generators
        m.RR_SD = Param(m.G_THERM, rule=ramp_rate_shutdown_rule)

        def ramp_rate_normal_up_rule(_m, g):
            """Ramp-rate up (MW/h) - when running"""

            return float(self.data.generators.loc[g, 'RR_UP'])

        # Ramp-rate up (normal operation)
        m.RR_UP = Param(m.G_THERM, rule=ramp_rate_normal_up_rule)

        def ramp_rate_normal_down_rule(_m, g):
            """Ramp-rate down (MW/h) - when running"""

            return float(self.data.generators.loc[g, 'RR_DOWN'])

        # Ramp-rate down (normal operation)
        m.RR_DOWN = Param(m.G_THERM, rule=ramp_rate_normal_down_rule)

        def min_power_output_rule(_m, g):
            """Minimum power output for thermal generators"""

            return float(self.data.generators.loc[g, 'MIN_GEN'])

        # Minimum power output
        m.P_MIN = Param(m.G_THERM, rule=min_power_output_rule)

        def network_incidence_matrix_rule(_m, l, z):
            """Incidence matrix describing connections between adjacent NEM zones"""

            return float(self.data.network_incidence_matrix.loc[l, z])

        # Network incidence matrix
        m.INCIDENCE_MATRIX = Param(m.L,
                                   m.Z,
                                   rule=network_incidence_matrix_rule)

        def powerflow_min_rule(_m, l):
            """Minimum powerflow over network link"""

            return float(-self.data.powerflow_limits[l]['reverse'])

        # Lower bound for powerflow over link
        m.POWERFLOW_MIN = Param(m.L_I, rule=powerflow_min_rule)

        def powerflow_max_rule(_m, l):
            """Maximum powerflow over network link"""

            return float(self.data.powerflow_limits[l]['forward'])

        # Lower bound for powerflow over link
        m.POWERFLOW_MAX = Param(m.L_I, rule=powerflow_max_rule)

        def battery_efficiency_rule(_m, g):
            """Battery efficiency"""

            return float(self.data.storage.loc[g, 'EFFICIENCY'])

        # Battery efficiency
        m.BATTERY_EFFICIENCY = Param(m.G_STORAGE, rule=battery_efficiency_rule)

        # Lower bound for energy in storage unit at end of interval (assume = 0)
        m.Q_INTERVAL_END_LB = Param(m.G_STORAGE, initialize=0)

        def storage_unit_interval_end_ub_rule(_m, g):
            """
            Max energy in storage unit at end of interval

            Assume energy capacity (MWh) = registered capacity (MW). I.e unit can completely discharge within 1hr.
            """

            return float(self.data.storage.loc[g, 'REG_CAP'])

        # Upper bound for energy in storage unit at end of interval (assume = P_MAX)
        m.Q_INTERVAL_END_UB = Param(
            m.G_STORAGE, initialize=storage_unit_interval_end_ub_rule)

        # Energy in battery in interval prior to model start (assume battery initially completely discharged)
        m.Q0 = Param(m.G_STORAGE, initialize=0, mutable=True)

        def marginal_cost_rule(_m, g):
            """Marginal costs for existing and candidate generators

            Note: Marginal costs for existing and candidate thermal plant depend on fuel costs, which are time
            varying. Therefore marginal costs for thermal plant must be updated for each year in model horizon.
            """

            if g in m.G:
                marginal_cost = float(self.data.generators.loc[g,
                                                               'SRMC_2016-17'])

            elif g in m.G_STORAGE:
                marginal_cost = float(self.data.storage.loc[g, 'SRMC_2016-17'])

            else:
                raise Exception(f'Unexpected generator: {g}')

            assert marginal_cost >= 0, 'Cannot have negative marginal cost'

            return marginal_cost

        # Marginal costs for all generators and time periods
        m.C_MC = Param(m.G.union(m.G_STORAGE), rule=marginal_cost_rule)

        def startup_cost_rule(_m, g):
            """
            Startup costs for existing and candidate thermal units
            """

            startup_cost = float(self.data.generators.loc[g, 'SU_COST_WARM'])

            # Shutdown cost cannot be negative
            assert startup_cost >= 0, 'Negative startup cost'

            return startup_cost

        def max_power_output_rule(_m, g):
            """Max power output for generators. Max discharging or charging power for storage units"""

            if g in m.G:
                return float(self.data.generators.loc[g, 'REG_CAP'])
            elif g in m.G_STORAGE:
                return float(self.data.storage.loc[g, 'REG_CAP'])
            else:
                raise Exception(f'Unexpected generator DUID: {g}')

        # Max power output
        m.P_MAX = Param(m.G.union(m.G_STORAGE), rule=max_power_output_rule)

        def max_storage_unit_energy_rule(_m, g):
            """Max energy storage capability. Assume capacity (MWh) = registered capacity (MW)"""

            return float(self.data.storage.loc[g, 'REG_CAP'])

        # Max energy capacity
        m.Q_MAX = Param(m.G_STORAGE, rule=max_storage_unit_energy_rule)

        # Generator startup costs - per MW
        m.C_SU = Param(m.G_THERM, rule=startup_cost_rule)

        # Generator shutdown costs - per MW - assume zero for now TODO: May need to update this assumption
        m.C_SD = Param(m.G_THERM, initialize=0)

        # Value of lost load [$/MWh]
        m.C_L = Param(initialize=10000)

        # Penalty for violating up reserve constraint
        m.C_UV = Param(initialize=1000)

        # Permit price - positive if generators eligible under scheme.
        m.PERMIT_PRICE = Param(m.G, initialize=0, mutable=True)

        # -------------------------------------------------------------------------------------------------------------
        # Parameters to update each time model is run
        # -------------------------------------------------------------------------------------------------------------
        # Baseline - defined for each dispatch interval (correct calculation of scheme revenue when update occurs)
        m.BASELINE = Param(m.T, initialize=0, mutable=True)

        # Initial on-state rule - must be updated each time model is run
        m.U0 = Param(m.G_THERM, within=Binary, mutable=True, initialize=1)

        # Power output in interval prior to model start (assume = 0 for now)
        m.P0 = Param(m.G.union(m.G_STORAGE),
                     mutable=True,
                     within=NonNegativeReals,
                     initialize=0)

        # Wind output
        m.P_WIND = Param(m.G_WIND,
                         m.T,
                         mutable=True,
                         within=NonNegativeReals,
                         initialize=0)

        # Solar output
        m.P_SOLAR = Param(m.G_SOLAR,
                          m.T,
                          mutable=True,
                          within=NonNegativeReals,
                          initialize=0)

        # Hydro output
        m.P_HYDRO = Param(m.G_HYDRO,
                          m.T,
                          mutable=True,
                          within=NonNegativeReals,
                          initialize=0)

        # Demand
        m.DEMAND = Param(m.Z,
                         m.T,
                         mutable=True,
                         within=NonNegativeReals,
                         initialize=0)

        return m

    @staticmethod
    def define_variables(m):
        """Define unit commitment problem variables"""

        # Upward reserve allocation [MW]
        m.r_up = Var(m.G_THERM.union(m.G_STORAGE),
                     m.T,
                     within=NonNegativeReals,
                     initialize=0)

        # Startup state variable
        m.v = Var(m.G_THERM, m.T, within=Binary, initialize=0)

        # On-state variable
        m.u = Var(m.G_THERM, m.T, within=Binary, initialize=1)

        # Shutdown state variable
        m.w = Var(m.G_THERM, m.T, within=Binary, initialize=0)

        # Power output above minimum dispatchable level
        m.p = Var(m.G_THERM, m.T, within=NonNegativeReals, initialize=0)

        # Total power output
        m.p_total = Var(m.G.difference(m.G_STORAGE),
                        m.T,
                        within=NonNegativeReals,
                        initialize=0)

        # Storage unit charging power
        m.p_in = Var(m.G_STORAGE, m.T, within=NonNegativeReals, initialize=0)

        # Storage unit discharging power
        m.p_out = Var(m.G_STORAGE, m.T, within=NonNegativeReals, initialize=0)

        # Storage unit energy (state of charge)
        m.q = Var(m.G_STORAGE, m.T, within=NonNegativeReals, initialize=0)

        # Powerflow between NEM zones
        m.p_flow = Var(m.L, m.T, initialize=0)

        # Lost-load
        m.p_V = Var(m.Z, m.T, within=NonNegativeReals, initialize=0)

        # Up reserve constraint violation
        m.reserve_up_violation = Var(m.R,
                                     m.T,
                                     within=NonNegativeReals,
                                     initialize=0)

        return m

    @staticmethod
    def define_expressions(m):
        """Define unit commitment problem expressions"""
        def energy_output_rule(_m, g, t):
            """Energy output"""

            # If a storage unit - power output assumed constant over interval
            if g in m.G_STORAGE:
                return m.p_out[g, t]

            # For all other units - assume constant ramp between power output targets over interval
            elif g in m.G.difference(m.G_STORAGE):
                if t != m.T.first():
                    return (m.p_total[g, t - 1] + m.p_total[g, t]) / 2
                else:
                    return (m.P0[g] + m.p_total[g, t]) / 2

            else:
                raise Exception(f'Unexpected unit encountered: {g}')

        # Energy output
        m.e = Expression(m.G.union(m.G_STORAGE), m.T, rule=energy_output_rule)

        def lost_load_energy_rule(_m, z, t):
            """
            Amount of lost-load energy.

            Note: Assumes lost-load energy in interval prior to model start (t=0) is zero.
            """

            if t != m.T.first():
                return (m.p_V[z, t] + m.p_V[z, t - 1]) / 2

            else:
                # Assumes no lost energy in interval preceding model start
                return m.p_V[z, t] / 2

        # Lost-load energy
        m.e_V = Expression(m.Z, m.T, rule=lost_load_energy_rule)

        def day_emissions_rule(_m):
            """Total emissions for a given interval"""

            return sum(m.e[g, t] * m.EMISSIONS_RATE[g] for g in m.G_THERM
                       for t in range(1, 25))

        # Total scenario emissions
        m.DAY_EMISSIONS = Expression(rule=day_emissions_rule)

        def day_demand_rule(_m):
            """Total demand accounted for by given scenario"""

            return sum(m.DEMAND[z, t] for z in m.Z for t in range(1, 25))

        # Total scenario energy demand
        m.DAY_DEMAND = Expression(rule=day_demand_rule)

        def day_emissions_intensity_rule(_m):
            """Emissions intensity for a given interval"""

            return m.DAY_EMISSIONS / m.DAY_DEMAND

        # Emissions intensity for given day
        m.DAY_EMISSIONS_INTENSITY = Expression(
            rule=day_emissions_intensity_rule)

        def net_penalty_rule(_m, g, t):
            """Net penalty per MWh"""

            return (m.EMISSIONS_RATE[g] - m.BASELINE[t]) * m.PERMIT_PRICE[g]

        # Net penalty per MWh
        m.NET_PENALTY = Expression(m.G, m.T, rule=net_penalty_rule)

        def day_scheme_revenue_rule(_m):
            """Total scheme revenue accrued for a given day"""

            return sum(m.NET_PENALTY[g, t] * m.e[g, t] for g in m.G
                       for t in range(1, 25))

        # Scheme revenue for a given day
        m.DAY_SCHEME_REVENUE = Expression(rule=day_scheme_revenue_rule)

        def thermal_operating_costs_rule(_m):
            """Cost to operate thermal generators for given scenario"""

            # Operating cost related to energy output + emissions charge
            operating_costs = sum((m.C_MC[g] + m.NET_PENALTY[g, t]) * m.e[g, t]
                                  for g in m.G_THERM for t in m.T)

            # Existing unit start-up and shutdown costs
            startup_shutdown_costs = (sum(
                (m.C_SU[g] * m.v[g, t]) + (m.C_SD[g] * m.w[g, t])
                for g in m.G_THERM for t in m.T))

            # Total thermal unit costs
            total_cost = operating_costs + startup_shutdown_costs

            return total_cost

        # Operating cost - thermal units
        m.OP_T = Expression(rule=thermal_operating_costs_rule)

        def hydro_operating_costs_rule(_m):
            """Cost to operate hydro generators"""

            return sum((m.C_MC[g] + m.NET_PENALTY[g, t]) * m.e[g, t]
                       for g in m.G_HYDRO for t in m.T)

        # Operating cost - hydro generators
        m.OP_H = Expression(rule=hydro_operating_costs_rule)

        def wind_operating_costs_rule(_m):
            """Cost to operate wind generators"""

            # Existing wind generators - not eligible for subsidy
            total_cost = sum((m.C_MC[g] + m.NET_PENALTY[g, t]) * m.e[g, t]
                             for g in m.G_WIND for t in m.T)

            return total_cost

        # Operating cost - wind units
        m.OP_W = Expression(rule=wind_operating_costs_rule)

        def solar_operating_costs_rule(_m):
            """Cost to operate solar generators"""

            # Existing solar generators - not eligible for subsidy
            total_cost = sum((m.C_MC[g] + m.NET_PENALTY[g, t]) * m.e[g, t]
                             for g in m.G_SOLAR for t in m.T)

            return total_cost

        # Operating cost - solar units
        m.OP_S = Expression(rule=solar_operating_costs_rule)

        def storage_operating_costs_rule(_m):
            """Cost to operate storage units"""

            return sum(m.C_MC[g] * m.e[g, t] for g in m.G_STORAGE for t in m.T)

        # Operating cost - storage units
        m.OP_Q = Expression(rule=storage_operating_costs_rule)

        def lost_load_value_rule(_m):
            """Vale of lost load"""

            return sum(m.C_L * m.e_V[z, t] for z in m.Z for t in m.T)

        # Value of lost load
        m.OP_L = Expression(rule=lost_load_value_rule)

        def reserve_up_violation_value_rule(_m):
            """Value of up reserve constraint violation"""

            return sum(m.C_UV * m.reserve_up_violation[r, t] for r in m.R
                       for t in m.T)

        # Up reserve violation penalty
        m.OP_U = Expression(rule=reserve_up_violation_value_rule)

        # Total operating cost for a given interval
        m.INTERVAL_COST = Expression(expr=m.OP_T + m.OP_H + m.OP_W + m.OP_S +
                                     m.OP_Q + m.OP_L + m.OP_U)

        # Objective function - sum of operational costs for a given interval
        m.OBJECTIVE_FUNCTION = Expression(expr=m.INTERVAL_COST)

        return m

    def define_constraints(self, m):
        """Define unit commitment problem constraints"""
        def reserve_up_rule(_m, r, t):
            """Ensure sufficient up power reserve in each region"""

            # Existing and candidate thermal gens + candidate storage units
            gens = m.G_THERM.union(m.G_STORAGE)

            # Subset of generators with NEM region
            gens_subset = [
                g for g in gens if self.data.duid_zone_map[g] in
                self.data.nem_region_zone_map[r]
            ]

            return sum(m.r_up[g, t]
                       for g in gens_subset) + m.reserve_up_violation[
                           r, t] >= m.RESERVE_UP[r]

        # Upward power reserve rule for each NEM region
        m.RESERVE_UP_CONS = Constraint(m.R, m.T, rule=reserve_up_rule)

        def generator_state_logic_rule(_m, g, t):
            """
            Determine the operating state of the generator (startup, shutdown running, off)
            """

            if t == m.T.first():
                # Must use U0 if first period (otherwise index out of range)
                return m.u[g, t] - m.U0[g] == m.v[g, t] - m.w[g, t]

            else:
                # Otherwise operating state is coupled to previous period
                return m.u[g, t] - m.u[g, t - 1] == m.v[g, t] - m.w[g, t]

        # Unit operating state
        m.GENERATOR_STATE_LOGIC = Constraint(m.G_THERM,
                                             m.T,
                                             rule=generator_state_logic_rule)

        def minimum_on_time_rule(_m, g, t):
            """Minimum number of hours generator must be on"""

            if g in m.G_THERM:
                hours = self.data.generators.loc[g, 'MIN_ON_TIME']

            else:
                raise Exception(
                    f'Min on time hours not found for generator: {g}')

            # Time index used in summation
            time_index = [
                k for k in range(t - int(hours) + 1, t + 1) if k >= 1
            ]

            # Constraint only defined over subset of timestamps
            if t >= hours:
                return sum(m.v[g, j] for j in time_index) <= m.u[g, t]
            else:
                return Constraint.Skip

        # Minimum on time constraint
        m.MINIMUM_ON_TIME = Constraint(m.G_THERM,
                                       m.T,
                                       rule=minimum_on_time_rule)

        def minimum_off_time_rule(_m, g, t):
            """Minimum number of hours generator must be off"""

            if g in m.G_THERM:
                hours = self.data.generators.loc[g, 'MIN_OFF_TIME']

            else:
                raise Exception(
                    f'Min off time hours not found for generator: {g}')

            # Time index used in summation
            time_index = [
                k for k in range(t - int(hours) + 1, t + 1) if k >= 1
            ]

            # Constraint only defined over subset of timestamps
            if t >= hours:
                return sum(m.w[g, j] for j in time_index) <= 1 - m.u[g, t]
            else:
                return Constraint.Skip

        # Minimum off time constraint
        m.MINIMUM_OFF_TIME = Constraint(m.G_THERM,
                                        m.T,
                                        rule=minimum_off_time_rule)

        def ramp_rate_up_rule(_m, g, t):
            """Ramp-rate up constraint - normal operation"""

            # For all other intervals apart from the first
            if t != m.T.first():
                return (m.p[g, t] + m.r_up[g, t]) - m.p[g, t - 1] <= m.RR_UP[g]

            else:
                # Ramp-rate for first interval
                return m.p[g, t] + m.r_up[g, t] - m.P0[g] <= m.RR_UP[g]

        # Ramp-rate up limit
        m.RAMP_RATE_UP = Constraint(m.G_THERM, m.T, rule=ramp_rate_up_rule)

        def ramp_rate_down_rule(_m, g, t):
            """Ramp-rate down constraint - normal operation"""

            # For all other intervals apart from the first
            if t != m.T.first():
                return -m.p[g, t] + m.p[g, t - 1] <= m.RR_DOWN[g]

            else:
                # Ramp-rate for first interval
                return -m.p[g, t] + m.P0[g] <= m.RR_DOWN[g]

        # Ramp-rate up limit
        m.RAMP_RATE_DOWN = Constraint(m.G_THERM, m.T, rule=ramp_rate_down_rule)

        def power_output_within_limits_rule(_m, g, t):
            """Ensure power output + reserves within capacity limits"""

            # Left hand-side of constraint
            lhs = m.p[g, t] + m.r_up[g, t]

            # Existing thermal units - fixed capacity
            if g in m.G_THERM:
                rhs_1 = (m.P_MAX[g] - m.P_MIN[g]) * m.u[g, t]

                # If not the last period
                if t != m.T.last():
                    rhs_2 = (m.P_MAX[g] - m.RR_SD[g]) * m.w[g, t + 1]
                    rhs_3 = (m.RR_SU[g] - m.P_MIN[g]) * m.v[g, t + 1]

                    return lhs <= rhs_1 - rhs_2 + rhs_3

                # If the last period - startup and shutdown state variables assumed = 0
                else:
                    return lhs <= rhs_1

            else:
                raise Exception(f'Unknown generator: {g}')

        # Power output and reserves within limits
        m.POWER_OUTPUT_WITHIN_LIMITS = Constraint(
            m.G_THERM, m.T, rule=power_output_within_limits_rule)

        def total_power_thermal_rule(_m, g, t):
            """Total power output for thermal generators"""

            # Quick-start thermal generators
            if g in m.G_THERM_QUICK:
                # If not the last index
                if t != m.T.last():
                    return m.p_total[
                        g,
                        t] == m.P_MIN[g] * (m.u[g, t] + m.v[g, t + 1]) + m.p[g,
                                                                             t]

                # If the last index assume shutdown and startup indicator = 0
                else:
                    return m.p_total[g,
                                     t] == (m.P_MIN[g] * m.u[g, t]) + m.p[g, t]

            # Slow-start thermal generators
            elif g in m.G_THERM_SLOW:
                # Startup duration
                SU_D = ceil(m.P_MIN[g] / m.RR_SU[g])

                # Startup power output trajectory increment
                ramp_up_increment = m.P_MIN[g] / SU_D

                # Startup power output trajectory
                P_SU = OrderedDict(
                    {k + 1: ramp_up_increment * k
                     for k in range(0, SU_D + 1)})

                # Shutdown duration
                SD_D = ceil(m.P_MIN[g] / m.RR_SD[g])

                # Shutdown power output trajectory increment
                ramp_down_increment = m.P_MIN[g] / SD_D

                # Shutdown power output trajectory
                P_SD = OrderedDict({
                    k + 1: m.P_MIN[g] - (ramp_down_increment * k)
                    for k in range(0, SD_D + 1)
                })

                if t != m.T.last():
                    return (m.p_total[g, t] == (
                        (m.P_MIN[g] *
                         (m.u[g, t] + m.v[g, t + 1])) + m.p[g, t] +
                        sum(P_SU[k] * m.v[g, t - k + SU_D + 2] if t - k +
                            SU_D + 2 in m.T else 0
                            for k in range(1, SU_D + 1)) +
                        sum(P_SD[k] * m.w[g, t - k + 2] if t - k +
                            2 in m.T else 0 for k in range(2, SD_D + 2))))
                else:
                    return (m.p_total[g, t] == (
                        (m.P_MIN[g] * m.u[g, t]) + m.p[g, t] +
                        sum(P_SU[k] * m.v[g, t - k + SU_D + 2] if t - k +
                            SU_D + 2 in m.T else 0
                            for k in range(1, SU_D + 1)) +
                        sum(P_SD[k] * m.w[g, t - k + 2] if t - k +
                            2 in m.T else 0 for k in range(2, SD_D + 2))))
            else:
                raise Exception(f'Unexpected generator: {g}')

        # Constraint yielding total power output
        m.TOTAL_POWER_THERMAL = Constraint(m.G_THERM,
                                           m.T,
                                           rule=total_power_thermal_rule)

        def max_power_output_thermal_rule(_m, g, t):
            """Ensure max power + reserve is always less than installed capacity for thermal generators"""

            return m.p_total[g, t] + m.r_up[g, t] <= m.P_MAX[g]

        # Max power output + reserve is always less than installed capacity
        m.MAX_POWER_THERMAL = Constraint(m.G_THERM,
                                         m.T,
                                         rule=max_power_output_thermal_rule)

        def max_power_output_wind_rule(_m, g, t):
            """Max power output from wind generators"""

            return m.p_total[g, t] <= m.P_WIND[g, t]

        # Max power output from wind generators
        m.MAX_POWER_WIND = Constraint(m.G_WIND,
                                      m.T,
                                      rule=max_power_output_wind_rule)

        def max_power_output_solar_rule(_m, g, t):
            """Max power output from solar generators"""

            return m.p_total[g, t] <= m.P_SOLAR[g, t]

        # Max power output from wind generators
        m.MAX_POWER_SOLAR = Constraint(m.G_SOLAR,
                                       m.T,
                                       rule=max_power_output_solar_rule)

        def max_power_output_hydro_rule(_m, g, t):
            """Max power output from hydro generators"""

            return m.p_total[g, t] <= m.P_HYDRO[g, t]

        # Max power output from hydro generators
        m.MAX_POWER_HYDRO = Constraint(m.G_HYDRO,
                                       m.T,
                                       rule=max_power_output_hydro_rule)

        def storage_max_power_out_rule(_m, g, t):
            """
            Maximum discharging power of storage unit - set equal to energy capacity. Assumes
            storage unit can completely discharge in 1 hour
            """

            return m.p_in[g, t] <= m.P_MAX[g]

        # Max MW out of storage device - discharging
        m.P_STORAGE_MAX_OUT = Constraint(m.G_STORAGE,
                                         m.T,
                                         rule=storage_max_power_out_rule)

        def storage_max_power_in_rule(_m, g, t):
            """
            Maximum charging power of storage unit - set equal to energy capacity. Assumes
            storage unit can completely charge in 1 hour
            """

            return m.p_out[g, t] + m.r_up[g, t] <= m.P_MAX[g]

        # Max MW into storage device - charging
        m.P_STORAGE_MAX_IN = Constraint(m.G_STORAGE,
                                        m.T,
                                        rule=storage_max_power_in_rule)

        def max_storage_energy_rule(_m, g, t):
            """Ensure storage unit energy is within unit's capacity"""

            return m.q[g, t] <= m.Q_MAX[g]

        # Storage unit energy is within unit's limits
        m.STORAGE_ENERGY_BOUNDS = Constraint(m.G_STORAGE,
                                             m.T,
                                             rule=max_storage_energy_rule)

        def storage_energy_transition_rule(_m, g, t):
            """Constraint that couples energy + power between periods for storage units"""

            # If not the first period
            if t != m.T.first():
                return (m.q[g, t] == m.q[g, t - 1] +
                        (m.BATTERY_EFFICIENCY[g] * m.p_in[g, t]) -
                        (m.p_out[g, t] / m.BATTERY_EFFICIENCY[g]))
            else:
                # Assume battery completely discharged in first period (given by m.Q0)
                return (m.q[g, t] == m.Q0[g] +
                        (m.BATTERY_EFFICIENCY[g] * m.p_in[g, t]) -
                        (m.p_out[g, t] / m.BATTERY_EFFICIENCY[g]))

        # Account for inter-temporal energy transition within storage units
        m.STORAGE_ENERGY_TRANSITION = Constraint(
            m.G_STORAGE, m.T, rule=storage_energy_transition_rule)

        def storage_interval_end_lower_bound_rule(_m, g):
            """Ensure energy within storage unit at end of interval is greater than desired lower bound"""

            return m.Q_INTERVAL_END_LB[g] <= m.q[g, m.T.last()]

        # Ensure energy in storage unit at end of interval is above some desired lower bound
        m.STORAGE_INTERVAL_END_LOWER_BOUND = Constraint(
            m.G_STORAGE, rule=storage_interval_end_lower_bound_rule)

        def storage_interval_end_upper_bound_rule(_m, g):
            """
            Ensure energy within storage unit at end of interval is less than desired upper bound

            Note: Assuming upper bound for desired energy in unit at end of interval = installed capacity
            """

            return m.q[g, m.T.last()] <= m.Q_MAX[g]

        # Ensure energy in storage unit at end of interval is above some desired lower bound
        m.STORAGE_INTERVAL_END_UPPER_BOUND = Constraint(
            m.G_STORAGE, rule=storage_interval_end_upper_bound_rule)

        def power_balance_rule(_m, z, t):
            """Power balance for each NEM zone"""

            # Existing units within zone
            generators = [
                gen
                for gen, zone in self.data.generators.loc[:,
                                                          'NEM_ZONE'].items()
                if zone == z
            ]

            # Storage units within a given zone
            storage_units = [
                gen
                for gen, zone in self.data.storage.loc[:, 'NEM_ZONE'].items()
                if zone == z
            ]

            return (sum(m.p_total[g, t] for g in generators) - m.DEMAND[z, t] -
                    sum(m.INCIDENCE_MATRIX[l, z] * m.p_flow[l, t]
                        for l in m.L) + sum(m.p_out[g, t] - m.p_in[g, t]
                                            for g in storage_units) +
                    m.p_V[z, t] == 0)

        # Power balance constraint for each zone and time period
        m.POWER_BALANCE = Constraint(m.Z, m.T, rule=power_balance_rule)

        def powerflow_lower_bound_rule(_m, l, t):
            """Minimum powerflow over a link connecting adjacent NEM zones"""

            return m.p_flow[l, t] >= m.POWERFLOW_MIN[l]

        # Constrain max power flow over given network link
        m.POWERFLOW_MIN_CONS = Constraint(m.L_I,
                                          m.T,
                                          rule=powerflow_lower_bound_rule)

        def powerflow_max_constraint_rule(_m, l, t):
            """Maximum powerflow over a link connecting adjacent NEM zones"""

            return m.p_flow[l, t] <= m.POWERFLOW_MAX[l]

        # Constrain max power flow over given network link
        m.POWERFLOW_MAX_CONS = Constraint(m.L_I,
                                          m.T,
                                          rule=powerflow_max_constraint_rule)

        return m

    @staticmethod
    def define_objective(m):
        """Define unit commitment problem objective function"""

        # Objective function
        m.OBJECTIVE = Objective(expr=m.OBJECTIVE_FUNCTION, sense=minimize)

        return m

    def construct_model(self, overlap):
        """Construct unit commitment model"""

        # Initialise model object
        m = ConcreteModel()

        # Add component allowing dual variables to be imported
        m.dual = Suffix(direction=Suffix.IMPORT)

        # Define sets
        m = self.define_sets(m, overlap)

        # Define parameters specific to unit commitment sub-problem
        m = self.define_parameters(m)

        # Define variables
        m = self.define_variables(m)

        # Define expressions
        m = self.define_expressions(m)

        # Define constraints
        m = self.define_constraints(m)

        # Define objective
        m = self.define_objective(m)

        return m

    def update_demand(self, m, year, week, day):
        """Update demand for a given day"""

        # New demand values
        new_demand = {
            k: v
            for k, v in self.data.demand[(year, week, day)].items()
            if k[1] in m.T
        }

        # Update demand value in model object
        m.DEMAND.store_values(new_demand)

        return m

    def update_wind(self, m, year, week, day):
        """Update max power output for wind generators"""

        # Initialise container for updated wind data values
        new_values = {}

        # New output values
        for k, v in self.data.dispatch[(year, week, day)].items():

            # Check that element refers to generator in model, and time index within interval
            if (k[0] in m.G_WIND) and (k[1] in m.T):

                # Ensure wind data values are greater than zero and not too small (prevent numerical instability)
                if v < 0.1:
                    v = float(0)

                # Update container
                new_values[k] = float(v)

        # Update wind value in model object
        m.P_WIND.store_values(new_values)

        return m

    def update_solar(self, m, year, week, day):
        """Update max power output for solar generators"""

        # Initialise container for updated solar data values
        new_values = {}

        # New output values
        for k, v in self.data.dispatch[(year, week, day)].items():

            # Check that element refers to generator in model, and time index within interval
            if (k[0] in m.G_SOLAR) and (k[1] in m.T):

                # Ensure wind data values are greater than zero and not too small (prevent numerical instability)
                if v < 0.1:
                    v = float(0)

                # Update container
                new_values[k] = float(v)

        # Update solar values in model object
        m.P_SOLAR.store_values(new_values)

        return m

    def update_hydro(self, m, year, week, day):
        """Update max power output for hydro generators"""

        # Initialise container for updated hydro data values
        new_values = {}

        # New output values
        for k, v in self.data.dispatch[(year, week, day)].items():

            # Check that element refers to generator in model, and time index within interval
            if (k[0] in m.G_HYDRO) and (k[1] in m.T):

                # Ensure wind data values are greater than zero and not too small (prevent numerical instability)
                if v < 0.1:
                    v = float(0)

                # Update container
                new_values[k] = float(v)

        # Update hydro values in model object
        m.P_HYDRO.store_values(new_values)

        return m

    def update_parameters(self, m, year, month, day):
        """Update model parameters for a given day"""

        # Update demand
        m = self.update_demand(m, year, month, day)

        # Update wind power output
        m = self.update_wind(m, year, month, day)

        # Update solar output
        m = self.update_solar(m, year, month, day)

        # Update hydro output
        m = self.update_hydro(m, year, month, day)

        return m

    def solve_model(self, m):
        """Solve model"""

        # Solve model
        solve_status = self.opt.solve(m,
                                      tee=True,
                                      options=self.solver_options,
                                      keepfiles=self.keepfiles)

        # Log infeasible constraints if they exist
        log_infeasible_constraints(m)

        return m, solve_status

    @staticmethod
    def fix_binary_variables(m):
        """Fix all binary variables"""

        for k in m.u.keys():
            m.u[k].fix(round(m.u[k].value))
        for k in m.v.keys():
            m.v[k].fix(round(m.v[k].value))
        for k in m.w.keys():
            m.w[k].fix(round(m.w[k].value))

        return m

    @staticmethod
    def unfix_binary_variables(m):
        """Fix all binary variables"""

        m.u.unfix()
        m.v.unfix()
        m.w.unfix()

        return m

    @staticmethod
    def save_solution(m, year, week, day, output_dir, update=False):
        """Save solution"""

        # Primal objects to extract
        primal = [
            'u', 'v', 'w', 'p_in', 'p_out', 'q', 'p', 'p_total', 'p_flow',
            'p_V', 'r_up'
        ]

        # Dual objects to extract
        dual = ['POWER_BALANCE']

        # Expressions
        expressions = {
            'DAY_EMISSIONS': m.DAY_EMISSIONS.expr(),
            'DAY_DEMAND': m.DAY_EMISSIONS.expr(),
            'DAY_EMISSIONS_INTENSITY': m.DAY_EMISSIONS_INTENSITY.expr(),
            'DAY_SCHEME_REVENUE': m.DAY_SCHEME_REVENUE.expr(),
            'e': {k: m.e[k].expr()
                  for k in m.e.keys()}
        }

        # Primal results
        primal_results = {
            v: m.__getattribute__(v).get_values()
            for v in primal
        }

        # Dual results
        dual_results = {
            d: {k: m.dual[v]
                for k, v in m.__getattribute__(d).items()}
            for d in dual
        }

        if update:
            # Load previously saved results
            with open(
                    os.path.join(output_dir,
                                 f'interval_{year}_{week}_{day}.pickle'),
                    'rb') as f:
                previous_results = pickle.load(f)

            # Use previous power balance dual values for first 24 hours
            dual_results = {
                d: {
                    k: previous_results[d][k]
                    if k[1] <= 24 else dual_results[d][k]
                    for k in dual_results[d].keys()
                }
                for d in dual
            }

        # Combine primal a single dictionary
        results = {**primal_results, **dual_results, **expressions}

        # Save results
        with open(
                os.path.join(output_dir,
                             f'interval_{year}_{week}_{day}.pickle'),
                'wb') as f:
            pickle.dump(results, f)

        return results

    @staticmethod
    def fix_interval_overlap(m, year, week, day, overlap, output_dir):
        """Fix variables at the start of a given dispatch interval based on the solution of the preceding interval"""

        if day == 1:
            week = week - 1

            # If beginning of following year
            if week == 0:
                year = year - 1
                week = 52

            day = 7
        else:
            day = day - 1

        # Load solution for previous day
        previous_solution = pd.read_pickle(
            os.path.join(output_dir, f'interval_{year}_{week}_{day}.pickle'))

        # Map time index between beginning of current day and end of preceding interval
        interval_map = {i: 24 + i for i in range(0, overlap + 1)}

        # Fix variables to values obtained in preceding interval
        for t in range(1, overlap + 1):
            for g in m.G.difference(m.G_STORAGE, m.G_THERM):
                m.p_total[g, t].fix(
                    previous_solution['p_total'][(g, interval_map[t])])

            for g in m.G_THERM:
                m.u[g,
                    t].fix(round(previous_solution['u'][(g, interval_map[t])]))
                m.v[g,
                    t].fix(round(previous_solution['v'][(g, interval_map[t])]))
                m.w[g,
                    t].fix(round(previous_solution['w'][(g, interval_map[t])]))
                m.p[g, t].fix(previous_solution['p'][(g, interval_map[t])])
                m.r_up[g,
                       t].fix(previous_solution['r_up'][(g, interval_map[t])])

                m.U0[g] = int(
                    round(previous_solution['u'][(g, interval_map[0])]))
                m.P0[g] = previous_solution['p'][(g, interval_map[0])]

            for g in m.G_STORAGE:
                m.q[g, t].fix(previous_solution['q'][(g, interval_map[t])])
                m.p_in[g,
                       t].fix(previous_solution['p_in'][(g, interval_map[t])])
                m.p_out[g,
                        t].fix(previous_solution['p_out'][(g,
                                                           interval_map[t])])
                m.r_up[g,
                       t].fix(previous_solution['r_up'][(g, interval_map[t])])

                m.Q0[g] = previous_solution['q'][(g, interval_map[0])]

            for l in m.L:
                m.p_flow[l,
                         t].fix(previous_solution['p_flow'][(l,
                                                             interval_map[t])])

            for z in m.Z:
                m.p_V[z, t].fix(previous_solution['p_V'][(z, interval_map[t])])

        return m

    @staticmethod
    def fix_interval(m, start, end):
        """Fix all variables for a defined interval"""

        # Fix variables to values obtained in preceding interval
        for t in range(start, end + 1):
            for g in m.G.difference(m.G_STORAGE, m.G_THERM):
                m.p_total[g, t].fix()

            for g in m.G_THERM:
                m.u[g, t].fix(round(m.u[g, t].value))
                m.v[g, t].fix(round(m.v[g, t].value))
                m.w[g, t].fix(round(m.w[g, t].value))
                m.p[g, t].fix()
                m.r_up[g, t].fix()

            for g in m.G_STORAGE:
                m.q[g, t].fix()
                m.p_in[g, t].fix()
                m.p_out[g, t].fix()
                m.r_up[g, t].fix()

            for l in m.L:
                m.p_flow[l, t].fix()

            for z in m.Z:
                m.p_V[z, t].fix()

        return m

    @staticmethod
    def unfix_interval(m, start, end):
        """Fix all variables for a defined interval"""

        # Fix variables to values obtained in preceding interval
        for t in range(start, end + 1):
            for g in m.G.difference(m.G_STORAGE, m.G_THERM):
                m.p_total[g, t].unfix()

            for g in m.G_THERM:
                m.u[g, t].unfix()
                m.v[g, t].unfix()
                m.w[g, t].unfix()
                m.p[g, t].unfix()
                m.r_up[g, t].unfix()

            for g in m.G_STORAGE:
                m.q[g, t].unfix()
                m.p_in[g, t].unfix()
                m.p_out[g, t].unfix()
                m.r_up[g, t].unfix()

            for l in m.L:
                m.p_flow[l, t].unfix()

            for z in m.Z:
                m.p_V[z, t].unfix()

        return m
Exemple #14
0
class CommonComponents:
    # Model data
    data = ModelData()

    def __init__(self):
        pass

    def define_sets(self, m):
        """Define sets to be used in model"""

        # NEM regions
        m.R = Set(initialize=self.data.nem_regions)

        # NEM zones
        m.Z = Set(initialize=self.data.nem_zones)

        # Links between NEM zones
        m.L = Set(initialize=self.data.network_links)

        # Interconnectors for which flow limits are defined
        m.L_I = Set(initialize=list(self.data.powerflow_limits.keys()))

        # NEM wind bubbles
        m.B = Set(initialize=self.data.wind_bubbles)

        # Existing thermal units
        m.G_E_THERM = Set(initialize=self.data.existing_thermal_unit_ids)

        # Candidate thermal units
        m.G_C_THERM = Set(initialize=self.data.candidate_thermal_unit_ids)

        # Index for candidate thermal unit size options
        m.G_C_THERM_SIZE_OPTIONS = RangeSet(0, 3, ordered=True)

        # Existing wind units
        m.G_E_WIND = Set(initialize=self.data.existing_wind_unit_ids)

        # Candidate wind units
        m.G_C_WIND = Set(initialize=self.data.candidate_wind_unit_ids)

        # Existing solar units
        m.G_E_SOLAR = Set(initialize=self.data.existing_solar_unit_ids)

        # Candidate solar units
        m.G_C_SOLAR = Set(initialize=self.data.candidate_solar_unit_ids)

        # Available technologies
        m.G_C_SOLAR_TECHNOLOGIES = Set(
            initialize=list(set(i.split('-')[-1] for i in m.G_C_SOLAR)))

        # Existing hydro units
        m.G_E_HYDRO = Set(initialize=self.data.existing_hydro_unit_ids)

        # Candidate storage units
        m.G_C_STORAGE = Set(initialize=self.data.candidate_storage_units)

        # Slow start thermal generators (existing and candidate)
        m.G_THERM_SLOW = Set(
            initialize=self.data.slow_start_thermal_generator_ids)

        # Quick start thermal generators (existing and candidate)
        m.G_THERM_QUICK = Set(
            initialize=self.data.quick_start_thermal_generator_ids)

        # All existing generators
        m.G_E = m.G_E_THERM.union(m.G_E_WIND).union(m.G_E_SOLAR).union(
            m.G_E_HYDRO)

        # All candidate generators
        m.G_C = m.G_C_THERM.union(m.G_C_WIND).union(m.G_C_SOLAR)

        # All generators
        m.G = m.G_E.union(m.G_C)

        # All years in model horizon
        m.I = RangeSet(2016, 2017)

        # Operating scenarios for each year
        m.O = RangeSet(0, 9)

        # Operating scenario hour
        m.T = RangeSet(0, 23, ordered=True)

        # Build limit technology types
        m.BUILD_LIMIT_TECHNOLOGIES = Set(
            initialize=self.data.candidate_unit_build_limits.index)

        return m

    def define_parameters(self, m):
        """Define model parameters - these are common to all blocks"""

        # Cumulative revenue target (for entire model horizon)
        m.REVENUE_TARGET = Param(initialize=0, mutable=True)

        # Penalty imposed for each dollar scheme revenue falls short of target revenue
        m.REVENUE_SHORTFALL_PENALTY = Param(initialize=1000)

        # Emissions target
        m.EMISSIONS_TARGET = Param(initialize=9999999, mutable=True)

        # Penalty imposed for each tCO2 by which emissions target is exceeded
        m.EMISSIONS_EXCEEDED_PENALTY = Param(initialize=1000)

        # Min state of charge for storage unit at end of operating scenario (assume = 0)
        m.STORAGE_INTERVAL_END_MIN_ENERGY = Param(m.G_C_STORAGE, initialize=0)

        # Value of lost-load [$/MWh]
        m.C_LOST_LOAD = Param(initialize=float(1e4))

        # Fixed emissions intensity baseline [tCO2/MWh]
        m.FIXED_BASELINE = Param(m.I, initialize=0, mutable=True)

        # Fixed permit price [$/tCO2]
        m.FIXED_PERMIT_PRICE = Param(m.I, initialize=0, mutable=True)

        # Fixed capacities for candidate units
        m.FIXED_X_C = Param(m.G_C_WIND.union(m.G_C_SOLAR).union(m.G_C_STORAGE),
                            m.I,
                            initialize=0,
                            mutable=True)

        # Fixed integer variables for candidate thermal unit discrete sizing options
        m.FIXED_D = Param(m.G_C_THERM,
                          m.I,
                          m.G_C_THERM_SIZE_OPTIONS,
                          initialize=0,
                          mutable=True)

        def startup_cost_rule(m, g):
            """
            Startup costs for existing and candidate thermal units

            Note: costs are normalised by plant capacity e.g. $/MW
            """

            if g in m.G_E_THERM:
                # Startup cost for existing thermal units
                startup_cost = (
                    self.data.existing_units.loc[g, ('PARAMETERS',
                                                     'SU_COST_WARM')] /
                    self.data.existing_units.loc[g, ('PARAMETERS', 'REG_CAP')])

            elif g in m.G_C_THERM:
                # Startup cost for candidate thermal units
                startup_cost = self.data.candidate_units.loc[g, (
                    'PARAMETERS', 'SU_COST_WARM_MW')]

            else:
                # Assume startup cost = 0 for all solar, wind, hydro generators
                startup_cost = 0

            # Shutdown cost cannot be negative
            assert startup_cost >= 0, 'Negative startup cost'

            return float(startup_cost)

        # Generator startup costs - per MW
        m.C_SU_MW = Param(m.G, rule=startup_cost_rule)

        def shutdown_cost_rule(m, g):
            """
            Shutdown costs for existing and candidate thermal units

            Note: costs are normalised by plant capacity e.g. $/MW

            No data for shutdown costs, so assume it is half the value of
            startup cost for given generator.
            """

            if g in m.G_E_THERM:
                # Shutdown cost for existing thermal units
                shutdown_cost = (
                    self.data.existing_units.loc[g, ('PARAMETERS',
                                                     'SU_COST_WARM')] /
                    self.data.existing_units.loc[g, ('PARAMETERS', 'REG_CAP')])

            elif g in m.G_C_THERM:
                # Shutdown cost for candidate thermal units
                shutdown_cost = self.data.candidate_units.loc[g, (
                    'PARAMETERS', 'SU_COST_WARM_MW')]

            else:
                # Assume shutdown cost = 0 for all solar, wind, hydro generators
                shutdown_cost = 0

            # Shutdown cost cannot be negative
            assert shutdown_cost >= 0, 'Negative shutdown cost'

            return float(shutdown_cost)

        # Generator shutdown costs - per MW
        m.C_SD_MW = Param(m.G, rule=shutdown_cost_rule)

        def startup_ramp_rate_rule(m, g):
            """Startup ramp-rate (MW)"""

            if g in m.G_E_THERM:
                # Startup ramp-rate for existing thermal generators
                startup_ramp = self.data.existing_units.loc[g, ('PARAMETERS',
                                                                'RR_STARTUP')]

            elif g in m.G_C_THERM:
                # Startup ramp-rate for candidate thermal generators
                startup_ramp = self.data.candidate_units.loc[g, ('PARAMETERS',
                                                                 'RR_STARTUP')]

            else:
                raise Exception(f'Unexpected generator {g}')

            return float(startup_ramp)

        # Startup ramp-rate for existing and candidate thermal generators
        m.RR_SU = Param(m.G_E_THERM.union(m.G_C_THERM),
                        rule=startup_ramp_rate_rule)

        def shutdown_ramp_rate_rule(m, g):
            """Shutdown ramp-rate (MW)"""

            if g in m.G_E_THERM:
                # Shutdown ramp-rate for existing thermal generators
                shutdown_ramp = self.data.existing_units.loc[g,
                                                             ('PARAMETERS',
                                                              'RR_SHUTDOWN')]

            elif g in m.G_C_THERM:
                # Shutdown ramp-rate for candidate thermal generators
                shutdown_ramp = self.data.candidate_units.loc[g,
                                                              ('PARAMETERS',
                                                               'RR_SHUTDOWN')]

            else:
                raise Exception(f'Unexpected generator {g}')

            return float(shutdown_ramp)

        # Shutdown ramp-rate for existing and candidate thermal generators
        m.RR_SD = Param(m.G_E_THERM.union(m.G_C_THERM),
                        rule=shutdown_ramp_rate_rule)

        def ramp_rate_up_rule(m, g):
            """Ramp-rate up (MW/h) - when running"""

            if g in m.G_E_THERM:
                # Ramp-rate up for existing generators
                ramp_up = self.data.existing_units.loc[g,
                                                       ('PARAMETERS', 'RR_UP')]

            elif g in m.G_C_THERM:
                # Ramp-rate up for candidate generators
                ramp_up = self.data.candidate_units.loc[g, ('PARAMETERS',
                                                            'RR_UP')]

            else:
                raise Exception(f'Unexpected generator {g}')

            return float(ramp_up)

        # Ramp-rate up (normal operation)
        m.RR_UP = Param(m.G_E_THERM.union(m.G_C_THERM), rule=ramp_rate_up_rule)

        def ramp_rate_down_rule(m, g):
            """Ramp-rate down (MW/h) - when running"""

            if g in m.G_E_THERM:
                # Ramp-rate down for existing generators
                ramp_down = self.data.existing_units.loc[g, ('PARAMETERS',
                                                             'RR_DOWN')]

            elif g in m.G_C_THERM:
                # Ramp-rate down for candidate generators
                ramp_down = self.data.candidate_units.loc[g, ('PARAMETERS',
                                                              'RR_DOWN')]

            else:
                raise Exception(f'Unexpected generator {g}')

            return float(ramp_down)

        # Ramp-rate down (normal operation)
        m.RR_DOWN = Param(m.G_E_THERM.union(m.G_C_THERM),
                          rule=ramp_rate_down_rule)

        def existing_generator_registered_capacities_rule(m, g):
            """Registered capacities of existing generators"""

            return float(self.data.existing_units.loc[g, ('PARAMETERS',
                                                          'REG_CAP')])

        # Registered capacities of existing generators
        m.EXISTING_GEN_REG_CAP = Param(
            m.G_E, rule=existing_generator_registered_capacities_rule)

        def emissions_intensity_rule(m, g):
            """Emissions intensity (tCO2/MWh)"""

            if g in m.G_E_THERM:
                # Emissions intensity
                emissions = float(self.data.existing_units.loc[g,
                                                               ('PARAMETERS',
                                                                'EMISSIONS')])

            elif g in m.G_C_THERM:
                # Emissions intensity
                emissions = float(self.data.candidate_units.loc[g,
                                                                ('PARAMETERS',
                                                                 'EMISSIONS')])

            else:
                # Set emissions intensity = 0 for all solar, wind, hydro, and storage units
                emissions = float(0)

            return emissions

        # Emissions intensities for all generators
        m.EMISSIONS_RATE = Param(m.G.union(m.G_C_STORAGE),
                                 rule=emissions_intensity_rule)

        def min_power_output_proportion_rule(m, g):
            """Minimum generator power output as a proportion of maximum output"""

            if g in m.G_E_THERM:
                # Minimum power output for existing generators
                min_output = (
                    self.data.existing_units.loc[g,
                                                 ('PARAMETERS', 'MIN_GEN')] /
                    self.data.existing_units.loc[g, ('PARAMETERS', 'REG_CAP')])

            elif g in m.G_E_HYDRO:
                # Set minimum power output for existing hydro generators = 0
                min_output = 0

            elif g in m.G_C_THERM:
                # Minimum power output for candidate thermal generators
                min_output = self.data.candidate_units.loc[g, (
                    'PARAMETERS', 'MIN_GEN_PERCENT')] / 100

            else:
                # Minimum power output = 0
                min_output = 0

            return float(min_output)

        # Minimum power output (as a proportion of max capacity) for existing and candidate thermal generators
        m.P_MIN_PROP = Param(m.G, rule=min_power_output_proportion_rule)

        def thermal_unit_discrete_size_rule(m, g, n):
            """Possible discrete sizes for candidate thermal units"""

            # Discrete sizes available for candidate thermal unit investment
            options = {0: 0, 1: 100, 2: 200, 3: 400}

            return float(options[n])

        # Candidate thermal unit size options
        m.X_C_THERM_SIZE = Param(m.G_C_THERM,
                                 m.G_C_THERM_SIZE_OPTIONS,
                                 rule=thermal_unit_discrete_size_rule)

        def initial_on_state_rule(m, g):
            """Defines which units should be on in period preceding model start"""

            if g in m.G_THERM_SLOW:
                return float(1)
            else:
                return float(0)

        # Initial on-state rule - assumes slow-start (effectively baseload) units are on in period prior to model start
        m.U0 = Param(m.G_E_THERM.union(m.G_C_THERM),
                     within=Binary,
                     mutable=True,
                     rule=initial_on_state_rule)

        def battery_efficiency_rule(m, g):
            """Battery efficiency"""

            return float(self.data.battery_properties.loc[g,
                                                          'CHARGE_EFFICIENCY'])

        # Battery efficiency
        m.BATTERY_EFFICIENCY = Param(m.G_C_STORAGE,
                                     rule=battery_efficiency_rule)

        def network_incidence_matrix_rule(m, l, z):
            """Incidence matrix describing connections between adjacent NEM zones"""

            # Network incidence matrix
            df = self.data.network_incidence_matrix.copy()

            return float(df.loc[l, z])

        # Network incidence matrix
        m.INCIDENCE_MATRIX = Param(m.L,
                                   m.Z,
                                   rule=network_incidence_matrix_rule)

        def candidate_unit_build_limits_rule(m, g, z):
            """Build limits in each zone for each candidate technology"""
            return float(self.data.candidate_unit_build_limits.loc[g, z])

        # Build limits for each candidate technology
        m.BUILD_LIMITS = Param(m.BUILD_LIMIT_TECHNOLOGIES,
                               m.Z,
                               rule=candidate_unit_build_limits_rule)

        def minimum_region_up_reserve_rule(m, r):
            """Minimum upward reserve rule"""

            # Minimum upward reserve for region
            up_reserve = self.data.minimum_reserve_levels.loc[
                r, 'MINIMUM_RESERVE_LEVEL']

            return float(up_reserve)

        # Minimum upward reserve
        m.RESERVE_UP = Param(m.R, rule=minimum_region_up_reserve_rule)

        def powerflow_min_rule(m, l):
            """Minimum powerflow over network link"""

            return float(-self.data.powerflow_limits[l]['reverse'] * 100)

        # Lower bound for powerflow over link
        m.POWERFLOW_MIN = Param(m.L_I, rule=powerflow_min_rule)

        def powerflow_max_rule(m, l):
            """Maximum powerflow over network link"""

            return float(self.data.powerflow_limits[l]['forward'] * 100)

        # Lower bound for powerflow over link
        m.POWERFLOW_MAX = Param(m.L_I, rule=powerflow_max_rule)

        def marginal_cost_rule(_s, g, i):
            """Marginal costs for existing and candidate generators

            Note: Marginal costs for existing and candidate thermal plant depend on fuel costs, which are time
            varying. Therefore marginal costs for thermal plant must be define for each year in model horizon.
            """

            if g in m.G_E_THERM:

                # Last year in the dataset for which fuel cost information exists
                max_year = self.data.existing_units.loc[
                    g, 'FUEL_COST'].index.max()

                # If year in model horizon exceeds max year for which data are available use values for last
                # available year
                if i > max_year:
                    # Use final year in dataset to max year
                    i = max_year

                marginal_cost = float(
                    self.data.existing_units.loc[g, ('FUEL_COST', i)] *
                    self.data.existing_units.loc[g,
                                                 ('PARAMETERS', 'HEAT_RATE')])

            elif g in m.G_C_THERM:
                # Last year in the dataset for which fuel cost information exists
                max_year = self.data.candidate_units.loc[
                    g, 'FUEL_COST'].index.max()

                # If year in model horizon exceeds max year for which data are available use values for last
                # available year
                if i > max_year:
                    # Use final year in dataset to max year
                    i = max_year

                marginal_cost = float(
                    self.data.candidate_units.loc[g, ('FUEL_COST', i)])

            elif (g in m.G_E_WIND) or (g in m.G_E_SOLAR) or (g in m.G_E_HYDRO):
                # Marginal cost = VOM cost for wind and solar generators
                marginal_cost = self.data.existing_units.loc[g, ('PARAMETERS',
                                                                 'VOM')]

            elif (g in m.G_C_WIND) or (g in m.G_C_SOLAR):
                # Marginal cost = VOM cost for wind and solar generators
                marginal_cost = self.data.candidate_units.loc[g, ('PARAMETERS',
                                                                  'VOM')]

            elif g in m.G_C_STORAGE:
                # Assume marginal cost = VOM cost of typical hydro generator (7 $/MWh)
                marginal_cost = 7

            else:
                raise Exception(f'Unexpected generator: {g}')

            assert marginal_cost >= 0, 'Cannot have negative marginal cost'

            return float(marginal_cost)

        # Marginal costs for all generators and time periods
        m.C_MC = Param(m.G.union(m.G_C_STORAGE), m.I, rule=marginal_cost_rule)

        def fom_cost_rule(m, g):
            """Fixed operating and maintenance cost for all candidate generators"""

            if g in m.G_C_STORAGE:
                # TODO: Need to find reasonable FOM cost for storage units - setting = to MEL-WIND for now
                return float(
                    self.data.candidate_units_dict[('PARAMETERS',
                                                    'FOM')]['MEL-WIND'])
            else:
                return float(self.data.candidate_units_dict[('PARAMETERS',
                                                             'FOM')][g])

        # Fixed operating and maintenance cost for candidate generators (assuming no unit retirement for now, so can
        # excluded consideration of FOM costs for existing units)
        m.C_FOM = Param(m.G_C_THERM.union(m.G_C_WIND).union(m.G_C_SOLAR).union(
            m.G_C_STORAGE),
                        rule=fom_cost_rule)

        def investment_cost_rule(m, g, i):
            """Cost to build 1MW of the candidate technology"""

            if g in m.G_C_STORAGE:
                # Build costs for batteries
                return float(self.data.battery_build_costs_dict[i][g] * 1000)
            else:
                # Build costs for other candidate units
                return float(
                    self.data.candidate_units_dict[('BUILD_COST', i)][g] *
                    1000)

        # Investment / build cost per MW for candidate technologies
        m.C_INV = Param(m.G_C.union(m.G_C_STORAGE),
                        m.I,
                        rule=investment_cost_rule)

        return m

    @staticmethod
    def define_variables(m):
        """Define model variables common to all blocks"""

        # Capacity of candidate units (defined for all years in model horizon)
        m.x_c = Var(m.G_C_WIND.union(m.G_C_SOLAR).union(m.G_C_STORAGE),
                    m.I,
                    within=NonNegativeReals,
                    initialize=0)

        # Binary variable used to determine size of candidate thermal units
        m.d = Var(m.G_C_THERM,
                  m.I,
                  m.G_C_THERM_SIZE_OPTIONS,
                  within=NonNegativeReals,
                  initialize=0)

        # Emissions intensity baseline [tCO2/MWh] (must be fixed each time model is run)
        m.baseline = Var(m.I, initialize=0)

        # Permit price [$/tCO2] (must be fixed each time model is run)
        m.permit_price = Var(m.I, initialize=0)

        # Amount by which emissions target is exceeded
        m.emissions_target_exceeded = Var(within=NonNegativeReals,
                                          initialize=0)

        # Amount by which scheme revenue falls short of target
        m.revenue_shortfall = Var(within=NonNegativeReals, initialize=0)

        return m

    def define_expressions(self, m):
        """Define expressions common to both master and subproblem"""
        def capacity_sizing_rule(_m, g, i):
            """Size of candidate units"""

            # Continuous size option for wind, solar, and storage units
            if g in m.G_C_WIND.union(m.G_C_SOLAR).union(m.G_C_STORAGE):
                return m.x_c[g, i]

            # Discrete size options for candidate thermal units
            elif g in m.G_C_THERM:
                return sum(m.d[g, i, n] * m.X_C_THERM_SIZE[g, n]
                           for n in m.G_C_THERM_SIZE_OPTIONS)

            else:
                raise Exception(f'Unidentified generator: {g}')

        # Capacity sizing for candidate units
        m.X_C = Expression(m.G_C.union(m.G_C_STORAGE),
                           m.I,
                           rule=capacity_sizing_rule)

        def fom_cost_rule(_m):
            """Fixed operating and maintenance cost for candidate generators over model horizon"""

            return sum(m.C_FOM[g] * m.X_C[g, i]
                       for g in m.G_C.union(m.G_C_SOLAR) for i in m.I)

        # Fixed operation and maintenance cost - absolute cost [$]
        m.C_FOM_TOTAL = Expression(rule=fom_cost_rule)

        def investment_cost_rule(_m):
            """Cost to invest in candidate technologies"""

            return sum(m.C_INV[g, i] * m.X_C[g, i]
                       for g in m.G_C.union(m.G_C_STORAGE) for i in m.I)

        # Investment cost (fixed in subproblem) - absolute cost [$]
        m.C_INV_TOTAL = Expression(rule=investment_cost_rule)

        # Penalty imposed for violating emissions constraint
        m.C_EMISSIONS_VIOLATION = Expression(expr=m.emissions_target_exceeded *
                                             m.EMISSIONS_EXCEEDED_PENALTY)

        # Penalty imposed for violating revenue constraint
        m.C_REVENUE_VIOLATION = Expression(expr=m.revenue_shortfall *
                                           m.REVENUE_SHORTFALL_PENALTY)

        def max_generator_power_output_rule(_m, g, i):
            """
            Maximum power output from existing and candidate generators

            Note: candidate units will have their max power output determined by investment decisions which
            are made known in the master problem. Need to update these values each time model is run.
            """

            # Max output for existing generators equal to registered capacities
            if g in m.G_E:
                return m.EXISTING_GEN_REG_CAP[g]

            # Max output for candidate generators equal to installed capacities (variable in master problem)
            elif g in m.G_C.union(m.G_C_STORAGE):
                return sum(m.X_C[g, y] for y in m.I if y <= i)

            else:
                raise Exception(f'Unexpected generator: {g}')

        # Maximum power output for existing and candidate units (must be updated each time model is run)
        m.P_MAX = Expression(m.G, m.I, rule=max_generator_power_output_rule)

        def thermal_startup_cost_rule(_m, g, i):
            """Startup cost for existing and candidate thermal generators"""

            return m.C_SU_MW[g] * m.P_MAX[g, i]

        # Startup cost - absolute cost [$]
        m.C_SU = Expression(m.G_E_THERM.union(m.G_C_THERM),
                            m.I,
                            rule=thermal_startup_cost_rule)

        def thermal_shutdown_cost_rule(_m, g, i):
            """Startup cost for existing and candidate thermal generators"""
            # TODO: For now set shutdown cost = 0
            return m.C_SD_MW[g] * 0

        # Shutdown cost - absolute cost [$]
        m.C_SD = Expression(m.G_E_THERM.union(m.G_C_THERM),
                            m.I,
                            rule=thermal_shutdown_cost_rule)

        return m
Exemple #15
0
 def __init__(self, output_dir):
     self.output_dir = output_dir
     self.data = ModelData()
Exemple #16
0
 def __init__(self):
     # Model data
     self.data = ModelData()
Exemple #17
0
class CommonComponents:
    # Model data
    data = ModelData()

    def __init__(self):
        pass

    def define_sets(self, m):
        """Define sets to be used in model"""

        # NEM regions
        m.R = Set(initialize=self.data.nem_regions)

        # NEM zones
        m.Z = Set(initialize=self.data.nem_zones)

        # Links between NEM zones
        m.L = Set(initialize=self.data.network_links)

        # Interconnectors for which flow limits are defined
        m.L_I = Set(initialize=list(self.data.powerflow_limits.keys()))

        # NEM wind bubbles
        m.B = Set(initialize=self.data.wind_bubbles)

        # Existing thermal units
        m.G_E_THERM = Set(initialize=self.data.existing_thermal_unit_ids)

        # Candidate thermal units
        m.G_C_THERM = Set(initialize=self.data.candidate_thermal_unit_ids)

        # All existing and candidate thermal generators
        m.G_THERM = Set(initialize=m.G_E_THERM.union(m.G_C_THERM))

        # Index for candidate thermal unit size options
        m.G_C_THERM_SIZE_OPTIONS = RangeSet(0, 3, ordered=True)

        # Existing wind units
        m.G_E_WIND = Set(initialize=self.data.existing_wind_unit_ids)

        # Candidate wind units
        m.G_C_WIND = Set(initialize=self.data.candidate_wind_unit_ids)

        # Existing solar units
        m.G_E_SOLAR = Set(initialize=self.data.existing_solar_unit_ids)

        # Candidate solar units
        m.G_C_SOLAR = Set(initialize=self.data.candidate_solar_unit_ids)

        # Available technologies
        m.G_C_SOLAR_TECHNOLOGIES = Set(
            initialize=list(set(y.split('-')[-1] for y in m.G_C_SOLAR)))

        # Existing hydro units
        m.G_E_HYDRO = Set(initialize=self.data.existing_hydro_unit_ids)

        # Candidate storage units
        m.G_C_STORAGE = Set(initialize=self.data.candidate_storage_units)

        # Slow start thermal generators (existing and candidate)
        m.G_THERM_SLOW = Set(
            initialize=self.data.slow_start_thermal_generator_ids)

        # Quick start thermal generators (existing and candidate)
        m.G_THERM_QUICK = Set(
            initialize=self.data.quick_start_thermal_generator_ids)

        # All existing generators
        m.G_E = m.G_E_THERM.union(m.G_E_WIND).union(m.G_E_SOLAR).union(
            m.G_E_HYDRO)

        # All candidate generators
        m.G_C = m.G_C_THERM.union(m.G_C_WIND).union(m.G_C_SOLAR)

        # All generators
        m.G = m.G_E.union(m.G_C)

        # All years in model horizon
        m.Y = RangeSet(2016, 2017)

        # Operating scenarios for each year
        m.O = RangeSet(0, 9)

        # Operating scenario hour
        m.T = RangeSet(0, 23, ordered=True)

        # Build limit technology types
        m.BUILD_LIMIT_TECHNOLOGIES = Set(
            initialize=self.data.candidate_unit_build_limits.index)

        return m

    @staticmethod
    def define_parameters(m):
        """Define model parameters - these are common to all blocks"""
        def thermal_unit_discrete_size_rule(m, g, n):
            """Possible discrete sizes for candidate thermal units"""

            # Discrete sizes available for candidate thermal unit investment
            options = {0: 0, 1: 100, 2: 200, 3: 400}

            return float(options[n])

        # Candidate thermal unit size options
        m.X_CT = Param(m.G_C_THERM,
                       m.G_C_THERM_SIZE_OPTIONS,
                       rule=thermal_unit_discrete_size_rule)

        return m

    @staticmethod
    def define_variables(m):
        """Define model variables common to all sub-problems"""

        # Capacity of candidate units (defined for all years in model horizon)
        m.x_c = Var(m.G_C.union(m.G_C_STORAGE),
                    m.Y,
                    within=NonNegativeReals,
                    initialize=0)

        # Binary variable used to determine size of candidate thermal units
        m.d = Var(m.G_C_THERM,
                  m.Y,
                  m.G_C_THERM_SIZE_OPTIONS,
                  within=NonNegativeReals,
                  initialize=0)

        # Startup indicator
        m.v = Var(m.G_THERM, m.T, within=Binary)

        # Shutdown indicator
        m.w = Var(m.G_THERM, m.T, within=Binary)

        return m

    @staticmethod
    def define_expressions(m):
        """Define expressions common to all sub-problems"""

        return m
Exemple #18
0
 def __init__(self):
     self.data = ModelData()
     self.analysis = AnalyseResults()
Exemple #19
0
                    #mask 用来记录已经训练完成的session,训练完成的session的hidden state设置为0
                    hidden_states = self.get_states()[0]
                    mask_eles = np.ones((self.batch_size, 1))
                    mask_eles[mask] = 0
                    hidden_states = np.multiply(mask_eles, hidden_states)
                    hidden_states = np.array(hidden_states, dtype=np.float32)
                    self.model.layers[1].reset_states(hidden_states)

                    input_oh = to_categorical(input,
                                              num_classes=self.item_size)
                    input_oh = np.expand_dims(input_oh, axis=1)
                    output_oh = to_categorical(target,
                                               num_classes=self.item_size)

                    loss = self.model.train_on_batch(input_oh, output_oh)

                    pbar.set_description('Epoch{0} loss is {1:.5f}'.format(
                        epoch, loss))
                    pbar.update(session_data_loader.done_sessions_count)


if __name__ == '__main__':
    model_data = ModelData('../preprocess/data/ratings.csv')
    train_session_data = SessionData(model_data.tran_data)
    gru_model = GRUModel(hidden_layer_size=100,
                         item_size=len(train_session_data.item_id2index_map),
                         batch_size=50)
    gru_model.train_model(train_session_data)
    test_session_data = SessionData(model_data.test_data)
    print(gru_model.evaluate_model(test_session_data))
Exemple #20
0
class MasterProblem:
    # Pre-processed model data
    data = ModelData()

    def __init__(self):
        # Solver options
        self.keepfiles = False
        self.solver_options = {'Method': 1}  # 'MIPGap': 0.0005
        self.opt = SolverFactory('gurobi', solver_io='lp')

    @staticmethod
    def define_expressions(m):
        """Define master problem expressions"""
        return m

    def define_blocks(self, m):
        """Define blocks for operating logic constraints"""

        def operating_scenario_block_rule(s, i, o):
            """Define blocks corresponding to each operating scenario"""

            def define_block_parameters(s):
                """Define parameters for each block"""

                # Energy output for a given generator (must be updated each time solution from subproblem available)
                s.FIXED_ENERGY = Param(m.G, m.T, initialize=0, mutable=True)

                # Fixed energy into storage units
                s.FIXED_ENERGY_IN = Param(m.G_C_STORAGE, m.T, initialize=0, mutable=True)

                # Fixed energy out of storage units
                s.FIXED_ENERGY_OUT = Param(m.G_C_STORAGE, m.T, initialize=0, mutable=True)

                # Fixed lost load (up)
                s.FIXED_p_lost_up = Param(m.Z, m.T, initialize=0, mutable=True)

                # Fixed lost load (down)
                s.FIXED_p_lost_down = Param(m.Z, m.T, initialize=0, mutable=True)

                # Amount by which upward reserve constraint is violated
                s.FIXED_upward_reserve_violation = Param(m.R, m.T, initialize=0, mutable=True)

                return s

            def define_block_variables(s):
                """Define variables for each block"""

                # Startup state variable
                s.v = Var(m.G_E_THERM.union(m.G_C_THERM), m.T, within=Binary, initialize=0)

                # Shutdown state variable
                s.w = Var(m.G_E_THERM.union(m.G_C_THERM), m.T, within=Binary, initialize=0)

                # On-state variable
                s.u = Var(m.G_E_THERM.union(m.G_C_THERM), m.T, within=Binary, initialize=1)

                return s

            def define_block_expressions(s):
                """Define expressions for each block"""

                def thermal_operating_costs_rule(_s):
                    """Cost to operate existing and candidate thermal units"""

                    return (
                        sum((m.C_MC[g, i] + (m.EMISSIONS_RATE[g] - m.baseline[i]) * m.permit_price[i]) * s.FIXED_ENERGY[
                            g, t]
                            + (m.C_SU[g, i] * s.v[g, t]) + (m.C_SD[g, i] * s.w[g, t])
                            for g in m.G_E_THERM.union(m.G_C_THERM) for t in m.T))

                # Existing and candidate thermal unit operating costs for given scenario
                s.C_OP_THERM = Expression(rule=thermal_operating_costs_rule)

                def hydro_operating_costs_rule(_s):
                    """Cost to operate existing hydro generators"""

                    return sum(m.C_MC[g, i] * s.FIXED_ENERGY[g, t] for g in m.G_E_HYDRO for t in m.T)

                # Existing hydro unit operating costs (no candidate hydro generators)
                s.C_OP_HYDRO = Expression(rule=hydro_operating_costs_rule)

                def solar_operating_costs_rule(_s):
                    """Cost to operate existing and candidate solar units"""

                    return (sum(m.C_MC[g, i] * s.FIXED_ENERGY[g, t] for g in m.G_E_SOLAR for t in m.T)
                            + sum(
                                (m.C_MC[g, i] - m.baseline[i] * m.permit_price[i]) * s.FIXED_ENERGY[g, t] for g in
                                m.G_C_SOLAR
                                for t in
                                m.T))

                # Existing and candidate solar unit operating costs (only candidate solar eligible for credits)
                s.C_OP_SOLAR = Expression(rule=solar_operating_costs_rule)

                def wind_operating_costs_rule(_s):
                    """Cost to operate existing and candidate wind generators"""

                    return (sum(m.C_MC[g, i] * s.FIXED_ENERGY[g, t] for g in m.G_E_WIND for t in m.T)
                            + sum(
                                (m.C_MC[g, i] - m.baseline[i] * m.permit_price[i]) * s.FIXED_ENERGY[g, t] for g in
                                m.G_C_WIND
                                for t in
                                m.T))

                # Existing and candidate solar unit operating costs (only candidate solar eligible for credits)
                s.C_OP_WIND = Expression(rule=wind_operating_costs_rule)

                def storage_unit_charging_cost_rule(_s):
                    """Cost to charge storage unit"""

                    return sum(m.C_MC[g, i] * s.FIXED_ENERGY_IN[g, t] for g in m.G_C_STORAGE for t in m.T)

                # Charging cost rule - no subsidy received when purchasing energy
                s.C_OP_STORAGE_CHARGING = Expression(rule=storage_unit_charging_cost_rule)

                def storage_unit_discharging_cost_rule(_s):
                    """
                    Cost to charge storage unit

                    Note: If storage units are included in the scheme this could create an undesirable outcome. Units
                    would be subsidised for each MWh they generate. Therefore they could be incentivised to continually charge
                    and then immediately discharge in order to receive the subsidy. For now assume the storage units are not
                    eligible to receive a subsidy for each MWh under the policy.
                    """

                    return sum(m.C_MC[g, i] * s.FIXED_ENERGY_OUT[g, t] for g in m.G_C_STORAGE for t in m.T)

                # Discharging cost rule - assumes storage units are eligible under REP scheme
                s.C_OP_STORAGE_DISCHARGING = Expression(rule=storage_unit_discharging_cost_rule)

                # Candidate storage unit operating costs
                s.C_OP_STORAGE = Expression(expr=s.C_OP_STORAGE_CHARGING + s.C_OP_STORAGE_DISCHARGING)

                def lost_load_cost_rule(_s):
                    """Value of lost-load"""

                    return sum(
                        (s.FIXED_p_lost_up[z, t] + s.FIXED_p_lost_down[z, t]) * m.C_LOST_LOAD for z in m.Z for t in m.T)

                # Total cost of lost-load
                s.C_OP_LOST_LOAD = Expression(rule=lost_load_cost_rule)

                def total_operating_cost_rule(_s):
                    """Total operating cost"""

                    return s.C_OP_THERM + s.C_OP_HYDRO + s.C_OP_SOLAR + s.C_OP_WIND + s.C_OP_STORAGE + s.C_OP_LOST_LOAD

                # Total operating cost
                s.C_OP_TOTAL = Expression(rule=total_operating_cost_rule)

                # Penalty imposed on violating upward reserve requirement
                s.UPWARD_RESERVE_VIOLATION_PENALTY = Expression(
                    expr=sum(m.C_LOST_LOAD * s.FIXED_upward_reserve_violation[r, t] for r in m.R for t in m.T))

                return s

            def define_block_constraints(s):
                """Define constraints for each block representing an operating scenario"""

                def operating_state_logic_rule(_s, g, t):
                    """
                    Determine the operating state of the generator (startup, shutdown
                    running, off)
                    """

                    if t == m.T.first():
                        # Must use U0 if first period (otherwise index out of range)
                        return s.u[g, t] - m.U0[g] == s.v[g, t] - s.w[g, t]

                    else:
                        # Otherwise operating state is coupled to previous period
                        return s.u[g, t] - s.u[g, t - 1] == s.v[g, t] - s.w[g, t]

                # Unit operating state
                s.OPERATING_STATE = Constraint(m.G_E_THERM.union(m.G_C_THERM), m.T, rule=operating_state_logic_rule)

                def minimum_on_time_rule(_s, g, t):
                    """Minimum number of hours generator must be on"""

                    # Hours for existing units
                    if g in self.data.existing_units.index:
                        hours = self.data.existing_units_dict[('PARAMETERS', 'MIN_ON_TIME')][g]

                    # Hours for candidate units
                    elif g in self.data.candidate_units.index:
                        hours = self.data.candidate_units_dict[('PARAMETERS', 'MIN_ON_TIME')][g]

                    else:
                        raise Exception(f'Min on time hours not found for generator: {g}')

                    # Time index used in summation
                    time_index = [k for k in range(t - int(hours), t) if k >= 0]

                    # Constraint only defined over subset of timestamps
                    if t >= hours:
                        return sum(s.v[g, j] for j in time_index) <= s.u[g, t]
                    else:
                        return Constraint.Skip

                # Minimum on time constraint
                s.MINIMUM_ON_TIME = Constraint(m.G_E_THERM.union(m.G_C_THERM), m.T, rule=minimum_on_time_rule)

                def minimum_off_time_rule(_s, g, t):
                    """Minimum number of hours generator must be off"""

                    # Hours for existing units
                    if g in self.data.existing_units.index:
                        hours = self.data.existing_units_dict[('PARAMETERS', 'MIN_OFF_TIME')][g]

                    # Hours for candidate units
                    elif g in self.data.candidate_units.index:
                        hours = self.data.candidate_units_dict[('PARAMETERS', 'MIN_OFF_TIME')][g]

                    else:
                        raise Exception(f'Min off time hours not found for generator: {g}')

                    # Time index used in summation
                    time_index = [k for k in range(t - int(hours) + 1, t) if k >= 0]

                    # Constraint only defined over subset of timestamps
                    if t >= hours:
                        return sum(s.w[g, j] for j in time_index) <= 1 - s.u[g, t]
                    else:
                        return Constraint.Skip

                # Minimum off time constraint
                s.MINIMUM_OFF_TIME = Constraint(m.G_E_THERM.union(m.G_C_THERM), m.T, rule=minimum_off_time_rule)

                return s

            def construct_block(s):
                """Construct block for operating scenario"""

                # Define block parameters for given operating scenario
                start = time.time()
                s = define_block_parameters(s)
                print(f'Defined block parameters in: {time.time() - start}s')

                # Define block variables for given operating scenario
                start = time.time()
                s = define_block_variables(s)
                print(f'Defined block variables in: {time.time() - start}s')

                # Define block expressions for given operating scenario
                start = time.time()
                s = define_block_expressions(s)
                print(f'Defined block expressions in: {time.time() - start}s')

                # Define block constraints for given operating scenario
                start = time.time()
                s = define_block_constraints(s)
                print(f'Defined block constraints in: {time.time() - start}s')

                return s

            # Construct block
            construct_block(s)

        # Construct block
        m.SCENARIO = Block(m.I, m.O, rule=operating_scenario_block_rule)

        return m

    @staticmethod
    def define_constraints(m):
        """Define master problem constraints"""

        def discrete_investment_options_rule(m, g, i):
            """Can only select one capacity decision each year for units with discrete sizing options"""
            return sum(m.d[g, i, n] for n in m.G_C_THERM_SIZE_OPTIONS) == 1

        # Only one discrete investment option can be chosen
        m.DISCRETE_INVESTMENT_OPTIONS = Constraint(m.G_C_THERM, m.I, rule=discrete_investment_options_rule)

        def solar_build_limit_rule(m, i, z):
            """Ensure zone build limits are not violated in any period - solar units"""

            return sum(sum(m.x_c[g, i] for g in m.G_C_SOLAR) for j in m.I if j <= i) <= m.BUILD_LIMITS['SOLAR', z]

        # Max amount of solar capacity that can be built in a given zone
        m.SOLAR_BUILD_LIMIT = Constraint(m.I, m.Z, rule=solar_build_limit_rule)

        def wind_build_limit_rule(m, i, z):
            """Ensure zone build limits are not violated in any period - wind units"""

            return sum(sum(m.x_c[g, i] for g in m.G_C_WIND) for j in m.I if j <= i) <= m.BUILD_LIMITS['WIND', z]

        # Max amount of wind capacity that can be built in a given zone
        m.WIND_BUILD_LIMIT = Constraint(m.I, m.Z, rule=wind_build_limit_rule)

        def storage_build_limit_rule(m, i, z):
            """Ensure zone build limits are not violated in any period - storage units"""

            return sum(sum(m.x_c[g, i] for g in m.G_C_STORAGE) for j in m.I if j <= i) <= m.BUILD_LIMITS['STORAGE', z]

        # Max amount of storage capacity that can be built in a given zone
        m.STORAGE_BUILD_LIMIT = Constraint(m.I, m.Z, rule=storage_build_limit_rule)

        def scheme_revenue_constraint_rule(m):
            """Scheme must be revenue neutral over model horizon"""

            # TODO: Need to extract energy output values from subproblem
            return (sum(
                (m.EMISSIONS_RATE[g] - m.baseline[i]) * m.permit_price[i] * m.SCENARIO[i, o].FIXED_ENERGY[g, t] for i in
                m.I for o in m.O for g in m.G_E_THERM.union(m.G_C_THERM).union(m.G_C_WIND).union(m.G_C_SOLAR) for t in
                m.T)
                    <= m.EMISSIONS_TARGET + m.emissions_target_exceeded)

        # Revenue constraint - must break-even over model horizon
        m.REVENUE_CONSTRAINT = Constraint(rule=scheme_revenue_constraint_rule)

        def scheme_emissions_constraint_rule(m):
            """Emissions limit over model horizon"""

            # TODO: Need to extract energy output values from subproblem
            return (sum(
                (m.EMISSIONS_RATE[g] * m.SCENARIO[i, o].FIXED_ENERGY[g, t] for i in m.I for o in m.O
                 for g in m.G_E_THERM.union(m.G_C) for t in m.T))
                    <= m.EMISSIONS_TARGET + m.emissions_target_exceeded)

        # Emissions constraint - must be less than some target, else penalty imposed for each unit above target
        m.EMISSIONS_CONSTRAINT = Constraint(rule=scheme_emissions_constraint_rule)

        # Initialise list of constraints used to contain Benders cuts
        m.CUTS = ConstraintList()

        return m

    @staticmethod
    def define_objective(m):
        """Define master problem objective"""

        # Minimise total operating cost - include penalty for revenue / emissions constraint violations
        m.OBJECTIVE = Objective(expr=sum(m.SCENARIO[i, o].C_OP_TOTAL for i in m.I for o in m.O)
                                     + sum(m.SCENARIO[i, o].UPWARD_RESERVE_VIOLATION_PENALTY for i in m.I for o in m.O)
                                     + m.C_FOM_TOTAL
                                     + m.C_INV_TOTAL
                                     + m.C_EMISSIONS_VIOLATION + m.C_REVENUE_VIOLATION, sense=minimize)

        return m

    def construct_model(self):
        """Construct master problem model components"""

        # Used to define sets and parameters common to both master and master problem
        common_components = CommonComponents()

        # Initialise base model object
        m = ConcreteModel()

        # Define sets - common to both master and master problem
        m = common_components.define_sets(m)

        # Define parameters - common to both master and master problem
        m = common_components.define_parameters(m)

        # Define variables - common to both master and master problem
        m = common_components.define_variables(m)

        # Define expressions - common to both master and subproblem
        m = common_components.define_expressions(m)

        # Define expressions
        m = self.define_expressions(m)

        # Define blocks
        m = self.define_blocks(m)

        # Define constraints
        m = self.define_constraints(m)

        # Define objective
        m = self.define_objective(m)

        return m

    @staticmethod
    def add_cuts(m, subproblem_results):
        """
        Add Benders cuts to model using data obtained from subproblem solution

        Parameters
        ----------
        m : pyomo model object
            Master problem model object

        subproblem_results : dict
            Results obtained from subproblem solution

        Returns
        -------
        m : pyomo model object
            Master problem model object with additional constraints (Benders cuts) added
        """

        return m

    def solve_model(self, m):
        """Solve model"""

        # Solve model
        self.opt.solve(m, tee=True, options=self.solver_options, keepfiles=self.keepfiles)

        results = {'d': m.d.get_values(),
                   'x_c': m.x_c.get_values(),
                   'baseline': m.baseline.get_values(),
                   'permit_price': m.permit_price.get_values(),
                   'u': {i: {o: m.SCENARIO[i, o].u.get_values() for o in m.O} for i in m.I},
                   'v': {i: {o: m.SCENARIO[i, o].v.get_values() for o in m.O} for i in m.I},
                   'w': {i: {o: m.SCENARIO[i, o].w.get_values() for o in m.O} for i in m.I},
                   }

        return m, results
Exemple #21
0
class Subproblem:
    # Pre-processed model data
    data = ModelData()

    def __init__(self):
        # Solver options
        self.keepfiles = False
        self.solver_options = {'Method': 1}  # 'MIPGap': 0.0005
        self.opt = SolverFactory('gurobi', solver_io='lp')

        # Setup logger
        logging.basicConfig(
            filename='subproblem.log',
            filemode='a',
            format='%(asctime)s %(name)s %(levelname)s %(message)s',
            datefmt='%Y-%m-%d %H:%M:%S',
            level=logging.DEBUG)

        logging.info("Running subproblem")
        self.logger = logging.getLogger('Subproblem')

    def define_parameters(self, m):
        """Define parameters"""
        m.ZERO = Param(initialize=0)

        return m

    def define_variables(self, m):
        """Define variables used in model"""
        return m

    def define_expressions(self, m):
        """Define expressions to be used in model"""
        def discrete_investment_capacity_lhs_rule(m, g, y):
            """Discrete investment capacity expression"""

            return m.x_c[g, y] - sum(m.d[g, y, n] * m.X_CT[g, n]
                                     for n in m.G_C_THERM_SIZE_OPTIONS)

        # LHS for constraint enforcing candidate thermal units
        m.DISCRETE_CAPACITY_LHS = Expression(
            m.G_C_THERM, m.Y, rule=discrete_investment_capacity_lhs_rule)

        def discrete_investment_binary_variable_lhs_rule(m, g, y):
            """Discrete investment expression enforcing single selection per investment period and technology type"""

            return sum(m.d[g, y, n] for n in m.G_C_THERM_SIZE_OPTIONS) - 1

        # LHS for constraint enforcing single option selection for candidate capacity
        m.DISCRETE_CAPACITY_BINARY_LHS = Expression(
            m.G_C_THERM,
            m.Y,
            rule=discrete_investment_binary_variable_lhs_rule)

        def non_negative_installed_capacity_lhs_rule(m, g, y):
            """Candidate installed capacity is non-negative"""

            return -m.x_c[g, y]

        # LHS for constraint defining non-negative candidate capacity
        m.NON_NEGATIVE_CAPACITY_LHS = Expression(
            m.G_C_SOLAR.union(m.G_C_WIND).union(m.G_C_STORAGE),
            m.Y,
            rule=non_negative_installed_capacity_lhs_rule)

        def solar_build_limits_lhs_rule(m, z, y):
            """LHS for solar build limit constraint"""

            return (
                sum(m.x_c[g, j] for g in m.G_C_SOLAR for j in m.Y if j <= y) -
                float(self.data.candidate_unit_build_limits_dict[z]['SOLAR']))

        # LHS for constraint enforcing solar zone build limits
        m.SOLAR_BUILD_LIMITS_LHS = Expression(m.Z,
                                              m.Y,
                                              rule=solar_build_limits_lhs_rule)

        def wind_build_limits_lhs_rule(m, z, y):
            """LHS for wind build limit constraint"""

            return (
                sum(m.x_c[g, j] for g in m.G_C_WIND for j in m.Y if j <= y) -
                float(self.data.candidate_unit_build_limits_dict[z]['WIND']))

        # LHS for constraint enforcing solar zone build limits
        m.WIND_BUILD_LIMITS_LHS = Expression(m.Z,
                                             m.Y,
                                             rule=wind_build_limits_lhs_rule)

        def storage_build_limits_lhs_rule(m, z, y):
            """LHS for storage build limit constraint"""

            return (
                sum(m.x_c[g, j] for g in m.G_C_WIND for j in m.Y if j <= y) -
                float(
                    self.data.candidate_unit_build_limits_dict[z]['STORAGE']))

        # LHS for constraint enforcing solar zone build limits
        m.STORAGE_BUILD_LIMITS_LHS = Expression(
            m.Z, m.Y, rule=storage_build_limits_lhs_rule)

        return m

    def define_investment_constraints(self, m):
        """Define investment constraints"""
        def discrete_investment_capacity_rule(m, g, y):
            """Discrete capacity size"""
            return m.DISCRETE_CAPACITY_LHS[g, y] == float(0)

        # Discrete investment capacity constraint
        m.DISCRETE_CAPACITY = Constraint(
            m.G_C_THERM, m.Y, rule=discrete_investment_capacity_rule)

        def discrete_capacity_binary_variable_rule(m, g, y):
            """Define discrete capacity decision enforcing single selection"""
            return m.DISCRETE_CAPACITY_BINARY_LHS[g, y] == float(0)

        # Discrete capacity binary variable
        m.DISCRETE_CAPACITY_BINARY = Constraint(
            m.G_C_THERM, m.Y, rule=discrete_capacity_binary_variable_rule)

        def non_negative_installed_capacity_rule(m, g, y):
            """Enforce non-negative installed capacity"""
            return m.NON_NEGATIVE_CAPACITY_LHS[g, y] <= float(0)

        # Non-negative installed capacities for candidate wind, solar, and storage units
        m.NON_NEGATIVE_CAPACITY = Constraint(
            m.G_C_SOLAR.union(m.G_C_WIND).union(m.G_C_STORAGE),
            m.Y,
            rule=non_negative_installed_capacity_rule)

        def solar_build_limits_rule(m, z, y):
            """Build limits for candidate solar generators in each zone"""
            return m.SOLAR_BUILD_LIMITS_LHS[z, y] <= float(0)

        # Solar build limits for each zone
        m.SOLAR_BUILD_LIMITS = Constraint(m.Z,
                                          m.Y,
                                          rule=solar_build_limits_rule)

        def wind_build_limits_rule(m, z, y):
            """Build limits for candidate wind generators in each zone"""
            return m.SOLAR_BUILD_LIMITS_LHS[z, y] <= float(0)

        # Solar build limits for each zone
        m.WIND_BUILD_LIMITS = Constraint(m.Z, m.Y, rule=wind_build_limits_rule)

        def storage_build_limits_rule(m, z, y):
            """Build limits for candidate wind generators in each zone"""
            return m.STORAGE_BUILD_LIMITS_LHS[z, y] <= float(0)

        # Solar build limits for each zone
        m.STORAGE_BUILD_LIMITS = Constraint(m.Z,
                                            m.Y,
                                            rule=storage_build_limits_rule)

        return m

    def construct_model(self):
        """Construct subproblem model components"""

        # Used to define sets and parameters common to both master and subproblem
        common_components = CommonComponents()

        # Initialise base model object
        m = ConcreteModel()

        # Define sets - common to both master and subproblem
        m = common_components.define_sets(m)

        # Define parameters
        m = common_components.define_parameters(m)

        # Define variables
        m = common_components.define_variables(m)

        # Define parameters
        m = self.define_parameters(m)

        # Define expressions
        m = self.define_expressions(m)

        # Define investment constraints
        m = self.define_investment_constraints(m)

        return m