Exemple #1
0
                                     '3_model', 'linear', 'output', 'local')
    tmp_directory = os.path.join(os.path.dirname(__file__), 'output', 'tmp',
                                 'local')
    figures_directory = os.path.join(os.path.dirname(__file__), 'output',
                                     'figures')

    # Object used to analyse results and get price target trajectory
    analysis = AnalyseResults()

    # Get plot data
    plot_data = PlotData(tmp_directory)
    plots = CreatePlots(tmp_directory, figures_directory)

    # Get BAU price trajectories
    bau = analysis.load_results(results_directory, 'bau_case.pickle')
    bau_prices = analysis.get_year_average_price(bau['PRICES'], -1)
    bau_price_trajectory = bau_prices['average_price_real'].to_dict()
    bau_first_year_trajectory = {
        y: bau_price_trajectory[2016]
        for y in range(2016, 2031)
    }

    # Create plots
    plots.plot_tax_rep_comparison()
    plots.plot_transition_year_comparison('baudev')
    plots.plot_transition_year_comparison('ptar')
    plots.plot_transition_year_comparison('pdev')

    plot_params = {
        'price_trajectory': bau_price_trajectory,
        'bau_price': bau_first_year_trajectory
Exemple #2
0
    # Load results
    # r_bau = load_results(output_directory, 'bau_case.pickle')
    # r_m = load_results(output_directory, 'mppdc_ptar_ty-2025_cp-25.pickle')
    # r_h = load_results(output_directory, 'heuristic_ptar_ty-2025_cp-25.pickle')
    #
    # # Compute average prices
    # p_m = analysis.get_year_average_price(r_m['stage_3_price_targeting'][max(r_m['stage_3_price_targeting'].keys())]['lamb'], factor=1)
    # p_h = analysis.get_year_average_price(r_h['stage_3_price_targeting'][max(r_h['stage_3_price_targeting'].keys())]['primal']['PRICES'], factor=-1)
    # p_bau = analysis.get_year_average_price(r_bau['PRICES'], factor=-1)
    #
    # # Extract baselines
    # b_m = r_m['stage_3_price_targeting'][max(r_m['stage_3_price_targeting'].keys())]['baseline']
    # b_h = r_h['stage_3_price_targeting'][max(r_h['stage_3_price_targeting'].keys())]['primal']['baseline']
    #
    # # Compare results
    # b_m_info = {'label': 'MPPDC', 'color': 'blue', 'values': b_m}
    # b_h_info = {'label': 'Heuristic', 'color': 'red', 'values': b_h}
    # plot_results('Baseline', b_m_info, b_h_info)
    #
    # p_m_info = {'label': 'MPPDC', 'color': 'blue', 'values': p_m['average_price_real'].to_dict()}
    # p_h_info = {'label': 'Heuristic', 'color': 'red', 'values': p_h['average_price_real'].to_dict()}
    # p_bau = {'label': 'BAU', 'color': 'green', 'values': p_bau['average_price_real'].to_dict()}
    # plot_results('Prices', p_m_info, p_h_info, p_bau)
    # plot_results(p_m['average_price_real'].to_dict(), p_h['average_price_real'].to_dict(), 'Prices')

    r = load_results(output_directory, 'heuristic_pdev_ty-2025_cp-40.pickle')
    p = analysis.get_year_average_price(r['stage_3_price_targeting'][max(
        r['stage_3_price_targeting'].keys())]['primal']['PRICES'],
                                        factor=-1)
Exemple #3
0
class Targets:
    def __init__(self):
        # Object used to analyse results
        self.analysis = AnalyseResults()

    @staticmethod
    def get_year_emission_intensity_target(initial_emissions_intensity,
                                           half_life, year, start_year):
        """Get half-life emissions intensity target for each year in model horizon"""

        # Re-index such that first year in model horizon is t = 0
        t = year - start_year

        exponent = (-t / half_life)

        return initial_emissions_intensity * (2**exponent)

    def get_emissions_intensity_target(self, half_life):
        """Get sequence of yearly emissions intensity targets"""

        # Get emissions intensities for each year of model horizon - BAU case
        df_bau = self.analysis.get_year_system_emissions_intensities(
            'primal_bau_results.pickle')
        df_bau = df_bau.rename(
            columns={'emissions_intensity': 'bau_emissions_intensity'})

        # First and last years of model horizon
        start, end = df_bau.index[[0, -1]]

        # Initial emissions intensity
        E_0 = df_bau.loc[start, 'bau_emissions_intensity']

        # Emissions intensity target sequence
        target_sequence = {
            y: self.get_year_emission_intensity_target(E_0, half_life, y,
                                                       start)
            for y in range(start, end + 1)
        }

        # Convert to DataFrame
        df_sequence = pd.Series(target_sequence).rename_axis('year').to_frame(
            'emissions_intensity_target')

        # Combine with bau emissions intensities
        df_c = pd.concat([df_bau, df_sequence], axis=1)

        return df_c

    def get_first_year_average_real_bau_price(self):
        """Get average price in first year of model horizon"""

        # Get average price in first year of model horizon (real price)
        prices = self.analysis.get_year_average_price(
            'primal_bau_results.pickle')

        return prices.iloc[0]['average_price_real']

    @staticmethod
    def load_emissions_intensity_target(filename):
        """Load emissions intensity target"""

        # Check that emissions target loads correctly
        with open(os.path.join(os.path.dirname(__file__), 'output', filename),
                  'r') as f:
            target = json.load(f)

        # Convert keys from strings to integers
        target = {int(k): v for k, v in target.items()}

        return target

    @staticmethod
    def load_first_year_average_bau_price(filename):
        """Load average price in first year - BAU scenario"""

        # Check that price loads correctly
        with open(os.path.join(os.path.dirname(__file__), 'output', filename),
                  'r') as f:
            price = json.load(f)

        return price['first_year_average_price']

    def get_cumulative_emissions_target(self, filename, frac):
        """
        Load emissions target

        Parameters
        ----------
        filename : str
            Name of results file on which emissions target will be based

        frac : float
            Target emissions reduction. E.g. 0.5 would imply emissions should be less than or equal to 50% of total
            emissions observed in results associated with 'filename'
        """

        return float(self.analysis.get_total_emissions(filename) * frac)

    @staticmethod
    def load_cumulative_emissions_target():
        """Load cumulative emissions target"""

        with open(
                os.path.join(os.path.dirname(__file__), 'output',
                             'cumulative_emissions_target.json'), 'r') as f:
            emissions_target = json.load(f)

        return emissions_target['cumulative_emissions_target']

    def get_interim_emissions_target(self, filename):
        """Load total emissions in each year when pursuing a cumulative emissions cap"""

        # Get emissions in each year of model horizon when pursuing cumulative target
        year_emissions = self.analysis.get_year_emissions(filename)

        return year_emissions

    @staticmethod
    def load_interim_emissions_target():
        """Load interim emissions target"""

        with open(
                os.path.join(os.path.dirname(__file__), 'output',
                             'interim_emissions_target.json'), 'r') as f:
            emissions_target = json.load(f)

        # Convert years to integers
        emissions_target = {int(k): v for k, v in emissions_target.items()}

        return emissions_target

    def get_cumulative_emissions_cap_carbon_price(self):
        """Get carbon price from cumulative emissions cap model results"""

        # Results
        results = self.analysis.load_results(
            'cumulative_emissions_cap_results.pickle')

        return results['CUMULATIVE_EMISSIONS_CAP_CONS_DUAL']

    def get_interim_emissions_cap_carbon_price(self):
        """Get carbon price from interim emissions cap model results"""

        # Results
        results = self.analysis.load_results(
            'interim_emissions_cap_results.pickle')

        return results['INTERIM_EMISSIONS_CAP_CONS_DUAL']

    @staticmethod
    def get_envelope(n_0, half_life, first_year, year):
        """Get revenue envelope level for a given year"""

        # Year with first year = 0
        t = year - first_year

        return n_0 * np.power(0.5, (t / half_life))
Exemple #4
0
class ModelCases:
    def __init__(self, output_dir, log_name):
        logging.basicConfig(
            filename=os.path.join(output_dir, f'{log_name}.log'),
            filemode='a',
            format='%(asctime)s %(name)s %(levelname)s %(message)s',
            datefmt='%Y-%m-%d %H:%M:%S',
            level=logging.DEBUG)

        # Used to parse prices and analyse results
        self.analysis = AnalyseResults()

        # Get scheme targets
        self.targets = Targets()

    @staticmethod
    def algorithm_logger(function, message, print_message=False):
        """Write message to logfile and optionally print output"""

        # Get logging object
        logger = logging.getLogger(__name__)

        # Message to write to logfile
        log_message = f'{function} - {message}'

        if print_message:
            print(log_message)
            logger.info(log_message)
        else:
            logger.info(log_message)

    @staticmethod
    def extract_result(m, component_name):
        """Extract values associated with model components"""

        model_component = m.__getattribute__(component_name)

        if type(model_component
                ) == pyomo.core.base.expression.IndexedExpression:
            return {
                k: model_component[k].expr()
                for k in model_component.keys()
            }

        elif type(model_component
                  ) == pyomo.core.base.expression.SimpleExpression:
            return model_component.expr()

        elif type(model_component) == pyomo.core.base.var.SimpleVar:
            return model_component.value

        elif type(model_component) == pyomo.core.base.var.IndexedVar:
            return model_component.get_values()

        elif type(model_component) == pyomo.core.base.param.IndexedParam:
            try:
                return {k: v.value for k, v in model_component.items()}
            except AttributeError:
                return {k: v for k, v in model_component.items()}

        elif type(model_component) == pyomo.core.base.param.SimpleParam:
            return model_component.value

        elif type(
                model_component) == pyomo.core.base.objective.SimpleObjective:
            return model_component.expr()

        else:
            raise Exception(f'Unexpected model component: {component_name}')

    @staticmethod
    def save_results(results, output_dir, filename):
        """Save model results"""

        with open(os.path.join(output_dir, filename), 'wb') as f:
            pickle.dump(results, f)

    @staticmethod
    def get_hash(params):
        """Get hash string of model parameters. Used to identify cases in log file."""

        return hashlib.sha224(str(params).encode('utf-8',
                                                 'ignore')).hexdigest()[:10]

    @staticmethod
    def save_hash(case_id, params, output_dir):
        """Save case ID and associated parameters to file"""

        # Include case ID in dictionary
        params['case_id'] = case_id

        # Save case IDs and all associated params to file
        with open(os.path.join(output_dir, 'case_ids.txt'), 'a+') as f:
            f.write(str(params) + '\n')

    @staticmethod
    def save_solution_summary(summary, output_dir):
        """Save solution summary"""

        # Save summary of total solution time + number of iterations (if specified)
        with open(os.path.join(output_dir, 'solution_summary.txt'), 'a+') as f:
            f.write(str(summary) + '\n')

    def get_bau_initial_price(self, output_dir, first_year):
        """Get BAU price in first year"""

        # Load BAU results
        with open(os.path.join(output_dir, 'bau_case.pickle'), 'rb') as f:
            results = pickle.load(f)

        # Get BAU average price in first year
        prices = self.analysis.get_year_average_price(results['PRICES'],
                                                      factor=-1)
        initial_price = prices.loc[first_year, 'average_price_real']

        return initial_price

    @staticmethod
    def get_successive_iteration_difference(i_input, i_output, key):
        """Get max absolute difference between successive iterations for a particular model component"""

        return max([
            abs(i_input[key][k] - i_output[key][k])
            for k in i_input[key].keys()
        ])

    @staticmethod
    def run_mppdc_fixed_policy(final_year,
                               scenarios_per_year,
                               permit_prices,
                               baselines,
                               include_primal_constraints=True):
        """Run MPPDC model with fixed policy parameters"""

        # Initialise object and model used to run MPPDC model
        mppdc = MPPDCModel(final_year, scenarios_per_year)
        m = mppdc.construct_model(
            include_primal_constraints=include_primal_constraints)

        # Fix permit prices and baselines
        for y in m.Y:
            m.permit_price[y].fix(permit_prices[y])
            m.baseline[y].fix(baselines[y])

        # Solve MPPDC model with fixed policy parameters
        m, status = mppdc.solve_model(m)

        return m, status

    @staticmethod
    def run_primal_fixed_policy(start_year, final_year, scenarios_per_year,
                                permit_prices, baselines):
        """Run primal model with fixed policy parameters"""

        # Initialise object and model used to run primal model
        primal = Primal(start_year, final_year, scenarios_per_year)
        m = primal.construct_model()

        # Fix permit prices and baselines to specified levels
        for y in m.Y:
            m.permit_price[y].fix(permit_prices[y])
            m.baseline[y].fix(baselines[y])

        # Solve primal model with fixed policy parameters
        m, status = primal.solve_model(m)

        return m, status

    def run_bau_case(self, params, output_dir, overwrite=False):
        """Run business-as-usual case"""

        # Case filename
        filename = 'bau_case.pickle'

        # Check if case exists
        if (not overwrite) and (filename in os.listdir(output_dir)):
            print(f'Already solved: {filename}')
            return

        # Construct hash for case
        case_id = self.get_hash(params)

        # Save case params and associated hash
        self.save_hash(case_id, params, output_dir)

        # Extract case parameters for model
        start, end, scenarios = params['start'], params['end'], params[
            'scenarios']

        # Start timer for case run
        t_start = time.time()

        message = f"""Starting case: first_year={start}, final_year={end}, scenarios_per_year={scenarios}"""
        self.algorithm_logger('run_bau_case', message)

        # Permit prices and emissions intensity baselines for BAU case (all 0)
        permit_prices = {y: float(0) for y in range(start, end + 1)}
        baselines = {y: float(0) for y in range(start, end + 1)}

        # Run model
        self.algorithm_logger('run_bau_case', 'Starting solve')
        m, status = self.run_primal_fixed_policy(start, end, scenarios,
                                                 permit_prices, baselines)
        log_infeasible_constraints(m)
        self.algorithm_logger('run_bau_case', 'Finished solve')

        # Results to extract
        result_keys = [
            'x_c', 'p', 'p_V', 'p_in', 'p_out', 'p_L', 'baseline',
            'permit_price', 'YEAR_EMISSIONS', 'YEAR_EMISSIONS_INTENSITY',
            'YEAR_SCHEME_REVENUE', 'TOTAL_SCHEME_REVENUE', 'C_MC', 'ETA',
            'DELTA', 'RHO', 'EMISSIONS_RATE', 'OBJECTIVE'
        ]

        # Model results
        results = {k: self.extract_result(m, k) for k in result_keys}

        # Add dual variable from power balance constraint
        results['PRICES'] = {
            k: m.dual[m.POWER_BALANCE[k]]
            for k in m.POWER_BALANCE.keys()
        }

        # Save results
        self.save_results(results, output_dir, filename)

        # Combine output in dictionary. To be returned by method.
        output = {'results': results, 'model': m, 'status': status}

        # Solution summary
        solution_summary = {
            'case_id': case_id,
            'mode': params['mode'],
            'total_solution_time': time.time() - t_start
        }
        self.save_solution_summary(solution_summary, output_dir)

        self.algorithm_logger(
            'run_bau_case',
            f'Finished BAU case: case_id={case_id}, total_solution_time={time.time() - t_start}s'
        )

        return output

    def run_rep_case(self, params, output_dir, overwrite=False):
        """Run carbon tax scenario"""

        # Extract case parameters
        start, end, scenarios = params['start'], params['end'], params[
            'scenarios']
        permit_prices = params['permit_prices']

        # First run carbon tax case
        baselines = {y: float(0) for y in range(start, end + 1)}

        # Check that carbon tax is same for all years in model horizon
        assert len(set(permit_prices.values(
        ))) == 1, f'Permit price trajectory is not flat: {permit_prices}'

        # Extract carbon price in first year (same as all other used). To be used in filename.
        carbon_price = permit_prices[start]

        # Filename for REP case
        filename = f'rep_cp-{carbon_price:.0f}.pickle'

        # Check if model has already been solved
        if (not overwrite) and (filename in os.listdir(output_dir)):
            print(f'Already solved: {filename}')
            return

        # Construct hash for case ID
        case_id = self.get_hash(params)

        # Save hash and associated parameters
        self.save_hash(case_id, params, output_dir)

        # Start timer for model run
        t_start = time.time()

        self.algorithm_logger('run_rep_case',
                              'Starting case with params: ' + str(params))

        # Results to extract
        result_keys = [
            'x_c', 'p', 'p_V', 'p_in', 'p_out', 'q', 'p_L', 'baseline',
            'permit_price', 'YEAR_EMISSIONS', 'YEAR_EMISSIONS_INTENSITY',
            'YEAR_SCHEME_REVENUE', 'TOTAL_SCHEME_REVENUE', 'C_MC', 'ETA',
            'DELTA', 'RHO', 'EMISSIONS_RATE',
            'YEAR_SCHEME_EMISSIONS_INTENSITY',
            'YEAR_CUMULATIVE_SCHEME_REVENUE', 'OBJECTIVE'
        ]

        # Run carbon tax case
        self.algorithm_logger('run_rep_case', 'Starting carbon tax case solve')
        m, status = self.run_primal_fixed_policy(start, end, scenarios,
                                                 permit_prices, baselines)
        log_infeasible_constraints(m)
        self.algorithm_logger('run_rep_case', 'Finished carbon tax case solve')

        # Model results
        carbon_tax_results = {
            k: self.extract_result(m, k)
            for k in result_keys
        }

        # Add dual variable from power balance constraint
        carbon_tax_results['PRICES'] = {
            k: m.dual[m.POWER_BALANCE[k]]
            for k in m.POWER_BALANCE.keys()
        }

        # Update baselines so they = emissions intensity of output from participating generators
        baselines = carbon_tax_results['YEAR_SCHEME_EMISSIONS_INTENSITY']

        # Container for iteration results
        i_results = dict()

        # Iteration counter
        counter = 1

        # Initialise iteration input to carbon tax scenario results (used to check stopping criterion)
        i_input = carbon_tax_results

        while True:
            # Re-run model with new baselines
            self.algorithm_logger(
                'run_rep_case', f'Starting solve for REP iteration={counter}')
            m, status = self.run_primal_fixed_policy(start, end, scenarios,
                                                     permit_prices, baselines)
            log_infeasible_constraints(m)
            self.algorithm_logger(
                'run_rep_case', f'Finished solved for REP iteration={counter}')

            # Model results
            i_output = {k: self.extract_result(m, k) for k in result_keys}

            # Get dual variable values from power balance constraint
            i_output['PRICES'] = {
                k: m.dual[m.POWER_BALANCE[k]]
                for k in m.POWER_BALANCE.keys()
            }

            # Add results to iteration results container
            i_results[counter] = copy.deepcopy(i_output)

            # Check max absolute capacity difference between successive iterations
            max_capacity_difference = self.get_successive_iteration_difference(
                i_input, i_output, 'x_c')
            print(
                f'{counter}: Maximum capacity difference = {max_capacity_difference} MW'
            )

            # Max absolute baseline difference between successive iterations
            max_baseline_difference = self.get_successive_iteration_difference(
                i_input, i_output, 'baseline')
            print(
                f'{counter}: Maximum baseline difference = {max_baseline_difference} tCO2/MWh'
            )

            # If max absolute difference between successive iterations is sufficiently small stop iterating
            if max_baseline_difference < 0.05:
                break

            # If iteration limit exceeded
            elif counter > 9:
                message = f'Max iterations exceeded. Exiting loop.'
                print(message)
                self.algorithm_logger('run_rep_case', message)
                break

            # Update iteration inputs (used to check stopping criterion in next iteration)
            i_input = copy.deepcopy(i_output)

            # Update baselines to be used in next iteration
            baselines = i_output['YEAR_SCHEME_EMISSIONS_INTENSITY']

            # Update iteration counter
            counter += 1

        # Combine results into single dictionary
        results = {
            'stage_1_carbon_tax': carbon_tax_results,
            'stage_2_rep': i_results
        }

        # Save results
        self.save_results(results, output_dir, filename)

        # Dictionary to be returned by method
        output = {'results': results, 'model': m, 'status': status}
        self.algorithm_logger('run_rep_case', f'Finished REP case')

        # Total number of iterations processed
        total_iterations = max(i_results.keys())

        # Save summary of the solution time
        solution_summary = {
            'case_id': case_id,
            'mode': params['mode'],
            'carbon_price': carbon_price,
            'total_solution_time': time.time() - t_start,
            'total_iterations': total_iterations,
            'max_capacity_difference': max_capacity_difference,
            'max_baseline_difference': max_baseline_difference
        }

        self.save_solution_summary(solution_summary, output_dir)

        message = 'Finished REP case: ' + str(solution_summary)
        self.algorithm_logger('run_rep_case', message)

        return output

    def run_price_smoothing_heuristic_case(self,
                                           params,
                                           output_dir,
                                           overwrite=False):
        """Smooth prices over entire model horizon using approximated price functions"""

        # Get filename based on run mode
        if params['mode'] == 'bau_deviation_minimisation':
            filename = f"heuristic_baudev_ty-{params['transition_year']}_cp-{params['carbon_price']}.pickle"

        elif params['mode'] == 'price_change_minimisation':
            filename = f"heuristic_pdev_ty-{params['transition_year']}_cp-{params['carbon_price']}.pickle"

        elif params['mode'] == 'price_target':
            filename = f"heuristic_ptar_ty-{params['transition_year']}_cp-{params['carbon_price']}.pickle"

        else:
            raise Exception(f"Unexpected run mode: {params['mode']}")

        # Check if case already solved
        if (not overwrite) and (filename in os.listdir(output_dir)):
            print(f'Already solved: {filename}')
            return

        # Get hash for case
        case_id = self.get_hash(params)

        # Save case ID and associated model parameters
        self.save_hash(case_id, params, output_dir)

        # Start timer for model run
        t_start = time.time()

        self.algorithm_logger('run_price_smoothing_heuristic_case',
                              'Starting case with params: ' + str(params))

        # Load REP results
        with open(os.path.join(output_dir, params['rep_filename']), 'rb') as f:
            rep_results = pickle.load(f)

        # Get results corresponding to last iteration of REP solution
        rep_iteration = rep_results['stage_2_rep'][max(
            rep_results['stage_2_rep'].keys())]

        # Model parameters used to initialise classes that construct and run models
        start, end, scenarios = params['start'], params['end'], params[
            'scenarios']
        bau_initial_price = self.get_bau_initial_price(output_dir, start)

        # Classes used to construct and run primal and MPPDC programs
        primal = Primal(start, end, scenarios)
        baseline = BaselineUpdater(start, end, scenarios,
                                   params['transition_year'])

        # Construct primal program
        m_p = primal.construct_model()

        # Results to extract from primal program
        primal_keys = [
            'x_c', 'p', 'p_V', 'p_in', 'p_out', 'p_L', 'baseline',
            'permit_price', 'YEAR_EMISSIONS', 'YEAR_EMISSIONS_INTENSITY',
            'YEAR_SCHEME_REVENUE', 'TOTAL_SCHEME_REVENUE', 'C_MC', 'ETA',
            'DELTA', 'RHO', 'EMISSIONS_RATE', 'YEAR_CUMULATIVE_SCHEME_REVENUE',
            'YEAR_SCHEME_EMISSIONS_INTENSITY', 'OBJECTIVE'
        ]

        # Results to extract from baseline targeting model
        baseline_keys = [
            'YEAR_AVERAGE_PRICE', 'YEAR_AVERAGE_PRICE_0',
            'YEAR_ABSOLUTE_PRICE_DIFFERENCE',
            'TOTAL_ABSOLUTE_PRICE_DIFFERENCE', 'PRICE_WEIGHTS',
            'YEAR_SCHEME_REVENUE', 'YEAR_CUMULATIVE_SCHEME_REVENUE', 'baseline'
        ]

        # Container for iteration results
        i_results = dict()

        # Initialise price setting generator input as results obtained from final REP iteration
        psg_input = rep_iteration

        # Initialise iteration counter
        counter = 1

        while True:
            self.algorithm_logger('run_price_smoothing_heuristic_case',
                                  f'Starting iteration={counter}')

            # Identify price setting generators
            psg = baseline.prices.get_price_setting_generators_from_model_results(
                psg_input)

            # Construct model used to calibrate baseline
            m_b = baseline.construct_model(psg)

            # Update parameters
            m_b = baseline.update_parameters(m_b, psg_input)
            m_b.YEAR_AVERAGE_PRICE_0 = float(bau_initial_price)
            m_b.PRICE_WEIGHTS.store_values(params['price_weights'])

            # Activate constraints and objectives depending on case being run
            m_b.NON_NEGATIVE_TRANSITION_REVENUE_CONS.activate()

            if params['mode'] == 'bau_deviation_minimisation':
                # Set the price target to be BAU price
                bau_price_target = {y: bau_initial_price for y in m_b.Y}
                m_b.YEAR_AVERAGE_PRICE_TARGET.store_values(bau_price_target)

                # Activate price targeting constraints and objective
                m_b.PRICE_TARGET_DEVIATION_1.activate()
                m_b.PRICE_TARGET_DEVIATION_2.activate()
                m_b.OBJECTIVE_PRICE_TARGET_DIFFERENCE.activate()

                # Append name of objective so objective value can be extracted, and create filename for case
                baseline_keys.append('OBJECTIVE_PRICE_TARGET_DIFFERENCE')

            elif params['mode'] == 'price_change_minimisation':
                # Activate constraints penalised price deviations over successive years
                m_b.PRICE_CHANGE_DEVIATION_1.activate()
                m_b.PRICE_CHANGE_DEVIATION_2.activate()
                m_b.OBJECTIVE_PRICE_DEVIATION.activate()

                # Append name of objective so objective value can be extracted, and create filename for case
                baseline_keys.append('OBJECTIVE_PRICE_DEVIATION')

            elif params['mode'] == 'price_target':
                # Set target price trajectory to prices obtained from BAU model over same period
                m_b.YEAR_AVERAGE_PRICE_TARGET.store_values(
                    params['price_target'])

                # Activate price targeting constraints and objective function
                m_b.PRICE_TARGET_DEVIATION_1.activate()
                m_b.PRICE_TARGET_DEVIATION_2.activate()
                m_b.OBJECTIVE_PRICE_TARGET_DIFFERENCE.activate()

                # Append name of objective so objective value can be extracted, and create filename for case
                baseline_keys.append('OBJECTIVE_PRICE_TARGET_DIFFERENCE')

            else:
                raise Exception(f"Unexpected run mode: {params['mode']}")

            for y in m_b.Y:
                if y >= params['transition_year']:
                    m_b.YEAR_NET_SCHEME_REVENUE_NEUTRAL_CONS[y].activate()

            # Solve model
            m_b, m_b_status = baseline.solve_model(m_b)
            r_b = copy.deepcopy(
                {k: self.extract_result(m_b, k)
                 for k in baseline_keys})

            # Update baselines and permit prices in primal model
            for y in m_p.Y:
                m_p.baseline[y].fix(m_b.baseline[y].value)
                m_p.permit_price[y].fix(m_b.PERMIT_PRICE[y].value)

            # Solve primal program
            m_p, m_p_status = primal.solve_model(m_p)

            # Log all infeasible constraints
            log_infeasible_constraints(m_p)

            # Get results
            r_p = copy.deepcopy(
                {v: self.extract_result(m_p, v)
                 for v in primal_keys})
            r_p['PRICES'] = copy.deepcopy({
                k: m_p.dual[m_p.POWER_BALANCE[k]]
                for k in m_p.POWER_BALANCE.keys()
            })
            i_results[counter] = {'primal': r_p, 'auxiliary': r_b}

            # Max difference in capacity sizing decision between iterations
            max_capacity_difference = self.get_successive_iteration_difference(
                psg_input, r_p, 'x_c')
            print(f'Max capacity difference: {max_capacity_difference} MW')

            # Max absolute baseline difference between successive iterations
            max_baseline_difference = self.get_successive_iteration_difference(
                psg_input, r_p, 'baseline')
            print(
                f'{counter}: Maximum baseline difference = {max_baseline_difference} tCO2/MWh'
            )

            self.algorithm_logger('run_price_smoothing_heuristic_case',
                                  f'Finished iteration={counter}')

            # If baseline difference between successive iterations is sufficiently small then stop
            if max_baseline_difference < 0.05:
                break

            # Stop iterating if max iteration limit exceeded
            elif counter > 9:
                message = f'Max iterations exceeded. Exiting loop.'
                print(message)
                self.algorithm_logger('run_price_smoothing_heuristic_case',
                                      message)
                break

            else:
                # Update dictionary of price setting generator program inputs
                psg_input = r_p

            # Update iteration counter
            counter += 1

        self.algorithm_logger('run_price_smoothing_heuristic_case',
                              f'Finished solving model')

        # Combine results into a single dictionary
        results = {
            **rep_results, 'stage_3_price_targeting': i_results,
            'parameters': params
        }

        # Save results
        self.save_results(results, output_dir, filename)

        # Combine output for method (can be used for debugging)
        output = {
            'auxiliary_model': m_b,
            'auxiliary_status': m_b_status,
            'primal_model': m_p,
            'primal_status': m_p_status,
            'results': results
        }

        # Total iterations
        total_iterations = max(i_results.keys())

        # Save summary of the solution time
        solution_summary = {
            'case_id': case_id,
            'mode': params['mode'],
            'carbon_price': params['carbon_price'],
            'transition_year': params['transition_year'],
            'total_solution_time': time.time() - t_start,
            'total_iterations': total_iterations,
            'max_capacity_difference': max_capacity_difference,
            'max_baseline_difference': max_baseline_difference
        }
        self.save_solution_summary(solution_summary, output_dir)

        message = f"Finished heuristic case: " + str(solution_summary)
        self.algorithm_logger('run_price_smoothing_heuristic_case', message)

        return output

    def run_price_smoothing_mppdc_case(self,
                                       params,
                                       output_dir,
                                       overwrite=False):
        """Run case to smooth prices over model horizon, subject to total revenue constraint"""

        # Get case filename based on run mode
        if params['mode'] == 'bau_deviation_minimisation':
            filename = f"mppdc_baudev_ty-{params['transition_year']}_cp-{params['carbon_price']}.pickle"

        elif params['mode'] == 'price_change_minimisation':
            filename = f"mppdc_pdev_ty-{params['transition_year']}_cp-{params['carbon_price']}.pickle"

        elif params['mode'] == 'price_target':
            filename = f"mppdc_ptar_ty-{params['transition_year']}_cp-{params['carbon_price']}.pickle"

        else:
            raise Exception(f"Unexpected run mode: {params['mode']}")

        # Check if case already solved
        if (not overwrite) and (filename in os.listdir(output_dir)):
            print(f'Already solved: {filename}')
            return

        # Construct hash for case ID
        case_id = self.get_hash(params)

        # Save hash and associated parameters
        self.save_hash(case_id, params, output_dir)

        # Start timer for model run
        t_start = time.time()

        self.algorithm_logger(
            'run_price_smoothing_mppdc_case',
            'Starting MPPDC case with params: ' + str(params))

        # Load REP results
        with open(os.path.join(output_dir, params['rep_filename']), 'rb') as f:
            rep_results = pickle.load(f)

        # Get results corresponding to last iteration of REP solution
        rep_iteration = rep_results['stage_2_rep'][max(
            rep_results['stage_2_rep'].keys())]

        # Extract parameters from last iteration of REP program results
        start, end, scenarios = params['start'], params['end'], params[
            'scenarios']
        bau_initial_price = self.get_bau_initial_price(output_dir, start)

        # Classes used to construct and run primal and MPPDC programs
        mppdc = MPPDCModel(start, end, scenarios, params['transition_year'])
        primal = Primal(start, end, scenarios)

        # Construct MPPDC
        m_m = mppdc.construct_model(include_primal_constraints=True)

        # Construct primal model
        m_p = primal.construct_model()

        # Update MPPDC model parameters
        m_m.YEAR_AVERAGE_PRICE_0 = float(bau_initial_price)
        m_m.PRICE_WEIGHTS.store_values(params['price_weights'])

        # Activate necessary constraints depending on run mode
        m_m.NON_NEGATIVE_TRANSITION_REVENUE_CONS.activate()

        if params['mode'] == 'bau_deviation_minimisation':
            m_m.PRICE_BAU_DEVIATION_1.activate()
            m_m.PRICE_BAU_DEVIATION_2.activate()

        elif params['mode'] == 'price_change_minimisation':
            m_m.PRICE_CHANGE_DEVIATION_1.activate()
            m_m.PRICE_CHANGE_DEVIATION_2.activate()

        elif params['mode'] == 'price_target':
            m_m.YEAR_AVERAGE_PRICE_TARGET.store_values(params['price_target'])
            m_m.PRICE_TARGET_DEVIATION_1.activate()
            m_m.PRICE_TARGET_DEVIATION_2.activate()

        else:
            raise Exception(f"Unexpected run mode: {params['mode']}")

        for y in m_m.Y:
            if y >= params['transition_year']:
                m_m.YEAR_NET_SCHEME_REVENUE_NEUTRAL_CONS[y].activate()

        # Primal variables
        primal_vars = [
            'x_c', 'p', 'p_in', 'p_out', 'q', 'p_V', 'p_L', 'permit_price'
        ]
        fixed_vars = {v: rep_iteration[v] for v in primal_vars}

        # Results to extract from MPPDC model
        mppdc_keys = [
            'x_c', 'p', 'p_V', 'p_in', 'p_out', 'p_L', 'q', 'baseline',
            'permit_price', 'lamb', 'YEAR_EMISSIONS',
            'YEAR_EMISSIONS_INTENSITY', 'YEAR_SCHEME_EMISSIONS_INTENSITY',
            'YEAR_SCHEME_REVENUE', 'YEAR_CUMULATIVE_SCHEME_REVENUE',
            'TOTAL_SCHEME_REVENUE', 'YEAR_ABSOLUTE_PRICE_DIFFERENCE',
            'YEAR_AVERAGE_PRICE_0', 'YEAR_AVERAGE_PRICE',
            'YEAR_SUM_CUMULATIVE_PRICE_DIFFERENCE_WEIGHTED', 'OBJECTIVE',
            'YEAR_ABSOLUTE_PRICE_DIFFERENCE_WEIGHTED',
            'TOTAL_ABSOLUTE_PRICE_DIFFERENCE_WEIGHTED',
            'YEAR_CUMULATIVE_PRICE_DIFFERENCE_WEIGHTED', 'sd_1', 'sd_2',
            'STRONG_DUALITY_VIOLATION_COST', 'TRANSITION_YEAR', 'PRICE_WEIGHTS'
        ]

        # Container for iteration results
        i_results = {}

        # Initialise iteration counter
        counter = 1

        # Placeholder for max difference variables
        max_baseline_difference = None
        max_capacity_difference = None

        while True:
            self.algorithm_logger('run_price_smoothing_mppdc_case',
                                  f'Starting iteration={counter}')

            # Fix MPPDC variables
            m_m = mppdc.fix_variables(m_m, fixed_vars)

            # Solve MPPDC
            m_m, m_m_status = mppdc.solve_model(m_m)

            # Model timeout will cause sub-optimal termination condition
            if m_m_status.solver.termination_condition != TerminationCondition.optimal:
                i_results[counter] = None
                self.algorithm_logger('run_price_smoothing_mppdc_case',
                                      f'Sub-optimal solution')
                self.algorithm_logger(
                    'run_price_smoothing_mppdc_case',
                    f'User time: {m_m_status.solver.user_time}s')

                # No primal model solved
                m_p, m_p_status = None, None
                break

            # Log infeasible constraints
            log_infeasible_constraints(m_m)

            # Results from MPPDC program
            r_m = copy.deepcopy(
                {v: self.extract_result(m_m, v)
                 for v in mppdc_keys})
            i_results[counter] = r_m

            # Update primal program with baselines and permit prices obtained from MPPDC model
            for y in m_p.Y:
                m_p.baseline[y].fix(m_m.baseline[y].value)
                m_p.permit_price[y].fix(m_m.permit_price[y].value)

            # Solve primal model
            m_p, m_p_status = primal.solve_model(m_p)
            log_infeasible_constraints(m_p)

            # Results from primal program
            p_r = copy.deepcopy(
                {v: self.extract_result(m_p, v)
                 for v in primal_vars})
            p_r['PRICES'] = copy.deepcopy({
                k: m_p.dual[m_p.POWER_BALANCE[k]]
                for k in m_p.POWER_BALANCE.keys()
            })
            i_results[counter]['primal'] = p_r

            # Max absolute capacity difference between MPPDC and primal program
            max_capacity_difference = max(
                abs(m_m.x_c[k].value - m_p.x_c[k].value)
                for k in m_m.x_c.keys())
            print(f'Max capacity difference: {max_capacity_difference} MW')

            # Max absolute baseline difference between MPPDC and primal program
            max_baseline_difference = max(
                abs(m_m.baseline[k].value - m_p.baseline[k].value)
                for k in m_m.baseline.keys())
            print(
                f'Max baseline difference: {max_baseline_difference} tCO2/MWh')

            # Check if capacity variables have changed
            if max_baseline_difference < 0.05:
                break

            # Check if max iterations exceeded
            elif counter > 9:
                message = f'Max iterations exceeded. Exiting loop.'
                print(message)
                self.algorithm_logger('run_price_smoothing_mppdc_case',
                                      message)
                break

            else:
                # Update dictionary of fixed variables to be used in next iteration
                fixed_vars = {v: p_r[v] for v in primal_vars}

            self.algorithm_logger('run_price_smoothing_mppdc_case',
                                  f'Finished iteration={counter}')
            counter += 1

        self.algorithm_logger('run_price_smoothing_mppdc_case',
                              f'Finished solving model')

        # Combine results into a single dictionary
        results = {
            **rep_results, 'stage_3_price_targeting': i_results,
            'parameters': params
        }

        # Save results
        self.save_results(results, output_dir, filename)

        # Method output
        output = {
            'mppdc_model': m_m,
            'mppdc_status': m_m_status,
            'primal_model': m_p,
            'primal_status': m_p_status,
            'results': results
        }

        self.algorithm_logger('run_price_smoothing_mppdc_case',
                              'Finished MPPDC case')

        # Total iterations
        total_iterations = max(i_results.keys())

        # Save summary of the solution time
        solution_summary = {
            'case_id': case_id,
            'mode': params['mode'],
            'carbon_price': params['carbon_price'],
            'transition_year': params['transition_year'],
            'total_solution_time': time.time() - t_start,
            'total_iterations': total_iterations,
            'max_capacity_difference': max_capacity_difference,
            'max_baseline_difference': max_baseline_difference
        }
        self.save_solution_summary(solution_summary, output_dir)

        message = f"Finished MPPDC case: " + str(solution_summary)
        self.algorithm_logger('run_price_smoothing_mppdc_case', message)

        return output