Skip to content

Ev refshare extrapolator

EV provincial disaggregation share extrapolator.

Extrapolates future provincial EV shares using a Gompertz model fitted to historical vehicle ownership, GDP, and population data. These shares are used to spatially disaggregate national REMIND EV demand to provincial level.

GompertzModel

Simplified Gompertz model for vehicle ownership prediction.

Parameters:

Name Type Description Default
saturation_level float

Maximum vehicle ownership per 1000 people (default: 500)

500
alpha float

Fixed Gompertz parameter (default: -5.58)

-5.58
Source code in workflow/scripts/remind_coupling/ev_refshare_extrapolator.py
class GompertzModel:
    """Simplified Gompertz model for vehicle ownership prediction.

    Args:
        saturation_level: Maximum vehicle ownership per 1000 people (default: 500)
        alpha: Fixed Gompertz parameter (default: -5.58)
    """

    def __init__(self, saturation_level: float = 500, alpha: float = -5.58):
        self.saturation_level = saturation_level
        self.alpha = alpha
        self.beta = None
        self.fitted = False

    def gompertz_function(self, pgdp: np.ndarray, beta: float) -> np.ndarray:
        """Calculate Gompertz function values for vehicle ownership prediction.

        Args:
            pgdp: Per-capita GDP values
            beta: Fitted parameter

        Returns:
            np.ndarray: Predicted vehicle ownership per capita
        """
        return self.saturation_level * np.exp(self.alpha * np.exp(beta * pgdp))

    def fit_model(self, pgdp_data: np.ndarray, vehicle_data: np.ndarray) -> bool:
        """Fit Gompertz model beta parameter to historical data.

        Args:
            pgdp_data: Historical per-capita GDP data
            vehicle_data: Historical vehicle ownership data

        Returns:
            bool: True if fitting successful, False otherwise
        """
        try:

            def objective_function(pgdp, beta):
                return self.gompertz_function(pgdp, beta)

            popt, _ = curve_fit(
                objective_function, pgdp_data, vehicle_data, p0=[-0.0001], bounds=([-1], [0])
            )
            self.beta = popt[0]
            self.fitted = True
            logger.info(f"Gompertz model fitted successfully - α: {self.alpha}, β: {self.beta:.6f}")
            return True
        except Exception as e:
            logger.error(f"Gompertz model fitting failed: {e}")
            return False

    def predict_vehicles(self, pgdp: float, population: float) -> float:
        """Predict total vehicle numbers using fitted model.

        Args:
            pgdp: Per-capita GDP
            population: Population (in 10,000 persons)

        Returns:
            float: Predicted vehicles (in 10,000 vehicles)
        """
        if not self.fitted:
            raise ValueError("Model not fitted")

        vehicle_per_capita = self.gompertz_function(np.array([pgdp]), self.beta)[0]
        return vehicle_per_capita * population / 1000  # output in 10,000 vehicles

fit_model(pgdp_data, vehicle_data)

Fit Gompertz model beta parameter to historical data.

Parameters:

Name Type Description Default
pgdp_data ndarray

Historical per-capita GDP data

required
vehicle_data ndarray

Historical vehicle ownership data

required

Returns:

Name Type Description
bool bool

True if fitting successful, False otherwise

Source code in workflow/scripts/remind_coupling/ev_refshare_extrapolator.py
def fit_model(self, pgdp_data: np.ndarray, vehicle_data: np.ndarray) -> bool:
    """Fit Gompertz model beta parameter to historical data.

    Args:
        pgdp_data: Historical per-capita GDP data
        vehicle_data: Historical vehicle ownership data

    Returns:
        bool: True if fitting successful, False otherwise
    """
    try:

        def objective_function(pgdp, beta):
            return self.gompertz_function(pgdp, beta)

        popt, _ = curve_fit(
            objective_function, pgdp_data, vehicle_data, p0=[-0.0001], bounds=([-1], [0])
        )
        self.beta = popt[0]
        self.fitted = True
        logger.info(f"Gompertz model fitted successfully - α: {self.alpha}, β: {self.beta:.6f}")
        return True
    except Exception as e:
        logger.error(f"Gompertz model fitting failed: {e}")
        return False

gompertz_function(pgdp, beta)

Calculate Gompertz function values for vehicle ownership prediction.

Parameters:

Name Type Description Default
pgdp ndarray

Per-capita GDP values

required
beta float

Fitted parameter

required

Returns:

Type Description
ndarray

np.ndarray: Predicted vehicle ownership per capita

Source code in workflow/scripts/remind_coupling/ev_refshare_extrapolator.py
def gompertz_function(self, pgdp: np.ndarray, beta: float) -> np.ndarray:
    """Calculate Gompertz function values for vehicle ownership prediction.

    Args:
        pgdp: Per-capita GDP values
        beta: Fitted parameter

    Returns:
        np.ndarray: Predicted vehicle ownership per capita
    """
    return self.saturation_level * np.exp(self.alpha * np.exp(beta * pgdp))

