Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 54 additions & 35 deletions MINDSET_FTT_Power/SourceCode/Power/ftt_p_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@
Main solution function for the module
"""

# Standard library imports
import configparser
from pathlib import Path

# Third party imports
import numpy as np

Expand All @@ -73,6 +77,25 @@



# Settings parsed once at import time ÔÇö not per solve() call
_PACKAGE_ROOT = Path(__file__).parents[2]
_config = configparser.ConfigParser()
_config.read(str(_PACKAGE_ROOT / "settings.ini"))

_PRSC_BASE_YEAR = int(_config.get('settings', 'prsc_base_year', fallback='2013'))
_EX_BASE_YEAR = int(_config.get('settings', 'ex_base_year', fallback='2018'))
_PRSC_DEFL_YEAR = int(_config.get('settings', 'prsc_defl_year', fallback='2015'))
_RLDC_START_YEAR = int(_config.get('settings', 'rldc_start_year', fallback='2018'))
_BCET_COPY_RANGE_END = int(_config.get('settings', 'bcet_copy_range_end', fallback='17'))
_USD_EUR_RATE = float(_config.get('settings', 'usd_eur_rate', fallback='1.0018'))

_PRSC_VAR = f"PRSC{str(_PRSC_BASE_YEAR)[2:]}" # e.g. "PRSC13"
_PRSC_EX_VAR = f"PRSC{str(_EX_BASE_YEAR)[2:]}" # e.g. "PRSC18"
_EX_VAR = f"EX{str(_EX_BASE_YEAR)[2:]}" # e.g. "EX18"
_REX_VAR = f"REX{str(_EX_BASE_YEAR)[2:]}" # e.g. "REX18"
_PRSC_DEFL_VAR = f"PRSC{str(_PRSC_DEFL_YEAR)[2:]}" # e.g. "PRSC15"


# %% main function
# -----------------------------------------------------------------------------
# ----------------------------- Main ------------------------------------------
Expand Down Expand Up @@ -141,10 +164,10 @@ def solve(data, time_lag, iter_lag, titles, conv, histend, year, domain):

# Copy over PRSC/EX values

data['PRSC18'] = np.copy(time_lag['PRSC18'] )
data['EX18'] = np.copy(time_lag['EX18'] )
data['PRSC15'] = np.copy(time_lag['PRSC15'] )
data["REX18"] = np.copy(time_lag["REX18"])
data[_PRSC_EX_VAR] = np.copy(time_lag[_PRSC_EX_VAR])
data[_EX_VAR] = np.copy(time_lag[_EX_VAR])
data[_PRSC_DEFL_VAR] = np.copy(time_lag[_PRSC_DEFL_VAR])
data[_REX_VAR] = np.copy(time_lag[_REX_VAR])
data['FPIX'][data['FPIX'] == 0] = 1
time_lag['FPIX'][time_lag['FPIX'] == 0] = 1
# Increase fuel prices
Expand All @@ -155,13 +178,13 @@ def solve(data, time_lag, iter_lag, titles, conv, histend, year, domain):
T_Scal = 10 # Time scaling factor used in the share dynamics

# Initialisation, which corresponds to lines 389 to 556 in fortran
if year == 2013:
data['PRSC13'] = np.copy(data['PRSCX'])
if year == _PRSC_BASE_YEAR:
data[_PRSC_VAR] = np.copy(data['PRSCX'])

if year == 2018:
data['PRSC18'] = np.copy(data['PRSCX'])
data['EX18'] = np.copy(data['EXX'])
data['REX18'] = np.copy(data['REXX'])
if year == _EX_BASE_YEAR:
data[_PRSC_EX_VAR] = np.copy(data['PRSCX'])
data[_EX_VAR] = np.copy(data['EXX'])
data[_REX_VAR] = np.copy(data['REXX'])

data['MEWL'][:, :, 0] = data["MWLO"][:, :, 0]
data['MEWK'][:, :, 0] = np.divide(data['MEWG'][:, :, 0], data['MEWL'][:, :, 0],
Expand Down Expand Up @@ -255,15 +278,12 @@ def solve(data, time_lag, iter_lag, titles, conv, histend, year, domain):
#%%
# Up to the last year of historical market share data
elif year <= histend['MEWG']:
if year == 2015:
data['PRSC15'] = np.copy(data['PRSCX'])
if year == _PRSC_DEFL_YEAR:
data[_PRSC_DEFL_VAR] = np.copy(data['PRSCX'])


# Set starting values for MERC
data['MERC'][:, 0, 0] = 0.255
data['MERC'][:, 1, 0] = 5.689
data['MERC'][:, 2, 0] = 0.4246
data['MERC'][:, 3, 0] = 3.374
# Set starting values for MERC from lagged data (avoids hardcoded floats)
data['MERC'][:, :4, 0] = time_lag['MERC'][:, :4, 0]
data['MERC'][:, 4, 0] = 0.001
data['MERC'][:, 7, 0] = 0.001
# Calculate electricty trade shares
Expand All @@ -277,7 +297,7 @@ def solve(data, time_lag, iter_lag, titles, conv, histend, year, domain):
# loadfac = data['MWLO'][:, :, 0]
# data['MEWL'][:, :, 0] = np.copy(loadfac)

if year > 2018:
if year > _EX_BASE_YEAR:
data['MEWL'][:, :, 0] = time_lag['MEWL'][:, :, 0].copy()

cond = np.logical_and(data['MEWL'][:, :, 0] < 0.01, data['MWLO'][:, :, 0] > 0.0)
Expand All @@ -300,14 +320,14 @@ def solve(data, time_lag, iter_lag, titles, conv, histend, year, domain):


# Call RLDC function for capacity and load factor by LB, and storage costs
if year >= 2018:
if year >= _RLDC_START_YEAR:

# 1 and 2 -- Estimate RLDC and storage parameters
data = rldc(data, time_lag, iter_lag, year, titles)

# 3--- Call dispatch routine to connect market shares to load bands
# Call DSPCH function to dispatch flexible capacity based on MC
if year == 2018:
if year == _RLDC_START_YEAR:
mslb, mllb, mes1, mes2 = dspch(data['MWDD'], data['MEWS'], data['MKLB'], data['MCRT'],
data['MEWL'], data['MWMC'], data['MMCD'],
len(titles['RTI']), len(titles['T2TI']), len(titles['LBTI']))
Expand All @@ -321,12 +341,12 @@ def solve(data, time_lag, iter_lag, titles, conv, histend, year, domain):
data['MES2'] = mes2

# Change currency from EUR2015 to USD2013
if year >= 2015:
# usa_idx = titles['RTI_short'].index('USA')
data['MSSP'][:, :, 0] = data['MSSP'][:, :, 0] * (data['PRSC13'][:, 0, 0, np.newaxis]/data['PRSC15'][:, 0, 0, np.newaxis])/ 1.0018
data['MLSP'][:, :, 0] = data['MLSP'][:, :, 0] * (data['PRSC13'][:, 0, 0, np.newaxis]/data['PRSC15'][:, 0, 0, np.newaxis])/ 1.0018
data['MSSM'][:, :, 0] = data['MSSM'][:, :, 0] * (data['PRSC13'][:, 0, 0, np.newaxis]/data['PRSC15'][:, 0, 0, np.newaxis])/ 1.0018
data['MLSM'][:, :, 0] = data['MLSM'][:, :, 0] * (data['PRSC13'][:, 0, 0, np.newaxis]/data['PRSC15'][:, 0, 0, np.newaxis])/ 1.0018
if year >= _PRSC_DEFL_YEAR:
_prsc_ratio = data[_PRSC_VAR][:, 0, 0, np.newaxis] / data[_PRSC_DEFL_VAR][:, 0, 0, np.newaxis]
data['MSSP'][:, :, 0] = data['MSSP'][:, :, 0] * _prsc_ratio / _USD_EUR_RATE
data['MLSP'][:, :, 0] = data['MLSP'][:, :, 0] * _prsc_ratio / _USD_EUR_RATE
data['MSSM'][:, :, 0] = data['MSSM'][:, :, 0] * _prsc_ratio / _USD_EUR_RATE
data['MLSM'][:, :, 0] = data['MLSM'][:, :, 0] * _prsc_ratio / _USD_EUR_RATE

# TODO: This is not per se correct but it's how it is in E3ME
else:
Expand Down Expand Up @@ -457,7 +477,7 @@ def solve(data, time_lag, iter_lag, titles, conv, histend, year, domain):
data["MEWW"][0, :, 0] = time_lag['MEWW'][0, :, 0] + dw

# Copy over the technology cost categories that do not change (all except prices which are updated through learning-by-doing below)
data['BCET'][:, :, 1:17] = time_lag['BCET'][:, :, 1:17].copy()
data['BCET'][:, :, 1:_BCET_COPY_RANGE_END] = time_lag['BCET'][:, :, 1:_BCET_COPY_RANGE_END].copy()

# Store gamma values in the cost matrix (in case it varies over time)
data['BCET'][:, :, c2ti['21 Gamma ($/MWh)']] = data['MGAM'][:, :, 0]
Expand Down Expand Up @@ -641,13 +661,12 @@ def solve(data, time_lag, iter_lag, titles, conv, histend, year, domain):
# Call RLDC function for capacity and load factor by LB, and storage costs
data = rldc(data, time_lag, data_dt, year, titles)

# Change currency from EUR2015 to USD2013 (This is wrong, but in terms of logic and by misstating currency year for storage)
# usa_idx = titles['RTI_short'].index('USA')

data['MSSP'][:, :, 0] = data['MSSP'][:, :, 0] * (data['PRSC13'][:, 0, 0, np.newaxis]/data['PRSC15'][:, 0, 0, np.newaxis]) / 1.0018
data['MLSP'][:, :, 0] = data['MLSP'][:, :, 0] * (data['PRSC13'][:, 0, 0, np.newaxis]/data['PRSC15'][:, 0, 0, np.newaxis]) / 1.0018
data['MSSM'][:, :, 0] = data['MSSM'][:, :, 0] * (data['PRSC13'][:, 0, 0, np.newaxis]/data['PRSC15'][:, 0, 0, np.newaxis]) / 1.0018
data['MLSM'][:, :, 0] = data['MLSM'][:, :, 0] * (data['PRSC13'][:, 0, 0, np.newaxis]/data['PRSC15'][:, 0, 0, np.newaxis]) / 1.0018
# Change currency from EUR2015 to USD2013 (acknowledged approximation)
_prsc_ratio = data[_PRSC_VAR][:, 0, 0, np.newaxis] / data[_PRSC_DEFL_VAR][:, 0, 0, np.newaxis]
data['MSSP'][:, :, 0] = data['MSSP'][:, :, 0] * _prsc_ratio / _USD_EUR_RATE
data['MLSP'][:, :, 0] = data['MLSP'][:, :, 0] * _prsc_ratio / _USD_EUR_RATE
data['MSSM'][:, :, 0] = data['MSSM'][:, :, 0] * _prsc_ratio / _USD_EUR_RATE
data['MLSM'][:, :, 0] = data['MLSM'][:, :, 0] * _prsc_ratio / _USD_EUR_RATE


# =================================================================
Expand Down Expand Up @@ -788,7 +807,7 @@ def solve(data, time_lag, iter_lag, titles, conv, histend, year, domain):


# Copy over the technology cost categories. We update the investment and capacity factors below
data['BCET'][:, :, 1:17] = time_lag['BCET'][:, :, 1:17].copy()
data['BCET'][:, :, 1:_BCET_COPY_RANGE_END] = time_lag['BCET'][:, :, 1:_BCET_COPY_RANGE_END].copy()

# Store gamma values in the cost matrix (in case it varies over time)
data['BCET'][:, :, c2ti['21 Gamma ($/MWh)']] = data['MGAM'][:, :, 0]
Expand Down
119 changes: 91 additions & 28 deletions MINDSET_FTT_Power/SourceCode/model_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,14 @@

# Standard library imports
import configparser
import copy
from pathlib import Path

# Third party imports
import numpy as np
from tqdm import tqdm

# Local library imports
# Separate FTT modules
import MINDSET_FTT_Power.SourceCode.Power.ftt_p_main as ftt_p

from ftt_source.paths import set_paths
from ftt_source.Power.ftt_p_main import solve as ftt_p_solve, build_power_settings

# Support modules
import MINDSET_FTT_Power.SourceCode.support.input_functions as in_f
Expand Down Expand Up @@ -94,8 +92,10 @@ def __init__(self):
""" Instantiate model run object """

# Attributes given in settings.ini file
_mindset_root = Path(__file__).parents[1]
_settings_ini = str(_mindset_root / 'settings.ini')
config = configparser.ConfigParser()
config.read('MINDSET_FTT_Power/settings.ini')
config.read(_settings_ini)
self.name = config.get('settings', 'name')
self.model_start = int(config.get('settings', 'model_start'))
self.model_end = int(config.get('settings', 'model_end'))
Expand All @@ -107,21 +107,47 @@ def __init__(self):
self.ftt_modules = config.get('settings', 'enable_modules')
self.scenarios = config.get('settings', 'scenarios')

# Point FTT_Standalone at MINDSET's Utilities and settings (once at startup)
set_paths(utilities_path=str(_mindset_root / 'Utilities'))
self.ftt_settings_path = _settings_ini
self.carbon_price_conv_factor = float(
config.get('settings', 'carbon_price_conv_factor', fallback='1.3281'))
# Read exchange-rate variable names from settings (must match ftt_p_main.py)
_ex_base_year = int(config.get('settings', 'ex_base_year', fallback='2018'))
self._prsc_ex_var = f"PRSC{str(_ex_base_year)[2:]}" # e.g. "PRSC18"
self._ex_var = f"EX{str(_ex_base_year)[2:]}" # e.g. "EX18"

# Load classification titles
self.titles = titles_f.load_titles()
self.conv = titles_f.load_converters()
self.power_settings = build_power_settings(self.titles, config)

# Load variable dimensions
self.dims, self.histend, self.domain, self.forstart = dims_f.load_dims()

# # Set up csv files if they do not exist yet
# initialise_csv_files(self.ftt_modules, self.scenarios)

# Retrieve inputs

# Retrieve inputs ÔÇö C2TI size must match the CSV files at this point
self.input = in_f.load_data(self.titles, self.dims, self.timeline,
self.scenarios, self.ftt_modules,
self.forstart)

# After loading, extend BCET and C2TI so FTT_Standalone's ftt_p_lcoe can find
# '22 Gamma' (index 21) and '23 Value factor' (index 22) by name.
# The CSV data ends at '21 Gamma ($/MWh)' (index 20); we copy it to column 21
# and default Value factor to 1.0 for all technologies.
c2ti_list = list(self.titles['C2TI'])
gamma_col = c2ti_list.index('21 Gamma ($/MWh)') # index 20
new_entries = [e for e in ('22 Gamma', '23 Value factor') if e not in c2ti_list]
if new_entries:
n_extra = len(new_entries)
for scen in self.input:
bcet = self.input[scen]['BCET'] # (RTI, T2TI, C2TI, 1)
extra = np.ones((bcet.shape[0], bcet.shape[1], n_extra, bcet.shape[3]))
extra[:, :, 0, :] = bcet[:, :, gamma_col, :] # '22 Gamma' = copy of col 21
# '23 Value factor' stays 1.0
self.input[scen]['BCET'] = np.concatenate([bcet, extra], axis=2)
c2ti_list.extend(new_entries)
self.titles['C2TI'] = tuple(c2ti_list)


# Initialize remaining attributes
self.variables = {}
Expand All @@ -130,6 +156,17 @@ def __init__(self):
self.output = {scen: {var: np.full_like(self.input[scen][var], 0) \
for var in self.input[scen]} for scen in self.input}

# Carbon price coupling state (MSET-coupled mode only).
# Written by ftt_power.py before each solve_year(); consumed in solve_year().
# Shape (n_reg, n_tech, 1) ÔÇö one ctax per (region, technology) in EUR2015/tCO2.
self._mset_reppx = None

# Fuel price coupling state (MSET-coupled mode only).
# _mset_fpi is written by ftt_power.py before each solve_year() call.
# _fpix_carry accumulates the cumulative price index across years.
self._mset_fpi = None
self._fpix_carry = np.ones((len(self.titles['RTI']), len(self.titles['T2TI']), 1))

def run(self):
""" Solve model run and save results """

Expand Down Expand Up @@ -180,29 +217,55 @@ def solve_year(self, year, y, scenario, max_iter=1):

# Run update
variables, time_lags = self.update(year, y, scenario)
iter_lags = copy.deepcopy(time_lags)

# Define whole period
tl = self.timeline

# define modules list in for possible setting.ini selection
modules_list = ["FTT-P"]
# Iteration loop here
for itereration in range(max_iter):

if "FTT-P" in self.ftt_modules:
variables = ftt_p.solve(variables, time_lags, iter_lags,
self.titles, self.conv, self.histend, tl[y],
self.domain)

if not any(True for x in modules_list if x in self.ftt_modules):
print("Incorrect selection of modules. Check settings.ini")

# Third, solve energy supply
# Overwrite iter_lags to be used in the next iteration round
iter_lags = copy.deepcopy(variables)
# # Print any diagnstics
#

if "FTT-P" in self.ftt_modules:

# Convert MINDSET ctax (EUR2015/tCO2) to CO2taxP (USD2013/tCO2).
# Use time_lags for exchange-rate snapshots: variables has them as zero (no CSV in coupled Inputs).
if self._mset_reppx is not None:
_prsc18 = time_lags.get(self._prsc_ex_var)
_ex18 = time_lags.get(self._ex_var)
if _prsc18 is not None and _ex18 is not None:
denom = (variables['PRSCX'] * _ex18
/ np.maximum(_prsc18 * variables['EXX'], 1e-10))
variables['CO2taxP'] = (self._mset_reppx * self.carbon_price_conv_factor
/ np.where(denom != 0, denom, 1.0))
self._mset_reppx = None # consume ÔÇö must be re-set each year by ftt_power.py

# Overwrite BCET's '22 Gamma' column with MGAM before calling FTT_Standalone.
if 'MGAM' in variables:
c2ti_m = {cat: idx for idx, cat in enumerate(self.titles['C2TI'])}
n_c2ti = len(self.titles['C2TI']) # 23 after __init__ extension
for d in (variables, time_lags):
if d['BCET'].shape[2] < n_c2ti:
ext = np.zeros((*d['BCET'].shape[:2], n_c2ti))
ext[:, :, :d['BCET'].shape[2]] = d['BCET']
d['BCET'] = ext
Comment on lines +245 to +249
variables['BCET'][:, :, c2ti_m['22 Gamma']] = variables['MGAM'][:, :, 0]

# FPIX is cumulative fuel price index; _fpix_carry persists it. None = standalone (no change).
fpi = self._mset_fpi if self._mset_fpi is not None else 0.0
self._mset_fpi = None # consume ÔÇö must be re-set each year by ftt_power.py
if 'FPIX' in variables:
fpix_lag = np.where(self._fpix_carry == 0, 1.0, self._fpix_carry)
variables['FPIX'] = fpix_lag * (1.0 + fpi)
self._fpix_carry = variables['FPIX'].copy()

# Call FTT_Standalone
variables = ftt_p_solve(
variables, time_lags, self.titles, self.histend,
tl[y], self.domain, self.power_settings
)

if not any(True for x in modules_list if x in self.ftt_modules):
print("Incorrect selection of modules. Check settings.ini")

return variables, time_lags

def update(self, year, y, scenario):
Expand Down
6 changes: 6 additions & 0 deletions MINDSET_FTT_Power/SourceCode/support/input_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ def load_data(titles, dimensions, timeline, scenarios, ftt_modules, forstart):
modules_enabled = [x.strip() for x in ftt_modules.split(',')]
modules_enabled += ['General']

# Filter dims to variables whose dimensions are all resolvable in titles.
# VariableListing.csv may contain incomplete-feature variables (e.g. battery_ages,
# SCA, MAN) that reference dimension keys not yet in classification_titles.csv.
dims = {var: dimensions[var] for var in dimensions
if all(d in known_dims for d in dimensions[var])}

# Create container with the correct dimensions
data = {
scen : {
Expand Down
Loading