| """
|
| Carbon intensity profiles for 6 real data centre regions.
|
| Values in gCO2eq/kWh, calibrated from Electricity Map 2024 data.
|
|
|
| Profiles capture:
|
| - Solar regions: low midday, higher at night
|
| - Wind regions: variable, often lowest at night / early morning
|
| - Hydro regions: consistently low
|
| - Gas/coal: consistently high with minor variation
|
| """
|
|
|
| import math
|
| import random
|
| from typing import List
|
|
|
|
|
|
|
|
|
| def _solar_curve(base: float, peak_reduction: float) -> List[float]:
|
| """Dips around hours 10–15 when solar is generating."""
|
| result = []
|
| for h in range(24):
|
| solar = max(0.0, math.sin(math.pi * (h - 6) / 12))
|
| result.append(base - peak_reduction * solar)
|
| return result
|
|
|
|
|
| def _wind_curve(base: float, amplitude: float, phase: float) -> List[float]:
|
| """Wind is variable — modelled as a slow sinusoid with noise seed."""
|
| return [
|
| base + amplitude * math.sin(2 * math.pi * h / 24 + phase)
|
| for h in range(24)
|
| ]
|
|
|
|
|
|
|
| BASE_PROFILES = {
|
| "us-west-2": {
|
| "name": "Oregon (Hydro + Wind)",
|
| "profile": _wind_curve(base=45, amplitude=15, phase=0.5),
|
| "renewable": 0.82,
|
| "capacity": 200.0,
|
| },
|
| "us-west-1": {
|
| "name": "California (Solar + Grid)",
|
| "profile": _solar_curve(base=220, peak_reduction=140),
|
| "renewable": 0.52,
|
| "capacity": 150.0,
|
| },
|
| "us-east-1": {
|
| "name": "Virginia (Gas + Nuclear)",
|
| "profile": _wind_curve(base=360, amplitude=20, phase=1.0),
|
| "renewable": 0.24,
|
| "capacity": 180.0,
|
| },
|
| "eu-west-1": {
|
| "name": "Ireland (Wind + Gas)",
|
| "profile": _wind_curve(base=240, amplitude=80, phase=2.0),
|
| "renewable": 0.48,
|
| "capacity": 120.0,
|
| },
|
| "ap-southeast-1": {
|
| "name": "Singapore (Natural Gas)",
|
| "profile": _wind_curve(base=455, amplitude=10, phase=0.0),
|
| "renewable": 0.08,
|
| "capacity": 100.0,
|
| },
|
| "ap-south-1": {
|
| "name": "Mumbai (Coal + Solar)",
|
| "profile": _solar_curve(base=680, peak_reduction=200),
|
| "renewable": 0.18,
|
| "capacity": 90.0,
|
| },
|
| }
|
|
|
|
|
| def get_carbon_forecast(
|
| region: str,
|
| current_hour: int,
|
| noise_seed: int = 0,
|
| noise_level: float = 0.08,
|
| ) -> List[float]:
|
| """
|
| Return 24-hour forecast starting from current_hour.
|
| Adds realistic noise — the forecast is imperfect.
|
| """
|
| rng = random.Random(noise_seed + hash(region))
|
| profile = BASE_PROFILES[region]["profile"]
|
|
|
| forecast = []
|
| for offset in range(24):
|
| h = (current_hour + offset) % 24
|
| base_val = profile[h]
|
| noise = base_val * noise_level * (rng.random() * 2 - 1)
|
| forecast.append(max(10.0, base_val + noise))
|
|
|
| return forecast
|
|
|
|
|
| def get_carbon_now(region: str, current_hour: int, noise_seed: int = 0) -> float:
|
| """Current carbon intensity — less noisy than forecast."""
|
| forecast = get_carbon_forecast(region, current_hour, noise_seed, noise_level=0.03)
|
| return round(forecast[0], 1)
|
|
|
|
|
| def get_renewable_pct(region: str, current_hour: int) -> float:
|
| """Renewable percentage varies slightly with solar/wind availability."""
|
| base = BASE_PROFILES[region]["renewable"]
|
| profile_val = BASE_PROFILES[region]["profile"][current_hour]
|
| profile_min = min(BASE_PROFILES[region]["profile"])
|
| profile_max = max(BASE_PROFILES[region]["profile"])
|
| spread = max(1.0, profile_max - profile_min)
|
|
|
| renewable_boost = 0.15 * (1 - (profile_val - profile_min) / spread)
|
| return min(1.0, max(0.0, base + renewable_boost))
|
|
|
|
|
| def naive_carbon_for_job(
|
| energy_kwh: float,
|
| current_hour: int,
|
| noise_seed: int = 0,
|
| available_regions: List[str] = None,
|
| ) -> float:
|
| """
|
| FIX: Baseline is now 'run immediately in the best available region right now'.
|
|
|
| Previously used Mumbai (worst region, 680 gCO2/kWh) which made carbon_score
|
| trivially easy — even Virginia (360 gCO2/kWh) scored 47% efficiency without
|
| learning anything useful.
|
|
|
| Now the baseline is the true opportunity cost: what you would emit if you
|
| picked the cleanest region available right now but did zero temporal planning.
|
| The agent must beat THIS to prove it has learned to read forecasts and time
|
| jobs to cleaner future windows.
|
| """
|
| if available_regions is None:
|
| available_regions = list(BASE_PROFILES.keys())
|
|
|
| best_ci = min(
|
| get_carbon_now(region, current_hour, noise_seed)
|
| for region in available_regions
|
| )
|
| return energy_kwh * best_ci
|
|
|
|
|
| def worst_carbon_for_job(
|
| energy_kwh: float,
|
| current_hour: int,
|
| noise_seed: int = 0,
|
| ) -> float:
|
| """
|
| Worst-case baseline (Mumbai) — kept for episode summary context logging only.
|
| Not used in the reward function.
|
| """
|
| ci = get_carbon_now("ap-south-1", current_hour, noise_seed)
|
| return energy_kwh * ci
|
|
|
|
|
| def optimal_carbon_for_job(
|
| energy_kwh: float,
|
| current_hour: int,
|
| noise_seed: int = 0,
|
| ) -> float:
|
| """
|
| Oracle: run job at the cleanest future hour across all regions.
|
| Used to compute theoretical maximum savings (lower bound on reward).
|
| """
|
| best = float("inf")
|
| for region in BASE_PROFILES:
|
| forecast = get_carbon_forecast(region, current_hour, noise_seed)
|
| best = min(best, min(forecast))
|
| return energy_kwh * best |