predict_vehicles(pgdp, population)

Predict total vehicle numbers using fitted model.

Parameters:

Name Type Description Default
pgdp float

Per-capita GDP

required
population float

Population (in 10,000 persons)

required

Returns:

Name Type Description
float float

Predicted vehicles (in 10,000 vehicles)

Source code in workflow/scripts/remind_coupling/ev_refshare_extrapolator.py
def predict_vehicles(self, pgdp: float, population: float) -> float:
    """Predict total vehicle numbers using fitted model.

    Args:
        pgdp: Per-capita GDP
        population: Population (in 10,000 persons)

    Returns:
        float: Predicted vehicles (in 10,000 vehicles)
    """
    if not self.fitted:
        raise ValueError("Model not fitted")

    vehicle_per_capita = self.gompertz_function(np.array([pgdp]), self.beta)[0]
    return vehicle_per_capita * population / 1000  # output in 10,000 vehicles

extrapolate_reference(years, input_files, output_dir, config=None)

Extrapolate provincial EV disaggregation shares using Gompertz model.

Parameters:

Name Type Description Default
years list

Target years for projections (e.g., [2030, 2040, 2050])

required
input_files dict

Dictionary of input data file paths with keys: - 'historical_gdp': Historical GDP by province - 'historical_pop': Historical population by province - 'historical_cars': Historical vehicle ownership by province - 'ssp2_pop': SSP2 future population projections - 'ssp2_gdp': SSP2 future GDP projections

required
output_dir str

Output directory for results (CSV files will be saved here)

required
config dict

Gompertz model parameters from sectors.electric_vehicles.gompertz configuration: - 'saturation_level': Maximum vehicles per 1000 people (default: 500) - 'alpha': Fixed Gompertz parameter (default: -5.58)

None
Outputs

Saves two CSV files to output_dir: - ev_passenger_shares.csv: Provincial shares of passenger EV demand - ev_freight_shares.csv: Provincial shares of freight EV demand

Source code in workflow/scripts/remind_coupling/ev_refshare_extrapolator.py
def extrapolate_reference(years: list, input_files: dict, output_dir: str, config: dict = None):
    """Extrapolate provincial EV disaggregation shares using Gompertz model.

    Args:
        years (list): Target years for projections (e.g., [2030, 2040, 2050])
        input_files (dict): Dictionary of input data file paths with keys:
            - 'historical_gdp': Historical GDP by province
            - 'historical_pop': Historical population by province
            - 'historical_cars': Historical vehicle ownership by province
            - 'ssp2_pop': SSP2 future population projections
            - 'ssp2_gdp': SSP2 future GDP projections
        output_dir (str): Output directory for results (CSV files will be saved here)
        config (dict, optional): Gompertz model parameters from 
            sectors.electric_vehicles.gompertz configuration:
            - 'saturation_level': Maximum vehicles per 1000 people (default: 500)
            - 'alpha': Fixed Gompertz parameter (default: -5.58)

    Outputs:
        Saves two CSV files to output_dir:
        - ev_passenger_shares.csv: Provincial shares of passenger EV demand
        - ev_freight_shares.csv: Provincial shares of freight EV demand
    """
    logger.info("Extrapolating provincial EV disaggregation shares using Gompertz model")

    model = GompertzModel(
        saturation_level=config.get("saturation_level", 500)
        if config
        else 500,  # Nature Geoscience: https://doi.org/10.1038/s41561-023-01350-9
        alpha=config.get("alpha", -5.58)
        if config
        else -5.58,  # Energy Policy: https://doi.org/10.1016/j.enpol.2011.01.043
    )

    historical_data = _load_historical_data(input_files)
    pgdp_data = historical_data["pgdp"].values
    vehicle_data = historical_data["vehicle_per_capita"].values
    model.fit_model(pgdp_data, vehicle_data)

    future_data = _load_future_data(input_files, years)

    future_data["vehicles"] = future_data.apply(
        lambda row: model.predict_vehicles(row["pgdp"], row["population"]), axis=1
    )

    shares_by_year = {}
    for year in years:
        year_data = future_data[future_data["year"] == year]
        total_vehicles = year_data["vehicles"].sum()
        shares = year_data["vehicles"] / total_vehicles
        shares_by_year[year] = dict(zip(year_data["province"], shares))

    shares_df = pd.DataFrame(shares_by_year)
    shares_df.to_csv(f"{output_dir}/ev_passenger_shares.csv")
    shares_df.to_csv(f"{output_dir}/ev_freight_shares.csv")

    logger.info(f"EV disaggregation shares saved to {output_dir}")