feat: add hourly timeseries infrastructure with Total RE, Client End, Load columns
- Add client_load_mw to CommercialConfig (defaults to ppa_capacity_mw) - Add hourly_timestamps, hourly_fy, hourly_proj_year, hourly_total_re, hourly_client_end, hourly_load to ScenarioResult - Generate 25-year hourly data in runner.py with proper timestamps - Update API worker to return new hourly fields - Update WorkbookView HourlyGenerationSheet to show all 5 columns at each expandable level (Year > Month > Day > Hour) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
27866e86f0
commit
5e49926289
6 changed files with 164 additions and 6 deletions
|
|
@ -59,6 +59,13 @@ def _run_engine(inputs_json: str) -> dict[str, Any]:
|
||||||
# Hourly generation: 25 years × 8760 hours
|
# Hourly generation: 25 years × 8760 hours
|
||||||
"solar_hourly": result.solar_hourly,
|
"solar_hourly": result.solar_hourly,
|
||||||
"wind_hourly": result.wind_hourly,
|
"wind_hourly": result.wind_hourly,
|
||||||
|
# Hourly timeseries
|
||||||
|
"hourly_timestamps": result.hourly_timestamps,
|
||||||
|
"hourly_fy": result.hourly_fy,
|
||||||
|
"hourly_proj_year": result.hourly_proj_year,
|
||||||
|
"hourly_total_re": result.hourly_total_re,
|
||||||
|
"hourly_client_end": result.hourly_client_end,
|
||||||
|
"hourly_load": result.hourly_load,
|
||||||
},
|
},
|
||||||
"debt_schedule": [r.model_dump() for r in result.debt_schedule],
|
"debt_schedule": [r.model_dump() for r in result.debt_schedule],
|
||||||
"irr_metrics": result.irr_metrics.model_dump(),
|
"irr_metrics": result.irr_metrics.model_dump(),
|
||||||
|
|
|
||||||
|
|
@ -479,6 +479,56 @@ def run_scenario(inputs: ScenarioInput) -> ScenarioResult:
|
||||||
)
|
)
|
||||||
idc_phasing = _build_idc_phasing(inputs.capex, pipe.base_capex) if pipe.base_capex > 0 else {}
|
idc_phasing = _build_idc_phasing(inputs.capex, pipe.base_capex) if pipe.base_capex > 0 else {}
|
||||||
|
|
||||||
|
# Build hourly timeseries data
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
cod_year = inputs.project.cod_year
|
||||||
|
# Default client load to ppa_capacity if not set
|
||||||
|
client_load_mw = inputs.commercial.client_load_mw
|
||||||
|
if client_load_mw is None:
|
||||||
|
client_load_mw = inputs.commercial.ppa_capacity_mw
|
||||||
|
|
||||||
|
# Transmission loss factor for client_end
|
||||||
|
loss_factor = 1.0 - inputs.commercial.transmission_loss_pct - inputs.commercial.dsm_loss_pct
|
||||||
|
|
||||||
|
# Generate hourly data: 25 years × 8760 hours = 219,000 rows
|
||||||
|
# Start from Apr 1 of cod_year
|
||||||
|
hourly_timestamps: list[str] = []
|
||||||
|
hourly_fy: list[str] = []
|
||||||
|
hourly_proj_year: list[int] = []
|
||||||
|
hourly_total_re: list[float] = []
|
||||||
|
hourly_client_end: list[float] = []
|
||||||
|
hourly_load: list[float] = []
|
||||||
|
|
||||||
|
# Month lengths for non-leap year starting Apr 1
|
||||||
|
month_lengths = [31, 30, 31, 30, 31, 31, 30, 31, 30, 31, 30, 31] # Apr-Mar
|
||||||
|
|
||||||
|
for year in range(25):
|
||||||
|
proj_year = year + 1
|
||||||
|
fy_start = cod_year + year
|
||||||
|
fy_label = f"{fy_start}-{str(fy_start + 1)[-2:]}"
|
||||||
|
|
||||||
|
# Generate timestamps for this year
|
||||||
|
hour_idx = 0
|
||||||
|
for month in range(12):
|
||||||
|
month_len = month_lengths[month]
|
||||||
|
for day in range(1, month_len + 1):
|
||||||
|
for hour in range(24):
|
||||||
|
# ISO timestamp
|
||||||
|
month_num = month + 4 if month < 8 else month - 8
|
||||||
|
year_adj = fy_start + 1 if month >= 8 else fy_start
|
||||||
|
dt = f"{year_adj:04d}-{month_num:02d}-{day:02d}T{hour:02d}:00:00"
|
||||||
|
hourly_timestamps.append(dt)
|
||||||
|
hourly_fy.append(fy_label)
|
||||||
|
hourly_proj_year.append(proj_year)
|
||||||
|
|
||||||
|
idx = year * 8760 + hour_idx
|
||||||
|
total_re = pipe.solar_hourly[idx] + pipe.wind_hourly[idx]
|
||||||
|
hourly_total_re.append(total_re)
|
||||||
|
hourly_client_end.append(total_re * loss_factor)
|
||||||
|
hourly_load.append(client_load_mw)
|
||||||
|
hour_idx += 1
|
||||||
|
|
||||||
return ScenarioResult(
|
return ScenarioResult(
|
||||||
inputs=inputs,
|
inputs=inputs,
|
||||||
status="success",
|
status="success",
|
||||||
|
|
@ -493,4 +543,10 @@ def run_scenario(inputs: ScenarioInput) -> ScenarioResult:
|
||||||
idc_phasing=idc_phasing,
|
idc_phasing=idc_phasing,
|
||||||
solar_hourly=pipe.solar_hourly,
|
solar_hourly=pipe.solar_hourly,
|
||||||
wind_hourly=pipe.wind_hourly,
|
wind_hourly=pipe.wind_hourly,
|
||||||
|
hourly_timestamps=hourly_timestamps,
|
||||||
|
hourly_fy=hourly_fy,
|
||||||
|
hourly_proj_year=hourly_proj_year,
|
||||||
|
hourly_total_re=hourly_total_re,
|
||||||
|
hourly_client_end=hourly_client_end,
|
||||||
|
hourly_load=hourly_load,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ class CommercialConfig(BaseModel):
|
||||||
|
|
||||||
tariff_inr_per_kwh: float = Field(3.50, gt=0, description="PPA tariff (INR/kWh)")
|
tariff_inr_per_kwh: float = Field(3.50, gt=0, description="PPA tariff (INR/kWh)")
|
||||||
ppa_capacity_mw: float = Field(0.0, ge=0, description="Contracted RTC capacity (MW)")
|
ppa_capacity_mw: float = Field(0.0, ge=0, description="Contracted RTC capacity (MW)")
|
||||||
|
client_load_mw: float | None = Field(None, description="Client load for hourly dispatch; defaults to ppa_capacity_mw")
|
||||||
aux_consumption_pct: float = Field(0.005, ge=0, lt=1, description="Auxiliary consumption")
|
aux_consumption_pct: float = Field(0.005, ge=0, lt=1, description="Auxiliary consumption")
|
||||||
transmission_loss_pct: float = Field(0.01, ge=0, lt=1, description="Transmission losses")
|
transmission_loss_pct: float = Field(0.01, ge=0, lt=1, description="Transmission losses")
|
||||||
dsm_loss_pct: float = Field(0.02, ge=0, lt=1, description="DSM/RTC penalty provision")
|
dsm_loss_pct: float = Field(0.02, ge=0, lt=1, description="DSM/RTC penalty provision")
|
||||||
|
|
|
||||||
|
|
@ -108,3 +108,10 @@ class ScenarioResult(BaseModel):
|
||||||
# Stored as flat list: solar_hourly[year * 8760 + hour] for each year 0-24, hour 0-8759
|
# Stored as flat list: solar_hourly[year * 8760 + hour] for each year 0-24, hour 0-8759
|
||||||
solar_hourly: list[float] = Field(default_factory=list)
|
solar_hourly: list[float] = Field(default_factory=list)
|
||||||
wind_hourly: list[float] = Field(default_factory=list)
|
wind_hourly: list[float] = Field(default_factory=list)
|
||||||
|
# Full hourly timeseries data
|
||||||
|
hourly_timestamps: list[str] = Field(default_factory=list) # ISO timestamps
|
||||||
|
hourly_fy: list[str] = Field(default_factory=list) # FY labels like "2024-25"
|
||||||
|
hourly_proj_year: list[int] = Field(default_factory=list) # 1-25
|
||||||
|
hourly_total_re: list[float] = Field(default_factory=list) # Solar + Wind MW
|
||||||
|
hourly_client_end: list[float] = Field(default_factory=list) # After losses
|
||||||
|
hourly_load: list[float] = Field(default_factory=list) # Client load MW (default = PPA)
|
||||||
|
|
|
||||||
|
|
@ -531,13 +531,23 @@ const MONTH_DAYS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
||||||
interface HourlyData {
|
interface HourlyData {
|
||||||
solar_hourly?: number[];
|
solar_hourly?: number[];
|
||||||
wind_hourly?: number[];
|
wind_hourly?: number[];
|
||||||
|
// New columns
|
||||||
|
hourly_timestamps?: string[];
|
||||||
|
hourly_fy?: string[];
|
||||||
|
hourly_proj_year?: number[];
|
||||||
|
hourly_total_re?: number[];
|
||||||
|
hourly_client_end?: number[];
|
||||||
|
hourly_load?: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function HourlyGenerationSheet({ hourly, codYear }: { hourly: HourlyData; codYear?: number }) {
|
function HourlyGenerationSheet({ hourly, codYear }: { hourly: HourlyData; codYear?: number }) {
|
||||||
const { solar_hourly, wind_hourly } = hourly;
|
const { solar_hourly, wind_hourly, hourly_total_re, hourly_client_end, hourly_load } = hourly;
|
||||||
const hasSolar = solar_hourly && solar_hourly.length > 0;
|
const hasSolar = solar_hourly && solar_hourly.length > 0;
|
||||||
const hasWind = wind_hourly && wind_hourly.length > 0;
|
const hasWind = wind_hourly && wind_hourly.length > 0;
|
||||||
const hasData = hasSolar || hasWind;
|
const hasTotalRe = hourly_total_re && hourly_total_re.length > 0;
|
||||||
|
const hasClientEnd = hourly_client_end && hourly_client_end.length > 0;
|
||||||
|
const hasLoad = hourly_load && hourly_load.length > 0;
|
||||||
|
const hasData = hasSolar || hasWind || hasTotalRe;
|
||||||
|
|
||||||
// FY labels: if COD is April 2026, FY 2026-27 = Year 1
|
// FY labels: if COD is April 2026, FY 2026-27 = Year 1
|
||||||
const startYear = codYear || new Date().getFullYear();
|
const startYear = codYear || new Date().getFullYear();
|
||||||
|
|
@ -624,6 +634,39 @@ function HourlyGenerationSheet({ hourly, codYear }: { hourly: HourlyData; codYea
|
||||||
return d.reduce((a, v) => a + v, 0);
|
return d.reduce((a, v) => a + v, 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Helper functions for new columns (Total RE, Client End, Load)
|
||||||
|
const getYearDataNew = (year: number, data?: number[]) => {
|
||||||
|
if (!data) return [];
|
||||||
|
const start = (year - 1) * 8760;
|
||||||
|
return data.slice(start, start + 8760);
|
||||||
|
};
|
||||||
|
|
||||||
|
const computeYearTotalNew = (year: number, data?: number[]) => {
|
||||||
|
const d = getYearDataNew(year, data);
|
||||||
|
return d.reduce((a, v) => a + v, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const computeMonthTotalNew = (year: number, month: number, data?: number[]) => {
|
||||||
|
if (!data) return 0;
|
||||||
|
const yearData = getYearDataNew(year, data);
|
||||||
|
if (!yearData.length) return 0;
|
||||||
|
const start = MONTH_DAYS.slice(0, month - 1).reduce((a, d) => a + d, 0) * 24;
|
||||||
|
const days = MONTH_DAYS[month - 1];
|
||||||
|
const monthData = yearData.slice(start, start + days * 24);
|
||||||
|
return monthData.reduce((a, v) => a + v, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const computeDayTotalNew = (year: number, month: number, day: number, data?: number[]) => {
|
||||||
|
if (!data) return 0;
|
||||||
|
const yearData = getYearDataNew(year, data);
|
||||||
|
if (!yearData.length) return 0;
|
||||||
|
const start = MONTH_DAYS.slice(0, month - 1).reduce((a, d) => a + d, 0) * 24;
|
||||||
|
const monthData = yearData.slice(start, start + MONTH_DAYS[month - 1] * 24);
|
||||||
|
const dayStart = (day - 1) * 24;
|
||||||
|
const dayData = monthData.slice(dayStart, dayStart + 24);
|
||||||
|
return dayData.reduce((a, v) => a + v, 0);
|
||||||
|
};
|
||||||
|
|
||||||
if (!hasData) {
|
if (!hasData) {
|
||||||
return <div className="text-muted-foreground p-4">No hourly generation data available</div>;
|
return <div className="text-muted-foreground p-4">No hourly generation data available</div>;
|
||||||
}
|
}
|
||||||
|
|
@ -634,6 +677,9 @@ function HourlyGenerationSheet({ hourly, codYear }: { hourly: HourlyData; codYea
|
||||||
<div className="flex items-center gap-2 pb-2 text-muted-foreground">
|
<div className="flex items-center gap-2 pb-2 text-muted-foreground">
|
||||||
{hasSolar && <span className="text-orange-600">● Solar</span>}
|
{hasSolar && <span className="text-orange-600">● Solar</span>}
|
||||||
{hasWind && <span className="text-blue-600">● Wind</span>}
|
{hasWind && <span className="text-blue-600">● Wind</span>}
|
||||||
|
{hasTotalRe && <span className="text-green-600">● Total RE</span>}
|
||||||
|
{hasClientEnd && <span className="text-purple-600">● Client End</span>}
|
||||||
|
{hasLoad && <span className="text-gray-600">● Load</span>}
|
||||||
<span className="text-muted-foreground/60">Click to expand: Year → Month → Day → Hour</span>
|
<span className="text-muted-foreground/60">Click to expand: Year → Month → Day → Hour</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -643,6 +689,9 @@ function HourlyGenerationSheet({ hourly, codYear }: { hourly: HourlyData; codYea
|
||||||
const isYearExpanded = expandedYears.has(year);
|
const isYearExpanded = expandedYears.has(year);
|
||||||
const solarYr = hasSolar ? computeYearTotal(year, true) : 0;
|
const solarYr = hasSolar ? computeYearTotal(year, true) : 0;
|
||||||
const windYr = hasWind ? computeYearTotal(year, false) : 0;
|
const windYr = hasWind ? computeYearTotal(year, false) : 0;
|
||||||
|
const totalReYr = hasTotalRe ? computeYearTotalNew(year, hourly_total_re) : 0;
|
||||||
|
const clientEndYr = hasClientEnd ? computeYearTotalNew(year, hourly_client_end) : 0;
|
||||||
|
const loadYr = hasLoad ? computeYearTotalNew(year, hourly_load) : 0;
|
||||||
const fyLabel = getFyLabel(i);
|
const fyLabel = getFyLabel(i);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -655,6 +704,9 @@ function HourlyGenerationSheet({ hourly, codYear }: { hourly: HourlyData; codYea
|
||||||
<span className="w-20 font-medium">{fyLabel}</span>
|
<span className="w-20 font-medium">{fyLabel}</span>
|
||||||
{hasSolar && <span className="text-orange-700 w-24">{Math.round(solarYr).toLocaleString()} MWh</span>}
|
{hasSolar && <span className="text-orange-700 w-24">{Math.round(solarYr).toLocaleString()} MWh</span>}
|
||||||
{hasWind && <span className="text-blue-700 w-24">{Math.round(windYr).toLocaleString()} MWh</span>}
|
{hasWind && <span className="text-blue-700 w-24">{Math.round(windYr).toLocaleString()} MWh</span>}
|
||||||
|
{hasTotalRe && <span className="text-green-700 w-24">{Math.round(totalReYr).toLocaleString()} MWh</span>}
|
||||||
|
{hasClientEnd && <span className="text-purple-700 w-24">{Math.round(clientEndYr).toLocaleString()} MWh</span>}
|
||||||
|
{hasLoad && <span className="text-gray-700 w-20">{Math.round(loadYr).toLocaleString()} MWh</span>}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Month rows - forward order (Apr first, then subsequent months) */}
|
{/* Month rows - forward order (Apr first, then subsequent months) */}
|
||||||
|
|
@ -665,6 +717,9 @@ function HourlyGenerationSheet({ hourly, codYear }: { hourly: HourlyData; codYea
|
||||||
const isMonthExpanded = expandedMonths.has(monthKey);
|
const isMonthExpanded = expandedMonths.has(monthKey);
|
||||||
const solarMo = hasSolar ? computeMonthTotal(year, month, true) : 0;
|
const solarMo = hasSolar ? computeMonthTotal(year, month, true) : 0;
|
||||||
const windMo = hasWind ? computeMonthTotal(year, month, false) : 0;
|
const windMo = hasWind ? computeMonthTotal(year, month, false) : 0;
|
||||||
|
const totalReMo = hasTotalRe ? computeMonthTotalNew(year, month, hourly_total_re) : 0;
|
||||||
|
const clientEndMo = hasClientEnd ? computeMonthTotalNew(year, month, hourly_client_end) : 0;
|
||||||
|
const loadMo = hasLoad ? computeMonthTotalNew(year, month, hourly_load) : 0;
|
||||||
const daysInMonth = MONTH_DAYS[month - 1];
|
const daysInMonth = MONTH_DAYS[month - 1];
|
||||||
const monthLabel = getMonthLabel(pos, i);
|
const monthLabel = getMonthLabel(pos, i);
|
||||||
|
|
||||||
|
|
@ -678,6 +733,9 @@ function HourlyGenerationSheet({ hourly, codYear }: { hourly: HourlyData; codYea
|
||||||
<span className="w-20 text-muted-foreground">{monthLabel}</span>
|
<span className="w-20 text-muted-foreground">{monthLabel}</span>
|
||||||
{hasSolar && <span className="text-orange-700/80 w-24">{Math.round(solarMo).toLocaleString()} MWh</span>}
|
{hasSolar && <span className="text-orange-700/80 w-24">{Math.round(solarMo).toLocaleString()} MWh</span>}
|
||||||
{hasWind && <span className="text-blue-700/80 w-24">{Math.round(windMo).toLocaleString()} MWh</span>}
|
{hasWind && <span className="text-blue-700/80 w-24">{Math.round(windMo).toLocaleString()} MWh</span>}
|
||||||
|
{hasTotalRe && <span className="text-green-700/80 w-24">{Math.round(totalReMo).toLocaleString()} MWh</span>}
|
||||||
|
{hasClientEnd && <span className="text-purple-700/80 w-24">{Math.round(clientEndMo).toLocaleString()} MWh</span>}
|
||||||
|
{hasLoad && <span className="text-gray-700/80 w-20">{Math.round(loadMo).toLocaleString()} MWh</span>}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Day rows (expandable) */}
|
{/* Day rows (expandable) */}
|
||||||
|
|
@ -687,6 +745,9 @@ function HourlyGenerationSheet({ hourly, codYear }: { hourly: HourlyData; codYea
|
||||||
const isDayExpanded = expandedDays.has(dayKey);
|
const isDayExpanded = expandedDays.has(dayKey);
|
||||||
const solarDy = hasSolar ? computeDayTotal(year, month, day, true) : 0;
|
const solarDy = hasSolar ? computeDayTotal(year, month, day, true) : 0;
|
||||||
const windDy = hasWind ? computeDayTotal(year, month, day, false) : 0;
|
const windDy = hasWind ? computeDayTotal(year, month, day, false) : 0;
|
||||||
|
const totalReDy = hasTotalRe ? computeDayTotalNew(year, month, day, hourly_total_re) : 0;
|
||||||
|
const clientEndDy = hasClientEnd ? computeDayTotalNew(year, month, day, hourly_client_end) : 0;
|
||||||
|
const loadDy = hasLoad ? computeDayTotalNew(year, month, day, hourly_load) : 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={dayKey} className="ml-4">
|
<div key={dayKey} className="ml-4">
|
||||||
|
|
@ -698,6 +759,9 @@ function HourlyGenerationSheet({ hourly, codYear }: { hourly: HourlyData; codYea
|
||||||
<span className="w-12 text-muted-foreground/80">{monthLabel} {day}</span>
|
<span className="w-12 text-muted-foreground/80">{monthLabel} {day}</span>
|
||||||
{hasSolar && <span className="text-orange-600/70 w-20">{Math.round(solarDy).toLocaleString()}</span>}
|
{hasSolar && <span className="text-orange-600/70 w-20">{Math.round(solarDy).toLocaleString()}</span>}
|
||||||
{hasWind && <span className="text-blue-600/70 w-20">{Math.round(windDy).toLocaleString()}</span>}
|
{hasWind && <span className="text-blue-600/70 w-20">{Math.round(windDy).toLocaleString()}</span>}
|
||||||
|
{hasTotalRe && <span className="text-green-600/70 w-20">{Math.round(totalReDy).toLocaleString()}</span>}
|
||||||
|
{hasClientEnd && <span className="text-purple-600/70 w-20">{Math.round(clientEndDy).toLocaleString()}</span>}
|
||||||
|
{hasLoad && <span className="text-gray-600/70 w-16">{Math.round(loadDy).toLocaleString()}</span>}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Hour values - vertical stack */}
|
{/* Hour values - vertical stack */}
|
||||||
|
|
@ -708,11 +772,21 @@ function HourlyGenerationSheet({ hourly, codYear }: { hourly: HourlyData; codYea
|
||||||
const hourIdx = (day - 1) * 24 + hour;
|
const hourIdx = (day - 1) * 24 + hour;
|
||||||
const solarHr = hasSolar ? getDayData(year, month, day, true)[hourIdx % 24] : 0;
|
const solarHr = hasSolar ? getDayData(year, month, day, true)[hourIdx % 24] : 0;
|
||||||
const windHr = hasWind ? getDayData(year, month, day, false)[hourIdx % 24] : 0;
|
const windHr = hasWind ? getDayData(year, month, day, false)[hourIdx % 24] : 0;
|
||||||
|
// Get new column data by hour index
|
||||||
|
const yearDataNew = getYearDataNew(year, hourly_total_re);
|
||||||
|
const hourDataTotalRe = yearDataNew.length > hourIdx ? yearDataNew[hourIdx] : 0;
|
||||||
|
const yearDataClient = getYearDataNew(year, hourly_client_end);
|
||||||
|
const hourDataClient = yearDataClient.length > hourIdx ? yearDataClient[hourIdx] : 0;
|
||||||
|
const yearDataLoad = getYearDataNew(year, hourly_load);
|
||||||
|
const hourDataLoad = yearDataLoad.length > hourIdx ? yearDataLoad[hourIdx] : 0;
|
||||||
return (
|
return (
|
||||||
<div key={h} className="flex items-center gap-2 text-[9px] border-b border-border/30 px-1">
|
<div key={h} className="flex items-center gap-2 text-[9px] border-b border-border/30 px-1">
|
||||||
<span className="w-8 text-muted-foreground">{String(h).padStart(2, '0')}:00</span>
|
<span className="w-8 text-muted-foreground">{String(h).padStart(2, '0')}:00</span>
|
||||||
{hasSolar && <span className="text-orange-700 w-16 text-right">{Math.round(solarHr)}</span>}
|
{hasSolar && <span className="text-orange-700 w-14 text-right">{Math.round(solarHr)}</span>}
|
||||||
{hasWind && <span className="text-blue-700 w-16 text-right">{Math.round(windHr)}</span>}
|
{hasWind && <span className="text-blue-700 w-14 text-right">{Math.round(windHr)}</span>}
|
||||||
|
{hasTotalRe && <span className="text-green-700 w-14 text-right">{Math.round(hourDataTotalRe)}</span>}
|
||||||
|
{hasClientEnd && <span className="text-purple-700 w-14 text-right">{Math.round(hourDataClient)}</span>}
|
||||||
|
{hasLoad && <span className="text-gray-700 w-12 text-right">{Math.round(hourDataLoad)}</span>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
@ -870,8 +944,14 @@ export function WorkbookView({ scenarioId, kpis, debtScheduleJson, activeSheet,
|
||||||
<HorizontalTable years={debtYears} rows={buildDebtRows(debtSchedule)} />
|
<HorizontalTable years={debtYears} rows={buildDebtRows(debtSchedule)} />
|
||||||
)}
|
)}
|
||||||
{activeSheet === "irr" && <IrrSheet kpis={kpis} />}
|
{activeSheet === "irr" && <IrrSheet kpis={kpis} />}
|
||||||
{activeSheet === "generation" && (stmts?.solar_hourly || stmts?.wind_hourly ? (
|
{activeSheet === "generation" && (stmts?.solar_hourly || stmts?.wind_hourly || stmts?.hourly_total_re ? (
|
||||||
<HourlyGenerationSheet hourly={{ solar_hourly: stmts?.solar_hourly, wind_hourly: stmts?.wind_hourly }} codYear={codYear} />
|
<HourlyGenerationSheet hourly={{
|
||||||
|
solar_hourly: stmts?.solar_hourly,
|
||||||
|
wind_hourly: stmts?.wind_hourly,
|
||||||
|
hourly_total_re: stmts?.hourly_total_re,
|
||||||
|
hourly_client_end: stmts?.hourly_client_end,
|
||||||
|
hourly_load: stmts?.hourly_load,
|
||||||
|
}} codYear={codYear} />
|
||||||
) : generation.length > 0 ? (
|
) : generation.length > 0 ? (
|
||||||
<HorizontalTable years={genYears} rows={buildGenerationRows(generation)} unit="MWh / ₹Cr" />
|
<HorizontalTable years={genYears} rows={buildGenerationRows(generation)} unit="MWh / ₹Cr" />
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
|
|
@ -148,6 +148,13 @@ export interface Statements {
|
||||||
// Hourly generation: 25 years × 8760 hours = 219,000 values per technology
|
// Hourly generation: 25 years × 8760 hours = 219,000 values per technology
|
||||||
solar_hourly?: number[];
|
solar_hourly?: number[];
|
||||||
wind_hourly?: number[];
|
wind_hourly?: number[];
|
||||||
|
// Hourly timeseries
|
||||||
|
hourly_timestamps?: string[];
|
||||||
|
hourly_fy?: string[];
|
||||||
|
hourly_proj_year?: number[];
|
||||||
|
hourly_total_re?: number[];
|
||||||
|
hourly_client_end?: number[];
|
||||||
|
hourly_load?: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CostBasis =
|
export type CostBasis =
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue